/* * 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 sIntentClass = Intent.class; private static List 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 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 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 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> intentFlagsByCategory() { Map> categories = new HashMap<>(); List allFlags = getIntentFlagsAsString(); List nonUser = new LinkedList<>(); List recentsAndUi = new LinkedList<>(); List newTask = new LinkedList<>(); List clearTask = new LinkedList<>(); List rearrangeTask = new LinkedList<>(); List 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 getActivityFlags(Context context, ComponentName activity) { loadActivityInfo(context); EnumSet flags = EnumSet.noneOf(ActivityFlag.class); Optional 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 getActivityFlags(ActivityInfo activity) { int flags = activity.flags; List 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 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); } }