Intent Playground sample application
The app allows a user to explore the behaviour of different launch modes, task affinities and intent flags. It displays the current state of all tasks in the application and their corresponding flags. It allows the user to launch a set amount of activities on launch. This bring the user directly into a state where many options for exploration are available, rather than having to go through a complicated setup first. Access the activity field of RecentTaskInfo using reflection and mirror the ActivityInstanceInfo into our own value object. This breaks the compile time dependency on the ActivityInstanceInfo api and turns it into a runtime dependency. If the api is missing on the device we can still show the task structure and log an error with the missing activity instance info. Known bug: The enable suggestion button crashes the application. Test: Build and Run Change-Id: Id0274bae159c16aee6dccd805deb53851ffcf21d
This commit is contained in:
@@ -0,0 +1,350 @@
|
||||
/*
|
||||
* 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.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 java.lang.reflect.Field;
|
||||
import java.util.Arrays;
|
||||
import java.util.EnumSet;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Static utility functions to query intent and activity manifest flags.
|
||||
*/
|
||||
class FlagUtils {
|
||||
private static Class<Intent> sIntentClass = Intent.class;
|
||||
private static List<ActivityInfo> sActivityInfos = null;
|
||||
private static Intent sIntent = new Intent();
|
||||
static final String INTENT_FLAG_PREFIX = "FLAG_ACTIVITY";
|
||||
private static final String ACTIVITY_INFO_FLAG_PREFIX = "FLAG";
|
||||
|
||||
/**
|
||||
* Returns a String list of flags active on this intent.
|
||||
* @param intent The intent on which to query flags.
|
||||
* @return A list of flags active on this intent.
|
||||
*/
|
||||
public static List<String> discoverFlags(Intent intent) {
|
||||
int flags = intent.getFlags();
|
||||
return Arrays.stream(intent.getClass().getDeclaredFields()) // iterate over Intent members
|
||||
.filter(f -> f.getName().startsWith(INTENT_FLAG_PREFIX)) // filter FLAG_ fields
|
||||
.filter(f -> {
|
||||
try {
|
||||
return (flags & f.getInt(intent)) > 0;
|
||||
} catch (IllegalAccessException e) {
|
||||
// Should never happen, the fields we are reading are public
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}) // filter fields that are present in intent
|
||||
.map(Field::getName) // map present Fields to their string names
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a full list of flags available to be set on an intent.
|
||||
* @return A string list of all intent flags.
|
||||
*/
|
||||
public static List<String> getIntentFlagsAsString() {
|
||||
return Arrays.stream(sIntentClass.getDeclaredFields())
|
||||
.filter(f -> f.getName().startsWith(INTENT_FLAG_PREFIX))
|
||||
.map(Field::getName)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all defined {@link IntentFlag}s.
|
||||
* @return All defined IntentFlags.
|
||||
*/
|
||||
public static List<IntentFlag> getAllIntentFlags() {
|
||||
return Arrays.asList(IntentFlag.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get intent flags by category/
|
||||
* @return List of string flags (value) organized by category/function (key).
|
||||
*/
|
||||
public static Map<String, List<String>> intentFlagsByCategory() {
|
||||
Map<String, List<String>> categories = new HashMap<>();
|
||||
List<String> allFlags = getIntentFlagsAsString();
|
||||
List<String> nonUser = new LinkedList<>();
|
||||
List<String> recentsAndUi = new LinkedList<>();
|
||||
List<String> newTask = new LinkedList<>();
|
||||
List<String> clearTask = new LinkedList<>();
|
||||
List<String> rearrangeTask = new LinkedList<>();
|
||||
List<String> other = new LinkedList<>();
|
||||
allFlags.forEach(flag -> {
|
||||
if (hasSuffix(flag, "BROUGHT_TO_FRONT", "LAUNCHED_FROM_HISTORY")) {
|
||||
nonUser.add(flag);
|
||||
} else if (hasSuffix(flag, "RECENTS", "LAUNCH_ADJACENT", "NO_ANIMATION", "NO_HISTORY",
|
||||
"RETAIN_IN_RECENTS")) {
|
||||
recentsAndUi.add(flag);
|
||||
} else if (hasSuffix(flag, "MULTIPLE_TASK", "NEW_TASK", "NEW_DOCUMENT",
|
||||
"RESET_TASK_IF_NEEDED")) {
|
||||
newTask.add(flag);
|
||||
} else if (hasSuffix(flag, "CLEAR_TASK", "CLEAR_TOP", "CLEAR_WHEN_TASK_RESET")) {
|
||||
clearTask.add(flag);
|
||||
} else if (hasSuffix(flag, "REORDER_TO_FRONT", "SINGLE_TOP", "TASK_ON_HOME")) {
|
||||
rearrangeTask.add(flag);
|
||||
} else {
|
||||
other.add(flag);
|
||||
}
|
||||
});
|
||||
categories.put("Non-user", nonUser);
|
||||
categories.put("Recents and UI", recentsAndUi);
|
||||
categories.put("New Task", newTask);
|
||||
categories.put("Clear Task", clearTask);
|
||||
categories.put("Rearrange Task", rearrangeTask);
|
||||
categories.put("Other", other);
|
||||
return categories;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the target string for any of the listed suffixes.
|
||||
* @param target The string to test for suffixes.
|
||||
* @param suffixes The suffixes to test the string for.
|
||||
* @return True if the target string has any of the suffixes, false if not.
|
||||
*/
|
||||
private static boolean hasSuffix(String target, String... suffixes) {
|
||||
for (String suffix: suffixes) {
|
||||
if (target.endsWith(suffix)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the integer value of an intent flag.
|
||||
* @param flagName The field name of the flag.
|
||||
*/
|
||||
public static int flagValue(String flagName) {
|
||||
try {
|
||||
return sIntentClass.getField(flagName).getInt(sIntent);
|
||||
} catch (Exception e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the passed intent has the specified flag.
|
||||
* @param intent The intent of which to examine the flags.
|
||||
* @param flagName The string name of the intent flag to check for.
|
||||
* @return True if the flag is present, false if not.
|
||||
*/
|
||||
public static boolean hasIntentFlag(Intent intent, String flagName) {
|
||||
return (intent.getFlags() & flagValue(flagName)) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the passed intent has the specified flag.
|
||||
* @param intent The intent of which to examine the flags.
|
||||
* @param flag The corresponding enum {@link IntentFlag} of the intent flag to check for.
|
||||
* @return True if the flag is present, false if not.
|
||||
*/
|
||||
public static boolean hasIntentFlag(Intent intent, IntentFlag flag) {
|
||||
return hasIntentFlag(intent, flag.getName());
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the passed activity has the specified flag set in its manifest.
|
||||
* @param context A context from this application (used to access {@link PackageManager}.
|
||||
* @param activity The activity of which to examine the flags.
|
||||
* @param flag The corresponding enum {@link ActivityFlag} of the activity flag to check for.
|
||||
* @return True if the flag is present, false if not.
|
||||
*/
|
||||
public static boolean hasActivityFlag(Context context, ComponentName activity,
|
||||
ActivityFlag flag) {
|
||||
return getActivityFlags(context, activity).contains(flag);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an {@link EnumSet} of {@link ActivityFlag} corresponding to activity manifest flags
|
||||
* activity on the specified activity.
|
||||
* @param context A context from this application (used to access {@link PackageManager}.
|
||||
* @param activity The activity of which to examine the flags.
|
||||
* @return A set of ActivityFlags corresponding to activity manifest flags.
|
||||
*/
|
||||
public static EnumSet<ActivityFlag> getActivityFlags(Context context, ComponentName activity) {
|
||||
loadActivityInfo(context);
|
||||
EnumSet<ActivityFlag> flags = EnumSet.noneOf(ActivityFlag.class);
|
||||
Optional<ActivityInfo> infoOptional = sActivityInfos.stream()
|
||||
.filter(i-> i.name.equals(activity.getClassName()))
|
||||
.findFirst();
|
||||
if (!infoOptional.isPresent()) {
|
||||
return flags;
|
||||
}
|
||||
ActivityInfo info = infoOptional.get();
|
||||
if ((info.flags & ActivityInfo.FLAG_CLEAR_TASK_ON_LAUNCH) > 0) {
|
||||
flags.add(ActivityFlag.CLEAR_TASK_ON_LAUNCH);
|
||||
}
|
||||
if ((info.flags & ActivityInfo.FLAG_ALLOW_TASK_REPARENTING) > 0) {
|
||||
flags.add(ActivityFlag.ALLOW_TASK_REPARENTING);
|
||||
}
|
||||
switch (info.launchMode) {
|
||||
case ActivityInfo.LAUNCH_SINGLE_INSTANCE:
|
||||
flags.add(ActivityFlag.LAUNCH_MODE_SINGLE_INSTANCE);
|
||||
break;
|
||||
case ActivityInfo.LAUNCH_SINGLE_TASK:
|
||||
flags.add(ActivityFlag.LAUNCH_MODE_SINGLE_TASK);
|
||||
break;
|
||||
case ActivityInfo.LAUNCH_SINGLE_TOP:
|
||||
flags.add(ActivityFlag.LAUNCH_MODE_SINGLE_TOP);
|
||||
break;
|
||||
case ActivityInfo.LAUNCH_MULTIPLE:
|
||||
default:
|
||||
flags.add(ActivityFlag.LAUNCH_MODE_STANDARD);
|
||||
break;
|
||||
}
|
||||
switch(info.documentLaunchMode) {
|
||||
case ActivityInfo.DOCUMENT_LAUNCH_INTO_EXISTING:
|
||||
flags.add(ActivityFlag.DOCUMENT_LAUNCH_MODE_INTO_EXISTING);
|
||||
break;
|
||||
case ActivityInfo.DOCUMENT_LAUNCH_ALWAYS:
|
||||
flags.add(ActivityFlag.DOCUMENT_LAUNCH_MODE_ALWAYS);
|
||||
break;
|
||||
case ActivityInfo.DOCUMENT_LAUNCH_NEVER:
|
||||
flags.add(ActivityFlag.DOCUMENT_LAUNCH_MODE_NEVER);
|
||||
break;
|
||||
case ActivityInfo.DOCUMENT_LAUNCH_NONE:
|
||||
default:
|
||||
flags.add(ActivityFlag.DOCUMENT_LAUNCH_MODE_NONE);
|
||||
break;
|
||||
}
|
||||
return flags;
|
||||
}
|
||||
|
||||
private static void loadActivityInfo(Context context) {
|
||||
if (sActivityInfos == null) {
|
||||
PackageInfo packInfo;
|
||||
|
||||
// Retrieve activities and their manifest flags
|
||||
PackageManager pm = context.getPackageManager();
|
||||
try {
|
||||
packInfo = pm.getPackageInfo(context.getPackageName(),
|
||||
PackageManager.GET_ACTIVITIES);
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
sActivityInfos = Arrays.asList(packInfo.activities);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Discover which flags on the specified {@link ActivityInfo} are enabled,
|
||||
* and return them as a list of strings.
|
||||
* @param activity The activity from which you want to find flags.
|
||||
* @return A list of flags.
|
||||
*/
|
||||
public static List<String> getActivityFlags(ActivityInfo activity) {
|
||||
int flags = activity.flags;
|
||||
List<String> flagStrings = Arrays.stream(activity.getClass().getDeclaredFields())
|
||||
.filter(f -> f.getName().startsWith(ACTIVITY_INFO_FLAG_PREFIX))
|
||||
.filter(f -> {
|
||||
try {
|
||||
return (flags & f.getInt(activity)) > 0;
|
||||
} catch (IllegalAccessException e) {
|
||||
// Should never happen, the fields we are reading are public
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}) // filter fields that are present in intent
|
||||
.map(Field::getName) // map present Fields to their string names
|
||||
.map(name -> camelify(name.substring(ACTIVITY_INFO_FLAG_PREFIX.length())))
|
||||
.map(s -> s.concat("=true"))
|
||||
.collect(Collectors.toList());
|
||||
// check for launchMode
|
||||
if (activity.launchMode != 0) {
|
||||
String lm = "launchMode=";
|
||||
switch(activity.launchMode) {
|
||||
case ActivityInfo.LAUNCH_SINGLE_INSTANCE:
|
||||
lm += "singleInstance";
|
||||
break;
|
||||
case ActivityInfo.LAUNCH_SINGLE_TASK:
|
||||
lm += "singleTask";
|
||||
break;
|
||||
case ActivityInfo.LAUNCH_SINGLE_TOP:
|
||||
lm += "singleTop";
|
||||
break;
|
||||
case ActivityInfo.LAUNCH_MULTIPLE:
|
||||
default:
|
||||
lm += "standard";
|
||||
break;
|
||||
}
|
||||
flagStrings.add(lm);
|
||||
}
|
||||
// check for documentLaunchMode
|
||||
if (activity.documentLaunchMode != 0) {
|
||||
String dlm = "documentLaunchMode=";
|
||||
switch(activity.documentLaunchMode) {
|
||||
case ActivityInfo.DOCUMENT_LAUNCH_INTO_EXISTING:
|
||||
dlm += "intoExisting";
|
||||
break;
|
||||
case ActivityInfo.DOCUMENT_LAUNCH_ALWAYS:
|
||||
dlm += "always";
|
||||
break;
|
||||
case ActivityInfo.DOCUMENT_LAUNCH_NEVER:
|
||||
dlm += "never";
|
||||
break;
|
||||
case ActivityInfo.DOCUMENT_LAUNCH_NONE:
|
||||
default:
|
||||
dlm += "none";
|
||||
break;
|
||||
}
|
||||
flagStrings.add(dlm);
|
||||
}
|
||||
if (activity.taskAffinity != null) {
|
||||
flagStrings.add("taskAffinity="+ activity.taskAffinity);
|
||||
}
|
||||
return flagStrings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a snake_case and converts to CamelCase.
|
||||
* @param snake A snake_case string.
|
||||
* @return A camelified string.
|
||||
*/
|
||||
public static String camelify(String snake) {
|
||||
List<String> words = Arrays.asList(snake.split("_"));
|
||||
StringBuilder output = new StringBuilder(words.get(0).toLowerCase());
|
||||
words.subList(1,words.size()).forEach(s -> {
|
||||
String first = s.substring(0,1).toUpperCase();
|
||||
String rest = s.substring(1).toLowerCase();
|
||||
output.append(first).append(rest);
|
||||
});
|
||||
return output.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the corresponding enum {@link IntentFlag} for the string flag.
|
||||
* @param stringFlag the name of the intent flag.
|
||||
* @return The corresponding IntentFlag.
|
||||
*/
|
||||
public static IntentFlag getFlagForString(String stringFlag) {
|
||||
return getAllIntentFlags().stream().filter(flag -> flag.getName().equals(stringFlag)).findAny()
|
||||
.orElse(null);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user