Liam Clark | bd43fbe | 2018-11-09 17:40:35 -0800 | [diff] [blame] | 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 | */ |
| 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_MULTIPLE: |
| 292 | default: |
| 293 | lm += "standard"; |
| 294 | break; |
| 295 | } |
| 296 | flagStrings.add(lm); |
| 297 | } |
| 298 | // check for documentLaunchMode |
| 299 | if (activity.documentLaunchMode != 0) { |
| 300 | String dlm = "documentLaunchMode="; |
| 301 | switch(activity.documentLaunchMode) { |
| 302 | case ActivityInfo.DOCUMENT_LAUNCH_INTO_EXISTING: |
| 303 | dlm += "intoExisting"; |
| 304 | break; |
| 305 | case ActivityInfo.DOCUMENT_LAUNCH_ALWAYS: |
| 306 | dlm += "always"; |
| 307 | break; |
| 308 | case ActivityInfo.DOCUMENT_LAUNCH_NEVER: |
| 309 | dlm += "never"; |
| 310 | break; |
| 311 | case ActivityInfo.DOCUMENT_LAUNCH_NONE: |
| 312 | default: |
| 313 | dlm += "none"; |
| 314 | break; |
| 315 | } |
| 316 | flagStrings.add(dlm); |
| 317 | } |
| 318 | if (activity.taskAffinity != null) { |
| 319 | flagStrings.add("taskAffinity="+ activity.taskAffinity); |
| 320 | } |
| 321 | return flagStrings; |
| 322 | } |
| 323 | |
| 324 | /** |
| 325 | * Takes a snake_case and converts to CamelCase. |
| 326 | * @param snake A snake_case string. |
| 327 | * @return A camelified string. |
| 328 | */ |
| 329 | public static String camelify(String snake) { |
| 330 | List<String> words = Arrays.asList(snake.split("_")); |
| 331 | StringBuilder output = new StringBuilder(words.get(0).toLowerCase()); |
| 332 | words.subList(1,words.size()).forEach(s -> { |
| 333 | String first = s.substring(0,1).toUpperCase(); |
| 334 | String rest = s.substring(1).toLowerCase(); |
| 335 | output.append(first).append(rest); |
| 336 | }); |
| 337 | return output.toString(); |
| 338 | } |
| 339 | |
| 340 | /** |
| 341 | * Retrieves the corresponding enum {@link IntentFlag} for the string flag. |
| 342 | * @param stringFlag the name of the intent flag. |
| 343 | * @return The corresponding IntentFlag. |
| 344 | */ |
| 345 | public static IntentFlag getFlagForString(String stringFlag) { |
| 346 | return getAllIntentFlags().stream().filter(flag -> flag.getName().equals(stringFlag)).findAny() |
| 347 | .orElse(null); |
| 348 | } |
| 349 | } |
| 350 | |