1 /* 2 * Copyright (C) 2018 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.example.android.intentplayground; 18 19 import android.content.ComponentName; 20 import android.content.Context; 21 import android.content.Intent; 22 import android.content.pm.ActivityInfo; 23 import android.content.pm.PackageInfo; 24 import android.content.pm.PackageManager; 25 26 import java.lang.reflect.Field; 27 import java.util.Arrays; 28 import java.util.EnumSet; 29 import java.util.HashMap; 30 import java.util.LinkedList; 31 import java.util.List; 32 import java.util.Map; 33 import java.util.Optional; 34 import java.util.stream.Collectors; 35 36 /** 37 * Static utility functions to query intent and activity manifest flags. 38 */ 39 class FlagUtils { 40 private static Class<Intent> sIntentClass = Intent.class; 41 private static List<ActivityInfo> sActivityInfos = null; 42 private static Intent sIntent = new Intent(); 43 static final String INTENT_FLAG_PREFIX = "FLAG_ACTIVITY"; 44 private static final String ACTIVITY_INFO_FLAG_PREFIX = "FLAG"; 45 46 /** 47 * Returns a String list of flags active on this intent. 48 * @param intent The intent on which to query flags. 49 * @return A list of flags active on this intent. 50 */ discoverFlags(Intent intent)51 public static List<String> discoverFlags(Intent intent) { 52 int flags = intent.getFlags(); 53 return Arrays.stream(intent.getClass().getDeclaredFields()) // iterate over Intent members 54 .filter(f -> f.getName().startsWith(INTENT_FLAG_PREFIX)) // filter FLAG_ fields 55 .filter(f -> { 56 try { 57 return (flags & f.getInt(intent)) > 0; 58 } catch (IllegalAccessException e) { 59 // Should never happen, the fields we are reading are public 60 throw new RuntimeException(e); 61 } 62 }) // filter fields that are present in intent 63 .map(Field::getName) // map present Fields to their string names 64 .collect(Collectors.toList()); 65 } 66 67 /** 68 * Returns a full list of flags available to be set on an intent. 69 * @return A string list of all intent flags. 70 */ 71 public static List<String> getIntentFlagsAsString() { 72 return Arrays.stream(sIntentClass.getDeclaredFields()) 73 .filter(f -> f.getName().startsWith(INTENT_FLAG_PREFIX)) 74 .map(Field::getName) 75 .collect(Collectors.toList()); 76 } 77 78 /** 79 * Get all defined {@link IntentFlag}s. 80 * @return All defined IntentFlags. 81 */ 82 public static List<IntentFlag> getAllIntentFlags() { 83 return Arrays.asList(IntentFlag.values()); 84 } 85 86 /** 87 * Get intent flags by category/ 88 * @return List of string flags (value) organized by category/function (key). 89 */ 90 public static Map<String, List<String>> intentFlagsByCategory() { 91 Map<String, List<String>> categories = new HashMap<>(); 92 List<String> allFlags = getIntentFlagsAsString(); 93 List<String> nonUser = new LinkedList<>(); 94 List<String> recentsAndUi = new LinkedList<>(); 95 List<String> newTask = new LinkedList<>(); 96 List<String> clearTask = new LinkedList<>(); 97 List<String> rearrangeTask = new LinkedList<>(); 98 List<String> other = new LinkedList<>(); 99 allFlags.forEach(flag -> { 100 if (hasSuffix(flag, "BROUGHT_TO_FRONT", "LAUNCHED_FROM_HISTORY")) { 101 nonUser.add(flag); 102 } else if (hasSuffix(flag, "RECENTS", "LAUNCH_ADJACENT", "NO_ANIMATION", "NO_HISTORY", 103 "RETAIN_IN_RECENTS")) { 104 recentsAndUi.add(flag); 105 } else if (hasSuffix(flag, "MULTIPLE_TASK", "NEW_TASK", "NEW_DOCUMENT", 106 "RESET_TASK_IF_NEEDED")) { 107 newTask.add(flag); 108 } else if (hasSuffix(flag, "CLEAR_TASK", "CLEAR_TOP", "CLEAR_WHEN_TASK_RESET")) { 109 clearTask.add(flag); 110 } else if (hasSuffix(flag, "REORDER_TO_FRONT", "SINGLE_TOP", "TASK_ON_HOME")) { 111 rearrangeTask.add(flag); 112 } else { 113 other.add(flag); 114 } 115 }); 116 categories.put("Non-user", nonUser); 117 categories.put("Recents and UI", recentsAndUi); 118 categories.put("New Task", newTask); 119 categories.put("Clear Task", clearTask); 120 categories.put("Rearrange Task", rearrangeTask); 121 categories.put("Other", other); 122 return categories; 123 } 124 125 /** 126 * Checks the target string for any of the listed suffixes. 127 * @param target The string to test for suffixes. 128 * @param suffixes The suffixes to test the string for. 129 * @return True if the target string has any of the suffixes, false if not. 130 */ 131 private static boolean hasSuffix(String target, String... suffixes) { 132 for (String suffix: suffixes) { 133 if (target.endsWith(suffix)) { 134 return true; 135 } 136 } 137 return false; 138 } 139 140 /** 141 * Gets the integer value of an intent flag. 142 * @param flagName The field name of the flag. 143 */ 144 public static int flagValue(String flagName) { 145 try { 146 return sIntentClass.getField(flagName).getInt(sIntent); 147 } catch (Exception e) { 148 return 0; 149 } 150 } 151 152 /** 153 * Checks if the passed intent has the specified flag. 154 * @param intent The intent of which to examine the flags. 155 * @param flagName The string name of the intent flag to check for. 156 * @return True if the flag is present, false if not. 157 */ 158 public static boolean hasIntentFlag(Intent intent, String flagName) { 159 return (intent.getFlags() & flagValue(flagName)) > 0; 160 } 161 162 /** 163 * Checks if the passed intent has the specified flag. 164 * @param intent The intent of which to examine the flags. 165 * @param flag The corresponding enum {@link IntentFlag} of the intent flag to check for. 166 * @return True if the flag is present, false if not. 167 */ 168 public static boolean hasIntentFlag(Intent intent, IntentFlag flag) { 169 return hasIntentFlag(intent, flag.getName()); 170 } 171 172 /** 173 * Checks if the passed activity has the specified flag set in its manifest. 174 * @param context A context from this application (used to access {@link PackageManager}. 175 * @param activity The activity of which to examine the flags. 176 * @param flag The corresponding enum {@link ActivityFlag} of the activity flag to check for. 177 * @return True if the flag is present, false if not. 178 */ 179 public static boolean hasActivityFlag(Context context, ComponentName activity, 180 ActivityFlag flag) { 181 return getActivityFlags(context, activity).contains(flag); 182 } 183 184 /** 185 * Returns an {@link EnumSet} of {@link ActivityFlag} corresponding to activity manifest flags 186 * activity on the specified activity. 187 * @param context A context from this application (used to access {@link PackageManager}. 188 * @param activity The activity of which to examine the flags. 189 * @return A set of ActivityFlags corresponding to activity manifest flags. 190 */ 191 public static EnumSet<ActivityFlag> getActivityFlags(Context context, ComponentName activity) { 192 loadActivityInfo(context); 193 EnumSet<ActivityFlag> flags = EnumSet.noneOf(ActivityFlag.class); 194 Optional<ActivityInfo> infoOptional = sActivityInfos.stream() 195 .filter(i-> i.name.equals(activity.getClassName())) 196 .findFirst(); 197 if (!infoOptional.isPresent()) { 198 return flags; 199 } 200 ActivityInfo info = infoOptional.get(); 201 if ((info.flags & ActivityInfo.FLAG_CLEAR_TASK_ON_LAUNCH) > 0) { 202 flags.add(ActivityFlag.CLEAR_TASK_ON_LAUNCH); 203 } 204 if ((info.flags & ActivityInfo.FLAG_ALLOW_TASK_REPARENTING) > 0) { 205 flags.add(ActivityFlag.ALLOW_TASK_REPARENTING); 206 } 207 switch (info.launchMode) { 208 case ActivityInfo.LAUNCH_SINGLE_INSTANCE: 209 flags.add(ActivityFlag.LAUNCH_MODE_SINGLE_INSTANCE); 210 break; 211 case ActivityInfo.LAUNCH_SINGLE_TASK: 212 flags.add(ActivityFlag.LAUNCH_MODE_SINGLE_TASK); 213 break; 214 case ActivityInfo.LAUNCH_SINGLE_TOP: 215 flags.add(ActivityFlag.LAUNCH_MODE_SINGLE_TOP); 216 break; 217 case ActivityInfo.LAUNCH_MULTIPLE: 218 default: 219 flags.add(ActivityFlag.LAUNCH_MODE_STANDARD); 220 break; 221 } 222 switch(info.documentLaunchMode) { 223 case ActivityInfo.DOCUMENT_LAUNCH_INTO_EXISTING: 224 flags.add(ActivityFlag.DOCUMENT_LAUNCH_MODE_INTO_EXISTING); 225 break; 226 case ActivityInfo.DOCUMENT_LAUNCH_ALWAYS: 227 flags.add(ActivityFlag.DOCUMENT_LAUNCH_MODE_ALWAYS); 228 break; 229 case ActivityInfo.DOCUMENT_LAUNCH_NEVER: 230 flags.add(ActivityFlag.DOCUMENT_LAUNCH_MODE_NEVER); 231 break; 232 case ActivityInfo.DOCUMENT_LAUNCH_NONE: 233 default: 234 flags.add(ActivityFlag.DOCUMENT_LAUNCH_MODE_NONE); 235 break; 236 } 237 return flags; 238 } 239 240 private static void loadActivityInfo(Context context) { 241 if (sActivityInfos == null) { 242 PackageInfo packInfo; 243 244 // Retrieve activities and their manifest flags 245 PackageManager pm = context.getPackageManager(); 246 try { 247 packInfo = pm.getPackageInfo(context.getPackageName(), 248 PackageManager.GET_ACTIVITIES); 249 } catch (PackageManager.NameNotFoundException e) { 250 throw new RuntimeException(e); 251 } 252 sActivityInfos = Arrays.asList(packInfo.activities); 253 } 254 } 255 256 /** 257 * Discover which flags on the specified {@link ActivityInfo} are enabled, 258 * and return them as a list of strings. 259 * @param activity The activity from which you want to find flags. 260 * @return A list of flags. 261 */ 262 public static List<String> getActivityFlags(ActivityInfo activity) { 263 int flags = activity.flags; 264 List<String> flagStrings = Arrays.stream(activity.getClass().getDeclaredFields()) 265 .filter(f -> f.getName().startsWith(ACTIVITY_INFO_FLAG_PREFIX)) 266 .filter(f -> { 267 try { 268 return (flags & f.getInt(activity)) > 0; 269 } catch (IllegalAccessException e) { 270 // Should never happen, the fields we are reading are public 271 throw new RuntimeException(e); 272 } 273 }) // filter fields that are present in intent 274 .map(Field::getName) // map present Fields to their string names 275 .map(name -> camelify(name.substring(ACTIVITY_INFO_FLAG_PREFIX.length()))) 276 .map(s -> s.concat("=true")) 277 .collect(Collectors.toList()); 278 // check for launchMode 279 if (activity.launchMode != 0) { 280 String lm = "launchMode="; 281 switch(activity.launchMode) { 282 case ActivityInfo.LAUNCH_SINGLE_INSTANCE: 283 lm += "singleInstance"; 284 break; 285 case ActivityInfo.LAUNCH_SINGLE_TASK: 286 lm += "singleTask"; 287 break; 288 case ActivityInfo.LAUNCH_SINGLE_TOP: 289 lm += "singleTop"; 290 break; 291 case ActivityInfo.LAUNCH_SINGLE_INSTANCE_PER_TASK: 292 lm += "singleInstancePerTask"; 293 break; 294 case ActivityInfo.LAUNCH_MULTIPLE: 295 default: 296 lm += "standard"; 297 break; 298 } 299 flagStrings.add(lm); 300 } 301 // check for documentLaunchMode 302 if (activity.documentLaunchMode != 0) { 303 String dlm = "documentLaunchMode="; 304 switch(activity.documentLaunchMode) { 305 case ActivityInfo.DOCUMENT_LAUNCH_INTO_EXISTING: 306 dlm += "intoExisting"; 307 break; 308 case ActivityInfo.DOCUMENT_LAUNCH_ALWAYS: 309 dlm += "always"; 310 break; 311 case ActivityInfo.DOCUMENT_LAUNCH_NEVER: 312 dlm += "never"; 313 break; 314 case ActivityInfo.DOCUMENT_LAUNCH_NONE: 315 default: 316 dlm += "none"; 317 break; 318 } 319 flagStrings.add(dlm); 320 } 321 if (activity.taskAffinity != null) { 322 flagStrings.add("taskAffinity="+ activity.taskAffinity); 323 } 324 return flagStrings; 325 } 326 327 /** 328 * Takes a snake_case and converts to CamelCase. 329 * @param snake A snake_case string. 330 * @return A camelified string. 331 */ 332 public static String camelify(String snake) { 333 List<String> words = Arrays.asList(snake.split("_")); 334 StringBuilder output = new StringBuilder(words.get(0).toLowerCase()); 335 words.subList(1,words.size()).forEach(s -> { 336 String first = s.substring(0,1).toUpperCase(); 337 String rest = s.substring(1).toLowerCase(); 338 output.append(first).append(rest); 339 }); 340 return output.toString(); 341 } 342 343 /** 344 * Retrieves the corresponding enum {@link IntentFlag} for the string flag. 345 * @param stringFlag the name of the intent flag. 346 * @return The corresponding IntentFlag. 347 */ 348 public static IntentFlag getFlagForString(String stringFlag) { 349 return getAllIntentFlags().stream().filter(flag -> flag.getName().equals(stringFlag)).findAny() 350 .orElse(null); 351 } 352 } 353 354