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