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