• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
newInstance(int thresholdType)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
onCreatePreferences(Bundle savedInstanceState, String rootKey)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 
getGaugeString( Context context, Intent intent, String packageName)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
onActivityCreated(Bundle savedInstanceState)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
setupEmptyState()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 
isEmptyState()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
onResume()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
onPause()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
onSaveInstanceState(Bundle outState)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
onFreeableChanged(int numItems, long bytesFreeable)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 
allTypesEmpty()269     private boolean allTypesEmpty() {
270         return mAppBackend.isEmpty()
271                 && (mDownloadsDeletion == null || mDownloadsDeletion.isEmpty())
272                 && (mPhotoVideoDeletion == null || mPhotoVideoDeletion.isEmpty());
273     }
274 
startEmptyState()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. */
clearData()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
onClick(View v)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
onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults)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
onCreateOptionsMenu(Menu menu, MenuInflater menuInflater)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
onCreateView( LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)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
setDownloadsDeletionType(DownloadsDeletionType downloadsDeletion)342     void setDownloadsDeletionType(DownloadsDeletionType downloadsDeletion) {
343         mDownloadsDeletion = downloadsDeletion;
344     }
345 
initializeButtons()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 
updateFreeButtonText()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 
getTotalFreeableSpace(boolean countUnchecked)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