| /* |
| * Copyright (C) 2017 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.settings.applications.appinfo; |
| |
| import android.app.Activity; |
| import android.app.ActivityManager; |
| import android.app.admin.DevicePolicyManager; |
| import android.content.BroadcastReceiver; |
| import android.content.ComponentName; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.pm.ApplicationInfo; |
| import android.content.pm.PackageInfo; |
| import android.content.pm.PackageManager; |
| import android.content.pm.ResolveInfo; |
| import android.net.Uri; |
| import android.os.Bundle; |
| import android.os.RemoteException; |
| import android.os.ServiceManager; |
| import android.os.UserHandle; |
| import android.os.UserManager; |
| import androidx.annotation.VisibleForTesting; |
| import androidx.preference.PreferenceScreen; |
| import android.util.Log; |
| import android.webkit.IWebViewUpdateService; |
| |
| import com.android.settings.R; |
| import com.android.settings.Utils; |
| import com.android.settings.applications.ApplicationFeatureProvider; |
| import com.android.settings.core.BasePreferenceController; |
| import com.android.settings.overlay.FeatureFactory; |
| import com.android.settings.widget.ActionButtonPreference; |
| import com.android.settingslib.RestrictedLockUtils; |
| import com.android.settingslib.applications.AppUtils; |
| import com.android.settingslib.applications.ApplicationsState.AppEntry; |
| |
| import java.util.ArrayList; |
| import java.util.HashSet; |
| import java.util.List; |
| |
| public class AppActionButtonPreferenceController extends BasePreferenceController |
| implements AppInfoDashboardFragment.Callback { |
| |
| private static final String TAG = "AppActionButtonControl"; |
| private static final String KEY_ACTION_BUTTONS = "action_buttons"; |
| |
| @VisibleForTesting |
| ActionButtonPreference mActionButtons; |
| private final AppInfoDashboardFragment mParent; |
| private final String mPackageName; |
| private final HashSet<String> mHomePackages = new HashSet<>(); |
| private final ApplicationFeatureProvider mApplicationFeatureProvider; |
| |
| private int mUserId; |
| private DevicePolicyManager mDpm; |
| private UserManager mUserManager; |
| private PackageManager mPm; |
| |
| private final BroadcastReceiver mCheckKillProcessesReceiver = new BroadcastReceiver() { |
| @Override |
| public void onReceive(Context context, Intent intent) { |
| final boolean enabled = getResultCode() != Activity.RESULT_CANCELED; |
| Log.d(TAG, "Got broadcast response: Restart status for " |
| + mParent.getAppEntry().info.packageName + " " + enabled); |
| updateForceStopButton(enabled); |
| } |
| }; |
| |
| public AppActionButtonPreferenceController(Context context, AppInfoDashboardFragment parent, |
| String packageName) { |
| super(context, KEY_ACTION_BUTTONS); |
| mParent = parent; |
| mPackageName = packageName; |
| mUserId = UserHandle.myUserId(); |
| mApplicationFeatureProvider = FeatureFactory.getFactory(context) |
| .getApplicationFeatureProvider(context); |
| } |
| |
| @Override |
| public int getAvailabilityStatus() { |
| return AppUtils.isInstant(mParent.getPackageInfo().applicationInfo) |
| ? DISABLED_FOR_USER : AVAILABLE; |
| } |
| |
| @Override |
| public void displayPreference(PreferenceScreen screen) { |
| super.displayPreference(screen); |
| mActionButtons = ((ActionButtonPreference) screen.findPreference(KEY_ACTION_BUTTONS)) |
| .setButton2Text(R.string.force_stop) |
| .setButton2Positive(false) |
| .setButton2Enabled(false); |
| } |
| |
| @Override |
| public void refreshUi() { |
| if (mPm == null) { |
| mPm = mContext.getPackageManager(); |
| } |
| if (mDpm == null) { |
| mDpm = (DevicePolicyManager) mContext.getSystemService(Context.DEVICE_POLICY_SERVICE); |
| } |
| if (mUserManager == null) { |
| mUserManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE); |
| } |
| final AppEntry appEntry = mParent.getAppEntry(); |
| final PackageInfo packageInfo = mParent.getPackageInfo(); |
| |
| // Get list of "home" apps and trace through any meta-data references |
| final List<ResolveInfo> homeActivities = new ArrayList<ResolveInfo>(); |
| mPm.getHomeActivities(homeActivities); |
| mHomePackages.clear(); |
| for (int i = 0; i < homeActivities.size(); i++) { |
| final ResolveInfo ri = homeActivities.get(i); |
| final String activityPkg = ri.activityInfo.packageName; |
| mHomePackages.add(activityPkg); |
| |
| // Also make sure to include anything proxying for the home app |
| final Bundle metadata = ri.activityInfo.metaData; |
| if (metadata != null) { |
| final String metaPkg = metadata.getString(ActivityManager.META_HOME_ALTERNATE); |
| if (signaturesMatch(metaPkg, activityPkg)) { |
| mHomePackages.add(metaPkg); |
| } |
| } |
| } |
| |
| checkForceStop(appEntry, packageInfo); |
| initUninstallButtons(appEntry, packageInfo); |
| } |
| |
| @VisibleForTesting |
| void initUninstallButtons(AppEntry appEntry, PackageInfo packageInfo) { |
| final boolean isBundled = (appEntry.info.flags & ApplicationInfo.FLAG_SYSTEM) != 0; |
| boolean enabled; |
| if (isBundled) { |
| enabled = handleDisableable(appEntry, packageInfo); |
| } else { |
| enabled = initUninstallButtonForUserApp(); |
| } |
| // If this is a device admin, it can't be uninstalled or disabled. |
| // We do this here so the text of the button is still set correctly. |
| if (isBundled && mDpm.packageHasActiveAdmins(packageInfo.packageName)) { |
| enabled = false; |
| } |
| |
| // We don't allow uninstalling DO/PO on *any* users, because if it's a system app, |
| // "uninstall" is actually "downgrade to the system version + disable", and "downgrade" |
| // will clear data on all users. |
| if (Utils.isProfileOrDeviceOwner(mUserManager, mDpm, packageInfo.packageName)) { |
| enabled = false; |
| } |
| |
| // Don't allow uninstalling the device provisioning package. |
| if (Utils.isDeviceProvisioningPackage(mContext.getResources(), appEntry.info.packageName)) { |
| enabled = false; |
| } |
| |
| // If the uninstall intent is already queued, disable the uninstall button |
| if (mDpm.isUninstallInQueue(mPackageName)) { |
| enabled = false; |
| } |
| |
| // Home apps need special handling. Bundled ones we don't risk downgrading |
| // because that can interfere with home-key resolution. Furthermore, we |
| // can't allow uninstallation of the only home app, and we don't want to |
| // allow uninstallation of an explicitly preferred one -- the user can go |
| // to Home settings and pick a different one, after which we'll permit |
| // uninstallation of the now-not-default one. |
| if (enabled && mHomePackages.contains(packageInfo.packageName)) { |
| if (isBundled) { |
| enabled = false; |
| } else { |
| ArrayList<ResolveInfo> homeActivities = new ArrayList<ResolveInfo>(); |
| ComponentName currentDefaultHome = mPm.getHomeActivities(homeActivities); |
| if (currentDefaultHome == null) { |
| // No preferred default, so permit uninstall only when |
| // there is more than one candidate |
| enabled = (mHomePackages.size() > 1); |
| } else { |
| // There is an explicit default home app -- forbid uninstall of |
| // that one, but permit it for installed-but-inactive ones. |
| enabled = !packageInfo.packageName.equals(currentDefaultHome.getPackageName()); |
| } |
| } |
| } |
| |
| if (RestrictedLockUtils.hasBaseUserRestriction( |
| mContext, UserManager.DISALLOW_APPS_CONTROL, mUserId)) { |
| enabled = false; |
| } |
| |
| try { |
| final IWebViewUpdateService webviewUpdateService = |
| IWebViewUpdateService.Stub.asInterface( |
| ServiceManager.getService("webviewupdate")); |
| if (webviewUpdateService.isFallbackPackage(appEntry.info.packageName)) { |
| enabled = false; |
| } |
| } catch (RemoteException e) { |
| throw new RuntimeException(e); |
| } |
| |
| mActionButtons.setButton1Enabled(enabled); |
| if (enabled) { |
| // Register listener |
| mActionButtons.setButton1OnClickListener(v -> mParent.handleUninstallButtonClick()); |
| } |
| } |
| |
| @VisibleForTesting |
| boolean initUninstallButtonForUserApp() { |
| boolean enabled = true; |
| final PackageInfo packageInfo = mParent.getPackageInfo(); |
| if ((packageInfo.applicationInfo.flags & ApplicationInfo.FLAG_INSTALLED) == 0 |
| && mUserManager.getUsers().size() >= 2) { |
| // When we have multiple users, there is a separate menu |
| // to uninstall for all users. |
| enabled = false; |
| } else if (AppUtils.isInstant(packageInfo.applicationInfo)) { |
| enabled = false; |
| mActionButtons.setButton1Visible(false); |
| } |
| mActionButtons.setButton1Text(R.string.uninstall_text).setButton1Positive(false); |
| return enabled; |
| } |
| |
| @VisibleForTesting |
| boolean handleDisableable(AppEntry appEntry, PackageInfo packageInfo) { |
| boolean disableable = false; |
| // Try to prevent the user from bricking their phone |
| // by not allowing disabling of apps signed with the |
| // system cert and any launcher app in the system. |
| if (mHomePackages.contains(appEntry.info.packageName) |
| || Utils.isSystemPackage(mContext.getResources(), mPm, packageInfo)) { |
| // Disable button for core system applications. |
| mActionButtons |
| .setButton1Text(R.string.disable_text) |
| .setButton1Positive(false); |
| } else if (appEntry.info.enabled && appEntry.info.enabledSetting |
| != PackageManager.COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED) { |
| mActionButtons |
| .setButton1Text(R.string.disable_text) |
| .setButton1Positive(false); |
| disableable = !mApplicationFeatureProvider.getKeepEnabledPackages() |
| .contains(appEntry.info.packageName); |
| } else { |
| mActionButtons |
| .setButton1Text(R.string.enable_text) |
| .setButton1Positive(true); |
| disableable = true; |
| } |
| |
| return disableable; |
| } |
| |
| private void updateForceStopButton(boolean enabled) { |
| final boolean disallowedBySystem = RestrictedLockUtils.hasBaseUserRestriction( |
| mContext, UserManager.DISALLOW_APPS_CONTROL, mUserId); |
| mActionButtons |
| .setButton2Enabled(disallowedBySystem ? false : enabled) |
| .setButton2OnClickListener( |
| disallowedBySystem ? null : v -> mParent.handleForceStopButtonClick()); |
| } |
| |
| void checkForceStop(AppEntry appEntry, PackageInfo packageInfo) { |
| if (mDpm.packageHasActiveAdmins(packageInfo.packageName)) { |
| // User can't force stop device admin. |
| Log.w(TAG, "User can't force stop device admin"); |
| updateForceStopButton(false); |
| } else if (mPm.isPackageStateProtected(packageInfo.packageName, |
| UserHandle.getUserId(appEntry.info.uid))) { |
| Log.w(TAG, "User can't force stop protected packages"); |
| updateForceStopButton(false); |
| } else if (AppUtils.isInstant(packageInfo.applicationInfo)) { |
| updateForceStopButton(false); |
| mActionButtons.setButton2Visible(false); |
| } else if ((appEntry.info.flags & ApplicationInfo.FLAG_STOPPED) == 0) { |
| // If the app isn't explicitly stopped, then always show the |
| // force stop button. |
| Log.w(TAG, "App is not explicitly stopped"); |
| updateForceStopButton(true); |
| } else { |
| final Intent intent = new Intent(Intent.ACTION_QUERY_PACKAGE_RESTART, |
| Uri.fromParts("package", appEntry.info.packageName, null)); |
| intent.putExtra(Intent.EXTRA_PACKAGES, new String[] {appEntry.info.packageName}); |
| intent.putExtra(Intent.EXTRA_UID, appEntry.info.uid); |
| intent.putExtra(Intent.EXTRA_USER_HANDLE, UserHandle.getUserId(appEntry.info.uid)); |
| Log.d(TAG, "Sending broadcast to query restart status for " |
| + appEntry.info.packageName); |
| mContext.sendOrderedBroadcastAsUser(intent, UserHandle.CURRENT, null, |
| mCheckKillProcessesReceiver, null, Activity.RESULT_CANCELED, null, null); |
| } |
| } |
| |
| private boolean signaturesMatch(String pkg1, String pkg2) { |
| if (pkg1 != null && pkg2 != null) { |
| try { |
| return mPm.checkSignatures(pkg1, pkg2) >= PackageManager.SIGNATURE_MATCH; |
| } catch (Exception e) { |
| // e.g. named alternate package not found during lookup; |
| // this is an expected case sometimes |
| } |
| } |
| return false; |
| } |
| |
| } |