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.wm;
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.wm.app.Components.TEST_ACTIVITY;
24 
25 import android.app.ActivityManager;
26 import android.app.ActivityOptions;
27 import android.app.PendingIntent;
28 import android.content.ComponentName;
29 import android.content.Context;
30 import android.content.Intent;
31 import android.net.Uri;
32 import android.os.Bundle;
33 import android.server.wm.CommandSession.LaunchInjector;
34 import android.server.wm.TestJournalProvider.TestJournalContainer;
35 import android.text.TextUtils;
36 import android.util.Log;
37 
38 /** Utility class which contains common code for launching activities. */
39 public class ActivityLauncher {
40     public static final String TAG = ActivityLauncher.class.getSimpleName();
41 
42     /** Key for boolean extra, indicates whether it should launch an activity. */
43     public static final String KEY_LAUNCH_ACTIVITY = "launch_activity";
44     /**
45      * Key for boolean extra, indicates whether it the activity should be launched to side in
46      * split-screen.
47      */
48     public static final String KEY_LAUNCH_TO_SIDE = "launch_to_the_side";
49     /**
50      * Key for boolean extra, indicates if launch intent should include random data to be different
51      * from other launch intents.
52      */
53     public static final String KEY_RANDOM_DATA = "random_data";
54     /**
55      * Key for boolean extra, indicates if launch intent should have
56      * {@link Intent#FLAG_ACTIVITY_NEW_TASK}.
57      */
58     public static final String KEY_NEW_TASK = "new_task";
59     /**
60      * Key for boolean extra, indicates if launch intent should have
61      * {@link Intent#FLAG_ACTIVITY_MULTIPLE_TASK}.
62      */
63     public static final String KEY_MULTIPLE_TASK = "multiple_task";
64     /**
65      * Key for boolean extra, indicates if launch intent should have
66      * {@link Intent#FLAG_ACTIVITY_REORDER_TO_FRONT}.
67      */
68     public static final String KEY_REORDER_TO_FRONT = "reorder_to_front";
69     /**
70      * Key for string extra with string representation of target component.
71      */
72     public static final String KEY_TARGET_COMPONENT = "target_component";
73     /**
74      * Key for int extra with target display id where the activity should be launched. Adding this
75      * automatically applies {@link Intent#FLAG_ACTIVITY_NEW_TASK} and
76      * {@link Intent#FLAG_ACTIVITY_MULTIPLE_TASK} to the intent.
77      */
78     public static final String KEY_DISPLAY_ID = "display_id";
79     /**
80      * Key for boolean extra, indicates if launch should be done from application context of the one
81      * passed in {@link #launchActivityFromExtras(Context, Bundle)}.
82      */
83     public static final String KEY_USE_APPLICATION_CONTEXT = "use_application_context";
84     /**
85      * Key for boolean extra, indicates if instrumentation context will be used for launch. This
86      * means that {@link PendingIntent} should be used instead of a regular one, because application
87      * switch will not be allowed otherwise.
88      */
89     public static final String KEY_USE_INSTRUMENTATION = "use_instrumentation";
90     /**
91      * Key for boolean extra, indicates if any exceptions thrown during launch other then
92      * {@link SecurityException} should be suppressed. A {@link SecurityException} is never thrown,
93      * it's always written to logs.
94      */
95     public static final String KEY_SUPPRESS_EXCEPTIONS = "suppress_exceptions";
96     /**
97      * Key for boolean extra, indicates the result of
98      * {@link ActivityManager#isActivityStartAllowedOnDisplay(Context, int, Intent)}
99      */
100     public static final String KEY_IS_ACTIVITY_START_ALLOWED_ON_DISPLAY =
101             "is_activity_start_allowed_on_display";
102     /**
103      * Key for boolean extra, indicates a security exception is caught when launching activity by
104      * {@link #launchActivityFromExtras}.
105      */
106     private static final String KEY_CAUGHT_SECURITY_EXCEPTION = "caught_security_exception";
107     /**
108      * Key for int extra with target activity type where activity should be launched as.
109      */
110     public static final String KEY_ACTIVITY_TYPE = "activity_type";
111     /**
112      * Key for int extra with intent flags which are used for launching an activity.
113      */
114     public static final String KEY_INTENT_FLAGS = "intent_flags";
115     /**
116      * Key for boolean extra, indicates if need to automatically applies
117      * {@link Intent#FLAG_ACTIVITY_NEW_TASK} and {@link Intent#FLAG_ACTIVITY_MULTIPLE_TASK} to
118      * the intent when target display id set.
119      */
120     public static final String KEY_MULTIPLE_INSTANCES = "multiple_instances";
121 
122     /**
123      * Key for bundle extra to the intent which are used for launching an activity.
124      */
125     public static final String KEY_INTENT_EXTRAS = "intent_extras";
126 
127 
128     /** Perform an activity launch configured by provided extras. */
launchActivityFromExtras(final Context context, Bundle extras)129     public static void launchActivityFromExtras(final Context context, Bundle extras) {
130         launchActivityFromExtras(context, extras, null /* launchInjector */);
131     }
132 
launchActivityFromExtras(final Context context, Bundle extras, LaunchInjector launchInjector)133     public static void launchActivityFromExtras(final Context context, Bundle extras,
134             LaunchInjector launchInjector) {
135         if (extras == null || !extras.getBoolean(KEY_LAUNCH_ACTIVITY)) {
136             return;
137         }
138 
139         Log.i(TAG, "launchActivityFromExtras: extras=" + extras);
140 
141         final String targetComponent = extras.getString(KEY_TARGET_COMPONENT);
142         final Intent newIntent = new Intent().setComponent(TextUtils.isEmpty(targetComponent)
143                 ? TEST_ACTIVITY : ComponentName.unflattenFromString(targetComponent));
144 
145         if (extras.getBoolean(KEY_LAUNCH_TO_SIDE)) {
146             newIntent.addFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_LAUNCH_ADJACENT);
147             if (extras.getBoolean(KEY_RANDOM_DATA)) {
148                 final Uri data = new Uri.Builder()
149                         .path(String.valueOf(System.currentTimeMillis()))
150                         .build();
151                 newIntent.setData(data);
152             }
153         }
154         if (extras.getBoolean(KEY_MULTIPLE_TASK)) {
155             newIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
156         }
157         if (extras.getBoolean(KEY_NEW_TASK)) {
158             newIntent.addFlags(FLAG_ACTIVITY_NEW_TASK);
159         }
160 
161         if (extras.getBoolean(KEY_REORDER_TO_FRONT)) {
162             newIntent.addFlags(FLAG_ACTIVITY_REORDER_TO_FRONT);
163         }
164 
165         final Bundle intentExtras = extras.getBundle(KEY_INTENT_EXTRAS) ;
166         if (intentExtras != null) {
167             newIntent.putExtras(intentExtras);
168         }
169 
170         ActivityOptions options = null;
171         final int displayId = extras.getInt(KEY_DISPLAY_ID, -1);
172         if (displayId != -1) {
173             options = ActivityOptions.makeBasic();
174             options.setLaunchDisplayId(displayId);
175             if (extras.getBoolean(KEY_MULTIPLE_INSTANCES, true)) {
176                 newIntent.addFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_MULTIPLE_TASK);
177             }
178         }
179         if (launchInjector != null) {
180             launchInjector.setupIntent(newIntent);
181         }
182         final int activityType = extras.getInt(KEY_ACTIVITY_TYPE, -1);
183         if (activityType != -1) {
184             if (options == null) {
185                 options = ActivityOptions.makeBasic();
186             }
187             options.setLaunchActivityType(activityType);
188         }
189         final int intentFlags = extras.getInt(KEY_INTENT_FLAGS); // 0 if key doesn't exist.
190         if (intentFlags != 0) {
191             newIntent.addFlags(intentFlags);
192         }
193         final Bundle optionsBundle = options != null ? options.toBundle() : null;
194 
195         final Context launchContext = extras.getBoolean(KEY_USE_APPLICATION_CONTEXT) ?
196                 context.getApplicationContext() : context;
197 
198         try {
199             if (extras.getBoolean(KEY_USE_INSTRUMENTATION)) {
200                 // Using PendingIntent for Instrumentation launches, because otherwise we won't
201                 // be allowed to switch the current activity with ours with different uid.
202                 // android.permission.STOP_APP_SWITCHES is needed to do this directly.
203                 // PendingIntent.FLAG_CANCEL_CURRENT is needed here, or we may get an existing
204                 // PendingIntent if it is same kind of PendingIntent request to previous one.
205                 // Note: optionsBundle is not taking into account for PendingIntentRecord.Key
206                 // hashcode calculation.
207                 final PendingIntent pendingIntent = PendingIntent.getActivity(launchContext, 0,
208                         newIntent, PendingIntent.FLAG_CANCEL_CURRENT, optionsBundle);
209                 pendingIntent.send();
210             } else {
211                 launchContext.startActivity(newIntent, optionsBundle);
212             }
213         } catch (SecurityException e) {
214             handleSecurityException(context, e);
215         } catch (PendingIntent.CanceledException e) {
216             if (extras.getBoolean(KEY_SUPPRESS_EXCEPTIONS)) {
217                 Log.e(TAG, "Exception launching activity with pending intent");
218             } else {
219                 throw new RuntimeException(e);
220             }
221             // Bypass the exception although it is not SecurityException.
222             handleSecurityException(context, e);
223         } catch (Exception e) {
224             if (extras.getBoolean(KEY_SUPPRESS_EXCEPTIONS)) {
225                 Log.e(TAG, "Exception launching activity");
226             } else {
227                 throw e;
228             }
229         }
230     }
231 
checkActivityStartOnDisplay(Context context, int displayId, ComponentName componentName)232     public static void checkActivityStartOnDisplay(Context context, int displayId,
233             ComponentName componentName) {
234         final Intent launchIntent = new Intent(Intent.ACTION_VIEW).setComponent(componentName);
235 
236         final boolean isAllowed = context.getSystemService(ActivityManager.class)
237                 .isActivityStartAllowedOnDisplay(context, displayId, launchIntent);
238         Log.i(TAG, "isActivityStartAllowedOnDisplay=" + isAllowed);
239         TestJournalProvider.putExtras(context, TAG, bundle -> {
240             bundle.putBoolean(KEY_IS_ACTIVITY_START_ALLOWED_ON_DISPLAY, isAllowed);
241         });
242     }
243 
handleSecurityException(Context context, Exception e)244     public static void handleSecurityException(Context context, Exception e) {
245         Log.e(TAG, "SecurityException launching activity: " + e);
246         TestJournalProvider.putExtras(context, TAG, bundle -> {
247             bundle.putBoolean(KEY_CAUGHT_SECURITY_EXCEPTION, true);
248         });
249     }
250 
hasCaughtSecurityException()251     static boolean hasCaughtSecurityException() {
252         return TestJournalContainer.get(TAG).extras.containsKey(KEY_CAUGHT_SECURITY_EXCEPTION);
253     }
254 }
255