Files
android_development/samples/IntentPlayground/src/com/example/android/intentplayground/TestBase.java
Liam Clark ba9b6ca212 IntentPlayground start activities for result
Test: Manual
Change-Id: I3004e6dd76c026e41d2c9a28f4dd59664f99cd09
2019-01-17 15:38:11 -08:00

359 lines
16 KiB
Java

/*
* Copyright (C) 2018 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.example.android.intentplayground;
import android.app.Activity;
import android.app.ActivityManager;
import android.app.TaskStackBuilder;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.util.Log;
import com.example.android.intentplayground.TaskInfo.ActivityInstanceInfoMirror;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import static com.example.android.intentplayground.FlagUtils.getActivityFlags;
import static com.example.android.intentplayground.FlagUtils.hasActivityFlag;
import static com.example.android.intentplayground.FlagUtils.hasIntentFlag;
import static java.util.Collections.singletonList;
/**
* TestBase holds methods to query, test and compare task hierarchies.
*/
public class TestBase {
static final String TAG = "TestBase";
private List<TaskStackBuilder> mBuilders;
private Context mContext;
private PackageInfo mPackageInfo;
TestBase(Context context, Node hierarchy) {
mBuilders = new LinkedList<>();
mContext = context;
setActivities(hierarchy);
}
/**
* Launch the activities specified by the constructor.
*
* @param style An enum that chooses which method to use to launch the activities.
*/
void startActivities(LaunchStyle style) {
switch (style) {
// COMMAND_LINE will only work if the application is installed with system permissions
// that allow it to use am shell command "am start ..."
case COMMAND_LINE:
mBuilders.forEach(tsb -> Arrays.stream(tsb.getIntents())
.forEach(AMControl::launchInBackground));
break;
case TASK_STACK_BUILDER:
mBuilders.forEach(tsb -> {
// TODO: does this indicate bug in ActivityManager?
// The launch of each activity needs to be delayed a bit or ActivityManager will7
// skip creating most of them
try {
Thread.sleep(500);
tsb.startActivities();
Thread.sleep(500);
} catch (InterruptedException ie) {
Log.e(TAG, ie.getMessage());
}
});
break;
case LAUNCH_FORWARD:
mBuilders.forEach(tsb -> {
// The launch of each activity needs to be delayed a bit or ActivityManager will
// skip creating most of them
try {
Thread.sleep(500);
} catch (InterruptedException ie) {
Log.e(TAG, ie.getMessage());
}
ArrayList<Intent> nextIntents = new ArrayList<>(Arrays.asList(
tsb.getIntents()));
Intent launch = nextIntents.remove(0)
.putParcelableArrayListExtra(BaseActivity.EXTRA_LAUNCH_FORWARD, nextIntents);
if (BuildConfig.DEBUG) {
Log.d(TAG, "Launching " + launch.getComponent().toString());
}
mContext.startActivity(launch);
});
break;
}
}
/**
* This method examines the flags on the given intent, as well as the <activity> flags of
* the intended component, and computes what the hierarchy of activities should look like
* after the launch of the intended component (WORK IN PROGRESS).
* @param intent The intent that will be passed to startActivity().
* @return A Node object that models the expected hierarchy.
*/
Node computeExpected(Intent intent) {
// Determine the effect of selected mIntent flags on expected hierarchy
Node currentTasks = describeTaskHierarchy(mContext);
Node startActivity = new Node(intent.getComponent()).setIntent(intent);
Node targetTask = findReusableTarget(currentTasks, intent)
.orElseGet(() -> createNewTaskIfNeeded(currentTasks, intent)
.orElse(findCurrentTask(currentTasks).get()));
clearIfNeeded(targetTask, intent);
if (needsStartActivity(currentTasks, intent)) {
targetTask.addFirstChild(startActivity);
}
return currentTasks;
}
/**
* Finds the taskAffinity of the target component
* @param actName The component for which to find the corresponding affinity
* @return the task affinity, or null if there is none associated with the component
*/
Optional<String> affinityOf(ComponentName actName) {
String affinity = null;
for (ActivityInfo activityInfo : mPackageInfo.activities) {
if (activityInfo.name.equals(actName.getClassName())) {
affinity = activityInfo.taskAffinity;
}
}
return Optional.ofNullable(affinity);
}
/**
* Describes the current set of tasks open in the application as
* a tree of Nodes. Returns a root node, whose children are task nodes.
* The children of those task nodes are activities, in order of most recently used.
* @param context The context of an activity in the application.
* @return A Node that models the current task hierarchy of the application.
*/
public static Node describeTaskHierarchy(Context context) {
ActivityManager am = context.getSystemService(ActivityManager.class);
Node root = Node.newRootNode();
int currentTaskId = ((Activity) context).getTaskId();
List<ActivityManager.RecentTaskInfo> tasks = am.getAppTasks().stream()
.map(ActivityManager.AppTask::getTaskInfo).collect(Collectors.toList());
for (ActivityManager.AppTask task : am.getAppTasks()) {
ActivityManager.RecentTaskInfo rti = task.getTaskInfo();
List<ActivityInstanceInfoMirror> activities = TaskInfo.getActivities(rti);
if (!activities.isEmpty()) {
Intent baseIntent = activities.get(0).getIntent();
Node taskRoot = new Node(rti.persistentId).setIntent(baseIntent);
if (taskRoot.mTaskId == currentTaskId) taskRoot.setCurrent(true);
activities.forEach(activity -> {
taskRoot.addChild(new Node(activity.getName().clone())
.setIntent(activity.getIntent()));
});
root.addChild(taskRoot);
}
}
return root;
}
private Optional<Node> findReusableTarget(Node tasks, Intent intent) {
ComponentName target = intent.getComponent();
boolean hasNewTask = hasIntentFlag(intent, IntentFlag.NEW_TASK);
boolean hasMultipleTask = hasIntentFlag(intent, IntentFlag.MULTIPLE_TASK);
boolean isSingleInstance = hasActivityFlag(mContext, target,
ActivityFlag.LAUNCH_MODE_SINGLE_INSTANCE);
boolean isSingleTask = hasActivityFlag(mContext, target,
ActivityFlag.LAUNCH_MODE_SINGLE_TASK);
boolean isIntoExisting = hasActivityFlag(mContext, target,
ActivityFlag.DOCUMENT_LAUNCH_MODE_INTO_EXISTING);
if (isSingleInstance || isSingleTask) {
Log.d(TAG, "found resuable target singleInstance/singleTask");
return findTaskOfActivity(tasks, target);
} else if (isIntoExisting || (hasNewTask && !hasMultipleTask)) {
Optional<Node> rootTask = findTaskWithRoot(tasks, target);
if (rootTask.isPresent()) {
Log.d(TAG, "found resuable target, same root task");
return rootTask;
}
else if (!isDocument(intent)) {
Log.d(TAG, "found resuable target, same affinity task");
return findTaskWithAffinity(tasks, affinityOf(target).
orElse(target.getPackageName()));
}
}
Log.d(TAG, "did not find resuable target");
return Optional.empty();
}
private Optional<Node> createNewTaskIfNeeded(Node tasks, Intent intent) {
// Everything in this method runs assuming there is no reuseable target for the intent
ComponentName target = intent.getComponent();
boolean hasNewTask = hasIntentFlag(intent, IntentFlag.NEW_TASK);
boolean hasMultipleTask = hasIntentFlag(intent, IntentFlag.MULTIPLE_TASK);
boolean hasNewDocument = hasIntentFlag(intent, IntentFlag.NEW_DOCUMENT);
Set<ActivityFlag> flags = getActivityFlags(mContext, target);
boolean isSingleInstance = flags.contains(ActivityFlag.LAUNCH_MODE_SINGLE_INSTANCE);
boolean isSingleTask = flags.contains(ActivityFlag.LAUNCH_MODE_SINGLE_TASK);
boolean isDocument = flags.contains(ActivityFlag.DOCUMENT_LAUNCH_MODE_ALWAYS)
|| flags.contains(ActivityFlag.DOCUMENT_LAUNCH_MODE_INTO_EXISTING);
if (hasNewTask || hasNewDocument || isDocument || isSingleInstance || isSingleTask) {
if (hasMultipleTask) {
// remove task with same root if present
findTaskWithRoot(tasks, target).ifPresent(task -> {
tasks.mChildren.remove(task);
});
}
Node newNode = new Node(Node.NEW_TASK_ID).setIntent(intent);
newNode.setNew(true);
tasks.addFirstChild(newNode);
Log.d(TAG, "create new task");
return Optional.of(newNode);
}
Log.d(TAG, "did not create new task");
return Optional.empty();
}
private static Optional<Node> findCurrentTask(Node stack) {
return stack.mChildren.stream().filter(Node::isCurrent).findFirst();
}
private static Optional<Node> findTaskOfActivity(Node stack, ComponentName target) {
for (Node task : stack.mChildren) {
for (Node activity : task.mChildren) {
if (activity.mName.equals(target)) return Optional.of(task);
}
}
return Optional.empty();
}
public static Optional<Node> findTaskWithRoot(Node stack, ComponentName target) {
return stack.mChildren.stream().filter(task -> task.mChildren.get(task.mChildren.size() - 1)
.mName.equals(target))
.findFirst();
}
public static List<Node> findTasksWithRoot(Node stack, ComponentName target) {
return stack.mChildren.stream().filter(task -> task.mChildren.get(task.mChildren.size() - 1)
.mName.equals(target))
.collect(Collectors.toList());
}
private Optional<Node> findTaskWithAffinity(Node stack, String affinity) {
// Need to iterate through tasks from least to most recent
ListIterator<Node> iterator = stack.mChildren.listIterator(stack.mChildren.size());
while (iterator.hasPrevious()) {
Node task = iterator.previous();
if (!hasActivityFlag(mContext, task.mChildren.get(0).mName,
ActivityFlag.LAUNCH_MODE_SINGLE_INSTANCE)) { // Exclude singleInstance
if (affinityOf(task.mChildren.get(0).mName)
.filter(a -> a.equals(affinity)).isPresent()) { // find matching affinity
return Optional.of(task);
}
}
}
return Optional.empty();
}
private boolean needsStartActivity(Node tasks, Intent intent) {
ComponentName sourceActivity = findCurrentTask(tasks).orElse(tasks.mChildren.get(0))
.mChildren.get(0).mName;
ComponentName target = intent.getComponent();
boolean hasSingleTop = hasIntentFlag(intent, IntentFlag.SINGLE_TOP);
boolean isSingleInstance = hasActivityFlag(mContext, target,
ActivityFlag.LAUNCH_MODE_SINGLE_INSTANCE);
boolean isSingleTask = hasActivityFlag(mContext, target,
ActivityFlag.LAUNCH_MODE_SINGLE_TASK);
boolean isSingleTop = hasActivityFlag(mContext, target,
ActivityFlag.LAUNCH_MODE_SINGLE_TOP);
if (sourceActivity.equals(target) &&
(isSingleInstance || isSingleTask || isSingleTop || hasSingleTop )) {
return false;
}
return true;
}
private boolean isDocument(Intent intent) {
Set<ActivityFlag> flags = getActivityFlags(mContext, intent.getComponent());
return flags.contains(ActivityFlag.DOCUMENT_LAUNCH_MODE_ALWAYS) ||
flags.contains(ActivityFlag.DOCUMENT_LAUNCH_MODE_INTO_EXISTING) ||
hasIntentFlag(intent, IntentFlag.NEW_DOCUMENT);
}
void setActivities(Node hierarchy) {
// load the package info for this app (used later to get <activity> flags)
PackageManager pm = mContext.getPackageManager();
try {
mPackageInfo = pm.getPackageInfo(mContext.getPackageName(),
PackageManager.GET_ACTIVITIES);
} catch (PackageManager.NameNotFoundException e) {
// Should not happen
throw new RuntimeException(e);
}
// Build list of TaskStackBuilders from task hierarchy modeled by Node
if (hierarchy.mChildren.isEmpty()) return;
mBuilders.clear();
hierarchy.mChildren.forEach(taskParent -> {
TaskStackBuilder tb = TaskStackBuilder.create(mContext);
Intent taskRoot = new Intent()
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_MULTIPLE_TASK)
.setComponent(taskParent.mChildren.get(0).mName);
tb.addNextIntent(taskRoot);
taskParent.mChildren.subList(1, taskParent.mChildren.size()).forEach(activity ->
tb.addNextIntent(new Intent().setComponent(activity.mName)));
mBuilders.add(tb);
});
// Edit the mIntent of the last activity in the last task so that it will relaunch the
// activity that constructed this TestBase
TaskStackBuilder tsb = mBuilders.get(mBuilders.size() - 1);
Intent lastIntent = tsb.editIntentAt(tsb.getIntentCount() - 1);
Intent launcherIntent = new Intent(mContext, mContext.getClass());
lastIntent.putParcelableArrayListExtra(BaseActivity.EXTRA_LAUNCH_FORWARD,
new ArrayList<>(singletonList(launcherIntent)));
}
private void clearIfNeeded(Node task, Intent intent) {
ComponentName target = intent.getComponent();
boolean hasClearTop = hasIntentFlag(intent, IntentFlag.CLEAR_TOP);
boolean shouldClearTask = hasIntentFlag(intent, IntentFlag.CLEAR_TASK)
&& hasIntentFlag(intent, IntentFlag.NEW_TASK);
boolean isDocument = isDocument(intent);
boolean isSingleInstance = hasActivityFlag(mContext, target,
ActivityFlag.LAUNCH_MODE_SINGLE_INSTANCE);
boolean isSingleTask = hasActivityFlag(mContext, target,
ActivityFlag.LAUNCH_MODE_SINGLE_TASK);
if (hasClearTop) {
int targetIndex = 0;
for (int i = 0; i < task.mChildren.size(); i++) {
if (task.mChildren.get(i).mName.equals(target)) targetIndex = i;
}
task.mChildren = task.mChildren.subList(targetIndex, task.mChildren.size());
} else if (shouldClearTask || isDocument || isSingleInstance || isSingleTask) {
task.clearChildren();
}
}
public Context getContext() { return mContext; }
/**
* An enum representing options for launching a series of tasks using this TestBase.
*/
enum LaunchStyle { TASK_STACK_BUILDER, COMMAND_LINE, LAUNCH_FORWARD}
}