1 /* 2 * Copyright (C) 2016 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.storagemanager.deletionhelper; 18 19 import android.Manifest; 20 import android.app.Activity; 21 import android.content.Context; 22 import android.content.Intent; 23 import android.content.pm.PackageManager; 24 import android.os.Bundle; 25 import android.os.storage.StorageManager; 26 import androidx.annotation.VisibleForTesting; 27 import androidx.preference.PreferenceFragment; 28 import androidx.preference.Preference; 29 import androidx.preference.PreferenceScreen; 30 import android.text.format.Formatter; 31 import android.view.LayoutInflater; 32 import android.view.Menu; 33 import android.view.MenuInflater; 34 import android.view.View; 35 import android.view.ViewGroup; 36 import android.widget.Button; 37 import com.android.internal.logging.MetricsLogger; 38 import com.android.internal.logging.nano.MetricsProto.MetricsEvent; 39 import com.android.internal.util.Preconditions; 40 import com.android.settingslib.HelpUtils; 41 import com.android.settingslib.applications.AppUtils; 42 import com.android.storagemanager.ButtonBarProvider; 43 import com.android.storagemanager.R; 44 import com.android.storagemanager.overlay.DeletionHelperFeatureProvider; 45 import com.android.storagemanager.overlay.FeatureFactory; 46 import java.util.ArrayList; 47 import java.util.HashSet; 48 import java.util.List; 49 50 /** 51 * Settings screen for the deletion helper, which manually removes data which is not recently used. 52 */ 53 public class DeletionHelperSettings extends PreferenceFragment 54 implements DeletionType.FreeableChangedListener, View.OnClickListener { 55 public static final boolean COUNT_UNCHECKED = true; 56 public static final boolean COUNT_CHECKED_ONLY = false; 57 58 protected static final String APPS_KEY = "apps_group"; 59 protected static final String KEY_DOWNLOADS_PREFERENCE = "delete_downloads"; 60 protected static final String KEY_PHOTOS_VIDEOS_PREFERENCE = "delete_photos"; 61 protected static final String KEY_GAUGE_PREFERENCE = "deletion_gauge"; 62 63 private static final String THRESHOLD_KEY = "threshold_key"; 64 private static final int DOWNLOADS_LOADER_ID = 1; 65 private static final int NUM_DELETION_TYPES = 3; 66 private static final long UNSET = -1; 67 68 private List<DeletionType> mDeletableContentList; 69 private AppDeletionPreferenceGroup mApps; 70 @VisibleForTesting AppDeletionType mAppBackend; 71 @VisibleForTesting DownloadsDeletionPreferenceGroup mDownloadsPreference; 72 private DownloadsDeletionType mDownloadsDeletion; 73 private PhotosDeletionPreference mPhotoPreference; 74 private Preference mGaugePreference; 75 private DeletionType mPhotoVideoDeletion; 76 private Button mCancel, mFree; 77 private DeletionHelperFeatureProvider mProvider; 78 private int mThresholdType; 79 @VisibleForTesting long mBytesToFree = UNSET; 80 private int mResult; 81 private LoadingSpinnerController mLoadingController; 82 newInstance(int thresholdType)83 public static DeletionHelperSettings newInstance(int thresholdType) { 84 DeletionHelperSettings instance = new DeletionHelperSettings(); 85 Bundle bundle = new Bundle(1); 86 bundle.putInt(THRESHOLD_KEY, thresholdType); 87 instance.setArguments(bundle); 88 return instance; 89 } 90 91 @Override onCreatePreferences(Bundle savedInstanceState, String rootKey)92 public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { 93 addPreferencesFromResource(R.xml.deletion_helper_list); 94 mThresholdType = getArguments().getInt(THRESHOLD_KEY, AppsAsyncLoader.NORMAL_THRESHOLD); 95 mApps = (AppDeletionPreferenceGroup) findPreference(APPS_KEY); 96 mPhotoPreference = (PhotosDeletionPreference) findPreference(KEY_PHOTOS_VIDEOS_PREFERENCE); 97 mProvider = FeatureFactory.getFactory(getActivity()).getDeletionHelperFeatureProvider(); 98 mLoadingController = new LoadingSpinnerController((DeletionHelperActivity) getActivity()); 99 if (mProvider != null) { 100 mPhotoVideoDeletion = 101 mProvider.createPhotoVideoDeletionType(getContext(), mThresholdType); 102 } 103 104 HashSet<String> checkedApplications = null; 105 if (savedInstanceState != null) { 106 checkedApplications = 107 (HashSet<String>) savedInstanceState.getSerializable( 108 AppDeletionType.EXTRA_CHECKED_SET); 109 } 110 mAppBackend = new AppDeletionType(this, checkedApplications, mThresholdType); 111 mAppBackend.registerView(mApps); 112 mAppBackend.registerFreeableChangedListener(this); 113 mApps.setDeletionType(mAppBackend); 114 115 mDeletableContentList = new ArrayList<>(NUM_DELETION_TYPES); 116 117 mGaugePreference = findPreference(KEY_GAUGE_PREFERENCE); 118 Activity activity = getActivity(); 119 if (activity != null && mGaugePreference != null) { 120 Intent intent = activity.getIntent(); 121 if (intent != null) { 122 CharSequence gaugeTitle = 123 getGaugeString(getContext(), intent, activity.getCallingPackage()); 124 if (gaugeTitle != null) { 125 mGaugePreference.setTitle(gaugeTitle); 126 127 long requestedBytes = 128 intent.getLongExtra(StorageManager.EXTRA_REQUESTED_BYTES, UNSET); 129 mBytesToFree = requestedBytes; 130 } else { 131 getPreferenceScreen().removePreference(mGaugePreference); 132 } 133 } 134 } 135 } 136 getGaugeString( Context context, Intent intent, String packageName)137 protected static CharSequence getGaugeString( 138 Context context, Intent intent, String packageName) { 139 Preconditions.checkNotNull(intent); 140 long requestedBytes = intent.getLongExtra(StorageManager.EXTRA_REQUESTED_BYTES, UNSET); 141 if (requestedBytes > 0) { 142 CharSequence callerLabel = 143 AppUtils.getApplicationLabel(context.getPackageManager(), packageName); 144 // I really hope this isn't the case, but I can't ignore the possibility that we cannot 145 // determine what app the referrer is. 146 if (callerLabel == null) { 147 return null; 148 } 149 return context.getString( 150 R.string.app_requesting_space, 151 callerLabel, 152 Formatter.formatFileSize(context, requestedBytes)); 153 } 154 return null; 155 } 156 157 @Override onActivityCreated(Bundle savedInstanceState)158 public void onActivityCreated(Bundle savedInstanceState) { 159 super.onActivityCreated(savedInstanceState); 160 initializeButtons(); 161 setHasOptionsMenu(true); 162 Activity activity = getActivity(); 163 if (activity.checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE) 164 != PackageManager.PERMISSION_GRANTED) { 165 activity.requestPermissions( 166 new String[] {Manifest.permission.READ_EXTERNAL_STORAGE}, 167 0); 168 } 169 170 if (mProvider != null && mPhotoVideoDeletion != null) { 171 mPhotoPreference.setDaysToKeep(mProvider.getDaysToKeep(mThresholdType)); 172 mPhotoPreference.registerFreeableChangedListener(this); 173 mPhotoPreference.registerDeletionService(mPhotoVideoDeletion); 174 mDeletableContentList.add(mPhotoVideoDeletion); 175 } else { 176 getPreferenceScreen().removePreference(mPhotoPreference); 177 mPhotoPreference.setEnabled(false); 178 } 179 180 String[] uncheckedFiles = null; 181 if (savedInstanceState != null) { 182 uncheckedFiles = 183 savedInstanceState.getStringArray( 184 DownloadsDeletionType.EXTRA_UNCHECKED_DOWNLOADS); 185 } 186 mDownloadsPreference = 187 (DownloadsDeletionPreferenceGroup) findPreference(KEY_DOWNLOADS_PREFERENCE); 188 mDownloadsDeletion = new DownloadsDeletionType(getActivity(), uncheckedFiles); 189 mDownloadsPreference.registerFreeableChangedListener(this); 190 mDownloadsPreference.registerDeletionService(mDownloadsDeletion); 191 mDeletableContentList.add(mDownloadsDeletion); 192 if (isEmptyState()) { 193 setupEmptyState(); 194 } 195 mDeletableContentList.add(mAppBackend); 196 updateFreeButtonText(); 197 } 198 199 @VisibleForTesting setupEmptyState()200 void setupEmptyState() { 201 final PreferenceScreen screen = getPreferenceScreen(); 202 if (mDownloadsPreference != null) { 203 mDownloadsPreference.setChecked(false); 204 screen.removePreference(mDownloadsPreference); 205 } 206 screen.removePreference(mApps); 207 208 // Nulling out the downloads preferences means we won't accidentally delete what isn't 209 // visible. 210 mDownloadsDeletion = null; 211 mDownloadsPreference = null; 212 } 213 isEmptyState()214 private boolean isEmptyState() { 215 // We know we are in the empty state if our loader is not using a threshold. 216 return mThresholdType == AppsAsyncLoader.NO_THRESHOLD; 217 } 218 219 @Override onResume()220 public void onResume() { 221 super.onResume(); 222 223 mLoadingController.initializeLoading(getListView()); 224 225 for (int i = 0, size = mDeletableContentList.size(); i < size; i++) { 226 mDeletableContentList.get(i).onResume(); 227 } 228 229 if (mDownloadsDeletion != null 230 && getActivity().checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE) 231 == PackageManager.PERMISSION_GRANTED) { 232 getLoaderManager().initLoader(DOWNLOADS_LOADER_ID, new Bundle(), mDownloadsDeletion); 233 } 234 } 235 236 @Override onPause()237 public void onPause() { 238 super.onPause(); 239 for (int i = 0, size = mDeletableContentList.size(); i < size; i++) { 240 mDeletableContentList.get(i).onPause(); 241 } 242 } 243 244 @Override onSaveInstanceState(Bundle outState)245 public void onSaveInstanceState(Bundle outState) { 246 super.onSaveInstanceState(outState); 247 for (int i = 0, size = mDeletableContentList.size(); i < size; i++) { 248 mDeletableContentList.get(i).onSaveInstanceStateBundle(outState); 249 } 250 } 251 252 @Override onFreeableChanged(int numItems, long bytesFreeable)253 public void onFreeableChanged(int numItems, long bytesFreeable) { 254 if (numItems > 0 || bytesFreeable > 0 || allTypesEmpty()) { 255 if (mLoadingController != null) { 256 mLoadingController.onCategoryLoad(); 257 } 258 } 259 260 // bytesFreeable is the number of bytes freed by a single deletion type. If it is non-zero, 261 // there is stuff to free and we can enable it. If it is zero, though, we still need to get 262 // getTotalFreeableSpace to check all deletion types. 263 if (mFree != null) { 264 mFree.setEnabled(bytesFreeable != 0 || getTotalFreeableSpace(COUNT_CHECKED_ONLY) != 0); 265 } 266 updateFreeButtonText(); 267 268 // Transition to empty state if all types have reported there is nothing to delete. Skip 269 // the transition if we are already in no threshold mode 270 if (allTypesEmpty() && !isEmptyState()) { 271 startEmptyState(); 272 } 273 } 274 allTypesEmpty()275 private boolean allTypesEmpty() { 276 return mAppBackend.isEmpty() 277 && (mDownloadsDeletion == null || mDownloadsDeletion.isEmpty()) 278 && (mPhotoVideoDeletion == null || mPhotoVideoDeletion.isEmpty()); 279 } 280 startEmptyState()281 private void startEmptyState() { 282 if (getActivity() instanceof DeletionHelperActivity) { 283 DeletionHelperActivity activity = (DeletionHelperActivity) getActivity(); 284 activity.setIsEmptyState(true /* isEmptyState */); 285 } 286 } 287 288 /** Clears out the selected apps and data from the device and closes the fragment. */ clearData()289 protected void clearData() { 290 long bytesFreed = getTotalFreeableSpace(COUNT_CHECKED_ONLY); 291 if (mBytesToFree != UNSET && bytesFreed >= mBytesToFree) { 292 setResultCode(Activity.RESULT_OK); 293 } 294 295 // This should be fine as long as there is only one extra deletion feature. 296 // In the future, this should be done in an async queue in order to not 297 // interfere with the simultaneous PackageDeletionTask. 298 Activity activity = getActivity(); 299 if (mPhotoPreference != null && mPhotoPreference.isChecked()) { 300 mPhotoVideoDeletion.clearFreeableData(activity); 301 } 302 if (mDownloadsPreference != null) { 303 mDownloadsDeletion.clearFreeableData(activity); 304 } 305 if (mAppBackend != null) { 306 mAppBackend.clearFreeableData(activity); 307 } 308 } 309 310 @Override onClick(View v)311 public void onClick(View v) { 312 if (v.getId() == R.id.next_button) { 313 ConfirmDeletionDialog dialog = 314 ConfirmDeletionDialog.newInstance(getTotalFreeableSpace(COUNT_CHECKED_ONLY)); 315 // The 0 is a placeholder for an optional result code. 316 dialog.setTargetFragment(this, 0); 317 dialog.show(getFragmentManager(), ConfirmDeletionDialog.TAG); 318 MetricsLogger.action(getContext(), MetricsEvent.ACTION_DELETION_HELPER_CLEAR); 319 } else { 320 MetricsLogger.action(getContext(), MetricsEvent.ACTION_DELETION_HELPER_CANCEL); 321 getActivity().finish(); 322 } 323 } 324 325 @Override onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults)326 public void onRequestPermissionsResult(int requestCode, String permissions[], 327 int[] grantResults) { 328 if (requestCode == 0) { 329 if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { 330 mDownloadsDeletion.onResume(); 331 getLoaderManager().initLoader(DOWNLOADS_LOADER_ID, new Bundle(), 332 mDownloadsDeletion); 333 } 334 } 335 } 336 337 @Override onCreateOptionsMenu(Menu menu, MenuInflater menuInflater)338 public void onCreateOptionsMenu(Menu menu, MenuInflater menuInflater) { 339 Activity activity = getActivity(); 340 String mHelpUri = getResources().getString(R.string.help_uri_deletion_helper); 341 if (mHelpUri != null && activity != null) { 342 HelpUtils.prepareHelpMenuItem(activity, menu, mHelpUri, getClass().getName()); 343 } 344 } 345 346 @Override onCreateView( LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)347 public View onCreateView( 348 LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { 349 View view = super.onCreateView(inflater, container, savedInstanceState); 350 return view; 351 } 352 353 @VisibleForTesting setDownloadsDeletionType(DownloadsDeletionType downloadsDeletion)354 void setDownloadsDeletionType(DownloadsDeletionType downloadsDeletion) { 355 mDownloadsDeletion = downloadsDeletion; 356 } 357 initializeButtons()358 private void initializeButtons() { 359 ButtonBarProvider activity = (ButtonBarProvider) getActivity(); 360 activity.getButtonBar().setVisibility(View.VISIBLE); 361 362 mCancel = activity.getSkipButton(); 363 mCancel.setText(R.string.cancel); 364 mCancel.setOnClickListener(this); 365 mCancel.setVisibility(View.VISIBLE); 366 367 mFree = activity.getNextButton(); 368 mFree.setText(R.string.storage_menu_free); 369 mFree.setOnClickListener(this); 370 mFree.setEnabled(false); 371 } 372 updateFreeButtonText()373 private void updateFreeButtonText() { 374 Activity activity = getActivity(); 375 if (activity == null) { 376 return; 377 } 378 mFree.setText( 379 String.format( 380 activity.getString(R.string.deletion_helper_free_button), 381 Formatter.formatFileSize( 382 activity, getTotalFreeableSpace(COUNT_CHECKED_ONLY)))); 383 } 384 getTotalFreeableSpace(boolean countUnchecked)385 private long getTotalFreeableSpace(boolean countUnchecked) { 386 long freeableSpace = 0; 387 if (mAppBackend != null) { 388 freeableSpace += mAppBackend.getTotalAppsFreeableSpace(countUnchecked); 389 } 390 if (mPhotoPreference != null) { 391 freeableSpace += mPhotoPreference.getFreeableBytes(countUnchecked); 392 } 393 if (mDownloadsPreference != null) { 394 freeableSpace += mDownloadsDeletion.getFreeableBytes(countUnchecked); 395 } 396 return freeableSpace; 397 } 398 setResultCode(int result)399 private void setResultCode(int result) { 400 mResult = result; 401 Activity activity = getActivity(); 402 if (activity != null) { 403 activity.setResult(result); 404 } 405 } 406 407 @VisibleForTesting getResultCode()408 protected int getResultCode() { 409 return mResult; 410 } 411 } 412