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