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