2 * Copyright (C) 2016 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package com.android.settings.deletionhelper;
19 import android.app.Activity;
20 import android.app.Application;
21 import android.content.Intent;
22 import android.os.Bundle;
23 import android.support.v7.preference.Preference;
24 import android.text.format.Formatter;
25 import android.util.ArraySet;
26 import android.util.Log;
27 import android.view.View;
28 import android.widget.Button;
29 import com.android.settings.CollapsibleCheckboxPreferenceGroup;
30 import com.android.settings.PhotosDeletionPreference;
31 import com.android.settings.SettingsPreferenceFragment;
32 import com.android.settings.R;
33 import com.android.internal.logging.MetricsProto.MetricsEvent;
34 import com.android.settings.applications.AppStateBaseBridge;
35 import com.android.settings.overlay.DeletionHelperFeatureProvider;
36 import com.android.settings.overlay.FeatureFactory;
37 import com.android.settingslib.applications.ApplicationsState;
38 import com.android.settingslib.applications.ApplicationsState.AppEntry;
39 import com.android.settingslib.applications.ApplicationsState.Callbacks;
40 import com.android.settingslib.applications.ApplicationsState.Session;
42 import java.util.ArrayList;
43 import java.util.HashSet;
46 * Settings screen for the deletion helper, which manually removes data which is not recently used.
48 public class DeletionHelperFragment extends SettingsPreferenceFragment implements
49 ApplicationsState.Callbacks, AppStateBaseBridge.Callback,
50 Preference.OnPreferenceChangeListener, DeletionType.FreeableChangedListener,
51 View.OnClickListener {
52 public static final int CLEAR_DATA_RESULT = 1;
53 public static final String FREED_BYTES_KEY = "freed";
55 private static final String TAG = "DeletionHelperFragment";
57 private static final String EXTRA_HAS_BRIDGE = "hasBridge";
58 private static final String EXTRA_HAS_SIZES = "hasSizes";
59 private static final String EXTRA_CHECKED_SET = "checkedSet";
61 private static final String KEY_APPS_GROUP = "apps_group";
62 private static final String KEY_PHOTOS_VIDEOS_PREFERENCE = "delete_photos";
63 private static final String KEY_DOWNLOADS_PREFERENCE = "delete_downloads";
65 private static final int DOWNLOADS_LOADER_ID = 1;
67 private Button mCancel, mFree;
68 private CollapsibleCheckboxPreferenceGroup mApps;
69 private PhotosDeletionPreference mPhotoPreference;
70 private DownloadsDeletionPreferenceGroup mDownloadsPreference;
72 private ApplicationsState mState;
73 private Session mSession;
74 private HashSet<String> mCheckedApplications;
75 private AppStateUsageStatsBridge mDataUsageBridge;
76 private ArrayList<AppEntry> mAppEntries;
77 private boolean mHasReceivedAppEntries, mHasReceivedBridgeCallback, mFinishedLoading;
78 private DeletionHelperFeatureProvider mProvider;
79 private DeletionType mPhotoVideoDeletion;
80 private DownloadsDeletionType mDownloadsDeletion;
83 public void onCreate(Bundle savedInstanceState) {
84 super.onCreate(savedInstanceState);
85 setAnimationAllowed(true);
86 Application app = getActivity().getApplication();
87 mState = ApplicationsState.getInstance(app);
88 mSession = mState.newSession(this);
89 mCheckedApplications = new HashSet<>();
90 mDataUsageBridge = new AppStateUsageStatsBridge(getActivity(), mState, this);
92 addPreferencesFromResource(R.xml.deletion_helper_list);
93 mApps = (CollapsibleCheckboxPreferenceGroup) findPreference(KEY_APPS_GROUP);
94 mPhotoPreference = (PhotosDeletionPreference) findPreference(KEY_PHOTOS_VIDEOS_PREFERENCE);
95 mDownloadsPreference =
96 (DownloadsDeletionPreferenceGroup) findPreference(KEY_DOWNLOADS_PREFERENCE);
98 FeatureFactory.getFactory(app).getDeletionHelperFeatureProvider();
99 if (mProvider != null) {
100 mPhotoVideoDeletion = mProvider.createPhotoVideoDeletionType(getContext());
102 mDownloadsDeletion = new DownloadsDeletionType(getActivity());
104 if (savedInstanceState != null) {
105 mHasReceivedAppEntries =
106 savedInstanceState.getBoolean(EXTRA_HAS_SIZES, false);
107 mHasReceivedBridgeCallback =
108 savedInstanceState.getBoolean(EXTRA_HAS_BRIDGE, false);
109 mCheckedApplications =
110 (HashSet<String>) savedInstanceState.getSerializable(EXTRA_CHECKED_SET);
114 private void initializeButtons(View v) {
115 mCancel = (Button) v.findViewById(R.id.skip_button);
116 mCancel.setText(R.string.cancel);
117 mCancel.setOnClickListener(this);
118 mCancel.setVisibility(View.VISIBLE);
120 mFree = (Button) v.findViewById(R.id.next_button);
121 mFree.setText(R.string.storage_menu_free);
122 mFree.setOnClickListener(this);
124 Button back = (Button) v.findViewById(R.id.back_button);
125 back.setVisibility(View.GONE);
128 private void initializeDeletionPreferences() {
129 if (mProvider == null) {
130 getPreferenceScreen().removePreference(mPhotoPreference);
131 mPhotoPreference = null;
133 mPhotoPreference.registerFreeableChangedListener(this);
134 mPhotoPreference.registerDeletionService(mPhotoVideoDeletion);
137 mDownloadsPreference.registerFreeableChangedListener(this);
138 mDownloadsPreference.registerDeletionService(mDownloadsDeletion);
139 mApps.setOnPreferenceChangeListener(this);
143 public void onViewCreated(View v, Bundle savedInstanceState) {
144 super.onViewCreated(v, savedInstanceState);
145 initializeButtons(v);
146 initializeDeletionPreferences();
147 setLoading(true, false);
151 public void onResume() {
154 mDataUsageBridge.resume();
155 mDownloadsDeletion.onResume();
156 getLoaderManager().initLoader(DOWNLOADS_LOADER_ID, new Bundle(), mDownloadsDeletion);
158 if (mPhotoVideoDeletion != null) {
159 mPhotoVideoDeletion.onResume();
165 public void onSaveInstanceState(Bundle outState) {
166 super.onSaveInstanceState(outState);
167 outState.putBoolean(EXTRA_HAS_SIZES, mHasReceivedAppEntries);
168 outState.putBoolean(EXTRA_HAS_BRIDGE, mHasReceivedBridgeCallback);
169 outState.putSerializable(EXTRA_CHECKED_SET, mCheckedApplications);
174 public void onPause() {
176 mDataUsageBridge.pause();
178 mDownloadsDeletion.onPause();
180 if (mPhotoVideoDeletion != null) {
181 mPhotoVideoDeletion.onPause();
185 private void rebuild() {
186 // Only rebuild if we have the packages and their usage stats.
187 if (!mHasReceivedBridgeCallback || !mHasReceivedAppEntries) {
191 final ArrayList<AppEntry> apps =
192 mSession.rebuild(AppStateUsageStatsBridge.FILTER_USAGE_STATS,
193 ApplicationsState.SIZE_COMPARATOR);
194 if (apps == null) return;
196 refreshAppGroup(apps);
198 // All applications should be filled in if we've received the sizes.
199 // setLoading being called multiple times causes flickering, so we only do it once.
200 if (mHasReceivedAppEntries && !mFinishedLoading) {
201 mFinishedLoading = true;
202 setLoading(false, true);
203 getButtonBar().setVisibility(View.VISIBLE);
205 updateFreeButtonText();
208 private void updateFreeButtonText() {
209 mFree.setText(String.format(getActivity().getString(R.string.deletion_helper_free_button),
210 Formatter.formatFileSize(getActivity(), getTotalFreeableSpace())));
214 public void onRunningStateChanged(boolean running) {
219 public void onPackageListChanged() {
224 public void onRebuildComplete(ArrayList<AppEntry> apps) {
228 public void onPackageIconChanged() {
232 public void onPackageSizeChanged(String packageName) {
237 public void onAllSizesComputed() {
242 public void onLauncherInfoChanged() {
246 public void onLoadEntriesCompleted() {
247 mHasReceivedAppEntries = true;
252 public void onExtraInfoUpdated() {
253 mHasReceivedBridgeCallback = true;
258 protected int getMetricsCategory() {
259 return MetricsEvent.DEVICEINFO_STORAGE;
263 public boolean onPreferenceChange(Preference preference, Object newValue) {
264 boolean checked = (boolean) newValue;
265 if (preference.getKey().equals(mApps.getKey())) {
266 return toggleAllApps(checked);
269 String packageName = ((AppDeletionPreference) preference).getPackageName();
271 mCheckedApplications.add(packageName);
273 mCheckedApplications.remove(packageName);
275 // We remove the preference change listener to avoid toggling every app on and off.
276 mApps.setOnPreferenceChangeListener(null);
277 mApps.setChecked(false);
278 mApps.setOnPreferenceChangeListener(this);
280 updateFreeButtonText();
285 public void onFreeableChanged(int numItems, long freeableBytes) {
286 updateFreeButtonText();
290 public void onClick(View v) {
291 if (v.getId() == mFree.getId()) {
292 ConfirmDeletionDialog dialog =
293 ConfirmDeletionDialog.newInstance(getTotalFreeableSpace());
294 // The 0 is a placeholder for an optional result code.
295 dialog.setTargetFragment(this, 0);
296 dialog.show(getFragmentManager(), ConfirmDeletionDialog.TAG);
303 * Clears out the selected apps and data from the device and closes the fragment.
305 protected void clearData() {
306 // This should be fine as long as there is only one extra deletion feature.
307 // In the future, this should be done in an async queue in order to not
308 // interfere with the simultaneous PackageDeletionTask.
309 if (mPhotoPreference != null && mPhotoPreference.isChecked()) {
310 mPhotoVideoDeletion.clearFreeableData();
312 mDownloadsDeletion.clearFreeableData();
314 ArraySet<String> apps = new ArraySet<>();
315 for (AppEntry entry : mAppEntries) {
316 if (mCheckedApplications.contains(entry.label)) {
317 synchronized (entry) {
318 apps.add(entry.info.packageName);
322 // TODO: If needed, add an action on the callback.
323 PackageDeletionTask task = new PackageDeletionTask(getActivity().getPackageManager(), apps,
324 new PackageDeletionTask.Callback() {
326 public void onSuccess() {
330 public void onError() {
331 Log.e(TAG, "An error occurred while uninstalling packages.");
334 Intent data = new Intent();
335 data.putExtra(FREED_BYTES_KEY, getTotalFreeableSpace());
336 getActivity().setResult(CLEAR_DATA_RESULT, data);
342 private long getTotalFreeableSpace() {
343 long freeableSpace = 0;
344 freeableSpace += getTotalAppsFreeableSpace(false);
345 if (mPhotoPreference != null) {
346 freeableSpace += mPhotoPreference.getFreeableBytes();
348 freeableSpace += mDownloadsDeletion.getFreeableBytes();
349 return freeableSpace;
352 private void refreshAppGroup(ArrayList<AppEntry> apps) {
353 int entryCount = apps.size();
354 cacheRemoveAllPrefs(mApps);
355 for (int i = 0; i < entryCount; i++) {
356 AppEntry entry = apps.get(i);
357 final String packageName = entry.label;
358 AppDeletionPreference preference =
359 (AppDeletionPreference) getCachedPreference(entry.label);
360 if (preference == null) {
361 preference = new AppDeletionPreference(getActivity(), entry, mState);
362 preference.setKey(packageName);
363 preference.setOnPreferenceChangeListener(this);
364 mApps.addPreference(preference);
366 preference.setChecked(mCheckedApplications.contains(packageName));
367 preference.setOrder(i);
369 removeCachedPrefs(mApps);
370 updateAppsGroupText();
373 private long getTotalAppsFreeableSpace(boolean countUnchecked) {
374 long freeableSpace = 0;
375 if (mAppEntries != null) {
376 for (int i = 0; i < mAppEntries.size(); i++) {
377 final AppEntry entry = mAppEntries.get(i);
378 long entrySize = mAppEntries.get(i).size;
379 // If the entrySize is negative, it is either an unknown size or an error occurred.
380 if ((countUnchecked ||
381 mCheckedApplications.contains(entry.label)) && entrySize > 0) {
382 freeableSpace += entrySize;
387 return freeableSpace;
390 private void updateAppsGroupText() {
391 if (mAppEntries != null) {
392 Activity app = getActivity();
393 mApps.setTitle(app.getString(R.string.deletion_helper_apps_group_title,
394 mAppEntries.size()));
395 mApps.setSummary(app.getString(R.string.deletion_helper_apps_group_summary,
396 Formatter.formatFileSize(app,
397 getTotalAppsFreeableSpace(true)),
398 AppStateUsageStatsBridge.UNUSED_DAYS_DELETION_THRESHOLD));
402 private boolean toggleAllApps(boolean checked) {
403 for (AppEntry entry : mAppEntries) {
404 final String packageName = entry.label;
406 mCheckedApplications.add(packageName);
408 mCheckedApplications.remove(packageName);
411 refreshAppGroup(mAppEntries);
412 updateFreeButtonText();