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