1 /* 2 * Copyright (C) 2020 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.car.settings.storage; 18 19 import static android.content.pm.ApplicationInfo.FLAG_ALLOW_CLEAR_USER_DATA; 20 import static android.content.pm.ApplicationInfo.FLAG_SYSTEM; 21 22 import static com.android.car.settings.common.ActionButtonsPreference.ActionButtons; 23 24 import android.app.ActivityManager; 25 import android.car.drivingstate.CarUxRestrictions; 26 import android.content.Context; 27 import android.content.Intent; 28 import android.content.pm.ApplicationInfo; 29 import android.content.pm.IPackageDataObserver; 30 import android.content.pm.PackageManager; 31 import android.os.Handler; 32 import android.os.Message; 33 import android.os.UserHandle; 34 import android.os.UserManager; 35 36 import androidx.annotation.VisibleForTesting; 37 import androidx.loader.app.LoaderManager; 38 39 import com.android.car.settings.R; 40 import com.android.car.settings.common.ActionButtonInfo; 41 import com.android.car.settings.common.ActionButtonsPreference; 42 import com.android.car.settings.common.ConfirmationDialogFragment; 43 import com.android.car.settings.common.FragmentController; 44 import com.android.car.settings.common.Logger; 45 import com.android.car.settings.common.PreferenceController; 46 import com.android.settingslib.RestrictedLockUtils; 47 import com.android.settingslib.RestrictedLockUtilsInternal; 48 import com.android.settingslib.applications.ApplicationsState; 49 import com.android.settingslib.applications.StorageStatsSource; 50 51 /** 52 * Displays the action buttons to clear an applications cache and user data. 53 */ 54 public class StorageApplicationActionButtonsPreferenceController extends 55 PreferenceController<ActionButtonsPreference> implements 56 AppsStorageStatsManager.Callback { 57 private static final Logger LOG = new Logger( 58 StorageApplicationActionButtonsPreferenceController.class); 59 60 @VisibleForTesting 61 static final String CONFIRM_CLEAR_STORAGE_DIALOG_TAG = 62 "com.android.car.settings.storage.ConfirmClearStorageDialog"; 63 64 @VisibleForTesting 65 static final String CONFIRM_CANNOT_CLEAR_STORAGE_DIALOG_TAG = 66 "com.android.car.settings.storage.ConfirmCannotClearStorageDialog"; 67 68 public static final String EXTRA_PACKAGE_NAME = "extra_package_name"; 69 // Result code identifiers 70 public static final int REQUEST_MANAGE_SPACE = 2; 71 72 // Internal constants used in Handler 73 private static final int OP_SUCCESSFUL = 1; 74 private static final int OP_FAILED = 2; 75 76 // Constant used in handler to determine when the user data is cleared. 77 private static final int MSG_CLEAR_USER_DATA = 1; 78 // Constant used in handler to determine when the cache is cleared. 79 private static final int MSG_CLEAR_CACHE = 2; 80 81 private ActionButtonInfo mClearStorageButton; 82 private ActionButtonInfo mClearCacheButton; 83 84 private ApplicationsState.AppEntry mAppEntry; 85 private String mPackageName; 86 private ApplicationInfo mInfo; 87 private AppsStorageStatsManager mAppsStorageStatsManager; 88 private LoaderManager mLoaderManager; 89 90 // An observer callback to get notified when the cache file deletion is complete. 91 private ClearCacheObserver mClearCacheObserver; 92 // An observer callback to get notified when the user data deletion is complete. 93 private ClearUserDataObserver mClearDataObserver; 94 95 private PackageManager mPackageManager; 96 private RestrictedLockUtils.EnforcedAdmin mAppsControlDisallowedAdmin; 97 private boolean mAppsControlDisallowedBySystem; 98 private int mUserId; 99 100 private boolean mCacheCleared; 101 private boolean mDataCleared; 102 103 private final ConfirmationDialogFragment.ConfirmListener mConfirmClearStorageDialog = 104 arguments -> initiateClearUserData(); 105 106 private final ConfirmationDialogFragment.ConfirmListener mConfirmCannotClearStorageDialog = 107 arguments -> mClearStorageButton.setEnabled(false); 108 StorageApplicationActionButtonsPreferenceController(Context context, String preferenceKey, FragmentController fragmentController, CarUxRestrictions uxRestrictions)109 public StorageApplicationActionButtonsPreferenceController(Context context, 110 String preferenceKey, FragmentController fragmentController, 111 CarUxRestrictions uxRestrictions) { 112 super(context, preferenceKey, fragmentController, uxRestrictions); 113 mUserId = UserHandle.myUserId(); 114 mPackageManager = context.getPackageManager(); 115 } 116 117 @Override getPreferenceType()118 protected Class<ActionButtonsPreference> getPreferenceType() { 119 return ActionButtonsPreference.class; 120 } 121 122 /** 123 * Sets the {@link ApplicationsState.AppEntry} which is used to load the app name and icon. 124 */ setAppEntry( ApplicationsState.AppEntry appEntry)125 public StorageApplicationActionButtonsPreferenceController setAppEntry( 126 ApplicationsState.AppEntry appEntry) { 127 mAppEntry = appEntry; 128 return this; 129 } 130 131 /** 132 * Set the packageName, which is used to perform actions on a particular package. 133 */ setPackageName(String packageName)134 public StorageApplicationActionButtonsPreferenceController setPackageName(String packageName) { 135 mPackageName = packageName; 136 return this; 137 } 138 139 /** 140 * Sets the {@link AppsStorageStatsManager} which will be used to register the controller to the 141 * Listener {@link AppsStorageStatsManager.Callback}. 142 */ setAppsStorageStatsManager( AppsStorageStatsManager appsStorageStatsManager)143 public StorageApplicationActionButtonsPreferenceController setAppsStorageStatsManager( 144 AppsStorageStatsManager appsStorageStatsManager) { 145 mAppsStorageStatsManager = appsStorageStatsManager; 146 return this; 147 } 148 149 /** 150 * Sets the {@link LoaderManager} used to load app storage stats. 151 */ setLoaderManager( LoaderManager loaderManager)152 public StorageApplicationActionButtonsPreferenceController setLoaderManager( 153 LoaderManager loaderManager) { 154 mLoaderManager = loaderManager; 155 return this; 156 } 157 158 @Override checkInitialized()159 protected void checkInitialized() { 160 if (mAppEntry == null || mPackageName == null || mAppsStorageStatsManager == null 161 || mLoaderManager == null) { 162 throw new IllegalStateException( 163 "AppEntry, PackageName, AppStorageStatsManager, and LoaderManager should be " 164 + "set before calling this function"); 165 } 166 } 167 168 @Override onCreateInternal()169 protected void onCreateInternal() { 170 mAppsStorageStatsManager.registerListener(this); 171 172 mClearStorageButton = getPreference().getButton(ActionButtons.BUTTON1); 173 mClearCacheButton = getPreference().getButton(ActionButtons.BUTTON2); 174 175 ConfirmationDialogFragment.resetListeners( 176 (ConfirmationDialogFragment) getFragmentController().findDialogByTag( 177 CONFIRM_CLEAR_STORAGE_DIALOG_TAG), 178 mConfirmClearStorageDialog, 179 /* rejectListener= */ null, 180 /* neutralListener= */ null); 181 ConfirmationDialogFragment.resetListeners( 182 (ConfirmationDialogFragment) getFragmentController().findDialogByTag( 183 CONFIRM_CANNOT_CLEAR_STORAGE_DIALOG_TAG), 184 mConfirmCannotClearStorageDialog, 185 /* rejectListener= */ null, 186 /* neutralListener= */ null); 187 188 mClearStorageButton 189 .setText(R.string.storage_clear_user_data_text) 190 .setIcon(R.drawable.ic_delete) 191 .setOnClickListener(i -> handleClearDataClick()) 192 .setEnabled(false); 193 mClearCacheButton 194 .setText(R.string.storage_clear_cache_btn_text) 195 .setIcon(R.drawable.ic_delete) 196 .setOnClickListener(i -> handleClearCacheClick()) 197 .setEnabled(false); 198 } 199 200 @Override onStartInternal()201 protected void onStartInternal() { 202 mAppsControlDisallowedAdmin = RestrictedLockUtilsInternal.checkIfRestrictionEnforced( 203 getContext(), UserManager.DISALLOW_APPS_CONTROL, mUserId); 204 mAppsControlDisallowedBySystem = RestrictedLockUtilsInternal.hasBaseUserRestriction( 205 getContext(), UserManager.DISALLOW_APPS_CONTROL, mUserId); 206 } 207 208 @Override updateState(ActionButtonsPreference preference)209 protected void updateState(ActionButtonsPreference preference) { 210 try { 211 mInfo = mPackageManager.getApplicationInfo(mPackageName, 0); 212 } catch (PackageManager.NameNotFoundException e) { 213 LOG.e("Could not find package", e); 214 } 215 if (mInfo == null) { 216 return; 217 } 218 mAppsStorageStatsManager.startLoading(mLoaderManager, mInfo, mUserId, mCacheCleared, 219 mDataCleared); 220 } 221 222 @Override onDataLoaded(StorageStatsSource.AppStorageStats data, boolean cacheCleared, boolean dataCleared)223 public void onDataLoaded(StorageStatsSource.AppStorageStats data, boolean cacheCleared, 224 boolean dataCleared) { 225 if (data == null || mAppsControlDisallowedBySystem) { 226 mClearStorageButton.setEnabled(false); 227 mClearCacheButton.setEnabled(false); 228 } else { 229 long cacheSize = data.getCacheBytes(); 230 long dataSize = data.getDataBytes() - cacheSize; 231 232 mClearStorageButton.setEnabled( 233 dataSize > 0 && !mDataCleared && clearDataAllowedBySystemFlag()); 234 mClearCacheButton.setEnabled(cacheSize > 0 && !mCacheCleared); 235 } 236 } 237 238 @VisibleForTesting setPackageManager(PackageManager packageManager)239 void setPackageManager(PackageManager packageManager) { 240 mPackageManager = packageManager; 241 } 242 243 @VisibleForTesting setAppsControlDisallowedAdmin(RestrictedLockUtils.EnforcedAdmin admin)244 void setAppsControlDisallowedAdmin(RestrictedLockUtils.EnforcedAdmin admin) { 245 mAppsControlDisallowedAdmin = admin; 246 } 247 248 @VisibleForTesting setAppsControlDisallowedBySystem(boolean disallowed)249 void setAppsControlDisallowedBySystem(boolean disallowed) { 250 mAppsControlDisallowedBySystem = disallowed; 251 } 252 handleClearCacheClick()253 private void handleClearCacheClick() { 254 if (mAppsControlDisallowedAdmin != null && !mAppsControlDisallowedBySystem) { 255 RestrictedLockUtils.sendShowAdminSupportDetailsIntent( 256 getContext(), mAppsControlDisallowedAdmin); 257 return; 258 } 259 // Lazy initialization of observer. 260 if (mClearCacheObserver == null) { 261 mClearCacheObserver = new ClearCacheObserver(); 262 } 263 mPackageManager.deleteApplicationCacheFiles(mPackageName, mClearCacheObserver); 264 } 265 handleClearDataClick()266 private void handleClearDataClick() { 267 if (mAppsControlDisallowedAdmin != null && !mAppsControlDisallowedBySystem) { 268 RestrictedLockUtils.sendShowAdminSupportDetailsIntent( 269 getContext(), mAppsControlDisallowedAdmin); 270 } else { 271 Intent intent = new Intent(Intent.ACTION_DEFAULT); 272 boolean isManageSpaceActivityAvailable = false; 273 if (mAppEntry.info.manageSpaceActivityName != null) { 274 intent.setClassName(mAppEntry.info.packageName, 275 mAppEntry.info.manageSpaceActivityName); 276 isManageSpaceActivityAvailable = mPackageManager.resolveActivity( 277 intent, /* flags= */ 0) != null; 278 } 279 280 if (isManageSpaceActivityAvailable) { 281 getFragmentController().startActivityForResult(intent, 282 REQUEST_MANAGE_SPACE, /* callback= */ null); 283 } else { 284 showClearDataDialog(); 285 } 286 } 287 } 288 289 /** 290 * Clearing data can only be disabled for system apps. For all non-system apps it is enabled. 291 * System apps disable it explicitly via the android:allowClearUserData tag. 292 */ clearDataAllowedBySystemFlag()293 private boolean clearDataAllowedBySystemFlag() { 294 boolean sysApp = (mAppEntry.info.flags & FLAG_SYSTEM) == FLAG_SYSTEM; 295 boolean allowClearData = 296 (mAppEntry.info.flags & FLAG_ALLOW_CLEAR_USER_DATA) == FLAG_ALLOW_CLEAR_USER_DATA; 297 return !sysApp || allowClearData; 298 } 299 300 301 /* 302 * Private method to initiate clearing user data when the user clicks the clear data 303 * button for a system package 304 */ initiateClearUserData()305 private void initiateClearUserData() { 306 mClearStorageButton.setEnabled(false); 307 // Invoke uninstall or clear user data based on sysPackage 308 String packageName = mAppEntry.info.packageName; 309 LOG.i("Clearing user data for package : " + packageName); 310 if (mClearDataObserver == null) { 311 mClearDataObserver = new ClearUserDataObserver(); 312 } 313 ActivityManager am = (ActivityManager) 314 getContext().getSystemService(Context.ACTIVITY_SERVICE); 315 boolean res = am.clearApplicationUserData(packageName, mClearDataObserver); 316 if (!res) { 317 // Clearing data failed for some obscure reason. Just log error for now 318 LOG.i("Couldn't clear application user data for package:" + packageName); 319 showCannotClearDataDialog(); 320 } 321 } 322 323 /* 324 * Private method to handle clear message notification from observer when 325 * the async operation from PackageManager is complete 326 */ processClearMsg(Message msg)327 private void processClearMsg(Message msg) { 328 int result = msg.arg1; 329 String packageName = mAppEntry.info.packageName; 330 if (result == OP_SUCCESSFUL) { 331 LOG.i("Cleared user data for package : " + packageName); 332 refreshUi(); 333 } else { 334 mClearStorageButton.setEnabled(true); 335 } 336 } 337 showClearDataDialog()338 private void showClearDataDialog() { 339 ConfirmationDialogFragment confirmClearStorageDialog = 340 new ConfirmationDialogFragment.Builder(getContext()) 341 .setTitle(R.string.storage_clear_user_data_text) 342 .setMessage(getContext().getString(R.string.storage_clear_data_dlg_text)) 343 .setPositiveButton(R.string.okay, mConfirmClearStorageDialog) 344 .setNegativeButton(android.R.string.cancel, /* rejectListener= */ null) 345 .build(); 346 getFragmentController().showDialog(confirmClearStorageDialog, 347 CONFIRM_CLEAR_STORAGE_DIALOG_TAG); 348 } 349 showCannotClearDataDialog()350 private void showCannotClearDataDialog() { 351 ConfirmationDialogFragment dialogFragment = 352 new ConfirmationDialogFragment.Builder(getContext()) 353 .setTitle(R.string.storage_clear_data_dlg_title) 354 .setMessage(getContext().getString(R.string.storage_clear_failed_dlg_text)) 355 .setPositiveButton(R.string.okay, mConfirmCannotClearStorageDialog) 356 .build(); 357 getFragmentController().showDialog(dialogFragment, CONFIRM_CANNOT_CLEAR_STORAGE_DIALOG_TAG); 358 } 359 360 private final Handler mHandler = new Handler() { 361 public void handleMessage(Message msg) { 362 switch (msg.what) { 363 case MSG_CLEAR_USER_DATA: 364 mDataCleared = true; 365 mCacheCleared = true; 366 processClearMsg(msg); 367 break; 368 case MSG_CLEAR_CACHE: 369 mCacheCleared = true; 370 // Refresh info 371 refreshUi(); 372 break; 373 } 374 } 375 }; 376 377 class ClearCacheObserver extends IPackageDataObserver.Stub { onRemoveCompleted(final String packageName, final boolean succeeded)378 public void onRemoveCompleted(final String packageName, final boolean succeeded) { 379 Message msg = mHandler.obtainMessage(MSG_CLEAR_CACHE); 380 msg.arg1 = succeeded ? OP_SUCCESSFUL : OP_FAILED; 381 mHandler.sendMessage(msg); 382 } 383 } 384 385 class ClearUserDataObserver extends IPackageDataObserver.Stub { onRemoveCompleted(final String packageName, final boolean succeeded)386 public void onRemoveCompleted(final String packageName, final boolean succeeded) { 387 Message msg = mHandler.obtainMessage(MSG_CLEAR_USER_DATA); 388 msg.arg1 = succeeded ? OP_SUCCESSFUL : OP_FAILED; 389 mHandler.sendMessage(msg); 390 } 391 } 392 } 393