blob: a77ad9bb7909f2b427608cd826d392de4005dba5 [file] [log] [blame]
/*
* Copyright (C) 2021 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 com.android.activitycontext;
import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK;
import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
import android.app.Activity;
import android.app.Instrumentation;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
import androidx.test.platform.app.InstrumentationRegistry;
import com.android.bedstead.nene.TestApis;
import com.android.bedstead.nene.exceptions.NeneException;
import com.android.bedstead.nene.users.UserReference;
import com.android.compatibility.common.util.BlockingCallback;
import com.android.compatibility.common.util.ShellIdentityUtils.QuadFunction;
import com.android.compatibility.common.util.ShellIdentityUtils.TriFunction;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
import javax.annotation.Nullable;
/**
* Activity used for tests which need an actual {@link Context}.
*/
public class ActivityContext extends Activity {
/** Stores the request code, result code, and intent of an activity result. */
public static final class ActivityResult {
private final int mRequestCode;
private final int mResultCode;
private final Intent mIntent;
public ActivityResult(int requestCode, int resultCode, Intent intent) {
mRequestCode = requestCode;
mResultCode = resultCode;
mIntent = intent;
}
public int getRequestCode() {
return mRequestCode;
}
public int getResultCode() {
return mResultCode;
}
public Intent getIntent() {
return mIntent;
}
}
private static final class BlockingCallbackImpl extends BlockingCallback<ActivityResult> {
public void set(ActivityResult result) {
callbackTriggered(result);
}
}
private static final String LOG_TAG = "ActivityContext";
private static final Context sContext =
InstrumentationRegistry.getInstrumentation().getContext();
private static Function<ActivityContext, ?> sRunnable;
private static ActivityContext sActivityContext;
private static @Nullable Object sReturnValue;
private static @Nullable Object sThrowValue;
private static CountDownLatch sLatch;
private static BlockingCallbackImpl sGetActivityCallback = new BlockingCallbackImpl();
/**
* Blocks the current thread until a result is set by an activity started by this activity, then
* returns that result.
*/
public ActivityResult blockForActivityResult() throws InterruptedException {
try {
return sGetActivityCallback.await();
} finally {
sGetActivityCallback = new BlockingCallbackImpl();
}
}
/**
* Run some code using an Activity {@link Context}.
*
* <p>This method should only be called from an instrumented app.
*
* <p>The {@link Activity} will be valid within the {@code runnable} callback. Passing the
* {@link Activity} outside of the callback is not recommended because it may become invalid
* due to lifecycle changes.
*
* <p>This method will block until the callback has been executed. It will return the same value
* as returned by the callback.
*/
public static <E> E getWithContext(Function<ActivityContext, E> runnable)
throws InterruptedException {
return getWithContextInternal(runnable, null);
}
/**
* As {@link #getWithContext(Function)}, and also accepts a callback {@code blocking} will be
* called before returning, so it can block the thread until some condition is met.
*/
public static <E> E getWithContext(Function<ActivityContext, E> runnable,
Consumer<ActivityContext> blocking)
throws InterruptedException {
return getWithContextInternal(runnable, blocking);
}
/** {@link #getWithContext(Function)} which does not return a value. */
public static void runWithContext(Consumer<ActivityContext> runnable)
throws InterruptedException {
getWithContext((inContext) -> {
runnable.accept(inContext);
return null;
});
}
/**
* {@link #getWithContext(Function, Consumer)} which does not return a value, and also accepts a
* callback {@code blocking} will be called before returning, so it can block the thread until
* some condition is met.
*/
public static void runWithContext(Consumer<ActivityContext> runnable,
Consumer<ActivityContext> blocking) throws InterruptedException {
getWithContext((inContext) -> {
runnable.accept(inContext);
return null;
}, blocking);
}
/** {@link #getWithContext(Function)} with an additional argument. */
public static <E, F> F getWithContext(E arg1,
BiFunction<ActivityContext, E, F> runnable) throws InterruptedException {
return getWithContext((inContext) -> runnable.apply(inContext, arg1));
}
/**
* {@link #getWithContext(Function, Consumer)} with an additional argument, and also accepts a
* callback {@code blocking} will be called before returning, so it can block the thread until
* some condition is met.
*/
public static <E, F> F getWithContext(E arg1,
BiFunction<ActivityContext, E, F> runnable,
Consumer<ActivityContext> blocking) throws InterruptedException {
return getWithContext((inContext) -> runnable.apply(inContext, arg1), blocking);
}
/**
* {@link #getWithContext(Function)} which takes an additional argument and does not
* return a value.
*/
public static <E> void runWithContext(E arg1, BiConsumer<ActivityContext, E> runnable)
throws InterruptedException {
getWithContext((inContext) -> {
runnable.accept(inContext, arg1);
return null;
});
}
/**
* {@link #getWithContext(Function, Consumer)} which takes an additional argument and does not
* return a value, and also accepts a callback {@code blocking} will be called before returning,
* so it can block the thread until some condition is met.
*/
public static <E> void runWithContext(E arg1, BiConsumer<ActivityContext, E> runnable,
Consumer<ActivityContext> blocking)
throws InterruptedException {
getWithContext((inContext) -> {
runnable.accept(inContext, arg1);
return null;
}, blocking);
}
/** {@link #getWithContext(Function)} with two additional arguments. */
public static <E, F, G> G getWithContext(E arg1, F arg2,
TriFunction<ActivityContext, E, F, G> runnable) throws InterruptedException {
return getWithContext((inContext) -> runnable.apply(inContext, arg1, arg2));
}
/**
* {@link #getWithContext(Function, Consumer)} with two additional arguments, and also accepts a
* callback {@code blocking} will be called before returning, so it can block the thread until
* some condition is met.
*/
public static <E, F, G> G getWithContext(E arg1, F arg2,
TriFunction<ActivityContext, E, F, G> runnable,
Consumer<ActivityContext> blocking) throws InterruptedException {
return getWithContext((inContext) -> runnable.apply(inContext, arg1, arg2), blocking);
}
/** {@link #getWithContext(Function)} with three additional arguments. */
public static <E, F, G, H> H getWithContext(E arg1, F arg2, G arg3,
QuadFunction<ActivityContext, E, F, G, H> runnable) throws InterruptedException {
return getWithContext((inContext) -> runnable.apply(inContext, arg1, arg2, arg3));
}
/**
* {@link #getWithContext(Function, Consumer)} with three additional arguments, and also accepts
* a callback {@code blocking} will be called before returning, so it can block the thread until
* some condition is met.
*/
public static <E, F, G, H> H getWithContext(E arg1, F arg2, G arg3,
QuadFunction<ActivityContext, E, F, G, H> runnable,
Consumer<ActivityContext> blocking) throws InterruptedException {
return getWithContext((inContext) -> runnable.apply(inContext, arg1, arg2, arg3), blocking);
}
private static <E> E getWithContextInternal(Function<ActivityContext, E> runnable,
@Nullable Consumer<ActivityContext> blocking)
throws InterruptedException {
if (runnable == null) {
throw new NullPointerException();
}
// As we show an Activity we must be in the foreground
UserReference currentUser = TestApis.users().current();
try {
TestApis.users().instrumented().switchTo();
Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
if (!instrumentation.getContext().getPackageName().equals(
instrumentation.getTargetContext().getPackageName())) {
throw new IllegalStateException(
"ActivityContext can only be used in test apps which instrument themselves."
+ " Consider ActivityScenario for this case.");
}
synchronized (ActivityContext.class) {
sRunnable = runnable;
sLatch = new CountDownLatch(1);
sReturnValue = null;
sThrowValue = null;
Intent intent = new Intent();
intent.setClass(sContext, ActivityContext.class);
intent.setFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TASK);
sContext.startActivity(intent);
}
if (!sLatch.await(5, TimeUnit.MINUTES)) {
throw new NeneException("Timed out while waiting for lambda with context to"
+ " complete.");
}
if (blocking != null) {
blocking.accept(sActivityContext);
}
synchronized (ActivityContext.class) {
sRunnable = null;
sActivityContext = null;
if (sThrowValue != null) {
if (sThrowValue instanceof RuntimeException) {
throw (RuntimeException) sThrowValue;
}
if (sThrowValue instanceof Error) {
throw (Error) sThrowValue;
}
throw new IllegalStateException("Invalid value for sThrowValue");
}
return (E) sReturnValue;
}
} finally {
currentUser.switchTo();
}
}
@Override
protected void onResume() {
super.onResume();
synchronized (ActivityContext.class) {
sActivityContext = this;
if (sRunnable == null) {
Log.e(LOG_TAG, "Launched ActivityContext without runnable");
} else {
try {
sReturnValue = sRunnable.apply(this);
} catch (RuntimeException | Error e) {
sThrowValue = e;
}
sLatch.countDown();
}
}
}
@Override
// TODO(b/198280332): Remove this temporary solution to set return values for methods
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
sGetActivityCallback.set(new ActivityResult(requestCode, resultCode, data));
}
}