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