1 /* 2 * Copyright (C) 2019 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 android.app.ActivityManager; 20 import android.car.userlib.CarUserManagerHelper; 21 import android.content.Context; 22 import android.content.Intent; 23 import android.content.pm.ApplicationInfo; 24 import android.content.pm.IPackageDataObserver; 25 import android.content.pm.PackageManager; 26 import android.os.Bundle; 27 import android.os.Handler; 28 import android.os.Message; 29 import android.os.UserHandle; 30 import android.os.UserManager; 31 32 import androidx.annotation.VisibleForTesting; 33 import androidx.annotation.XmlRes; 34 import androidx.loader.app.LoaderManager; 35 36 import com.android.car.settings.R; 37 import com.android.car.settings.common.ConfirmationDialogFragment; 38 import com.android.car.settings.common.Logger; 39 import com.android.car.settings.common.SettingsFragment; 40 import com.android.car.ui.toolbar.MenuItem; 41 import com.android.settingslib.RestrictedLockUtils; 42 import com.android.settingslib.RestrictedLockUtilsInternal; 43 import com.android.settingslib.applications.ApplicationsState; 44 import com.android.settingslib.applications.StorageStatsSource; 45 46 import java.util.Arrays; 47 import java.util.List; 48 49 /** 50 * Fragment to display the applications storage information. Also provide buttons to clear the 51 * applications cache data and user data. 52 */ 53 public class AppStorageSettingsDetailsFragment extends SettingsFragment implements 54 AppsStorageStatsManager.Callback { 55 private static final Logger LOG = new Logger(AppStorageSettingsDetailsFragment.class); 56 57 @VisibleForTesting 58 static final String CONFIRM_CLEAR_STORAGE_DIALOG_TAG = 59 "com.android.car.settings.storage.ConfirmClearStorageDialog"; 60 61 @VisibleForTesting 62 static final String CONFIRM_CANNOT_CLEAR_STORAGE_DIALOG_TAG = 63 "com.android.car.settings.storage.ConfirmCannotClearStorageDialog"; 64 65 public static final String EXTRA_PACKAGE_NAME = "extra_package_name"; 66 // Result code identifiers 67 public static final int REQUEST_MANAGE_SPACE = 2; 68 69 // Internal constants used in Handler 70 private static final int OP_SUCCESSFUL = 1; 71 private static final int OP_FAILED = 2; 72 73 // Constant used in handler to determine when the user data is cleared. 74 private static final int MSG_CLEAR_USER_DATA = 1; 75 // Constant used in handler to determine when the cache is cleared. 76 private static final int MSG_CLEAR_CACHE = 2; 77 78 // Keys to save the instance values. 79 private static final String KEY_CACHE_CLEARED = "cache_cleared"; 80 private static final String KEY_DATA_CLEARED = "data_cleared"; 81 82 // Package information 83 protected PackageManager mPackageManager; 84 private String mPackageName; 85 86 // Application state info 87 private ApplicationsState.AppEntry mAppEntry; 88 private ApplicationsState mAppState; 89 private ApplicationInfo mInfo; 90 private AppsStorageStatsManager mAppsStorageStatsManager; 91 92 // User info 93 private int mUserId; 94 private CarUserManagerHelper mCarUserManagerHelper; 95 96 // An observer callback to get notified when the cache file deletion is complete. 97 private ClearCacheObserver mClearCacheObserver; 98 // An observer callback to get notified when the user data deletion is complete. 99 private ClearUserDataObserver mClearDataObserver; 100 101 // The restriction enforced by admin. 102 private RestrictedLockUtils.EnforcedAdmin mAppsControlDisallowedAdmin; 103 private boolean mAppsControlDisallowedBySystem; 104 105 // Clear user data and cache buttons and state. 106 private MenuItem mClearStorageButton; 107 private MenuItem mClearCacheButton; 108 private boolean mCanClearData = true; 109 private boolean mCacheCleared; 110 private boolean mDataCleared; 111 112 private final ConfirmationDialogFragment.ConfirmListener mConfirmClearStorageDialog = 113 arguments -> initiateClearUserData(); 114 115 116 private final ConfirmationDialogFragment.ConfirmListener mConfirmCannotClearStorageDialog = 117 arguments -> mClearStorageButton.setEnabled(false); 118 119 /** Creates an instance of this fragment, passing packageName as an argument. */ getInstance(String packageName)120 public static AppStorageSettingsDetailsFragment getInstance(String packageName) { 121 AppStorageSettingsDetailsFragment applicationDetailFragment = 122 new AppStorageSettingsDetailsFragment(); 123 Bundle bundle = new Bundle(); 124 bundle.putString(EXTRA_PACKAGE_NAME, packageName); 125 applicationDetailFragment.setArguments(bundle); 126 return applicationDetailFragment; 127 } 128 129 @Override 130 @XmlRes getPreferenceScreenResId()131 protected int getPreferenceScreenResId() { 132 return R.xml.app_storage_settings_details_fragment; 133 } 134 135 @Override onAttach(Context context)136 public void onAttach(Context context) { 137 super.onAttach(context); 138 mCarUserManagerHelper = new CarUserManagerHelper(context); 139 mUserId = mCarUserManagerHelper.getCurrentProcessUserId(); 140 mPackageName = getArguments().getString(EXTRA_PACKAGE_NAME); 141 mAppState = ApplicationsState.getInstance(requireActivity().getApplication()); 142 mAppEntry = mAppState.getEntry(mPackageName, mUserId); 143 StorageStatsSource storageStatsSource = new StorageStatsSource(context); 144 StorageStatsSource.AppStorageStats stats = null; 145 mPackageManager = context.getPackageManager(); 146 try { 147 stats = storageStatsSource.getStatsForPackage(/* volumeUuid= */ null, mPackageName, 148 UserHandle.of(mUserId)); 149 } catch (Exception e) { 150 // This may happen if the package was removed during our calculation. 151 LOG.w("App unexpectedly not found", e); 152 } 153 mAppsStorageStatsManager = new AppsStorageStatsManager(context); 154 mAppsStorageStatsManager.registerListener(this); 155 use(StorageApplicationPreferenceController.class, 156 R.string.pk_storage_application_details) 157 .setAppEntry(mAppEntry) 158 .setAppState(mAppState); 159 160 List<StorageSizeBasePreferenceController> preferenceControllers = Arrays.asList( 161 use(StorageApplicationSizePreferenceController.class, 162 R.string.pk_storage_application_size), 163 use(StorageApplicationTotalSizePreferenceController.class, 164 R.string.pk_storage_application_total_size), 165 use(StorageApplicationUserDataPreferenceController.class, 166 R.string.pk_storage_application_data_size), 167 use(StorageApplicationCacheSizePreferenceController.class, 168 R.string.pk_storage_application_cache_size) 169 ); 170 171 for (StorageSizeBasePreferenceController pc : preferenceControllers) { 172 pc.setAppsStorageStatsManager(mAppsStorageStatsManager); 173 pc.setAppStorageStats(stats); 174 } 175 } 176 177 @Override getToolbarMenuItems()178 public List<MenuItem> getToolbarMenuItems() { 179 return Arrays.asList(mClearStorageButton, mClearCacheButton); 180 } 181 182 @Override onSaveInstanceState(Bundle outState)183 public void onSaveInstanceState(Bundle outState) { 184 super.onSaveInstanceState(outState); 185 outState.putBoolean(KEY_CACHE_CLEARED, mCacheCleared); 186 outState.putBoolean(KEY_DATA_CLEARED, mDataCleared); 187 } 188 189 @Override onCreate(Bundle savedInstanceState)190 public void onCreate(Bundle savedInstanceState) { 191 super.onCreate(savedInstanceState); 192 if (savedInstanceState != null) { 193 mCacheCleared = savedInstanceState.getBoolean(KEY_CACHE_CLEARED, false); 194 mDataCleared = savedInstanceState.getBoolean(KEY_DATA_CLEARED, false); 195 mCacheCleared = mCacheCleared || mDataCleared; 196 } 197 ConfirmationDialogFragment.resetListeners( 198 (ConfirmationDialogFragment) findDialogByTag(CONFIRM_CLEAR_STORAGE_DIALOG_TAG), 199 mConfirmClearStorageDialog, /* rejectListener= */ null); 200 ConfirmationDialogFragment.resetListeners( 201 (ConfirmationDialogFragment) findDialogByTag( 202 CONFIRM_CANNOT_CLEAR_STORAGE_DIALOG_TAG), 203 mConfirmCannotClearStorageDialog, /* rejectListener= */ null); 204 205 mClearStorageButton = new MenuItem.Builder(getContext()) 206 .setTitle(R.string.storage_clear_user_data_text) 207 .setOnClickListener(i -> handleClearDataClick()) 208 .setEnabled(false) 209 .build(); 210 mClearCacheButton = new MenuItem.Builder(getContext()) 211 .setTitle(R.string.storage_clear_cache_btn_text) 212 .setOnClickListener(i -> handleClearCacheClick()) 213 .setEnabled(false) 214 .build(); 215 } 216 217 @Override onResume()218 public void onResume() { 219 super.onResume(); 220 mAppsControlDisallowedAdmin = RestrictedLockUtilsInternal.checkIfRestrictionEnforced( 221 getActivity(), UserManager.DISALLOW_APPS_CONTROL, mUserId); 222 mAppsControlDisallowedBySystem = RestrictedLockUtilsInternal.hasBaseUserRestriction( 223 getActivity(), UserManager.DISALLOW_APPS_CONTROL, mUserId); 224 updateSize(); 225 } 226 227 @Override onDataLoaded(StorageStatsSource.AppStorageStats data, boolean cacheCleared, boolean dataCleared)228 public void onDataLoaded(StorageStatsSource.AppStorageStats data, boolean cacheCleared, 229 boolean dataCleared) { 230 if (data == null || mAppsControlDisallowedBySystem) { 231 mClearStorageButton.setEnabled(false); 232 mClearCacheButton.setEnabled(false); 233 } else { 234 long cacheSize = data.getCacheBytes(); 235 long dataSize = data.getDataBytes() - cacheSize; 236 237 mClearStorageButton.setEnabled(dataSize > 0 && mCanClearData && !mDataCleared); 238 mClearCacheButton.setEnabled(cacheSize > 0 && !mCacheCleared); 239 } 240 } 241 handleClearCacheClick()242 private void handleClearCacheClick() { 243 if (mAppsControlDisallowedAdmin != null && !mAppsControlDisallowedBySystem) { 244 RestrictedLockUtils.sendShowAdminSupportDetailsIntent( 245 getActivity(), mAppsControlDisallowedAdmin); 246 return; 247 } 248 // Lazy initialization of observer. 249 if (mClearCacheObserver == null) { 250 mClearCacheObserver = new ClearCacheObserver(); 251 } 252 mPackageManager.deleteApplicationCacheFiles(mPackageName, mClearCacheObserver); 253 } 254 handleClearDataClick()255 private void handleClearDataClick() { 256 if (mAppsControlDisallowedAdmin != null && !mAppsControlDisallowedBySystem) { 257 RestrictedLockUtils.sendShowAdminSupportDetailsIntent( 258 getActivity(), mAppsControlDisallowedAdmin); 259 } else if (mAppEntry.info.manageSpaceActivityName != null) { 260 Intent intent = new Intent(Intent.ACTION_DEFAULT); 261 intent.setClassName(mAppEntry.info.packageName, 262 mAppEntry.info.manageSpaceActivityName); 263 startActivityForResult(intent, REQUEST_MANAGE_SPACE); 264 } else { 265 showClearDataDialog(); 266 } 267 } 268 269 /* 270 * Private method to initiate clearing user data when the user clicks the clear data 271 * button for a system package 272 */ initiateClearUserData()273 private void initiateClearUserData() { 274 mClearStorageButton.setEnabled(false); 275 // Invoke uninstall or clear user data based on sysPackage 276 String packageName = mAppEntry.info.packageName; 277 LOG.i("Clearing user data for package : " + packageName); 278 if (mClearDataObserver == null) { 279 mClearDataObserver = new ClearUserDataObserver(); 280 } 281 ActivityManager am = (ActivityManager) 282 getActivity().getSystemService(Context.ACTIVITY_SERVICE); 283 boolean res = am.clearApplicationUserData(packageName, mClearDataObserver); 284 if (!res) { 285 // Clearing data failed for some obscure reason. Just log error for now 286 LOG.i("Couldn't clear application user data for package:" + packageName); 287 showCannotClearDataDialog(); 288 } 289 } 290 291 /* 292 * Private method to handle clear message notification from observer when 293 * the async operation from PackageManager is complete 294 */ processClearMsg(Message msg)295 private void processClearMsg(Message msg) { 296 int result = msg.arg1; 297 String packageName = mAppEntry.info.packageName; 298 if (result == OP_SUCCESSFUL) { 299 LOG.i("Cleared user data for package : " + packageName); 300 updateSize(); 301 } else { 302 mClearStorageButton.setEnabled(true); 303 } 304 } 305 updateSize()306 private void updateSize() { 307 PackageManager packageManager = getActivity().getPackageManager(); 308 try { 309 mInfo = packageManager.getApplicationInfo(mPackageName, 0); 310 } catch (PackageManager.NameNotFoundException e) { 311 LOG.e("Could not find package", e); 312 } 313 if (mInfo == null) { 314 return; 315 } 316 LoaderManager loaderManager = LoaderManager.getInstance(this); 317 mAppsStorageStatsManager.startLoading(loaderManager, mInfo, mUserId, mCacheCleared, 318 mDataCleared); 319 } 320 showClearDataDialog()321 private void showClearDataDialog() { 322 ConfirmationDialogFragment confirmClearStorageDialog = 323 new ConfirmationDialogFragment.Builder(getContext()) 324 .setTitle(R.string.storage_clear_user_data_text) 325 .setMessage(getString(R.string.storage_clear_data_dlg_text)) 326 .setPositiveButton(R.string.okay, mConfirmClearStorageDialog) 327 .setNegativeButton(android.R.string.cancel, /* rejectListener= */ null) 328 .build(); 329 showDialog(confirmClearStorageDialog, CONFIRM_CLEAR_STORAGE_DIALOG_TAG); 330 } 331 showCannotClearDataDialog()332 private void showCannotClearDataDialog() { 333 ConfirmationDialogFragment dialogFragment = 334 new ConfirmationDialogFragment.Builder(getContext()) 335 .setTitle(R.string.storage_clear_data_dlg_title) 336 .setMessage(getString(R.string.storage_clear_failed_dlg_text)) 337 .setPositiveButton(R.string.okay, mConfirmCannotClearStorageDialog) 338 .build(); 339 showDialog(dialogFragment, CONFIRM_CANNOT_CLEAR_STORAGE_DIALOG_TAG); 340 } 341 342 private final Handler mHandler = new Handler() { 343 public void handleMessage(Message msg) { 344 if (getView() == null) { 345 return; 346 } 347 switch (msg.what) { 348 case MSG_CLEAR_USER_DATA: 349 mDataCleared = true; 350 mCacheCleared = true; 351 processClearMsg(msg); 352 break; 353 case MSG_CLEAR_CACHE: 354 mCacheCleared = true; 355 // Refresh size info 356 updateSize(); 357 break; 358 } 359 } 360 }; 361 362 class ClearCacheObserver extends IPackageDataObserver.Stub { onRemoveCompleted(final String packageName, final boolean succeeded)363 public void onRemoveCompleted(final String packageName, final boolean succeeded) { 364 Message msg = mHandler.obtainMessage(MSG_CLEAR_CACHE); 365 msg.arg1 = succeeded ? OP_SUCCESSFUL : OP_FAILED; 366 mHandler.sendMessage(msg); 367 } 368 } 369 370 class ClearUserDataObserver extends IPackageDataObserver.Stub { onRemoveCompleted(final String packageName, final boolean succeeded)371 public void onRemoveCompleted(final String packageName, final boolean succeeded) { 372 Message msg = mHandler.obtainMessage(MSG_CLEAR_USER_DATA); 373 msg.arg1 = succeeded ? OP_SUCCESSFUL : OP_FAILED; 374 mHandler.sendMessage(msg); 375 } 376 } 377 } 378