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 android.server.am;
18 
19 import static android.content.Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT;
20 import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK;
21 import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
22 import static android.content.Intent.FLAG_ACTIVITY_REORDER_TO_FRONT;
23 import static android.server.am.Components.TEST_ACTIVITY;
24 
25 import android.app.ActivityOptions;
26 import android.app.PendingIntent;
27 import android.content.ComponentName;
28 import android.content.Context;
29 import android.content.Intent;
30 import android.net.Uri;
31 import android.os.Bundle;
32 import android.text.TextUtils;
33 import android.util.Log;
34 
35 /** Utility class which contains common code for launching activities. */
36 public class ActivityLauncher {
37     private static final String TAG = ActivityLauncher.class.getSimpleName();
38 
39     /** Key for boolean extra, indicates whether it should launch an activity. */
40     public static final String KEY_LAUNCH_ACTIVITY = "launch_activity";
41     /**
42      * Key for boolean extra, indicates whether it the activity should be launched to side in
43      * split-screen.
44      */
45     public static final String KEY_LAUNCH_TO_SIDE = "launch_to_the_side";
46     /**
47      * Key for boolean extra, indicates if launch intent should include random data to be different
48      * from other launch intents.
49      */
50     public static final String KEY_RANDOM_DATA = "random_data";
51     /**
52      * Key for boolean extra, indicates if launch intent should have
53      * {@link Intent#FLAG_ACTIVITY_NEW_TASK}.
54      */
55     public static final String KEY_NEW_TASK = "new_task";
56     /**
57      * Key for boolean extra, indicates if launch intent should have
58      * {@link Intent#FLAG_ACTIVITY_MULTIPLE_TASK}.
59      */
60     public static final String KEY_MULTIPLE_TASK = "multiple_task";
61     /**
62      * Key for boolean extra, indicates if launch intent should have
63      * {@link Intent#FLAG_ACTIVITY_REORDER_TO_FRONT}.
64      */
65     public static final String KEY_REORDER_TO_FRONT = "reorder_to_front";
66     /**
67      * Key for string extra with string representation of target component.
68      */
69     public static final String KEY_TARGET_COMPONENT = "target_component";
70     /**
71      * Key for int extra with target display id where the activity should be launched. Adding this
72      * automatically applies {@link Intent#FLAG_ACTIVITY_NEW_TASK} and
73      * {@link Intent#FLAG_ACTIVITY_MULTIPLE_TASK} to the intent.
74      */
75     public static final String KEY_DISPLAY_ID = "display_id";
76     /**
77      * Key for boolean extra, indicates if launch should be done from application context of the one
78      * passed in {@link #launchActivityFromExtras(Context, Bundle)}.
79      */
80     public static final String KEY_USE_APPLICATION_CONTEXT = "use_application_context";
81     /**
82      * Key for boolean extra, indicates if instrumentation context will be used for launch. This
83      * means that {@link PendingIntent} should be used instead of a regular one, because application
84      * switch will not be allowed otherwise.
85      */
86     public static final String KEY_USE_INSTRUMENTATION = "use_instrumentation";
87     /**
88      * Key for boolean extra, indicates if any exceptions thrown during launch other then
89      * {@link SecurityException} should be suppressed. A {@link SecurityException} is never thrown,
90      * it's always written to logs.
91      */
92     public static final String KEY_SUPPRESS_EXCEPTIONS = "suppress_exceptions";
93 
94 
95     /** Perform an activity launch configured by provided extras. */
launchActivityFromExtras(final Context context, Bundle extras)96     public static void launchActivityFromExtras(final Context context, Bundle extras) {
97         if (extras == null || !extras.getBoolean(KEY_LAUNCH_ACTIVITY)) {
98             return;
99         }
100 
101         Log.i(TAG, "launchActivityFromExtras: extras=" + extras);
102 
103         final String targetComponent = extras.getString(KEY_TARGET_COMPONENT);
104         final Intent newIntent = new Intent().setComponent(TextUtils.isEmpty(targetComponent)
105                 ? TEST_ACTIVITY : ComponentName.unflattenFromString(targetComponent));
106 
107         if (extras.getBoolean(KEY_LAUNCH_TO_SIDE)) {
108             newIntent.addFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_LAUNCH_ADJACENT);
109             if (extras.getBoolean(KEY_RANDOM_DATA)) {
110                 final Uri data = new Uri.Builder()
111                         .path(String.valueOf(System.currentTimeMillis()))
112                         .build();
113                 newIntent.setData(data);
114             }
115         }
116         if (extras.getBoolean(KEY_MULTIPLE_TASK)) {
117             newIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
118         }
119         if (extras.getBoolean(KEY_NEW_TASK)) {
120             newIntent.addFlags(FLAG_ACTIVITY_NEW_TASK);
121         }
122 
123         if (extras.getBoolean(KEY_REORDER_TO_FRONT)) {
124             newIntent.addFlags(FLAG_ACTIVITY_REORDER_TO_FRONT);
125         }
126 
127         ActivityOptions options = null;
128         final int displayId = extras.getInt(KEY_DISPLAY_ID, -1);
129         if (displayId != -1) {
130             options = ActivityOptions.makeBasic();
131             options.setLaunchDisplayId(displayId);
132             newIntent.addFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_MULTIPLE_TASK);
133         }
134         final Bundle optionsBundle = options != null ? options.toBundle() : null;
135 
136         final Context launchContext = extras.getBoolean(KEY_USE_APPLICATION_CONTEXT) ?
137                 context.getApplicationContext() : context;
138 
139         try {
140             if (extras.getBoolean(KEY_USE_INSTRUMENTATION)) {
141                 // Using PendingIntent for Instrumentation launches, because otherwise we won't
142                 // be allowed to switch the current activity with ours with different uid.
143                 // android.permission.STOP_APP_SWITCHES is needed to do this directly.
144                 final PendingIntent pendingIntent = PendingIntent.getActivity(launchContext, 0,
145                         newIntent, 0, optionsBundle);
146                 pendingIntent.send();
147             } else {
148                 launchContext.startActivity(newIntent, optionsBundle);
149             }
150         } catch (SecurityException e) {
151             Log.e(TAG, "SecurityException launching activity");
152         } catch (PendingIntent.CanceledException e) {
153             if (extras.getBoolean(KEY_SUPPRESS_EXCEPTIONS)) {
154                 Log.e(TAG, "Exception launching activity with pending intent");
155             } else {
156                 throw new RuntimeException(e);
157             }
158             Log.e(TAG, "SecurityException launching activity");
159         } catch (Exception e) {
160             if (extras.getBoolean(KEY_SUPPRESS_EXCEPTIONS)) {
161                 Log.e(TAG, "Exception launching activity");
162             } else {
163                 throw e;
164             }
165         }
166     }
167 }
168