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