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.android.internal.app;
18 
19 import static android.app.admin.flags.Flags.crossUserSuspensionEnabledRo;
20 import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_AWARE;
21 import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_UNAWARE;
22 import static android.content.pm.SuspendDialogInfo.BUTTON_ACTION_MORE_DETAILS;
23 import static android.content.pm.SuspendDialogInfo.BUTTON_ACTION_UNSUSPEND;
24 import static android.content.res.Resources.ID_NULL;
25 
26 import android.Manifest;
27 import android.annotation.Nullable;
28 import android.app.ActivityOptions;
29 import android.app.AlertDialog;
30 import android.app.AppGlobals;
31 import android.app.KeyguardManager;
32 import android.app.usage.UsageStatsManager;
33 import android.content.BroadcastReceiver;
34 import android.content.Context;
35 import android.content.DialogInterface;
36 import android.content.Intent;
37 import android.content.IntentFilter;
38 import android.content.IntentSender;
39 import android.content.pm.IPackageManager;
40 import android.content.pm.PackageManager;
41 import android.content.pm.ResolveInfo;
42 import android.content.pm.SuspendDialogInfo;
43 import android.content.pm.UserPackage;
44 import android.content.res.Resources;
45 import android.graphics.drawable.Drawable;
46 import android.os.Bundle;
47 import android.os.RemoteException;
48 import android.os.UserHandle;
49 import android.util.Slog;
50 import android.view.WindowManager;
51 
52 import com.android.internal.R;
53 import com.android.internal.util.ArrayUtils;
54 
55 public class SuspendedAppActivity extends AlertActivity
56         implements DialogInterface.OnClickListener {
57     private static final String TAG = SuspendedAppActivity.class.getSimpleName();
58     private static final String PACKAGE_NAME = "com.android.internal.app";
59 
60     public static final String EXTRA_SUSPENDED_PACKAGE = PACKAGE_NAME + ".extra.SUSPENDED_PACKAGE";
61     public static final String EXTRA_SUSPENDING_PACKAGE =
62             PACKAGE_NAME + ".extra.SUSPENDING_PACKAGE";
63     public static final String EXTRA_SUSPENDING_USER = PACKAGE_NAME + ".extra.SUSPENDING_USER";
64     public static final String EXTRA_DIALOG_INFO = PACKAGE_NAME + ".extra.DIALOG_INFO";
65     public static final String EXTRA_ACTIVITY_OPTIONS = PACKAGE_NAME + ".extra.ACTIVITY_OPTIONS";
66     public static final String EXTRA_UNSUSPEND_INTENT = PACKAGE_NAME + ".extra.UNSUSPEND_INTENT";
67 
68     private Intent mMoreDetailsIntent;
69     private IntentSender mOnUnsuspend;
70     private String mSuspendedPackage;
71     private String mSuspendingPackage;
72     private int mSuspendingUserId;
73     private int mNeutralButtonAction;
74     private int mUserId;
75     private PackageManager mPm;
76     private UsageStatsManager mUsm;
77     private Resources mSuspendingAppResources;
78     private SuspendDialogInfo mSuppliedDialogInfo;
79     private Bundle mOptions;
80     private BroadcastReceiver mSuspendModifiedReceiver = new BroadcastReceiver() {
81         @Override
82         public void onReceive(Context context, Intent intent) {
83             if (Intent.ACTION_PACKAGES_SUSPENSION_CHANGED.equals(intent.getAction())) {
84                 // Suspension conditions were modified, dismiss any related visible dialogs.
85                 final String[] modified = intent.getStringArrayExtra(
86                         Intent.EXTRA_CHANGED_PACKAGE_LIST);
87                 if (ArrayUtils.contains(modified, mSuspendedPackage)
88                         && !isPackageSuspended(mSuspendedPackage)) {
89                     if (!isFinishing()) {
90                         Slog.w(TAG, "Package " + mSuspendedPackage + " has modified"
91                                 + " suspension conditions while dialog was visible. Finishing.");
92                         SuspendedAppActivity.this.finish();
93                         // TODO (b/198201994): reload the suspend dialog to show most relevant info
94                     }
95                 }
96             }
97         }
98     };
99 
isPackageSuspended(String packageName)100     private boolean isPackageSuspended(String packageName) {
101         try {
102             return mPm.isPackageSuspended(packageName);
103         } catch (PackageManager.NameNotFoundException ne) {
104             Slog.e(TAG, "Package " + packageName + " not found", ne);
105         }
106         return false;
107     }
108 
getAppLabel(String packageName)109     private CharSequence getAppLabel(String packageName) {
110         try {
111             return mPm.getApplicationInfoAsUser(packageName, 0, mUserId).loadLabel(mPm);
112         } catch (PackageManager.NameNotFoundException ne) {
113             Slog.e(TAG, "Package " + packageName + " not found", ne);
114         }
115         return packageName;
116     }
117 
getMoreDetailsActivity()118     private Intent getMoreDetailsActivity() {
119         final Intent moreDetailsIntent = new Intent(Intent.ACTION_SHOW_SUSPENDED_APP_DETAILS)
120                 .setPackage(mSuspendingPackage);
121         final String requiredPermission = Manifest.permission.SEND_SHOW_SUSPENDED_APP_DETAILS;
122         final ResolveInfo resolvedInfo = mPm.resolveActivityAsUser(moreDetailsIntent,
123                 MATCH_DIRECT_BOOT_UNAWARE | MATCH_DIRECT_BOOT_AWARE, mSuspendingUserId);
124         if (resolvedInfo != null && resolvedInfo.activityInfo != null
125                 && requiredPermission.equals(resolvedInfo.activityInfo.permission)) {
126             moreDetailsIntent.putExtra(Intent.EXTRA_PACKAGE_NAME, mSuspendedPackage)
127                     .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
128             return moreDetailsIntent;
129         }
130         return null;
131     }
132 
resolveIcon()133     private Drawable resolveIcon() {
134         final int iconId = (mSuppliedDialogInfo != null) ? mSuppliedDialogInfo.getIconResId()
135                 : ID_NULL;
136         if (iconId != ID_NULL && mSuspendingAppResources != null) {
137             try {
138                 return mSuspendingAppResources.getDrawable(iconId, getTheme());
139             } catch (Resources.NotFoundException nfe) {
140                 Slog.e(TAG, "Could not resolve drawable resource id " + iconId);
141             }
142         }
143         return null;
144     }
145 
resolveTitle()146     private String resolveTitle() {
147         if (mSuppliedDialogInfo != null) {
148             final int titleId = mSuppliedDialogInfo.getTitleResId();
149             final String title = mSuppliedDialogInfo.getTitle();
150             if (titleId != ID_NULL && mSuspendingAppResources != null) {
151                 try {
152                     return mSuspendingAppResources.getString(titleId);
153                 } catch (Resources.NotFoundException nfe) {
154                     Slog.e(TAG, "Could not resolve string resource id " + titleId);
155                 }
156             } else if (title != null) {
157                 return title;
158             }
159         }
160         return getString(R.string.app_suspended_title);
161     }
162 
resolveDialogMessage()163     private String resolveDialogMessage() {
164         final CharSequence suspendedAppLabel = getAppLabel(mSuspendedPackage);
165         if (mSuppliedDialogInfo != null) {
166             final int messageId = mSuppliedDialogInfo.getDialogMessageResId();
167             final String message = mSuppliedDialogInfo.getDialogMessage();
168             if (messageId != ID_NULL && mSuspendingAppResources != null) {
169                 try {
170                     return mSuspendingAppResources.getString(messageId, suspendedAppLabel);
171                 } catch (Resources.NotFoundException nfe) {
172                     Slog.e(TAG, "Could not resolve string resource id " + messageId);
173                 }
174             } else if (message != null) {
175                 return String.format(getResources().getConfiguration().getLocales().get(0), message,
176                         suspendedAppLabel);
177             }
178         }
179         return getString(R.string.app_suspended_default_message, suspendedAppLabel,
180                 getAppLabel(mSuspendingPackage));
181     }
182 
183     /**
184      * Returns a text to be displayed on the neutral button or {@code null} if the button should
185      * not be shown.
186      */
187     @Nullable
resolveNeutralButtonText()188     private String resolveNeutralButtonText() {
189         final int defaultButtonTextId;
190         switch (mNeutralButtonAction) {
191             case BUTTON_ACTION_MORE_DETAILS:
192                 if (mMoreDetailsIntent == null) {
193                     return null;
194                 }
195                 defaultButtonTextId = R.string.app_suspended_more_details;
196                 break;
197             case BUTTON_ACTION_UNSUSPEND:
198                 defaultButtonTextId = R.string.app_suspended_unsuspend_message;
199                 break;
200             default:
201                 Slog.w(TAG, "Unknown neutral button action: " + mNeutralButtonAction);
202                 return null;
203         }
204         if (mSuppliedDialogInfo != null) {
205             final int buttonTextId = mSuppliedDialogInfo.getNeutralButtonTextResId();
206             final String buttonText = mSuppliedDialogInfo.getNeutralButtonText();
207             if (buttonTextId != ID_NULL && mSuspendingAppResources != null) {
208                 try {
209                     return mSuspendingAppResources.getString(buttonTextId);
210                 } catch (Resources.NotFoundException nfe) {
211                     Slog.e(TAG, "Could not resolve string resource id " + buttonTextId);
212                 }
213             } else if (buttonText != null) {
214                 return buttonText;
215             }
216         }
217         return getString(defaultButtonTextId);
218     }
219 
220     @Override
onCreate(Bundle icicle)221     public void onCreate(Bundle icicle) {
222         super.onCreate(icicle);
223         mPm = getPackageManager();
224         mUsm = getSystemService(UsageStatsManager.class);
225         getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_DIALOG);
226 
227         final Intent intent = getIntent();
228         mOptions = intent.getBundleExtra(EXTRA_ACTIVITY_OPTIONS);
229         mUserId = intent.getIntExtra(Intent.EXTRA_USER_ID, -1);
230         if (mUserId < 0) {
231             Slog.wtf(TAG, "Invalid user: " + mUserId);
232             finish();
233             return;
234         }
235         mSuspendedPackage = intent.getStringExtra(EXTRA_SUSPENDED_PACKAGE);
236         mSuspendingPackage = intent.getStringExtra(EXTRA_SUSPENDING_PACKAGE);
237         if (crossUserSuspensionEnabledRo()) {
238             mSuspendingUserId = intent.getIntExtra(EXTRA_SUSPENDING_USER, mUserId);
239         } else {
240             mSuspendingUserId = mUserId;
241         }
242         mSuppliedDialogInfo = intent.getParcelableExtra(EXTRA_DIALOG_INFO, android.content.pm.SuspendDialogInfo.class);
243         mOnUnsuspend = intent.getParcelableExtra(EXTRA_UNSUSPEND_INTENT, android.content.IntentSender.class);
244         if (mSuppliedDialogInfo != null) {
245             try {
246                 mSuspendingAppResources = createContextAsUser(
247                         UserHandle.of(mSuspendingUserId), /* flags */ 0).getPackageManager()
248                         .getResourcesForApplication(mSuspendingPackage);
249             } catch (PackageManager.NameNotFoundException ne) {
250                 Slog.e(TAG, "Could not find resources for " + mSuspendingPackage, ne);
251             }
252         }
253         mNeutralButtonAction = (mSuppliedDialogInfo != null)
254                 ? mSuppliedDialogInfo.getNeutralButtonAction() : BUTTON_ACTION_MORE_DETAILS;
255         mMoreDetailsIntent = (mNeutralButtonAction == BUTTON_ACTION_MORE_DETAILS)
256                 ? getMoreDetailsActivity() : null;
257 
258         final AlertController.AlertParams ap = mAlertParams;
259         ap.mIcon = resolveIcon();
260         ap.mTitle = resolveTitle();
261         ap.mMessage = resolveDialogMessage();
262         ap.mPositiveButtonText = getString(android.R.string.ok);
263         ap.mNeutralButtonText = resolveNeutralButtonText();
264         ap.mPositiveButtonListener = ap.mNeutralButtonListener = this;
265 
266         requestDismissKeyguardIfNeeded(ap.mMessage);
267 
268         setupAlert();
269 
270         final IntentFilter suspendModifiedFilter =
271                 new IntentFilter(Intent.ACTION_PACKAGES_SUSPENSION_CHANGED);
272         registerReceiverAsUser(mSuspendModifiedReceiver, UserHandle.of(mUserId),
273                 suspendModifiedFilter, null, null);
274     }
275 
276     @Override
onDestroy()277     protected void onDestroy() {
278         super.onDestroy();
279         unregisterReceiver(mSuspendModifiedReceiver);
280     }
281 
requestDismissKeyguardIfNeeded(CharSequence dismissMessage)282     private void requestDismissKeyguardIfNeeded(CharSequence dismissMessage) {
283         final KeyguardManager km = getSystemService(KeyguardManager.class);
284         if (km.isKeyguardLocked()) {
285             km.requestDismissKeyguard(this, dismissMessage,
286                     new KeyguardManager.KeyguardDismissCallback() {
287                         @Override
288                         public void onDismissError() {
289                             Slog.e(TAG, "Error while dismissing keyguard."
290                                     + " Keeping the dialog visible.");
291                         }
292 
293                         @Override
294                         public void onDismissCancelled() {
295                             Slog.w(TAG, "Keyguard dismiss was cancelled. Finishing.");
296                             SuspendedAppActivity.this.finish();
297                         }
298                     });
299         }
300     }
301 
302     @Override
onClick(DialogInterface dialog, int which)303     public void onClick(DialogInterface dialog, int which) {
304         switch (which) {
305             case AlertDialog.BUTTON_NEUTRAL:
306                 switch (mNeutralButtonAction) {
307                     case BUTTON_ACTION_MORE_DETAILS:
308                         if (mMoreDetailsIntent != null) {
309                             startActivityAsUser(mMoreDetailsIntent, mOptions,
310                                     UserHandle.of(mSuspendingUserId));
311                         } else {
312                             Slog.wtf(TAG, "Neutral button should not have existed!");
313                         }
314                         break;
315                     case BUTTON_ACTION_UNSUSPEND:
316                         final IPackageManager ipm = AppGlobals.getPackageManager();
317                         try {
318                             final String[] errored = ipm.setPackagesSuspendedAsUser(
319                                     new String[]{mSuspendedPackage}, false, null, null, null, 0,
320                                     mSuspendingPackage, mUserId /* suspendingUserId */,
321                                     mUserId /* targetUserId */);
322                             if (ArrayUtils.contains(errored, mSuspendedPackage)) {
323                                 Slog.e(TAG, "Could not unsuspend " + mSuspendedPackage);
324                                 break;
325                             }
326                         } catch (RemoteException re) {
327                             Slog.e(TAG, "Can't talk to system process", re);
328                             break;
329                         }
330                         final Intent reportUnsuspend = new Intent()
331                                 .setAction(Intent.ACTION_PACKAGE_UNSUSPENDED_MANUALLY)
332                                 .putExtra(Intent.EXTRA_PACKAGE_NAME, mSuspendedPackage)
333                                 .setPackage(mSuspendingPackage)
334                                 .addFlags(Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND);
335                         sendBroadcastAsUser(reportUnsuspend, UserHandle.of(mSuspendingUserId));
336 
337                         if (mOnUnsuspend != null) {
338                             Bundle activityOptions =
339                                     ActivityOptions.makeBasic()
340                                             .setPendingIntentBackgroundActivityStartMode(
341                                                     ActivityOptions
342                                                             .MODE_BACKGROUND_ACTIVITY_START_ALLOWED)
343                                             .toBundle();
344                             try {
345                                 mOnUnsuspend.sendIntent(this, 0, null, null, null, null,
346                                         activityOptions);
347                             } catch (IntentSender.SendIntentException e) {
348                                 Slog.e(TAG, "Error while starting intent " + mOnUnsuspend, e);
349                             }
350                         }
351                         break;
352                     default:
353                         Slog.e(TAG, "Unexpected action on neutral button: " + mNeutralButtonAction);
354                         break;
355                 }
356                 break;
357         }
358         mUsm.reportUserInteraction(mSuspendingPackage, mUserId);
359         finish();
360     }
361 
createSuspendedAppInterceptIntent(String suspendedPackage, UserPackage suspendingPackage, SuspendDialogInfo dialogInfo, Bundle options, IntentSender onUnsuspend, int userId)362     public static Intent createSuspendedAppInterceptIntent(String suspendedPackage,
363             UserPackage suspendingPackage, SuspendDialogInfo dialogInfo, Bundle options,
364             IntentSender onUnsuspend, int userId) {
365         Intent intent = new Intent()
366                 .setClassName("android", SuspendedAppActivity.class.getName())
367                 .putExtra(EXTRA_SUSPENDED_PACKAGE, suspendedPackage)
368                 .putExtra(EXTRA_DIALOG_INFO, dialogInfo)
369                 .putExtra(EXTRA_SUSPENDING_PACKAGE,
370                         suspendingPackage != null ? suspendingPackage.packageName : null)
371                 .putExtra(EXTRA_UNSUSPEND_INTENT, onUnsuspend)
372                 .putExtra(EXTRA_ACTIVITY_OPTIONS, options)
373                 .putExtra(Intent.EXTRA_USER_ID, userId)
374                 .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
375                         | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
376         if (crossUserSuspensionEnabledRo() && suspendingPackage != null) {
377             intent.putExtra(EXTRA_SUSPENDING_USER, suspendingPackage.userId);
378         }
379         return intent;
380     }
381 }
382