/* * Copyright (C) 2018 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.internal.app; import static android.app.admin.flags.Flags.crossUserSuspensionEnabledRo; import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_AWARE; import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_UNAWARE; import static android.content.pm.SuspendDialogInfo.BUTTON_ACTION_MORE_DETAILS; import static android.content.pm.SuspendDialogInfo.BUTTON_ACTION_UNSUSPEND; import static android.content.res.Resources.ID_NULL; import android.Manifest; import android.annotation.Nullable; import android.app.ActivityOptions; import android.app.AlertDialog; import android.app.AppGlobals; import android.app.KeyguardManager; import android.app.usage.UsageStatsManager; import android.content.BroadcastReceiver; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.IntentFilter; import android.content.IntentSender; import android.content.pm.IPackageManager; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.pm.SuspendDialogInfo; import android.content.pm.UserPackage; import android.content.res.Resources; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.os.RemoteException; import android.os.UserHandle; import android.util.Slog; import android.view.WindowManager; import com.android.internal.R; import com.android.internal.util.ArrayUtils; public class SuspendedAppActivity extends AlertActivity implements DialogInterface.OnClickListener { private static final String TAG = SuspendedAppActivity.class.getSimpleName(); private static final String PACKAGE_NAME = "com.android.internal.app"; public static final String EXTRA_SUSPENDED_PACKAGE = PACKAGE_NAME + ".extra.SUSPENDED_PACKAGE"; public static final String EXTRA_SUSPENDING_PACKAGE = PACKAGE_NAME + ".extra.SUSPENDING_PACKAGE"; public static final String EXTRA_SUSPENDING_USER = PACKAGE_NAME + ".extra.SUSPENDING_USER"; public static final String EXTRA_DIALOG_INFO = PACKAGE_NAME + ".extra.DIALOG_INFO"; public static final String EXTRA_ACTIVITY_OPTIONS = PACKAGE_NAME + ".extra.ACTIVITY_OPTIONS"; public static final String EXTRA_UNSUSPEND_INTENT = PACKAGE_NAME + ".extra.UNSUSPEND_INTENT"; private Intent mMoreDetailsIntent; private IntentSender mOnUnsuspend; private String mSuspendedPackage; private String mSuspendingPackage; private int mSuspendingUserId; private int mNeutralButtonAction; private int mUserId; private PackageManager mPm; private UsageStatsManager mUsm; private Resources mSuspendingAppResources; private SuspendDialogInfo mSuppliedDialogInfo; private Bundle mOptions; private BroadcastReceiver mSuspendModifiedReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { if (Intent.ACTION_PACKAGES_SUSPENSION_CHANGED.equals(intent.getAction())) { // Suspension conditions were modified, dismiss any related visible dialogs. final String[] modified = intent.getStringArrayExtra( Intent.EXTRA_CHANGED_PACKAGE_LIST); if (ArrayUtils.contains(modified, mSuspendedPackage) && !isPackageSuspended(mSuspendedPackage)) { if (!isFinishing()) { Slog.w(TAG, "Package " + mSuspendedPackage + " has modified" + " suspension conditions while dialog was visible. Finishing."); SuspendedAppActivity.this.finish(); // TODO (b/198201994): reload the suspend dialog to show most relevant info } } } } }; private boolean isPackageSuspended(String packageName) { try { return mPm.isPackageSuspended(packageName); } catch (PackageManager.NameNotFoundException ne) { Slog.e(TAG, "Package " + packageName + " not found", ne); } return false; } private CharSequence getAppLabel(String packageName) { try { return mPm.getApplicationInfoAsUser(packageName, 0, mUserId).loadLabel(mPm); } catch (PackageManager.NameNotFoundException ne) { Slog.e(TAG, "Package " + packageName + " not found", ne); } return packageName; } private Intent getMoreDetailsActivity() { final Intent moreDetailsIntent = new Intent(Intent.ACTION_SHOW_SUSPENDED_APP_DETAILS) .setPackage(mSuspendingPackage); final String requiredPermission = Manifest.permission.SEND_SHOW_SUSPENDED_APP_DETAILS; final ResolveInfo resolvedInfo = mPm.resolveActivityAsUser(moreDetailsIntent, MATCH_DIRECT_BOOT_UNAWARE | MATCH_DIRECT_BOOT_AWARE, mSuspendingUserId); if (resolvedInfo != null && resolvedInfo.activityInfo != null && requiredPermission.equals(resolvedInfo.activityInfo.permission)) { moreDetailsIntent.putExtra(Intent.EXTRA_PACKAGE_NAME, mSuspendedPackage) .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); return moreDetailsIntent; } return null; } private Drawable resolveIcon() { final int iconId = (mSuppliedDialogInfo != null) ? mSuppliedDialogInfo.getIconResId() : ID_NULL; if (iconId != ID_NULL && mSuspendingAppResources != null) { try { return mSuspendingAppResources.getDrawable(iconId, getTheme()); } catch (Resources.NotFoundException nfe) { Slog.e(TAG, "Could not resolve drawable resource id " + iconId); } } return null; } private String resolveTitle() { if (mSuppliedDialogInfo != null) { final int titleId = mSuppliedDialogInfo.getTitleResId(); final String title = mSuppliedDialogInfo.getTitle(); if (titleId != ID_NULL && mSuspendingAppResources != null) { try { return mSuspendingAppResources.getString(titleId); } catch (Resources.NotFoundException nfe) { Slog.e(TAG, "Could not resolve string resource id " + titleId); } } else if (title != null) { return title; } } return getString(R.string.app_suspended_title); } private String resolveDialogMessage() { final CharSequence suspendedAppLabel = getAppLabel(mSuspendedPackage); if (mSuppliedDialogInfo != null) { final int messageId = mSuppliedDialogInfo.getDialogMessageResId(); final String message = mSuppliedDialogInfo.getDialogMessage(); if (messageId != ID_NULL && mSuspendingAppResources != null) { try { return mSuspendingAppResources.getString(messageId, suspendedAppLabel); } catch (Resources.NotFoundException nfe) { Slog.e(TAG, "Could not resolve string resource id " + messageId); } } else if (message != null) { return String.format(getResources().getConfiguration().getLocales().get(0), message, suspendedAppLabel); } } return getString(R.string.app_suspended_default_message, suspendedAppLabel, getAppLabel(mSuspendingPackage)); } /** * Returns a text to be displayed on the neutral button or {@code null} if the button should * not be shown. */ @Nullable private String resolveNeutralButtonText() { final int defaultButtonTextId; switch (mNeutralButtonAction) { case BUTTON_ACTION_MORE_DETAILS: if (mMoreDetailsIntent == null) { return null; } defaultButtonTextId = R.string.app_suspended_more_details; break; case BUTTON_ACTION_UNSUSPEND: defaultButtonTextId = R.string.app_suspended_unsuspend_message; break; default: Slog.w(TAG, "Unknown neutral button action: " + mNeutralButtonAction); return null; } if (mSuppliedDialogInfo != null) { final int buttonTextId = mSuppliedDialogInfo.getNeutralButtonTextResId(); final String buttonText = mSuppliedDialogInfo.getNeutralButtonText(); if (buttonTextId != ID_NULL && mSuspendingAppResources != null) { try { return mSuspendingAppResources.getString(buttonTextId); } catch (Resources.NotFoundException nfe) { Slog.e(TAG, "Could not resolve string resource id " + buttonTextId); } } else if (buttonText != null) { return buttonText; } } return getString(defaultButtonTextId); } @Override public void onCreate(Bundle icicle) { super.onCreate(icicle); mPm = getPackageManager(); mUsm = getSystemService(UsageStatsManager.class); getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_DIALOG); final Intent intent = getIntent(); mOptions = intent.getBundleExtra(EXTRA_ACTIVITY_OPTIONS); mUserId = intent.getIntExtra(Intent.EXTRA_USER_ID, -1); if (mUserId < 0) { Slog.wtf(TAG, "Invalid user: " + mUserId); finish(); return; } mSuspendedPackage = intent.getStringExtra(EXTRA_SUSPENDED_PACKAGE); mSuspendingPackage = intent.getStringExtra(EXTRA_SUSPENDING_PACKAGE); if (crossUserSuspensionEnabledRo()) { mSuspendingUserId = intent.getIntExtra(EXTRA_SUSPENDING_USER, mUserId); } else { mSuspendingUserId = mUserId; } mSuppliedDialogInfo = intent.getParcelableExtra(EXTRA_DIALOG_INFO, android.content.pm.SuspendDialogInfo.class); mOnUnsuspend = intent.getParcelableExtra(EXTRA_UNSUSPEND_INTENT, android.content.IntentSender.class); if (mSuppliedDialogInfo != null) { try { mSuspendingAppResources = createContextAsUser( UserHandle.of(mSuspendingUserId), /* flags */ 0).getPackageManager() .getResourcesForApplication(mSuspendingPackage); } catch (PackageManager.NameNotFoundException ne) { Slog.e(TAG, "Could not find resources for " + mSuspendingPackage, ne); } } mNeutralButtonAction = (mSuppliedDialogInfo != null) ? mSuppliedDialogInfo.getNeutralButtonAction() : BUTTON_ACTION_MORE_DETAILS; mMoreDetailsIntent = (mNeutralButtonAction == BUTTON_ACTION_MORE_DETAILS) ? getMoreDetailsActivity() : null; final AlertController.AlertParams ap = mAlertParams; ap.mIcon = resolveIcon(); ap.mTitle = resolveTitle(); ap.mMessage = resolveDialogMessage(); ap.mPositiveButtonText = getString(android.R.string.ok); ap.mNeutralButtonText = resolveNeutralButtonText(); ap.mPositiveButtonListener = ap.mNeutralButtonListener = this; requestDismissKeyguardIfNeeded(ap.mMessage); setupAlert(); final IntentFilter suspendModifiedFilter = new IntentFilter(Intent.ACTION_PACKAGES_SUSPENSION_CHANGED); registerReceiverAsUser(mSuspendModifiedReceiver, UserHandle.of(mUserId), suspendModifiedFilter, null, null); } @Override protected void onDestroy() { super.onDestroy(); unregisterReceiver(mSuspendModifiedReceiver); } private void requestDismissKeyguardIfNeeded(CharSequence dismissMessage) { final KeyguardManager km = getSystemService(KeyguardManager.class); if (km.isKeyguardLocked()) { km.requestDismissKeyguard(this, dismissMessage, new KeyguardManager.KeyguardDismissCallback() { @Override public void onDismissError() { Slog.e(TAG, "Error while dismissing keyguard." + " Keeping the dialog visible."); } @Override public void onDismissCancelled() { Slog.w(TAG, "Keyguard dismiss was cancelled. Finishing."); SuspendedAppActivity.this.finish(); } }); } } @Override public void onClick(DialogInterface dialog, int which) { switch (which) { case AlertDialog.BUTTON_NEUTRAL: switch (mNeutralButtonAction) { case BUTTON_ACTION_MORE_DETAILS: if (mMoreDetailsIntent != null) { startActivityAsUser(mMoreDetailsIntent, mOptions, UserHandle.of(mSuspendingUserId)); } else { Slog.wtf(TAG, "Neutral button should not have existed!"); } break; case BUTTON_ACTION_UNSUSPEND: final IPackageManager ipm = AppGlobals.getPackageManager(); try { final String[] errored = ipm.setPackagesSuspendedAsUser( new String[]{mSuspendedPackage}, false, null, null, null, 0, mSuspendingPackage, mUserId /* suspendingUserId */, mUserId /* targetUserId */); if (ArrayUtils.contains(errored, mSuspendedPackage)) { Slog.e(TAG, "Could not unsuspend " + mSuspendedPackage); break; } } catch (RemoteException re) { Slog.e(TAG, "Can't talk to system process", re); break; } final Intent reportUnsuspend = new Intent() .setAction(Intent.ACTION_PACKAGE_UNSUSPENDED_MANUALLY) .putExtra(Intent.EXTRA_PACKAGE_NAME, mSuspendedPackage) .setPackage(mSuspendingPackage) .addFlags(Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND); sendBroadcastAsUser(reportUnsuspend, UserHandle.of(mSuspendingUserId)); if (mOnUnsuspend != null) { Bundle activityOptions = ActivityOptions.makeBasic() .setPendingIntentBackgroundActivityStartMode( ActivityOptions .MODE_BACKGROUND_ACTIVITY_START_ALLOWED) .toBundle(); try { mOnUnsuspend.sendIntent(this, 0, null, null, null, null, activityOptions); } catch (IntentSender.SendIntentException e) { Slog.e(TAG, "Error while starting intent " + mOnUnsuspend, e); } } break; default: Slog.e(TAG, "Unexpected action on neutral button: " + mNeutralButtonAction); break; } break; } mUsm.reportUserInteraction(mSuspendingPackage, mUserId); finish(); } public static Intent createSuspendedAppInterceptIntent(String suspendedPackage, UserPackage suspendingPackage, SuspendDialogInfo dialogInfo, Bundle options, IntentSender onUnsuspend, int userId) { Intent intent = new Intent() .setClassName("android", SuspendedAppActivity.class.getName()) .putExtra(EXTRA_SUSPENDED_PACKAGE, suspendedPackage) .putExtra(EXTRA_DIALOG_INFO, dialogInfo) .putExtra(EXTRA_SUSPENDING_PACKAGE, suspendingPackage != null ? suspendingPackage.packageName : null) .putExtra(EXTRA_UNSUSPEND_INTENT, onUnsuspend) .putExtra(EXTRA_ACTIVITY_OPTIONS, options) .putExtra(Intent.EXTRA_USER_ID, userId) .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); if (crossUserSuspensionEnabledRo() && suspendingPackage != null) { intent.putExtra(EXTRA_SUSPENDING_USER, suspendingPackage.userId); } return intent; } }