Home | History | Annotate | Download | only in appinfo
      1 /*
      2  * Copyright (C) 2017 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.settings.applications.appinfo;
     18 
     19 import android.app.Activity;
     20 import android.app.ActivityManager;
     21 import android.app.admin.DevicePolicyManager;
     22 import android.content.BroadcastReceiver;
     23 import android.content.ComponentName;
     24 import android.content.Context;
     25 import android.content.Intent;
     26 import android.content.pm.ApplicationInfo;
     27 import android.content.pm.PackageInfo;
     28 import android.content.pm.PackageManager;
     29 import android.content.pm.ResolveInfo;
     30 import android.net.Uri;
     31 import android.os.Bundle;
     32 import android.os.RemoteException;
     33 import android.os.ServiceManager;
     34 import android.os.UserHandle;
     35 import android.os.UserManager;
     36 import android.support.annotation.VisibleForTesting;
     37 import android.support.v7.preference.PreferenceScreen;
     38 import android.util.Log;
     39 import android.webkit.IWebViewUpdateService;
     40 
     41 import com.android.settings.R;
     42 import com.android.settings.Utils;
     43 import com.android.settings.applications.ApplicationFeatureProvider;
     44 import com.android.settings.core.BasePreferenceController;
     45 import com.android.settings.overlay.FeatureFactory;
     46 import com.android.settings.widget.ActionButtonPreference;
     47 import com.android.settingslib.RestrictedLockUtils;
     48 import com.android.settingslib.applications.AppUtils;
     49 import com.android.settingslib.applications.ApplicationsState.AppEntry;
     50 
     51 import java.util.ArrayList;
     52 import java.util.HashSet;
     53 import java.util.List;
     54 
     55 public class AppActionButtonPreferenceController extends BasePreferenceController
     56         implements AppInfoDashboardFragment.Callback {
     57 
     58     private static final String TAG = "AppActionButtonControl";
     59     private static final String KEY_ACTION_BUTTONS = "action_buttons";
     60 
     61     @VisibleForTesting
     62     ActionButtonPreference mActionButtons;
     63     private final AppInfoDashboardFragment mParent;
     64     private final String mPackageName;
     65     private final HashSet<String> mHomePackages = new HashSet<>();
     66     private final ApplicationFeatureProvider mApplicationFeatureProvider;
     67 
     68     private int mUserId;
     69     private DevicePolicyManager mDpm;
     70     private UserManager mUserManager;
     71     private PackageManager mPm;
     72 
     73     private final BroadcastReceiver mCheckKillProcessesReceiver = new BroadcastReceiver() {
     74         @Override
     75         public void onReceive(Context context, Intent intent) {
     76             final boolean enabled = getResultCode() != Activity.RESULT_CANCELED;
     77             Log.d(TAG, "Got broadcast response: Restart status for "
     78                     + mParent.getAppEntry().info.packageName + " " + enabled);
     79             updateForceStopButton(enabled);
     80         }
     81     };
     82 
     83     public AppActionButtonPreferenceController(Context context, AppInfoDashboardFragment parent,
     84             String packageName) {
     85         super(context, KEY_ACTION_BUTTONS);
     86         mParent = parent;
     87         mPackageName = packageName;
     88         mUserId = UserHandle.myUserId();
     89         mApplicationFeatureProvider = FeatureFactory.getFactory(context)
     90                 .getApplicationFeatureProvider(context);
     91     }
     92 
     93     @Override
     94     public int getAvailabilityStatus() {
     95         return AppUtils.isInstant(mParent.getPackageInfo().applicationInfo)
     96                 ? DISABLED_FOR_USER : AVAILABLE;
     97     }
     98 
     99     @Override
    100     public void displayPreference(PreferenceScreen screen) {
    101         super.displayPreference(screen);
    102         mActionButtons = ((ActionButtonPreference) screen.findPreference(KEY_ACTION_BUTTONS))
    103                 .setButton2Text(R.string.force_stop)
    104                 .setButton2Positive(false)
    105                 .setButton2Enabled(false);
    106     }
    107 
    108     @Override
    109     public void refreshUi() {
    110         if (mPm == null) {
    111             mPm = mContext.getPackageManager();
    112         }
    113         if (mDpm == null) {
    114             mDpm = (DevicePolicyManager) mContext.getSystemService(Context.DEVICE_POLICY_SERVICE);
    115         }
    116         if (mUserManager == null) {
    117             mUserManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE);
    118         }
    119         final AppEntry appEntry = mParent.getAppEntry();
    120         final PackageInfo packageInfo = mParent.getPackageInfo();
    121 
    122         // Get list of "home" apps and trace through any meta-data references
    123         final List<ResolveInfo> homeActivities = new ArrayList<ResolveInfo>();
    124         mPm.getHomeActivities(homeActivities);
    125         mHomePackages.clear();
    126         for (int i = 0; i < homeActivities.size(); i++) {
    127             final ResolveInfo ri = homeActivities.get(i);
    128             final String activityPkg = ri.activityInfo.packageName;
    129             mHomePackages.add(activityPkg);
    130 
    131             // Also make sure to include anything proxying for the home app
    132             final Bundle metadata = ri.activityInfo.metaData;
    133             if (metadata != null) {
    134                 final String metaPkg = metadata.getString(ActivityManager.META_HOME_ALTERNATE);
    135                 if (signaturesMatch(metaPkg, activityPkg)) {
    136                     mHomePackages.add(metaPkg);
    137                 }
    138             }
    139         }
    140 
    141         checkForceStop(appEntry, packageInfo);
    142         initUninstallButtons(appEntry, packageInfo);
    143     }
    144 
    145     @VisibleForTesting
    146     void initUninstallButtons(AppEntry appEntry, PackageInfo packageInfo) {
    147         final boolean isBundled = (appEntry.info.flags & ApplicationInfo.FLAG_SYSTEM) != 0;
    148         boolean enabled;
    149         if (isBundled) {
    150             enabled = handleDisableable(appEntry, packageInfo);
    151         } else {
    152             enabled = initUninstallButtonForUserApp();
    153         }
    154         // If this is a device admin, it can't be uninstalled or disabled.
    155         // We do this here so the text of the button is still set correctly.
    156         if (isBundled && mDpm.packageHasActiveAdmins(packageInfo.packageName)) {
    157             enabled = false;
    158         }
    159 
    160         // We don't allow uninstalling DO/PO on *any* users, because if it's a system app,
    161         // "uninstall" is actually "downgrade to the system version + disable", and "downgrade"
    162         // will clear data on all users.
    163         if (Utils.isProfileOrDeviceOwner(mUserManager, mDpm, packageInfo.packageName)) {
    164             enabled = false;
    165         }
    166 
    167         // Don't allow uninstalling the device provisioning package.
    168         if (Utils.isDeviceProvisioningPackage(mContext.getResources(), appEntry.info.packageName)) {
    169             enabled = false;
    170         }
    171 
    172         // If the uninstall intent is already queued, disable the uninstall button
    173         if (mDpm.isUninstallInQueue(mPackageName)) {
    174             enabled = false;
    175         }
    176 
    177         // Home apps need special handling.  Bundled ones we don't risk downgrading
    178         // because that can interfere with home-key resolution.  Furthermore, we
    179         // can't allow uninstallation of the only home app, and we don't want to
    180         // allow uninstallation of an explicitly preferred one -- the user can go
    181         // to Home settings and pick a different one, after which we'll permit
    182         // uninstallation of the now-not-default one.
    183         if (enabled && mHomePackages.contains(packageInfo.packageName)) {
    184             if (isBundled) {
    185                 enabled = false;
    186             } else {
    187                 ArrayList<ResolveInfo> homeActivities = new ArrayList<ResolveInfo>();
    188                 ComponentName currentDefaultHome = mPm.getHomeActivities(homeActivities);
    189                 if (currentDefaultHome == null) {
    190                     // No preferred default, so permit uninstall only when
    191                     // there is more than one candidate
    192                     enabled = (mHomePackages.size() > 1);
    193                 } else {
    194                     // There is an explicit default home app -- forbid uninstall of
    195                     // that one, but permit it for installed-but-inactive ones.
    196                     enabled = !packageInfo.packageName.equals(currentDefaultHome.getPackageName());
    197                 }
    198             }
    199         }
    200 
    201         if (RestrictedLockUtils.hasBaseUserRestriction(
    202                 mContext, UserManager.DISALLOW_APPS_CONTROL, mUserId)) {
    203             enabled = false;
    204         }
    205 
    206         try {
    207             final IWebViewUpdateService webviewUpdateService =
    208                     IWebViewUpdateService.Stub.asInterface(
    209                             ServiceManager.getService("webviewupdate"));
    210             if (webviewUpdateService.isFallbackPackage(appEntry.info.packageName)) {
    211                 enabled = false;
    212             }
    213         } catch (RemoteException e) {
    214             throw new RuntimeException(e);
    215         }
    216 
    217         mActionButtons.setButton1Enabled(enabled);
    218         if (enabled) {
    219             // Register listener
    220             mActionButtons.setButton1OnClickListener(v -> mParent.handleUninstallButtonClick());
    221         }
    222     }
    223 
    224     @VisibleForTesting
    225     boolean initUninstallButtonForUserApp() {
    226         boolean enabled = true;
    227         final PackageInfo packageInfo = mParent.getPackageInfo();
    228         if ((packageInfo.applicationInfo.flags & ApplicationInfo.FLAG_INSTALLED) == 0
    229                 && mUserManager.getUsers().size() >= 2) {
    230             // When we have multiple users, there is a separate menu
    231             // to uninstall for all users.
    232             enabled = false;
    233         } else if (AppUtils.isInstant(packageInfo.applicationInfo)) {
    234             enabled = false;
    235             mActionButtons.setButton1Visible(false);
    236         }
    237         mActionButtons.setButton1Text(R.string.uninstall_text).setButton1Positive(false);
    238         return enabled;
    239     }
    240 
    241     @VisibleForTesting
    242     boolean handleDisableable(AppEntry appEntry, PackageInfo packageInfo) {
    243         boolean disableable = false;
    244         // Try to prevent the user from bricking their phone
    245         // by not allowing disabling of apps signed with the
    246         // system cert and any launcher app in the system.
    247         if (mHomePackages.contains(appEntry.info.packageName)
    248                 || Utils.isSystemPackage(mContext.getResources(), mPm, packageInfo)) {
    249             // Disable button for core system applications.
    250             mActionButtons
    251                     .setButton1Text(R.string.disable_text)
    252                     .setButton1Positive(false);
    253         } else if (appEntry.info.enabled && appEntry.info.enabledSetting
    254                 != PackageManager.COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED) {
    255             mActionButtons
    256                     .setButton1Text(R.string.disable_text)
    257                     .setButton1Positive(false);
    258             disableable = !mApplicationFeatureProvider.getKeepEnabledPackages()
    259                     .contains(appEntry.info.packageName);
    260         } else {
    261             mActionButtons
    262                     .setButton1Text(R.string.enable_text)
    263                     .setButton1Positive(true);
    264             disableable = true;
    265         }
    266 
    267         return disableable;
    268     }
    269 
    270     private void updateForceStopButton(boolean enabled) {
    271         final boolean disallowedBySystem = RestrictedLockUtils.hasBaseUserRestriction(
    272                 mContext, UserManager.DISALLOW_APPS_CONTROL, mUserId);
    273         mActionButtons
    274                 .setButton2Enabled(disallowedBySystem ? false : enabled)
    275                 .setButton2OnClickListener(
    276                         disallowedBySystem ? null : v -> mParent.handleForceStopButtonClick());
    277     }
    278 
    279     void checkForceStop(AppEntry appEntry, PackageInfo packageInfo) {
    280         if (mDpm.packageHasActiveAdmins(packageInfo.packageName)) {
    281             // User can't force stop device admin.
    282             Log.w(TAG, "User can't force stop device admin");
    283             updateForceStopButton(false);
    284         } else if (mPm.isPackageStateProtected(packageInfo.packageName,
    285                 UserHandle.getUserId(appEntry.info.uid))) {
    286             Log.w(TAG, "User can't force stop protected packages");
    287             updateForceStopButton(false);
    288         } else if (AppUtils.isInstant(packageInfo.applicationInfo)) {
    289             updateForceStopButton(false);
    290             mActionButtons.setButton2Visible(false);
    291         } else if ((appEntry.info.flags & ApplicationInfo.FLAG_STOPPED) == 0) {
    292             // If the app isn't explicitly stopped, then always show the
    293             // force stop button.
    294             Log.w(TAG, "App is not explicitly stopped");
    295             updateForceStopButton(true);
    296         } else {
    297             final Intent intent = new Intent(Intent.ACTION_QUERY_PACKAGE_RESTART,
    298                     Uri.fromParts("package", appEntry.info.packageName, null));
    299             intent.putExtra(Intent.EXTRA_PACKAGES, new String[] {appEntry.info.packageName});
    300             intent.putExtra(Intent.EXTRA_UID, appEntry.info.uid);
    301             intent.putExtra(Intent.EXTRA_USER_HANDLE, UserHandle.getUserId(appEntry.info.uid));
    302             Log.d(TAG, "Sending broadcast to query restart status for "
    303                     + appEntry.info.packageName);
    304             mContext.sendOrderedBroadcastAsUser(intent, UserHandle.CURRENT, null,
    305                     mCheckKillProcessesReceiver, null, Activity.RESULT_CANCELED, null, null);
    306         }
    307     }
    308 
    309     private boolean signaturesMatch(String pkg1, String pkg2) {
    310         if (pkg1 != null && pkg2 != null) {
    311             try {
    312                 return mPm.checkSignatures(pkg1, pkg2) >= PackageManager.SIGNATURE_MATCH;
    313             } catch (Exception e) {
    314                 // e.g. named alternate package not found during lookup;
    315                 // this is an expected case sometimes
    316             }
    317         }
    318         return false;
    319     }
    320 
    321 }
    322