blob: 36d959f67b4ed289302ba057395f8f890f238cfa [file] [log] [blame]
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.activity.result;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import androidx.activity.result.contract.ActivityResultContract;
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.app.ActivityOptionsCompat;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleEventObserver;
import androidx.lifecycle.LifecycleOwner;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
/**
* A registry that stores {@link ActivityResultCallback activity result callbacks} for
* {@link ActivityResultCaller#registerForActivityResult registered calls}.
*
* You can create your own instance for testing by overriding {@link #onLaunch} and calling
* {@link #dispatchResult} immediately within it, thus skipping the actual
* {@link Activity#startActivityForResult} call.
*
* When testing, make sure to explicitly provide a registry instance whenever calling
* {@link ActivityResultCaller#registerForActivityResult}, to be able to inject a test instance.
*/
public abstract class ActivityResultRegistry {
private static final String KEY_COMPONENT_ACTIVITY_REGISTERED_RCS =
"KEY_COMPONENT_ACTIVITY_REGISTERED_RCS";
private static final String KEY_COMPONENT_ACTIVITY_REGISTERED_KEYS =
"KEY_COMPONENT_ACTIVITY_REGISTERED_KEYS";
private static final String KEY_COMPONENT_ACTIVITY_PENDING_RESULTS =
"KEY_COMPONENT_ACTIVITY_PENDING_RESULT";
private static final String KEY_COMPONENT_ACTIVITY_RANDOM_OBJECT =
"KEY_COMPONENT_ACTIVITY_RANDOM_OBJECT";
private static final String LOG_TAG = "ActivityResultRegistry";
// Use upper 16 bits for request codes
private static final int INITIAL_REQUEST_CODE_VALUE = 0x00010000;
private Random mRandom = new Random();
private final Map<Integer, String> mRcToKey = new HashMap<>();
private final Map<String, Integer> mKeyToRc = new HashMap<>();
private final Map<String, LifecycleContainer> mKeyToLifecycleContainers = new HashMap<>();
@SuppressWarnings("WeakerAccess") /* synthetic access */
final transient Map<String, CallbackAndContract<?>> mKeyToCallback = new HashMap<>();
@SuppressWarnings("WeakerAccess") /* synthetic access */
final Map<String, Object> mParsedPendingResults = new HashMap<>();
@SuppressWarnings("WeakerAccess") /* synthetic access */
final Bundle/*<String, ActivityResult>*/ mPendingResults = new Bundle();
/**
* Start the process of executing an {@link ActivityResultContract} in a type-safe way,
* using the provided {@link ActivityResultContract contract}.
*
* @param requestCode request code to use
* @param contract contract to use for type conversions
* @param input input required to execute an ActivityResultContract.
* @param options Additional options for how the Activity should be started.
*/
@MainThread
public abstract <I, O> void onLaunch(
int requestCode,
@NonNull ActivityResultContract<I, O> contract,
@SuppressLint("UnknownNullness") I input,
@Nullable ActivityOptionsCompat options);
/**
* Register a new callback with this registry.
*
* This is normally called by a higher level convenience methods like
* {@link ActivityResultCaller#registerForActivityResult}.
*
* @param key a unique string key identifying this call
* @param lifecycleOwner a {@link LifecycleOwner} that makes this call.
* @param contract the contract specifying input/output types of the call
* @param callback the activity result callback
*
* @return a launcher that can be used to execute an ActivityResultContract.
*/
@NonNull
public final <I, O> ActivityResultLauncher<I> register(
@NonNull final String key,
@NonNull final LifecycleOwner lifecycleOwner,
@NonNull final ActivityResultContract<I, O> contract,
@NonNull final ActivityResultCallback<O> callback) {
Lifecycle lifecycle = lifecycleOwner.getLifecycle();
if (lifecycle.getCurrentState().isAtLeast(Lifecycle.State.STARTED)) {
throw new IllegalStateException("LifecycleOwner " + lifecycleOwner + " is "
+ "attempting to register while current state is "
+ lifecycle.getCurrentState() + ". LifecycleOwners must call register before "
+ "they are STARTED.");
}
final int requestCode = registerKey(key);
LifecycleContainer lifecycleContainer = mKeyToLifecycleContainers.get(key);
if (lifecycleContainer == null) {
lifecycleContainer = new LifecycleContainer(lifecycle);
}
LifecycleEventObserver observer = new LifecycleEventObserver() {
@Override
public void onStateChanged(
@NonNull LifecycleOwner lifecycleOwner,
@NonNull Lifecycle.Event event) {
if (Lifecycle.Event.ON_START.equals(event)) {
mKeyToCallback.put(key, new CallbackAndContract<>(callback, contract));
if (mParsedPendingResults.containsKey(key)) {
@SuppressWarnings("unchecked")
final O parsedPendingResult = (O) mParsedPendingResults.get(key);
mParsedPendingResults.remove(key);
callback.onActivityResult(parsedPendingResult);
}
final ActivityResult pendingResult = mPendingResults.getParcelable(key);
if (pendingResult != null) {
mPendingResults.remove(key);
callback.onActivityResult(contract.parseResult(
pendingResult.getResultCode(),
pendingResult.getData()));
}
} else if (Lifecycle.Event.ON_STOP.equals(event)) {
mKeyToCallback.remove(key);
} else if (Lifecycle.Event.ON_DESTROY.equals(event)) {
unregister(key);
}
}
};
lifecycleContainer.addObserver(observer);
mKeyToLifecycleContainers.put(key, lifecycleContainer);
return new ActivityResultLauncher<I>() {
@Override
public void launch(I input, @Nullable ActivityOptionsCompat options) {
onLaunch(requestCode, contract, input, options);
}
@Override
public void unregister() {
ActivityResultRegistry.this.unregister(key);
}
@NonNull
@Override
public ActivityResultContract<I, ?> getContract() {
return contract;
}
};
}
/**
* Register a new callback with this registry.
*
* This is normally called by a higher level convenience methods like
* {@link ActivityResultCaller#registerForActivityResult}.
*
* When calling this, you must call {@link ActivityResultLauncher#unregister()} on the
* returned {@link ActivityResultLauncher} when the launcher is no longer needed to
* release any values that might be captured in the registered callback.
*
* @param key a unique string key identifying this call
* @param contract the contract specifying input/output types of the call
* @param callback the activity result callback
*
* @return a launcher that can be used to execute an ActivityResultContract.
*/
@NonNull
public final <I, O> ActivityResultLauncher<I> register(
@NonNull final String key,
@NonNull final ActivityResultContract<I, O> contract,
@NonNull final ActivityResultCallback<O> callback) {
final int requestCode = registerKey(key);
mKeyToCallback.put(key, new CallbackAndContract<>(callback, contract));
if (mParsedPendingResults.containsKey(key)) {
@SuppressWarnings("unchecked")
final O parsedPendingResult = (O) mParsedPendingResults.get(key);
mParsedPendingResults.remove(key);
callback.onActivityResult(parsedPendingResult);
}
final ActivityResult pendingResult = mPendingResults.getParcelable(key);
if (pendingResult != null) {
mPendingResults.remove(key);
callback.onActivityResult(contract.parseResult(
pendingResult.getResultCode(),
pendingResult.getData()));
}
return new ActivityResultLauncher<I>() {
@Override
public void launch(I input, @Nullable ActivityOptionsCompat options) {
onLaunch(requestCode, contract, input, options);
}
@Override
public void unregister() {
ActivityResultRegistry.this.unregister(key);
}
@NonNull
@Override
public ActivityResultContract<I, ?> getContract() {
return contract;
}
};
}
/**
* Unregister a callback previously registered with {@link #register}. This shouldn't be
* called directly, but instead through {@link ActivityResultLauncher#unregister()}.
*
* @param key the unique key used when registering a callback.
*/
@MainThread
final void unregister(@NonNull String key) {
Integer rc = mKeyToRc.remove(key);
if (rc != null) {
mRcToKey.remove(rc);
}
mKeyToCallback.remove(key);
if (mParsedPendingResults.containsKey(key)) {
Log.w(LOG_TAG, "Dropping pending result for request " + key + ": "
+ mParsedPendingResults.get(key));
mParsedPendingResults.remove(key);
}
if (mPendingResults.containsKey(key)) {
Log.w(LOG_TAG, "Dropping pending result for request " + key + ": "
+ mPendingResults.<ActivityResult>getParcelable(key));
mPendingResults.remove(key);
}
LifecycleContainer lifecycleContainer = mKeyToLifecycleContainers.get(key);
if (lifecycleContainer != null) {
lifecycleContainer.clearObservers();
mKeyToLifecycleContainers.remove(key);
}
}
/**
* Save the state of this registry in the given {@link Bundle}
*
* @param outState the place to put state into
*/
public final void onSaveInstanceState(@NonNull Bundle outState) {
outState.putIntegerArrayList(KEY_COMPONENT_ACTIVITY_REGISTERED_RCS,
new ArrayList<>(mRcToKey.keySet()));
outState.putStringArrayList(KEY_COMPONENT_ACTIVITY_REGISTERED_KEYS,
new ArrayList<>(mRcToKey.values()));
outState.putBundle(KEY_COMPONENT_ACTIVITY_PENDING_RESULTS,
(Bundle) mPendingResults.clone());
outState.putSerializable(KEY_COMPONENT_ACTIVITY_RANDOM_OBJECT, mRandom);
}
/**
* Restore the state of this registry from the given {@link Bundle}
*
* @param savedInstanceState the place to restore from
*/
public final void onRestoreInstanceState(@Nullable Bundle savedInstanceState) {
if (savedInstanceState == null) {
return;
}
ArrayList<Integer> rcs =
savedInstanceState.getIntegerArrayList(KEY_COMPONENT_ACTIVITY_REGISTERED_RCS);
ArrayList<String> keys =
savedInstanceState.getStringArrayList(KEY_COMPONENT_ACTIVITY_REGISTERED_KEYS);
if (keys == null || rcs == null) {
return;
}
int numKeys = keys.size();
for (int i = 0; i < numKeys; i++) {
bindRcKey(rcs.get(i), keys.get(i));
}
mRandom = (Random) savedInstanceState.getSerializable(KEY_COMPONENT_ACTIVITY_RANDOM_OBJECT);
mPendingResults.putAll(
savedInstanceState.getBundle(KEY_COMPONENT_ACTIVITY_PENDING_RESULTS));
}
/**
* Dispatch a result received via {@link Activity#onActivityResult} to the callback on record,
* or store the result if callback was not yet registered.
*
* @param requestCode request code to identify the callback
* @param resultCode status to indicate the success of the operation
* @param data an intent that carries the result data
*
* @return whether there was a callback was registered for the given request code which was
* or will be called.
*/
@MainThread
public final boolean dispatchResult(int requestCode, int resultCode, @Nullable Intent data) {
String key = mRcToKey.get(requestCode);
if (key == null) {
return false;
}
doDispatch(key, resultCode, data, mKeyToCallback.get(key));
return true;
}
/**
* Dispatch a result object to the callback on record.
*
* @param requestCode request code to identify the callback
* @param result the result to propagate
*
* @return true if there is a callback registered for the given request code, false otherwise.
*/
@MainThread
public final <O> boolean dispatchResult(int requestCode,
@SuppressLint("UnknownNullness") O result) {
String key = mRcToKey.get(requestCode);
if (key == null) {
return false;
}
CallbackAndContract<?> callbackAndContract = mKeyToCallback.get(key);
if (callbackAndContract == null || callbackAndContract.mCallback == null) {
// Remove any pending result
mPendingResults.remove(key);
// And add these pre-parsed pending results in their place
mParsedPendingResults.put(key, result);
} else {
@SuppressWarnings("unchecked")
ActivityResultCallback<O> callback =
(ActivityResultCallback<O>) callbackAndContract.mCallback;
callback.onActivityResult(result);
}
return true;
}
private <O> void doDispatch(String key, int resultCode, @Nullable Intent data,
@Nullable CallbackAndContract<O> callbackAndContract) {
if (callbackAndContract != null && callbackAndContract.mCallback != null) {
ActivityResultCallback<O> callback = callbackAndContract.mCallback;
ActivityResultContract<?, O> contract = callbackAndContract.mContract;
callback.onActivityResult(contract.parseResult(resultCode, data));
} else {
// Remove any parsed pending result
mParsedPendingResults.remove(key);
// And add these pending results in their place
mPendingResults.putParcelable(key, new ActivityResult(resultCode, data));
}
}
private int registerKey(String key) {
Integer existing = mKeyToRc.get(key);
if (existing != null) {
return existing;
}
int rc = generateRandomNumber();
bindRcKey(rc, key);
return rc;
}
/**
* Generate a random number between the initial value (00010000) inclusive, and the max
* integer value. If that number is already an existing request code, generate another until
* we find one that is new.
*
* @return the number
*/
private int generateRandomNumber() {
int number = mRandom.nextInt((Integer.MAX_VALUE - INITIAL_REQUEST_CODE_VALUE) + 1)
+ INITIAL_REQUEST_CODE_VALUE;
while (mRcToKey.containsKey(number)) {
number = mRandom.nextInt((Integer.MAX_VALUE - INITIAL_REQUEST_CODE_VALUE) + 1)
+ INITIAL_REQUEST_CODE_VALUE;
}
return number;
}
private void bindRcKey(int rc, String key) {
mRcToKey.put(rc, key);
mKeyToRc.put(key, rc);
}
private static class CallbackAndContract<O> {
final ActivityResultCallback<O> mCallback;
final ActivityResultContract<?, O> mContract;
CallbackAndContract(
ActivityResultCallback<O> callback,
ActivityResultContract<?, O> contract) {
mCallback = callback;
mContract = contract;
}
}
private static class LifecycleContainer {
final Lifecycle mLifecycle;
private final ArrayList<LifecycleEventObserver> mObservers;
LifecycleContainer(@NonNull Lifecycle lifecycle) {
mLifecycle = lifecycle;
mObservers = new ArrayList<>();
}
void addObserver(@NonNull LifecycleEventObserver observer) {
mLifecycle.addObserver(observer);
mObservers.add(observer);
}
void clearObservers() {
for (LifecycleEventObserver observer: mObservers) {
mLifecycle.removeObserver(observer);
}
mObservers.clear();
}
}
}