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