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