/* * 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 androidx.annotation.VisibleForTesting; import androidx.preference.PreferenceFragment; import androidx.preference.Preference; import androidx.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 mDeletableContentList; private AppDeletionPreferenceGroup mApps; @VisibleForTesting AppDeletionType mAppBackend; @VisibleForTesting DownloadsDeletionPreferenceGroup mDownloadsPreference; private DownloadsDeletionType mDownloadsDeletion; private PhotosDeletionPreference mPhotoPreference; private Preference mGaugePreference; private DeletionType mPhotoVideoDeletion; private Button mCancel, mFree; private DeletionHelperFeatureProvider mProvider; private int mThresholdType; @VisibleForTesting long mBytesToFree = UNSET; private int mResult; 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 checkedApplications = null; if (savedInstanceState != null) { checkedApplications = (HashSet) 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); long requestedBytes = intent.getLongExtra(StorageManager.EXTRA_REQUESTED_BYTES, UNSET); mBytesToFree = requestedBytes; } 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(); } @VisibleForTesting void setupEmptyState() { final PreferenceScreen screen = getPreferenceScreen(); if (mDownloadsPreference != null) { mDownloadsPreference.setChecked(false); screen.removePreference(mDownloadsPreference); } screen.removePreference(mApps); // Nulling out the downloads preferences means we won't accidentally delete what isn't // visible. mDownloadsDeletion = null; mDownloadsPreference = null; } 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 (numItems > 0 || bytesFreeable > 0 || allTypesEmpty()) { if (mLoadingController != null) { 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. if (mFree != null) { 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 == null || 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() { long bytesFreed = getTotalFreeableSpace(COUNT_CHECKED_ONLY); if (mBytesToFree != UNSET && bytesFreed >= mBytesToFree) { setResultCode(Activity.RESULT_OK); } // 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. Activity activity = getActivity(); if (mPhotoPreference != null && mPhotoPreference.isChecked()) { mPhotoVideoDeletion.clearFreeableData(activity); } if (mDownloadsPreference != null) { mDownloadsDeletion.clearFreeableData(activity); } if (mAppBackend != null) { mAppBackend.clearFreeableData(activity); } } @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; } @VisibleForTesting void setDownloadsDeletionType(DownloadsDeletionType downloadsDeletion) { mDownloadsDeletion = downloadsDeletion; } 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; if (mAppBackend != null) { freeableSpace += mAppBackend.getTotalAppsFreeableSpace(countUnchecked); } if (mPhotoPreference != null) { freeableSpace += mPhotoPreference.getFreeableBytes(countUnchecked); } if (mDownloadsPreference != null) { freeableSpace += mDownloadsDeletion.getFreeableBytes(countUnchecked); } return freeableSpace; } private void setResultCode(int result) { mResult = result; Activity activity = getActivity(); if (activity != null) { activity.setResult(result); } } @VisibleForTesting protected int getResultCode() { return mResult; } }