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.pm.PackageManager; 22 import android.os.Bundle; 23 import android.support.v14.preference.PreferenceFragment; 24 import android.support.v7.preference.PreferenceScreen; 25 import android.text.format.Formatter; 26 import android.view.Menu; 27 import android.view.MenuInflater; 28 import android.view.View; 29 import android.widget.Button; 30 import com.android.internal.logging.MetricsLogger; 31 import com.android.internal.logging.nano.MetricsProto.MetricsEvent; 32 import com.android.settingslib.HelpUtils; 33 import com.android.storagemanager.ButtonBarProvider; 34 import com.android.storagemanager.R; 35 import com.android.storagemanager.overlay.DeletionHelperFeatureProvider; 36 import com.android.storagemanager.overlay.FeatureFactory; 37 import java.util.ArrayList; 38 import java.util.HashSet; 39 import java.util.List; 40 41 /** 42 * Settings screen for the deletion helper, which manually removes data which is not recently used. 43 */ 44 public class DeletionHelperSettings extends PreferenceFragment 45 implements DeletionType.FreeableChangedListener, View.OnClickListener { 46 public static final boolean COUNT_UNCHECKED = true; 47 public static final boolean COUNT_CHECKED_ONLY = false; 48 49 protected static final String APPS_KEY = "apps_group"; 50 protected static final String KEY_DOWNLOADS_PREFERENCE = "delete_downloads"; 51 protected static final String KEY_PHOTOS_VIDEOS_PREFERENCE = "delete_photos"; 52 53 private static final String THRESHOLD_KEY = "threshold_key"; 54 private static final int DOWNLOADS_LOADER_ID = 1; 55 private static final int NUM_DELETION_TYPES = 3; 56 57 private List<DeletionType> mDeletableContentList; 58 private AppDeletionPreferenceGroup mApps; 59 private AppDeletionType mAppBackend; 60 private DownloadsDeletionPreferenceGroup mDownloadsPreference; 61 private DownloadsDeletionType mDownloadsDeletion; 62 private PhotosDeletionPreference mPhotoPreference; 63 private DeletionType mPhotoVideoDeletion; 64 private Button mCancel, mFree; 65 private DeletionHelperFeatureProvider mProvider; 66 private int mThresholdType; 67 68 public static DeletionHelperSettings newInstance(int thresholdType) { 69 DeletionHelperSettings instance = new DeletionHelperSettings(); 70 Bundle bundle = new Bundle(1); 71 bundle.putInt(THRESHOLD_KEY, thresholdType); 72 instance.setArguments(bundle); 73 return instance; 74 } 75 76 @Override 77 public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { 78 addPreferencesFromResource(R.xml.deletion_helper_list); 79 mThresholdType = getArguments().getInt(THRESHOLD_KEY, AppsAsyncLoader.NORMAL_THRESHOLD); 80 mApps = (AppDeletionPreferenceGroup) findPreference(APPS_KEY); 81 mPhotoPreference = (PhotosDeletionPreference) findPreference(KEY_PHOTOS_VIDEOS_PREFERENCE); 82 mProvider = FeatureFactory.getFactory(getActivity()).getDeletionHelperFeatureProvider(); 83 if (mProvider != null) { 84 mPhotoVideoDeletion = 85 mProvider.createPhotoVideoDeletionType(getContext(), mThresholdType); 86 } 87 88 HashSet<String> checkedApplications = null; 89 if (savedInstanceState != null) { 90 checkedApplications = 91 (HashSet<String>) savedInstanceState.getSerializable( 92 AppDeletionType.EXTRA_CHECKED_SET); 93 } 94 mAppBackend = new AppDeletionType(this, checkedApplications, mThresholdType); 95 mAppBackend.registerView(mApps); 96 mAppBackend.registerFreeableChangedListener(this); 97 mApps.setDeletionType(mAppBackend); 98 99 mDeletableContentList = new ArrayList<>(NUM_DELETION_TYPES); 100 } 101 102 @Override 103 public void onActivityCreated(Bundle savedInstanceState) { 104 super.onActivityCreated(savedInstanceState); 105 initializeButtons(); 106 setHasOptionsMenu(true); 107 Activity activity = getActivity(); 108 if (activity.checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE) 109 != PackageManager.PERMISSION_GRANTED) { 110 activity.requestPermissions( 111 new String[] {Manifest.permission.READ_EXTERNAL_STORAGE}, 112 0); 113 } 114 115 if (mProvider != null && mPhotoVideoDeletion != null) { 116 mPhotoPreference.setDaysToKeep(mProvider.getDaysToKeep(mThresholdType)); 117 mPhotoPreference.registerFreeableChangedListener(this); 118 mPhotoPreference.registerDeletionService(mPhotoVideoDeletion); 119 mDeletableContentList.add(mPhotoVideoDeletion); 120 } else { 121 getPreferenceScreen().removePreference(mPhotoPreference); 122 mPhotoPreference.setEnabled(false); 123 } 124 125 String[] uncheckedFiles = null; 126 if (savedInstanceState != null) { 127 uncheckedFiles = 128 savedInstanceState.getStringArray( 129 DownloadsDeletionType.EXTRA_UNCHECKED_DOWNLOADS); 130 } 131 mDownloadsPreference = 132 (DownloadsDeletionPreferenceGroup) findPreference(KEY_DOWNLOADS_PREFERENCE); 133 mDownloadsDeletion = new DownloadsDeletionType(getActivity(), uncheckedFiles); 134 mDownloadsPreference.registerFreeableChangedListener(this); 135 mDownloadsPreference.registerDeletionService(mDownloadsDeletion); 136 mDeletableContentList.add(mDownloadsDeletion); 137 if (isEmptyState()) { 138 setupEmptyState(); 139 } 140 mDeletableContentList.add(mAppBackend); 141 updateFreeButtonText(); 142 } 143 144 private void setupEmptyState() { 145 mDownloadsPreference.setChecked(false); 146 final PreferenceScreen screen = getPreferenceScreen(); 147 screen.removePreference(mDownloadsPreference); 148 screen.removePreference(mApps); 149 } 150 151 private boolean isEmptyState() { 152 // We know we are in the empty state if our loader is not using a threshold. 153 return mThresholdType == AppsAsyncLoader.NO_THRESHOLD; 154 } 155 156 @Override 157 public void onResume() { 158 super.onResume(); 159 for (int i = 0, size = mDeletableContentList.size(); i < size; i++) { 160 mDeletableContentList.get(i).onResume(); 161 } 162 163 if (mDownloadsDeletion != null 164 && getActivity().checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE) 165 == PackageManager.PERMISSION_GRANTED) { 166 getLoaderManager().initLoader(DOWNLOADS_LOADER_ID, new Bundle(), mDownloadsDeletion); 167 } 168 } 169 170 @Override 171 public void onPause() { 172 super.onPause(); 173 for (int i = 0, size = mDeletableContentList.size(); i < size; i++) { 174 mDeletableContentList.get(i).onPause(); 175 } 176 } 177 178 @Override 179 public void onSaveInstanceState(Bundle outState) { 180 super.onSaveInstanceState(outState); 181 for (int i = 0, size = mDeletableContentList.size(); i < size; i++) { 182 mDeletableContentList.get(i).onSaveInstanceStateBundle(outState); 183 } 184 } 185 186 @Override 187 public void onFreeableChanged(int numItems, long bytesFreeable) { 188 // bytesFreeable is the number of bytes freed by a single deletion type. If it is non-zero, 189 // there is stuff to free and we can enable it. If it is zero, though, we still need to get 190 // getTotalFreeableSpace to check all deletion types. 191 mFree.setEnabled(bytesFreeable != 0 || getTotalFreeableSpace(COUNT_CHECKED_ONLY) != 0); 192 updateFreeButtonText(); 193 194 // Transition to empty state if all types have reported there is nothing to delete. Skip 195 // the transition if we are already in no threshold mode 196 if (allTypesEmpty() && !isEmptyState()) { 197 startEmptyState(); 198 } 199 } 200 201 private boolean allTypesEmpty() { 202 203 return mAppBackend.isEmpty() 204 && mDownloadsDeletion.isEmpty() 205 && (mPhotoVideoDeletion == null || mPhotoVideoDeletion.isEmpty()); 206 } 207 208 private void startEmptyState() { 209 if (getActivity() instanceof DeletionHelperActivity) { 210 DeletionHelperActivity activity = (DeletionHelperActivity) getActivity(); 211 activity.setIsEmptyState(true /* isEmptyState */); 212 } 213 } 214 215 /** Clears out the selected apps and data from the device and closes the fragment. */ 216 protected void clearData() { 217 // This should be fine as long as there is only one extra deletion feature. 218 // In the future, this should be done in an async queue in order to not 219 // interfere with the simultaneous PackageDeletionTask. 220 if (mPhotoPreference != null && mPhotoPreference.isChecked()) { 221 mPhotoVideoDeletion.clearFreeableData(getActivity()); 222 } 223 if (mDownloadsPreference != null) { 224 mDownloadsDeletion.clearFreeableData(getActivity()); 225 } 226 mAppBackend.clearFreeableData(getActivity()); 227 } 228 229 @Override 230 public void onClick(View v) { 231 if (v.getId() == R.id.next_button) { 232 ConfirmDeletionDialog dialog = 233 ConfirmDeletionDialog.newInstance(getTotalFreeableSpace(COUNT_CHECKED_ONLY)); 234 // The 0 is a placeholder for an optional result code. 235 dialog.setTargetFragment(this, 0); 236 dialog.show(getFragmentManager(), ConfirmDeletionDialog.TAG); 237 MetricsLogger.action(getContext(), MetricsEvent.ACTION_DELETION_HELPER_CLEAR); 238 } else { 239 MetricsLogger.action(getContext(), MetricsEvent.ACTION_DELETION_HELPER_CANCEL); 240 getActivity().finish(); 241 } 242 } 243 244 @Override 245 public void onRequestPermissionsResult(int requestCode, String permissions[], 246 int[] grantResults) { 247 if (requestCode == 0) { 248 if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { 249 mDownloadsDeletion.onResume(); 250 getLoaderManager().initLoader(DOWNLOADS_LOADER_ID, new Bundle(), 251 mDownloadsDeletion); 252 } 253 } 254 } 255 256 @Override 257 public void onCreateOptionsMenu(Menu menu, MenuInflater menuInflater) { 258 Activity activity = getActivity(); 259 String mHelpUri = getResources().getString(R.string.help_uri_deletion_helper); 260 if (mHelpUri != null && activity != null) { 261 HelpUtils.prepareHelpMenuItem(activity, menu, mHelpUri, getClass().getName()); 262 } 263 } 264 265 private void initializeButtons() { 266 ButtonBarProvider activity = (ButtonBarProvider) getActivity(); 267 activity.getButtonBar().setVisibility(View.VISIBLE); 268 269 mCancel = activity.getSkipButton(); 270 mCancel.setText(R.string.cancel); 271 mCancel.setOnClickListener(this); 272 mCancel.setVisibility(View.VISIBLE); 273 274 mFree = activity.getNextButton(); 275 mFree.setText(R.string.storage_menu_free); 276 mFree.setOnClickListener(this); 277 mFree.setEnabled(false); 278 } 279 280 private void updateFreeButtonText() { 281 Activity activity = getActivity(); 282 if (activity == null) { 283 return; 284 } 285 mFree.setText( 286 String.format( 287 activity.getString(R.string.deletion_helper_free_button), 288 Formatter.formatFileSize( 289 activity, getTotalFreeableSpace(COUNT_CHECKED_ONLY)))); 290 } 291 292 private long getTotalFreeableSpace(boolean countUnchecked) { 293 long freeableSpace = 0; 294 freeableSpace += mAppBackend.getTotalAppsFreeableSpace(countUnchecked); 295 if (mPhotoPreference != null) { 296 freeableSpace += mPhotoPreference.getFreeableBytes(countUnchecked); 297 } 298 if (mDownloadsPreference != null) { 299 freeableSpace += mDownloadsDeletion.getFreeableBytes(countUnchecked); 300 } 301 return freeableSpace; 302 } 303 } 304