blob: fdad2f62422d48f4d391ecd25278c5b7c9548d36 [file] [log] [blame]
Liam Clarkbd43fbe2018-11-09 17:40:35 -08001/*
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
17package com.example.android.intentplayground;
18
19import android.content.ComponentName;
20import android.content.Context;
21import android.content.Intent;
22import android.content.pm.ActivityInfo;
23import android.content.pm.PackageInfo;
24import android.content.pm.PackageManager;
25
26import java.lang.reflect.Field;
27import java.util.Arrays;
28import java.util.EnumSet;
29import java.util.HashMap;
30import java.util.LinkedList;
31import java.util.List;
32import java.util.Map;
33import java.util.Optional;
34import java.util.stream.Collectors;
35
36/**
37 * Static utility functions to query intent and activity manifest flags.
38 */
39class 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