| /* |
| * Copyright (C) 2016 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.storagemanager.deletionhelper; |
| |
| import android.Manifest; |
| import android.app.Activity; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.pm.PackageManager; |
| import android.os.Bundle; |
| import android.os.storage.StorageManager; |
| import android.support.v14.preference.PreferenceFragment; |
| import android.support.v7.preference.Preference; |
| import android.support.v7.preference.PreferenceScreen; |
| import android.text.format.Formatter; |
| import android.view.LayoutInflater; |
| import android.view.Menu; |
| import android.view.MenuInflater; |
| import android.view.View; |
| import android.view.ViewGroup; |
| import android.widget.Button; |
| import com.android.internal.logging.MetricsLogger; |
| import com.android.internal.logging.nano.MetricsProto.MetricsEvent; |
| import com.android.internal.util.Preconditions; |
| import com.android.settingslib.HelpUtils; |
| import com.android.settingslib.applications.AppUtils; |
| import com.android.storagemanager.ButtonBarProvider; |
| import com.android.storagemanager.R; |
| import com.android.storagemanager.overlay.DeletionHelperFeatureProvider; |
| import com.android.storagemanager.overlay.FeatureFactory; |
| import java.util.ArrayList; |
| import java.util.HashSet; |
| import java.util.List; |
| |
| /** |
| * Settings screen for the deletion helper, which manually removes data which is not recently used. |
| */ |
| public class DeletionHelperSettings extends PreferenceFragment |
| implements DeletionType.FreeableChangedListener, View.OnClickListener { |
| public static final boolean COUNT_UNCHECKED = true; |
| public static final boolean COUNT_CHECKED_ONLY = false; |
| |
| protected static final String APPS_KEY = "apps_group"; |
| protected static final String KEY_DOWNLOADS_PREFERENCE = "delete_downloads"; |
| protected static final String KEY_PHOTOS_VIDEOS_PREFERENCE = "delete_photos"; |
| protected static final String KEY_GAUGE_PREFERENCE = "deletion_gauge"; |
| |
| private static final String THRESHOLD_KEY = "threshold_key"; |
| private static final int DOWNLOADS_LOADER_ID = 1; |
| private static final int NUM_DELETION_TYPES = 3; |
| private static final long UNSET = -1; |
| |
| private List<DeletionType> mDeletableContentList; |
| private AppDeletionPreferenceGroup mApps; |
| private AppDeletionType mAppBackend; |
| private DownloadsDeletionPreferenceGroup mDownloadsPreference; |
| private DownloadsDeletionType mDownloadsDeletion; |
| private PhotosDeletionPreference mPhotoPreference; |
| private Preference mGaugePreference; |
| private DeletionType mPhotoVideoDeletion; |
| private Button mCancel, mFree; |
| private DeletionHelperFeatureProvider mProvider; |
| private int mThresholdType; |
| private LoadingSpinnerController mLoadingController; |
| |
| public static DeletionHelperSettings newInstance(int thresholdType) { |
| DeletionHelperSettings instance = new DeletionHelperSettings(); |
| Bundle bundle = new Bundle(1); |
| bundle.putInt(THRESHOLD_KEY, thresholdType); |
| instance.setArguments(bundle); |
| return instance; |
| } |
| |
| @Override |
| public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { |
| addPreferencesFromResource(R.xml.deletion_helper_list); |
| mThresholdType = getArguments().getInt(THRESHOLD_KEY, AppsAsyncLoader.NORMAL_THRESHOLD); |
| mApps = (AppDeletionPreferenceGroup) findPreference(APPS_KEY); |
| mPhotoPreference = (PhotosDeletionPreference) findPreference(KEY_PHOTOS_VIDEOS_PREFERENCE); |
| mProvider = FeatureFactory.getFactory(getActivity()).getDeletionHelperFeatureProvider(); |
| mLoadingController = new LoadingSpinnerController((DeletionHelperActivity) getActivity()); |
| if (mProvider != null) { |
| mPhotoVideoDeletion = |
| mProvider.createPhotoVideoDeletionType(getContext(), mThresholdType); |
| } |
| |
| HashSet<String> checkedApplications = null; |
| if (savedInstanceState != null) { |
| checkedApplications = |
| (HashSet<String>) savedInstanceState.getSerializable( |
| AppDeletionType.EXTRA_CHECKED_SET); |
| } |
| mAppBackend = new AppDeletionType(this, checkedApplications, mThresholdType); |
| mAppBackend.registerView(mApps); |
| mAppBackend.registerFreeableChangedListener(this); |
| mApps.setDeletionType(mAppBackend); |
| |
| mDeletableContentList = new ArrayList<>(NUM_DELETION_TYPES); |
| |
| mGaugePreference = findPreference(KEY_GAUGE_PREFERENCE); |
| Activity activity = getActivity(); |
| if (activity != null && mGaugePreference != null) { |
| Intent intent = activity.getIntent(); |
| if (intent != null) { |
| CharSequence gaugeTitle = |
| getGaugeString(getContext(), intent, activity.getCallingPackage()); |
| if (gaugeTitle != null) { |
| mGaugePreference.setTitle(gaugeTitle); |
| } else { |
| getPreferenceScreen().removePreference(mGaugePreference); |
| } |
| } |
| } |
| } |
| |
| protected static CharSequence getGaugeString( |
| Context context, Intent intent, String packageName) { |
| Preconditions.checkNotNull(intent); |
| long requestedBytes = intent.getLongExtra(StorageManager.EXTRA_REQUESTED_BYTES, UNSET); |
| if (requestedBytes > 0) { |
| CharSequence callerLabel = |
| AppUtils.getApplicationLabel(context.getPackageManager(), packageName); |
| // I really hope this isn't the case, but I can't ignore the possibility that we cannot |
| // determine what app the referrer is. |
| if (callerLabel == null) { |
| return null; |
| } |
| return context.getString( |
| R.string.app_requesting_space, |
| callerLabel, |
| Formatter.formatFileSize(context, requestedBytes)); |
| } |
| return null; |
| } |
| |
| @Override |
| public void onActivityCreated(Bundle savedInstanceState) { |
| super.onActivityCreated(savedInstanceState); |
| initializeButtons(); |
| setHasOptionsMenu(true); |
| Activity activity = getActivity(); |
| if (activity.checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE) |
| != PackageManager.PERMISSION_GRANTED) { |
| activity.requestPermissions( |
| new String[] {Manifest.permission.READ_EXTERNAL_STORAGE}, |
| 0); |
| } |
| |
| if (mProvider != null && mPhotoVideoDeletion != null) { |
| mPhotoPreference.setDaysToKeep(mProvider.getDaysToKeep(mThresholdType)); |
| mPhotoPreference.registerFreeableChangedListener(this); |
| mPhotoPreference.registerDeletionService(mPhotoVideoDeletion); |
| mDeletableContentList.add(mPhotoVideoDeletion); |
| } else { |
| getPreferenceScreen().removePreference(mPhotoPreference); |
| mPhotoPreference.setEnabled(false); |
| } |
| |
| String[] uncheckedFiles = null; |
| if (savedInstanceState != null) { |
| uncheckedFiles = |
| savedInstanceState.getStringArray( |
| DownloadsDeletionType.EXTRA_UNCHECKED_DOWNLOADS); |
| } |
| mDownloadsPreference = |
| (DownloadsDeletionPreferenceGroup) findPreference(KEY_DOWNLOADS_PREFERENCE); |
| mDownloadsDeletion = new DownloadsDeletionType(getActivity(), uncheckedFiles); |
| mDownloadsPreference.registerFreeableChangedListener(this); |
| mDownloadsPreference.registerDeletionService(mDownloadsDeletion); |
| mDeletableContentList.add(mDownloadsDeletion); |
| if (isEmptyState()) { |
| setupEmptyState(); |
| } |
| mDeletableContentList.add(mAppBackend); |
| updateFreeButtonText(); |
| } |
| |
| private void setupEmptyState() { |
| mDownloadsPreference.setChecked(false); |
| final PreferenceScreen screen = getPreferenceScreen(); |
| screen.removePreference(mDownloadsPreference); |
| screen.removePreference(mApps); |
| } |
| |
| private boolean isEmptyState() { |
| // We know we are in the empty state if our loader is not using a threshold. |
| return mThresholdType == AppsAsyncLoader.NO_THRESHOLD; |
| } |
| |
| @Override |
| public void onResume() { |
| super.onResume(); |
| |
| mLoadingController.initializeLoading(getListView()); |
| |
| for (int i = 0, size = mDeletableContentList.size(); i < size; i++) { |
| mDeletableContentList.get(i).onResume(); |
| } |
| |
| if (mDownloadsDeletion != null |
| && getActivity().checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE) |
| == PackageManager.PERMISSION_GRANTED) { |
| getLoaderManager().initLoader(DOWNLOADS_LOADER_ID, new Bundle(), mDownloadsDeletion); |
| } |
| } |
| |
| @Override |
| public void onPause() { |
| super.onPause(); |
| for (int i = 0, size = mDeletableContentList.size(); i < size; i++) { |
| mDeletableContentList.get(i).onPause(); |
| } |
| } |
| |
| @Override |
| public void onSaveInstanceState(Bundle outState) { |
| super.onSaveInstanceState(outState); |
| for (int i = 0, size = mDeletableContentList.size(); i < size; i++) { |
| mDeletableContentList.get(i).onSaveInstanceStateBundle(outState); |
| } |
| } |
| |
| @Override |
| public void onFreeableChanged(int numItems, long bytesFreeable) { |
| if (bytesFreeable > 0 || allTypesEmpty()) { |
| mLoadingController.onCategoryLoad(); |
| } |
| |
| // bytesFreeable is the number of bytes freed by a single deletion type. If it is non-zero, |
| // there is stuff to free and we can enable it. If it is zero, though, we still need to get |
| // getTotalFreeableSpace to check all deletion types. |
| mFree.setEnabled(bytesFreeable != 0 || getTotalFreeableSpace(COUNT_CHECKED_ONLY) != 0); |
| updateFreeButtonText(); |
| |
| // Transition to empty state if all types have reported there is nothing to delete. Skip |
| // the transition if we are already in no threshold mode |
| if (allTypesEmpty() && !isEmptyState()) { |
| startEmptyState(); |
| } |
| } |
| |
| private boolean allTypesEmpty() { |
| return mAppBackend.isEmpty() |
| && mDownloadsDeletion.isEmpty() |
| && (mPhotoVideoDeletion == null || mPhotoVideoDeletion.isEmpty()); |
| } |
| |
| private void startEmptyState() { |
| if (getActivity() instanceof DeletionHelperActivity) { |
| DeletionHelperActivity activity = (DeletionHelperActivity) getActivity(); |
| activity.setIsEmptyState(true /* isEmptyState */); |
| } |
| } |
| |
| /** Clears out the selected apps and data from the device and closes the fragment. */ |
| protected void clearData() { |
| // This should be fine as long as there is only one extra deletion feature. |
| // In the future, this should be done in an async queue in order to not |
| // interfere with the simultaneous PackageDeletionTask. |
| if (mPhotoPreference != null && mPhotoPreference.isChecked()) { |
| mPhotoVideoDeletion.clearFreeableData(getActivity()); |
| } |
| if (mDownloadsPreference != null) { |
| mDownloadsDeletion.clearFreeableData(getActivity()); |
| } |
| mAppBackend.clearFreeableData(getActivity()); |
| } |
| |
| @Override |
| public void onClick(View v) { |
| if (v.getId() == R.id.next_button) { |
| ConfirmDeletionDialog dialog = |
| ConfirmDeletionDialog.newInstance(getTotalFreeableSpace(COUNT_CHECKED_ONLY)); |
| // The 0 is a placeholder for an optional result code. |
| dialog.setTargetFragment(this, 0); |
| dialog.show(getFragmentManager(), ConfirmDeletionDialog.TAG); |
| MetricsLogger.action(getContext(), MetricsEvent.ACTION_DELETION_HELPER_CLEAR); |
| } else { |
| MetricsLogger.action(getContext(), MetricsEvent.ACTION_DELETION_HELPER_CANCEL); |
| getActivity().finish(); |
| } |
| } |
| |
| @Override |
| public void onRequestPermissionsResult(int requestCode, String permissions[], |
| int[] grantResults) { |
| if (requestCode == 0) { |
| if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { |
| mDownloadsDeletion.onResume(); |
| getLoaderManager().initLoader(DOWNLOADS_LOADER_ID, new Bundle(), |
| mDownloadsDeletion); |
| } |
| } |
| } |
| |
| @Override |
| public void onCreateOptionsMenu(Menu menu, MenuInflater menuInflater) { |
| Activity activity = getActivity(); |
| String mHelpUri = getResources().getString(R.string.help_uri_deletion_helper); |
| if (mHelpUri != null && activity != null) { |
| HelpUtils.prepareHelpMenuItem(activity, menu, mHelpUri, getClass().getName()); |
| } |
| } |
| |
| @Override |
| public View onCreateView( |
| LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { |
| View view = super.onCreateView(inflater, container, savedInstanceState); |
| return view; |
| } |
| |
| private void initializeButtons() { |
| ButtonBarProvider activity = (ButtonBarProvider) getActivity(); |
| activity.getButtonBar().setVisibility(View.VISIBLE); |
| |
| mCancel = activity.getSkipButton(); |
| mCancel.setText(R.string.cancel); |
| mCancel.setOnClickListener(this); |
| mCancel.setVisibility(View.VISIBLE); |
| |
| mFree = activity.getNextButton(); |
| mFree.setText(R.string.storage_menu_free); |
| mFree.setOnClickListener(this); |
| mFree.setEnabled(false); |
| } |
| |
| private void updateFreeButtonText() { |
| Activity activity = getActivity(); |
| if (activity == null) { |
| return; |
| } |
| mFree.setText( |
| String.format( |
| activity.getString(R.string.deletion_helper_free_button), |
| Formatter.formatFileSize( |
| activity, getTotalFreeableSpace(COUNT_CHECKED_ONLY)))); |
| } |
| |
| private long getTotalFreeableSpace(boolean countUnchecked) { |
| long freeableSpace = 0; |
| freeableSpace += mAppBackend.getTotalAppsFreeableSpace(countUnchecked); |
| if (mPhotoPreference != null) { |
| freeableSpace += mPhotoPreference.getFreeableBytes(countUnchecked); |
| } |
| if (mDownloadsPreference != null) { |
| freeableSpace += mDownloadsDeletion.getFreeableBytes(countUnchecked); |
| } |
| return freeableSpace; |
| } |
| |
| } |