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