Merge "Remove the Deletion Helper and Automatic Storage Management jobs." into nyc-mr1-dev

This commit is contained in:
Daniel Nishi
2016-06-22 23:52:22 +00:00
committed by Android (Google) Code Review
31 changed files with 9 additions and 2380 deletions

View File

@@ -1,147 +0,0 @@
/*
* 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.settings;
import android.content.Context;
import android.support.v7.preference.Preference;
import android.support.v7.preference.PreferenceGroup;
import android.support.v7.preference.PreferenceViewHolder;
import android.util.AttributeSet;
import android.util.Log;
import android.util.TypedValue;
import android.view.View;
import android.widget.Checkable;
import android.widget.TextView;
import java.util.LinkedHashMap;
/**
* CollapsibleCheckboxPreferenceGroup is a preference group that can be expanded or collapsed and
* also has a checkbox.
*/
public class CollapsibleCheckboxPreferenceGroup extends PreferenceGroup implements
View.OnClickListener {
private boolean mCollapsed;
private boolean mChecked;
public CollapsibleCheckboxPreferenceGroup(Context context) {
this(context, null);
}
public CollapsibleCheckboxPreferenceGroup(Context context, AttributeSet attrs) {
super(context, attrs);
setWidgetLayoutResource(com.android.settings.R.layout.preference_widget_checkbox);
}
@Override
public void onBindViewHolder(PreferenceViewHolder holder) {
super.onBindViewHolder(holder);
View checkbox = holder.findViewById(com.android.internal.R.id.checkbox);
if (checkbox != null && checkbox instanceof Checkable) {
((Checkable) checkbox).setChecked(mChecked);
checkbox.setClickable(true);
checkbox.setFocusable(true);
checkbox.setOnClickListener(this);
}
final TextView titleView = (TextView) holder.findViewById(android.R.id.title);
if (titleView != null) {
Context context = getContext();
TypedValue value = new TypedValue();
context.getTheme().resolveAttribute(android.R.attr.colorAccent, value, true);
titleView.setTextColor(context.getColor(value.resourceId));
}
}
@Override
public boolean addPreference(Preference p) {
super.addPreference(p);
p.setVisible(!isCollapsed());
return true;
}
// The preference click handler.
@Override
protected void onClick() {
super.onClick();
setCollapse(!isCollapsed());
}
// The checkbox view click handler.
@Override
public void onClick(View v) {
setChecked(!isChecked());
}
/**
* Return if the view is collapsed.
*/
public boolean isCollapsed() {
return mCollapsed;
}
/**
* Returns the checked state of the preference.
*/
public boolean isChecked() {
return mChecked;
}
/**
* Sets the checked state and notifies listeners of the state change.
*/
public void setChecked(boolean checked) {
if (mChecked != checked) {
mChecked = checked;
callChangeListener(checked);
notifyDependencyChange(shouldDisableDependents());
notifyChanged();
}
}
private void setCollapse(boolean isCollapsed) {
if (mCollapsed == isCollapsed) {
return;
}
mCollapsed = isCollapsed;
if (isCollapsed) {
hideDropdownPreferences();
} else {
showDropdownPreferences();
}
}
private void showDropdownPreferences() {
setAllPreferencesVisibility(true);
setIcon(R.drawable.ic_keyboard_arrow_down_black_32);
}
private void hideDropdownPreferences() {
setAllPreferencesVisibility(false);
setIcon(R.drawable.ic_keyboard_arrow_up_black_32);
}
private void setAllPreferencesVisibility(boolean visible) {
for (int i = 0; i < getPreferenceCount(); i++) {
Preference p = getPreference(i);
p.setVisible(visible);
}
}
}

View File

@@ -1,117 +0,0 @@
/*
* 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.settings;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.drawable.Drawable;
import android.support.v7.preference.Preference;
import android.support.v7.preference.Preference.OnPreferenceChangeListener;
import android.support.v7.preference.CheckBoxPreference;
import android.support.v7.preference.PreferenceViewHolder;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.text.format.Formatter;
import android.widget.TextView;
import com.android.settings.deletionhelper.DeletionType;
/**
* Preference to handle the deletion of various data types in the Deletion Helper.
*/
public abstract class DeletionPreference extends CheckBoxPreference implements
DeletionType.FreeableChangedListener, OnPreferenceChangeListener {
private DeletionType.FreeableChangedListener mListener;
private boolean mChecked;
private long mFreeableBytes;
private int mFreeableItems;
private DeletionType mDeletionService;
public DeletionPreference(Context context, AttributeSet attrs) {
super(context, attrs);
setOnPreferenceChangeListener(this);
}
@Override
public void onBindViewHolder(PreferenceViewHolder holder) {
super.onBindViewHolder(holder);
final TextView titleView = (TextView) holder.findViewById(android.R.id.title);
if (titleView != null) {
titleView.setTextColor(getTintColor(getContext()));
}
}
/**
* Returns the number of bytes which can be cleared by the deletion service.
* @return The number of bytes.
*/
public long getFreeableBytes() {
return mChecked ? mFreeableBytes : 0;
}
/**
* Register a listener to be called back on when the freeable bytes have changed.
* @param listener The callback listener.
*/
public void registerFreeableChangedListener(DeletionType.FreeableChangedListener listener) {
mListener = listener;
}
/**
* Registers a deletion service to update the preference's information.
* @param deletionService A photo/video deletion service.
*/
public void registerDeletionService(DeletionType deletionService) {
mDeletionService = deletionService;
if (mDeletionService != null) {
mDeletionService.registerFreeableChangedListener(this);
}
}
/**
* Returns the deletion service powering the preference.
* @return The deletion service.
*/
public DeletionType getDeletionService() {
return mDeletionService;
}
@Override
public void onFreeableChanged(int numItems, long freeableBytes) {
mFreeableItems = numItems;
mFreeableBytes = freeableBytes;
maybeUpdateListener();
}
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
mChecked = (boolean) newValue;
maybeUpdateListener();
return true;
}
private int getTintColor(Context context) {
TypedValue value = new TypedValue();
context.getTheme().resolveAttribute(android.R.attr.colorAccent, value, true);
return context.getColor(value.resourceId);
}
private void maybeUpdateListener() {
if (mListener != null) {
mListener.onFreeableChanged(mFreeableItems, getFreeableBytes());
}
}
}

View File

@@ -1,65 +0,0 @@
/*
* 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.settings;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.text.format.Formatter;
/**
* Preference to handle the deletion of photos and videos in the Deletion Helper.
*/
public class PhotosDeletionPreference extends DeletionPreference {
public static final int DAYS_TO_KEEP = 30;
public PhotosDeletionPreference(Context context, AttributeSet attrs) {
super(context, attrs);
setIcon(getIcon(context));
updatePreferenceText(0, 0);
}
/**
* Updates the title and summary of the preference with fresh information.
*/
public void updatePreferenceText(int items, long bytes) {
Context context = getContext();
setTitle(context.getString(R.string.deletion_helper_photos_title, items));
setSummary(context.getString(R.string.deletion_helper_photos_summary,
Formatter.formatFileSize(context, bytes), DAYS_TO_KEEP));
}
@Override
public void onFreeableChanged(int items, long bytes) {
super.onFreeableChanged(items, bytes);
updatePreferenceText(items, bytes);
}
private Drawable getIcon(Context context) {
final Drawable iconDrawable;
try {
Resources resources = context.getResources();
final int resId = resources.getIdentifier("ic_photos_black_24", "drawable",
context.getPackageName());
iconDrawable = context.getDrawable(resId);
} catch (Resources.NotFoundException e) {
return null;
}
return iconDrawable;
}
}

View File

@@ -74,7 +74,6 @@ import com.android.settings.bluetooth.BluetoothSettings;
import com.android.settings.dashboard.DashboardContainerFragment;
import com.android.settings.dashboard.SearchResultsSummary;
import com.android.settings.datausage.DataUsageSummary;
import com.android.settings.deletionhelper.DeletionHelperFragment;
import com.android.settings.deviceinfo.ImeiInformation;
import com.android.settings.deviceinfo.PrivateVolumeForget;
import com.android.settings.deviceinfo.PrivateVolumeSettings;
@@ -343,7 +342,6 @@ public class SettingsActivity extends SettingsDrawerActivity
WallpaperTypeSettings.class.getName(),
VrListenerSettings.class.getName(),
ManagedProfileSettings.class.getName(),
DeletionHelperFragment.class.getName(),
ChooseAccountActivity.class.getName(),
IccLockSettings.class.getName(),
ImeiInformation.class.getName(),

View File

@@ -1,91 +0,0 @@
/*
* 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.settings.deletionhelper;
import android.content.Context;
import android.support.v14.preference.SwitchPreference;
import android.support.v7.preference.Preference;
import android.support.v7.preference.PreferenceViewHolder;
import android.text.format.Formatter;
import android.view.View;
import android.widget.Switch;
import android.widget.TextView;
import com.android.settings.deletionhelper.AppStateUsageStatsBridge.UsageStatsState;
import com.android.settings.R;
import com.android.settingslib.applications.ApplicationsState;
import com.android.settingslib.applications.ApplicationsState.AppEntry;
/**
* Preference item for an app with a switch to signify if it should be uninstalled.
* This shows the name and icon of the app along with the days since its last use.
*/
public class AppDeletionPreference extends SwitchPreference {
private AppEntry mEntry;
private Context mContext;
public AppDeletionPreference(Context context, AppEntry item, ApplicationsState state) {
super(context);
mEntry = item;
mContext = context;
setLayoutResource(com.android.settings.R.layout.preference_app);
setWidgetLayoutResource(R.layout.widget_text_views);
synchronized (item) {
state.ensureIcon(item);
if (item.icon != null)
setIcon(item.icon);
if (item.label != null)
setTitle(item.label);
}
}
@Override
public void onBindViewHolder(PreferenceViewHolder holder) {
super.onBindViewHolder(holder);
Switch switchWidget = (Switch) holder.findViewById(com.android.internal.R.id.switch_widget);
switchWidget.setVisibility(View.VISIBLE);
TextView summary = (TextView) holder.findViewById(R.id.widget_text1);
updateSummaryText(summary);
}
public String getPackageName() {
return mEntry.label;
}
private void updateSummaryText(TextView summary) {
if (mEntry.extraInfo == null) return;
if (mEntry.size == ApplicationsState.SIZE_UNKNOWN ||
mEntry.size == ApplicationsState.SIZE_INVALID) {
return;
}
UsageStatsState extraData = (UsageStatsState) mEntry.extraInfo;
String fileSize = Formatter.formatFileSize(mContext, mEntry.size);
if (extraData.daysSinceLastUse == AppStateUsageStatsBridge.NEVER_USED) {
summary.setText(mContext.getString(R.string.deletion_helper_app_summary_never_used,
fileSize));
} else if (extraData.daysSinceLastUse == AppStateUsageStatsBridge.UNKNOWN_LAST_USE) {
summary.setText(mContext.getString(R.string.deletion_helper_app_summary_unknown_used,
fileSize));
} else {
summary.setText(mContext.getString(R.string.deletion_helper_app_summary,
fileSize,
extraData.daysSinceLastUse));
}
}
}

View File

@@ -1,152 +0,0 @@
/*
* 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.settings.deletionhelper;
import android.app.usage.UsageStats;
import android.app.usage.UsageStatsManager;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.util.Log;
import com.android.settings.applications.AppStateBaseBridge;
import com.android.settingslib.applications.ApplicationsState;
import com.android.settingslib.applications.ApplicationsState.AppEntry;
import com.android.settingslib.applications.ApplicationsState.AppFilter;
import java.util.ArrayList;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* Connects data from the UsageStatsManager to the ApplicationsState.
*/
public class AppStateUsageStatsBridge extends AppStateBaseBridge {
private static final String TAG = "AppStateUsageStatsBridge";
private UsageStatsManager mUsageStatsManager;
private PackageManager mPm;
public static final long NEVER_USED = -1;
public static final long UNKNOWN_LAST_USE = -2;
public static final long UNUSED_DAYS_DELETION_THRESHOLD = 60;
public AppStateUsageStatsBridge(Context context, ApplicationsState appState,
Callback callback) {
super(appState, callback);
mUsageStatsManager =
(UsageStatsManager) context.getSystemService(Context.USAGE_STATS_SERVICE);
mPm = context.getPackageManager();
}
@Override
protected void loadAllExtraInfo() {
ArrayList<AppEntry> apps = mAppSession.getAllApps();
if (apps == null) return;
final Map<String, UsageStats> map = mUsageStatsManager.queryAndAggregateUsageStats(0,
System.currentTimeMillis());
for (AppEntry entry : apps) {
UsageStats usageStats = map.get(entry.info.packageName);
entry.extraInfo = new UsageStatsState(getDaysSinceLastUse(usageStats),
getDaysSinceInstalled(entry.info.packageName));
}
}
@Override
protected void updateExtraInfo(AppEntry app, String pkg, int uid) {
Map<String, UsageStats> map = mUsageStatsManager.queryAndAggregateUsageStats(0,
System.currentTimeMillis());
UsageStats usageStats = map.get(app.info.packageName);
app.extraInfo = new UsageStatsState(getDaysSinceLastUse(usageStats),
getDaysSinceInstalled(app.info.packageName));
}
private long getDaysSinceLastUse(UsageStats stats) {
if (stats == null) {
return NEVER_USED;
}
long lastUsed = stats.getLastTimeUsed();
// Sometimes, a usage is recorded without a time and we don't know when the use was.
if (lastUsed == 0) {
return UNKNOWN_LAST_USE;
}
return TimeUnit.MILLISECONDS.toDays(System.currentTimeMillis() - lastUsed);
}
private long getDaysSinceInstalled(String packageName) {
PackageInfo pi = null;
try {
pi = mPm.getPackageInfo(packageName, 0);
} catch (PackageManager.NameNotFoundException e) {
Log.e(TAG, packageName + " was not found.");
}
if (pi == null) {
return NEVER_USED;
}
return (TimeUnit.MILLISECONDS.toDays(System.currentTimeMillis() - pi.firstInstallTime));
}
/**
* Filters only non-system apps which haven't been used in the last 60 days. If an app's last
* usage is unknown, it is skipped.
*/
public static final AppFilter FILTER_USAGE_STATS = new AppFilter() {
@Override
public void init() {
}
@Override
public boolean filterApp(AppEntry info) {
if (info == null) return false;
return isExtraInfoValid(info.extraInfo) && !isBundled(info)
&& !isPersistentProcess(info);
}
private boolean isExtraInfoValid(Object extraInfo) {
if (extraInfo == null || !(extraInfo instanceof UsageStatsState)) {
return false;
}
UsageStatsState state = (UsageStatsState) extraInfo;
long mostRecentUse = Math.max(state.daysSinceFirstInstall, state.daysSinceLastUse);
return mostRecentUse >= UNUSED_DAYS_DELETION_THRESHOLD || mostRecentUse == NEVER_USED;
}
private boolean isBundled(AppEntry info) {
return (info.info.flags & ApplicationInfo.FLAG_SYSTEM) != 0;
}
private boolean isPersistentProcess(AppEntry info) {
return (info.info.flags & ApplicationInfo.FLAG_PERSISTENT) != 0;
}
};
/**
* UsageStatsState contains the days since the last use and first install of a given app.
*/
public static class UsageStatsState {
public long daysSinceLastUse;
public long daysSinceFirstInstall;
public UsageStatsState(long daysSinceLastUse, long daysSinceFirstInstall) {
this.daysSinceLastUse = daysSinceLastUse;
this.daysSinceFirstInstall = daysSinceFirstInstall;
}
}
}

View File

@@ -1,49 +0,0 @@
/*
* 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.settings.deletionhelper;
import android.app.job.JobInfo;
import android.app.job.JobScheduler;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.text.format.DateUtils;
/**
* A {@link BroadcastReceiver} listening for {@link Intent#ACTION_BOOT_COMPLETED} broadcasts to
* schedule an automatic storage management job. Automatic storage management jobs are only
* scheduled once a day for a plugged in device.
*/
public class AutomaticStorageBroadcastReceiver extends BroadcastReceiver {
private static final int AUTOMATIC_STORAGE_JOB_ID = 0;
private static final long PERIOD = DateUtils.DAY_IN_MILLIS;
@Override
public void onReceive(Context context, Intent intent) {
JobScheduler jobScheduler =
(JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
ComponentName component = new ComponentName(context,
AutomaticStorageManagementJobService.class);
JobInfo job = new JobInfo.Builder(AUTOMATIC_STORAGE_JOB_ID, component)
.setRequiresCharging(true)
.setRequiresDeviceIdle(true)
.setPeriodic(PERIOD)
.build();
jobScheduler.schedule(job);
}
}

View File

@@ -1,90 +0,0 @@
/*
* 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.settings.deletionhelper;
import android.app.job.JobParameters;
import android.app.job.JobService;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.storage.StorageManager;
import android.os.storage.VolumeInfo;
import android.provider.Settings;
import android.util.Log;
import com.android.settings.overlay.FeatureFactory;
import com.android.settings.overlay.StorageManagementJobProvider;
import java.io.File;
/**
* {@link JobService} class to start automatic storage clearing jobs to free up space. The job only
* starts if the device is under a certain percent of free storage.
*/
public class AutomaticStorageManagementJobService extends JobService {
private static final String TAG = "AsmJobService";
private static final String SHARED_PREFRENCES_NAME = "automatic_storage_manager_settings";
private static final String KEY_DAYS_TO_RETAIN = "days_to_retain";
private static final long DEFAULT_LOW_FREE_PERCENT = 15;
private StorageManagementJobProvider mProvider;
@Override
public boolean onStartJob(JobParameters args) {
boolean isEnabled =
Settings.Secure.getInt(getContentResolver(),
Settings.Secure.AUTOMATIC_STORAGE_MANAGER_ENABLED, 0) != 0;
if (!isEnabled) {
return false;
}
StorageManager manager = getSystemService(StorageManager.class);
VolumeInfo internalVolume = manager.findVolumeById(VolumeInfo.ID_PRIVATE_INTERNAL);
final File dataPath = internalVolume.getPath();
if (!volumeNeedsManagement(dataPath)) {
Log.i(TAG, "Skipping automatic storage management.");
return false;
}
mProvider = FeatureFactory.getFactory(this).getStorageManagementJobProvider();
if (mProvider != null) {
return mProvider.onStartJob(this, args, getDaysToRetain());
}
return false;
}
@Override
public boolean onStopJob(JobParameters args) {
if (mProvider != null) {
return mProvider.onStopJob(this, args);
}
return false;
}
private int getDaysToRetain() {
SharedPreferences sharedPreferences =
getSharedPreferences(SHARED_PREFRENCES_NAME, Context.MODE_PRIVATE);
return sharedPreferences.getInt(KEY_DAYS_TO_RETAIN,
AutomaticStorageManagerSettings.DEFAULT_DAYS_TO_RETAIN);
}
private boolean volumeNeedsManagement(final File dataPath) {
long lowStorageThreshold = (dataPath.getTotalSpace() * DEFAULT_LOW_FREE_PERCENT) / 100;
return dataPath.getFreeSpace() < lowStorageThreshold;
}
}

View File

@@ -18,6 +18,7 @@ package com.android.settings.deletionhelper;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.os.Bundle;
import android.provider.Settings;
@@ -115,8 +116,8 @@ public class AutomaticStorageManagerSettings extends SettingsPreferenceFragment
@Override
public boolean onPreferenceClick(Preference preference) {
if (KEY_DELETION_HELPER.equals(preference.getKey())) {
startFragment(this, DeletionHelperFragment.class.getCanonicalName(),
R.string.deletion_helper_title, 0, null);
Intent intent = new Intent(Settings.ACTION_DELETION_HELPER_SETTINGS);
getContext().startActivity(intent);
}
return true;
}

View File

@@ -1,64 +0,0 @@
/*
* 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.settings.deletionhelper;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.DialogFragment;
import android.content.Context;
import android.content.DialogInterface;
import android.os.Bundle;
import android.text.format.Formatter;
import com.android.settings.R;
/**
* Fragment used to confirm that the user wishes to delete a certain amount of data.
*/
public class ConfirmDeletionDialog extends DialogFragment implements
DialogInterface.OnClickListener {
public static final String TAG = "ConfirmDeletionDialog";
private static final String ARG_TOTAL_SPACE = "total_freeable";
public static ConfirmDeletionDialog newInstance(long freeableBytes) {
Bundle args = new Bundle(1);
args.putLong(ARG_TOTAL_SPACE, freeableBytes);
ConfirmDeletionDialog dialog = new ConfirmDeletionDialog();
dialog.setArguments(args);
return dialog;
}
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
final Bundle args = getArguments();
long totalFreeableSpace = args.getLong(ARG_TOTAL_SPACE);
final Context context = getContext();
return new AlertDialog.Builder(context)
.setMessage(context.getString(R.string.deletion_helper_clear_dialog_message,
Formatter.formatFileSize(context, totalFreeableSpace)))
.setPositiveButton(R.string.deletion_helper_clear_dialog_remove, this)
.setNegativeButton(android.R.string.cancel, null)
.create();
}
@Override
public void onClick(DialogInterface dialog, int which) {
((DeletionHelperFragment) getTargetFragment()).clearData();
}
}

View File

@@ -1,415 +0,0 @@
/*
* 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.settings.deletionhelper;
import android.app.Activity;
import android.app.Application;
import android.content.Intent;
import android.os.Bundle;
import android.support.v7.preference.Preference;
import android.text.format.Formatter;
import android.util.ArraySet;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import com.android.settings.CollapsibleCheckboxPreferenceGroup;
import com.android.settings.PhotosDeletionPreference;
import com.android.settings.SettingsPreferenceFragment;
import com.android.settings.R;
import com.android.internal.logging.MetricsProto.MetricsEvent;
import com.android.settings.applications.AppStateBaseBridge;
import com.android.settings.overlay.DeletionHelperFeatureProvider;
import com.android.settings.overlay.FeatureFactory;
import com.android.settingslib.applications.ApplicationsState;
import com.android.settingslib.applications.ApplicationsState.AppEntry;
import com.android.settingslib.applications.ApplicationsState.Callbacks;
import com.android.settingslib.applications.ApplicationsState.Session;
import java.util.ArrayList;
import java.util.HashSet;
/**
* Settings screen for the deletion helper, which manually removes data which is not recently used.
*/
public class DeletionHelperFragment extends SettingsPreferenceFragment implements
ApplicationsState.Callbacks, AppStateBaseBridge.Callback,
Preference.OnPreferenceChangeListener, DeletionType.FreeableChangedListener,
View.OnClickListener {
public static final int CLEAR_DATA_RESULT = 1;
public static final String FREED_BYTES_KEY = "freed";
private static final String TAG = "DeletionHelperFragment";
private static final String EXTRA_HAS_BRIDGE = "hasBridge";
private static final String EXTRA_HAS_SIZES = "hasSizes";
private static final String EXTRA_CHECKED_SET = "checkedSet";
private static final String KEY_APPS_GROUP = "apps_group";
private static final String KEY_PHOTOS_VIDEOS_PREFERENCE = "delete_photos";
private static final String KEY_DOWNLOADS_PREFERENCE = "delete_downloads";
private static final int DOWNLOADS_LOADER_ID = 1;
private Button mCancel, mFree;
private CollapsibleCheckboxPreferenceGroup mApps;
private PhotosDeletionPreference mPhotoPreference;
private DownloadsDeletionPreferenceGroup mDownloadsPreference;
private ApplicationsState mState;
private Session mSession;
private HashSet<String> mCheckedApplications;
private AppStateUsageStatsBridge mDataUsageBridge;
private ArrayList<AppEntry> mAppEntries;
private boolean mHasReceivedAppEntries, mHasReceivedBridgeCallback, mFinishedLoading;
private DeletionHelperFeatureProvider mProvider;
private DeletionType mPhotoVideoDeletion;
private DownloadsDeletionType mDownloadsDeletion;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setAnimationAllowed(true);
Application app = getActivity().getApplication();
mState = ApplicationsState.getInstance(app);
mSession = mState.newSession(this);
mCheckedApplications = new HashSet<>();
mDataUsageBridge = new AppStateUsageStatsBridge(getActivity(), mState, this);
addPreferencesFromResource(R.xml.deletion_helper_list);
mApps = (CollapsibleCheckboxPreferenceGroup) findPreference(KEY_APPS_GROUP);
mPhotoPreference = (PhotosDeletionPreference) findPreference(KEY_PHOTOS_VIDEOS_PREFERENCE);
mDownloadsPreference =
(DownloadsDeletionPreferenceGroup) findPreference(KEY_DOWNLOADS_PREFERENCE);
mProvider =
FeatureFactory.getFactory(app).getDeletionHelperFeatureProvider();
if (mProvider != null) {
mPhotoVideoDeletion = mProvider.createPhotoVideoDeletionType(getContext());
}
mDownloadsDeletion = new DownloadsDeletionType(getActivity());
if (savedInstanceState != null) {
mHasReceivedAppEntries =
savedInstanceState.getBoolean(EXTRA_HAS_SIZES, false);
mHasReceivedBridgeCallback =
savedInstanceState.getBoolean(EXTRA_HAS_BRIDGE, false);
mCheckedApplications =
(HashSet<String>) savedInstanceState.getSerializable(EXTRA_CHECKED_SET);
}
}
private void initializeButtons(View v) {
mCancel = (Button) v.findViewById(R.id.skip_button);
mCancel.setText(R.string.cancel);
mCancel.setOnClickListener(this);
mCancel.setVisibility(View.VISIBLE);
mFree = (Button) v.findViewById(R.id.next_button);
mFree.setText(R.string.storage_menu_free);
mFree.setOnClickListener(this);
Button back = (Button) v.findViewById(R.id.back_button);
back.setVisibility(View.GONE);
}
private void initializeDeletionPreferences() {
if (mProvider == null) {
getPreferenceScreen().removePreference(mPhotoPreference);
mPhotoPreference = null;
} else {
mPhotoPreference.registerFreeableChangedListener(this);
mPhotoPreference.registerDeletionService(mPhotoVideoDeletion);
}
mDownloadsPreference.registerFreeableChangedListener(this);
mDownloadsPreference.registerDeletionService(mDownloadsDeletion);
mApps.setOnPreferenceChangeListener(this);
}
@Override
public void onViewCreated(View v, Bundle savedInstanceState) {
super.onViewCreated(v, savedInstanceState);
initializeButtons(v);
initializeDeletionPreferences();
setLoading(true, false);
}
@Override
public void onResume() {
super.onResume();
mSession.resume();
mDataUsageBridge.resume();
mDownloadsDeletion.onResume();
getLoaderManager().initLoader(DOWNLOADS_LOADER_ID, new Bundle(), mDownloadsDeletion);
if (mPhotoVideoDeletion != null) {
mPhotoVideoDeletion.onResume();
}
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putBoolean(EXTRA_HAS_SIZES, mHasReceivedAppEntries);
outState.putBoolean(EXTRA_HAS_BRIDGE, mHasReceivedBridgeCallback);
outState.putSerializable(EXTRA_CHECKED_SET, mCheckedApplications);
}
@Override
public void onPause() {
super.onPause();
mDataUsageBridge.pause();
mSession.pause();
mDownloadsDeletion.onPause();
if (mPhotoVideoDeletion != null) {
mPhotoVideoDeletion.onPause();
}
}
private void rebuild() {
// Only rebuild if we have the packages and their usage stats.
if (!mHasReceivedBridgeCallback || !mHasReceivedAppEntries) {
return;
}
final ArrayList<AppEntry> apps =
mSession.rebuild(AppStateUsageStatsBridge.FILTER_USAGE_STATS,
ApplicationsState.SIZE_COMPARATOR);
if (apps == null) return;
mAppEntries = apps;
refreshAppGroup(apps);
// All applications should be filled in if we've received the sizes.
// setLoading being called multiple times causes flickering, so we only do it once.
if (mHasReceivedAppEntries && !mFinishedLoading) {
mFinishedLoading = true;
setLoading(false, true);
getButtonBar().setVisibility(View.VISIBLE);
}
updateFreeButtonText();
}
private void updateFreeButtonText() {
mFree.setText(String.format(getActivity().getString(R.string.deletion_helper_free_button),
Formatter.formatFileSize(getActivity(), getTotalFreeableSpace())));
}
@Override
public void onRunningStateChanged(boolean running) {
// No-op.
}
@Override
public void onPackageListChanged() {
rebuild();
}
@Override
public void onRebuildComplete(ArrayList<AppEntry> apps) {
}
@Override
public void onPackageIconChanged() {
}
@Override
public void onPackageSizeChanged(String packageName) {
rebuild();
}
@Override
public void onAllSizesComputed() {
rebuild();
}
@Override
public void onLauncherInfoChanged() {
}
@Override
public void onLoadEntriesCompleted() {
mHasReceivedAppEntries = true;
rebuild();
}
@Override
public void onExtraInfoUpdated() {
mHasReceivedBridgeCallback = true;
rebuild();
}
@Override
protected int getMetricsCategory() {
return MetricsEvent.DEVICEINFO_STORAGE;
}
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
boolean checked = (boolean) newValue;
if (preference.getKey().equals(mApps.getKey())) {
return toggleAllApps(checked);
}
String packageName = ((AppDeletionPreference) preference).getPackageName();
if (checked) {
mCheckedApplications.add(packageName);
} else {
mCheckedApplications.remove(packageName);
// We remove the preference change listener to avoid toggling every app on and off.
mApps.setOnPreferenceChangeListener(null);
mApps.setChecked(false);
mApps.setOnPreferenceChangeListener(this);
}
updateFreeButtonText();
return true;
}
@Override
public void onFreeableChanged(int numItems, long freeableBytes) {
updateFreeButtonText();
}
@Override
public void onClick(View v) {
if (v.getId() == mFree.getId()) {
ConfirmDeletionDialog dialog =
ConfirmDeletionDialog.newInstance(getTotalFreeableSpace());
// The 0 is a placeholder for an optional result code.
dialog.setTargetFragment(this, 0);
dialog.show(getFragmentManager(), ConfirmDeletionDialog.TAG);
} else {
finishFragment();
}
}
/**
* 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();
}
mDownloadsDeletion.clearFreeableData();
ArraySet<String> apps = new ArraySet<>();
for (AppEntry entry : mAppEntries) {
if (mCheckedApplications.contains(entry.label)) {
synchronized (entry) {
apps.add(entry.info.packageName);
}
}
}
// TODO: If needed, add an action on the callback.
PackageDeletionTask task = new PackageDeletionTask(getActivity().getPackageManager(), apps,
new PackageDeletionTask.Callback() {
@Override
public void onSuccess() {
}
@Override
public void onError() {
Log.e(TAG, "An error occurred while uninstalling packages.");
}
});
Intent data = new Intent();
data.putExtra(FREED_BYTES_KEY, getTotalFreeableSpace());
getActivity().setResult(CLEAR_DATA_RESULT, data);
task.run();
finishFragment();
}
private long getTotalFreeableSpace() {
long freeableSpace = 0;
freeableSpace += getTotalAppsFreeableSpace(false);
if (mPhotoPreference != null) {
freeableSpace += mPhotoPreference.getFreeableBytes();
}
freeableSpace += mDownloadsDeletion.getFreeableBytes();
return freeableSpace;
}
private void refreshAppGroup(ArrayList<AppEntry> apps) {
int entryCount = apps.size();
cacheRemoveAllPrefs(mApps);
for (int i = 0; i < entryCount; i++) {
AppEntry entry = apps.get(i);
final String packageName = entry.label;
AppDeletionPreference preference =
(AppDeletionPreference) getCachedPreference(entry.label);
if (preference == null) {
preference = new AppDeletionPreference(getActivity(), entry, mState);
preference.setKey(packageName);
preference.setOnPreferenceChangeListener(this);
mApps.addPreference(preference);
}
preference.setChecked(mCheckedApplications.contains(packageName));
preference.setOrder(i);
}
removeCachedPrefs(mApps);
updateAppsGroupText();
}
private long getTotalAppsFreeableSpace(boolean countUnchecked) {
long freeableSpace = 0;
if (mAppEntries != null) {
for (int i = 0; i < mAppEntries.size(); i++) {
final AppEntry entry = mAppEntries.get(i);
long entrySize = mAppEntries.get(i).size;
// If the entrySize is negative, it is either an unknown size or an error occurred.
if ((countUnchecked ||
mCheckedApplications.contains(entry.label)) && entrySize > 0) {
freeableSpace += entrySize;
}
}
}
return freeableSpace;
}
private void updateAppsGroupText() {
if (mAppEntries != null) {
Activity app = getActivity();
mApps.setTitle(app.getString(R.string.deletion_helper_apps_group_title,
mAppEntries.size()));
mApps.setSummary(app.getString(R.string.deletion_helper_apps_group_summary,
Formatter.formatFileSize(app,
getTotalAppsFreeableSpace(true)),
AppStateUsageStatsBridge.UNUSED_DAYS_DELETION_THRESHOLD));
}
}
private boolean toggleAllApps(boolean checked) {
for (AppEntry entry : mAppEntries) {
final String packageName = entry.label;
if (checked) {
mCheckedApplications.add(packageName);
} else {
mCheckedApplications.remove(packageName);
}
}
refreshAppGroup(mAppEntries);
updateFreeButtonText();
return true;
}
}

View File

@@ -1,55 +0,0 @@
/*
* 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.settings.deletionhelper;
import android.content.Context;
import android.support.v7.preference.PreferenceGroup;
import android.support.v7.preference.Preference;
/**
* Helper for the Deletion Helper which can query, clear out, and visualize deletable data.
* This could represent a helper for deleting photos, downloads, movies, etc.
*/
public interface DeletionType {
/**
* Registers a callback to call when the amount of freeable space is updated.
* @param listener A callback.
*/
void registerFreeableChangedListener(FreeableChangedListener listener);
/**
* Resumes an operation, intended to be called when the deletion fragment resumes.
*/
void onResume();
/**
* Pauses the feature's operations, intended to be called when the deletion fragment is paused.
*/
void onPause();
/**
* Asynchronously free up the freeable information for the feature.
*/
void clearFreeableData();
/**
* Callback interface to listen for when a deletion feature's amount of freeable space updates.
*/
interface FreeableChangedListener {
void onFreeableChanged(int numItems, long bytesFreeable);
}
}

View File

@@ -1,157 +0,0 @@
/*
* 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.settings.deletionhelper;
import android.content.Context;
import android.support.v7.preference.Preference;
import android.text.format.DateUtils;
import android.text.format.Formatter;
import android.util.ArrayMap;
import android.util.AttributeSet;
import com.android.settings.CollapsibleCheckboxPreferenceGroup;
import com.android.settings.R;
import java.io.File;
import java.util.Set;
/**
* DownloadsDeletionPreferenceGroup defines a checkable preference group which contains
* downloads file deletion preferences.
*/
public class DownloadsDeletionPreferenceGroup extends CollapsibleCheckboxPreferenceGroup
implements DeletionType.FreeableChangedListener, Preference.OnPreferenceChangeListener {
private DownloadsDeletionType mDeletionType;
private DeletionType.FreeableChangedListener mListener;
public DownloadsDeletionPreferenceGroup(Context context) {
this(context, null);
}
public DownloadsDeletionPreferenceGroup(Context context, AttributeSet attrs) {
super(context, attrs);
setOrderingAsAdded(false);
setOnPreferenceChangeListener(this);
}
/**
* Set up a deletion type to get info for the preference group.
* @param type A {@link DownloadsDeletionType}.
*/
public void registerDeletionService(DownloadsDeletionType type) {
mDeletionType = type;
mDeletionType.registerFreeableChangedListener(this);
}
/**
* Registers a callback to be called when the amount of freeable space updates.
* @param listener The callback listener.
*/
public void registerFreeableChangedListener(DeletionType.FreeableChangedListener listener) {
mListener = listener;
}
@Override
public void onFreeableChanged(int numItems, long freeableBytes) {
updatePreferenceText(numItems, freeableBytes, mDeletionType.getMostRecentLastModified());
maybeUpdateListener(numItems, freeableBytes);
updateFiles();
}
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
boolean checked = (boolean) newValue;
if (!checked) {
// Temporarily stop listening to avoid propagating the checked change to children.
setOnPreferenceChangeListener(null);
setChecked(false);
setOnPreferenceChangeListener(this);
}
// If the group checkbox changed, we need to toggle every child preference.
if (preference == this) {
for (int i = 0; i < getPreferenceCount(); i++) {
DownloadsFilePreference p = (DownloadsFilePreference) getPreference(i);
p.setOnPreferenceChangeListener(null);
mDeletionType.toggleFile(p.getFile(), checked);
p.setChecked(checked);
p.setOnPreferenceChangeListener(this);
}
maybeUpdateListener(mDeletionType.getFiles().size(), mDeletionType.getFreeableBytes());
return true;
}
// If a single DownloadFilePreference changed, we need to toggle just itself.
DownloadsFilePreference p = (DownloadsFilePreference) preference;
mDeletionType.toggleFile(p.getFile(), checked);
maybeUpdateListener(mDeletionType.getFiles().size(), mDeletionType.getFreeableBytes());
return true;
}
private void updatePreferenceText(int itemCount, long bytes, long mostRecent) {
Context context = getContext();
setTitle(context.getString(R.string.deletion_helper_downloads_title, itemCount));
// If there are no files to clear, show the empty text instead.
if (itemCount != 0) {
setSummary(context.getString(R.string.deletion_helper_downloads_summary,
Formatter.formatFileSize(context, bytes),
DateUtils.getRelativeTimeSpanString(mostRecent,
System.currentTimeMillis(),
DateUtils.DAY_IN_MILLIS,
DateUtils.FORMAT_ABBREV_RELATIVE)));
} else {
setSummary(context.getString(R.string.deletion_helper_downloads_summary_empty,
Formatter.formatFileSize(context, bytes)));
}
}
private void maybeUpdateListener(int numItems, long bytesFreeable) {
if (mListener != null) {
mListener.onFreeableChanged(numItems, bytesFreeable);
}
}
private void updateFiles() {
// TODO: Remove impl overlap with the cached preferences methods in
// SettingsPreferenceFragment.
// Cache the existing file preferences.
ArrayMap<String, Preference> cachedPreferences = new ArrayMap<>();
for (int i = 0; i < getPreferenceCount(); i++) {
Preference p = getPreference(i);
cachedPreferences.put(p.getKey(), p);
}
// Iterate over all of the files and re-use the old file preference, if it exists.
Set<File> files = mDeletionType.getFiles();
for (File file : files) {
DownloadsFilePreference filePreference =
(DownloadsFilePreference) cachedPreferences.remove(file.getPath());
if (filePreference == null) {
filePreference = new DownloadsFilePreference(getContext(), file);
filePreference.setChecked(isChecked());
filePreference.setOnPreferenceChangeListener(this);
}
addPreference(filePreference);
}
// Remove all of the unused preferences.
for (Preference p : cachedPreferences.values()) {
removePreference(p);
}
}
}

View File

@@ -1,149 +0,0 @@
/*
* 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.settings.deletionhelper;
import android.app.LoaderManager.LoaderCallbacks;
import android.content.Context;
import android.content.Loader;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Environment;
import android.util.ArrayMap;
import android.util.ArraySet;
import com.android.settings.deletionhelper.FetchDownloadsLoader.DownloadsResult;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* The DownloadsDeletionType provides stale download file information to the
* {@link DownloadsDeletionPreferenceGroup}.
*/
public class DownloadsDeletionType implements DeletionType, LoaderCallbacks<DownloadsResult> {
private long mBytes;
private long mMostRecent;
private FreeableChangedListener mListener;
private Context mContext;
private ArrayMap<File, Boolean> mFiles;
public DownloadsDeletionType(Context context) {
mContext = context;
mFiles = new ArrayMap<>();
}
@Override
public void registerFreeableChangedListener(FreeableChangedListener listener) {
mListener = listener;
if (mFiles != null) {
maybeUpdateListener();
}
}
@Override
public void onResume() {
}
@Override
public void onPause() {
}
@Override
public void clearFreeableData() {
if (mFiles != null) {
AsyncTask.execute(new Runnable() {
@Override
public void run() {
for (Map.Entry<File, Boolean> entry : mFiles.entrySet()) {
if (entry.getValue()) {
entry.getKey().delete();
}
}
}
});
}
}
@Override
public Loader<DownloadsResult> onCreateLoader(int id, Bundle args) {
return new FetchDownloadsLoader(mContext,
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS));
}
@Override
public void onLoadFinished(Loader<DownloadsResult> loader, DownloadsResult data) {
mMostRecent = data.youngestLastModified;
for (File file : data.files) {
if (mFiles.containsKey(file)) {
continue;
}
mFiles.put(file, false);
}
mBytes = data.totalSize;
maybeUpdateListener();
}
@Override
public void onLoaderReset(Loader<DownloadsResult> loader) {
}
/**
* Returns the most recent last modified time for any clearable file.
* @return The last modified time.
*/
public long getMostRecentLastModified() {
return mMostRecent;
}
/**
* Returns the files in the Downloads folder after the loader task finishes.
*/
public Set<File> getFiles() {
if (mFiles == null) {
return null;
}
return mFiles.keySet();
}
/**
* Toggle if a file should be deleted when the service is asked to clear files.
*/
public void toggleFile(File file, boolean checked) {
mFiles.put(file, checked);
}
/**
* Returns the number of bytes that would be cleared if the deletion tasks runs.
*/
public long getFreeableBytes() {
long freedBytes = 0;
for (Map.Entry<File, Boolean> entry : mFiles.entrySet()) {
if (entry.getValue()) {
freedBytes += entry.getKey().length();
}
}
return freedBytes;
}
private void maybeUpdateListener() {
if (mListener != null) {
mListener.onFreeableChanged(mFiles.size(), mBytes);
}
}
}

View File

@@ -1,68 +0,0 @@
/*
* 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.settings.deletionhelper;
import android.content.Context;
import android.support.v7.preference.Preference;
import android.support.v7.preference.CheckBoxPreference;
import android.text.format.DateUtils;
import android.text.format.Formatter;
import com.android.settings.R;
import java.io.File;
/**
* DownloadsFilePreference is a preference representing a file in the Downloads folder
* with a checkbox that represents if the file should be deleted.
*/
public class DownloadsFilePreference extends CheckBoxPreference {
private File mFile;
public DownloadsFilePreference(Context context, File file) {
super(context);
mFile = file;
setKey(mFile.getPath());
setTitle(file.getName());
setSummary(context.getString(R.string.deletion_helper_downloads_summary,
Formatter.formatFileSize(getContext(), file.length()),
DateUtils.getRelativeTimeSpanString(mFile.lastModified(),
System.currentTimeMillis(),
DateUtils.DAY_IN_MILLIS,
DateUtils.FORMAT_ABBREV_RELATIVE)));
}
public File getFile() {
return mFile;
}
@Override
public int compareTo(Preference other) {
if (other == null) {
return 1;
}
if (other instanceof DownloadsFilePreference) {
DownloadsFilePreference preference = (DownloadsFilePreference) other;
return Long.compare(getFile().length(), preference.getFile().length());
} else {
// If a non-DownloadsFilePreference appears, consider ourselves to be greater.
// This means if a non-DownloadsFilePreference sneaks into a DownloadsPreferenceGroup
// then the DownloadsFilePreference will appear higher.
return 1;
}
}
}

View File

@@ -1,96 +0,0 @@
/*
* 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.settings.deletionhelper;
import android.content.Context;
import android.support.annotation.VisibleForTesting;
import com.android.settings.utils.AsyncLoader;
import java.io.File;
import java.util.ArrayList;
/**
* FetchDownloadsLoader is an asynchronous task which returns files in the Downloads
* directory which have not been modified in longer than 90 days.
*/
public class FetchDownloadsLoader extends
AsyncLoader<FetchDownloadsLoader.DownloadsResult> {
private File mDirectory;
/**
* Sets up a FetchDownloadsLoader in any directory.
* @param directory The directory to look into.
*/
public FetchDownloadsLoader(Context context, File directory) {
super(context);
mDirectory = directory;
}
@Override
protected void onDiscardResult(DownloadsResult result) {}
@Override
public DownloadsResult loadInBackground() {
return collectFiles(mDirectory);
}
@VisibleForTesting
static DownloadsResult collectFiles(File dir) {
return collectFiles(dir, new DownloadsResult());
}
private static DownloadsResult collectFiles(File dir, DownloadsResult result) {
File downloadFiles[] = dir.listFiles();
if (downloadFiles == null) {
}
if (downloadFiles != null && downloadFiles.length > 0) {
for (File currentFile : downloadFiles) {
if (currentFile.isDirectory()) {
collectFiles(currentFile, result);
} else {
if (currentFile.lastModified() < result.youngestLastModified) {
result.youngestLastModified = currentFile.lastModified();
}
result.files.add(currentFile);
result.totalSize += currentFile.length();
}
}
}
return result;
}
/**
* The DownloadsResult is the result of a {@link FetchDownloadsLoader} with the files
* and the amount of space they use.
*/
public static class DownloadsResult {
public long totalSize;
public long youngestLastModified;
public ArrayList<File> files;
public DownloadsResult() {
this(0, Long.MAX_VALUE, new ArrayList<File>());
}
public DownloadsResult(long totalSize, long youngestLastModified, ArrayList<File> files) {
this.totalSize = totalSize;
this.youngestLastModified = youngestLastModified;
this.files = files;
}
}
}

View File

@@ -1,58 +0,0 @@
package com.android.settings.deletionhelper;
import android.content.pm.IPackageDeleteObserver;
import android.content.pm.PackageManager;
import android.os.UserHandle;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
/**
* Deletes a specified set of apps as a specified user and calls back once done.
*/
public class PackageDeletionTask {
private Set<String> mPackages;
private Callback mCallback;
private PackageManager mPm;
private UserHandle mUser;
public PackageDeletionTask(PackageManager pm, Set<String> packageNames, Callback callback) {
mPackages = packageNames;
mCallback = callback;
mPm = pm;
mUser = android.os.Process.myUserHandle();
}
public void run() {
PackageDeletionObserver observer = new PackageDeletionObserver(mPackages.size());
for (String packageName : mPackages) {
mPm.deletePackageAsUser(packageName, observer, 0, mUser.getIdentifier());
}
}
private class PackageDeletionObserver extends IPackageDeleteObserver.Stub {
private final AtomicInteger mPackagesRemaining = new AtomicInteger(0);
public PackageDeletionObserver(int packages) {
mPackagesRemaining.set(packages);
}
@Override
public void packageDeleted(String packageName, int returnCode) {
if (returnCode != PackageManager.DELETE_SUCCEEDED) {
mCallback.onError();
return;
}
int remaining = mPackagesRemaining.decrementAndGet();
if (remaining == 0) {
mCallback.onSuccess();
}
}
}
public static abstract class Callback {
public abstract void onSuccess();
public abstract void onError();
}
}

View File

@@ -1,128 +0,0 @@
/*
* 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.settings.deletionhelper;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.DialogFragment;
import android.content.Context;
import android.content.DialogInterface;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.text.format.Formatter;
import com.android.settings.R;
import android.util.Log;
import java.util.concurrent.TimeUnit;
/**
* Fragment for activating the storage manager after a manual clear.
*/
public class StorageManagerUpsellDialog extends DialogFragment
implements DialogInterface.OnClickListener, DialogInterface.OnDismissListener {
public static final String TAG = "StorageManagerUpsellDialog";
private static final String SHARED_PREFERENCES_NAME = "StorageManagerUpsellDialog";
private static final String NEXT_SHOW_TIME = "next_show_time";
private static final String DISMISSED_COUNT = "dismissed_count";
private static final String NO_THANKS_COUNT = "no_thanks_count";
private static final String ARGS_FREED_BYTES = "freed_bytes";
private static final long NEVER = -1;
private static final long DISMISS_SHORT_DELAY = TimeUnit.DAYS.toMillis(14);
private static final long DISMISS_LONG_DELAY = TimeUnit.DAYS.toMillis(90);
private static final int DISMISS_LONG_THRESHOLD = 9;
private static final long NO_THANKS_SHORT_DELAY = TimeUnit.DAYS.toMillis(90);
private static final long NO_THANKS_LONG_DELAY = NEVER;
private static final int NO_THANKS_LONG_THRESHOLD = 3;
public static StorageManagerUpsellDialog newInstance(long freedBytes) {
StorageManagerUpsellDialog dialog = new StorageManagerUpsellDialog();
Bundle args = new Bundle(1);
args.putLong(ARGS_FREED_BYTES, freedBytes);
dialog.setArguments(args);
return dialog;
}
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
final Bundle args = getArguments();
long freedBytes = args.getLong(ARGS_FREED_BYTES);
final Context context = getContext();
return new AlertDialog.Builder(context)
.setTitle(context.getString(R.string.deletion_helper_upsell_title,
Formatter.formatFileSize(context, freedBytes)))
.setMessage(context.getString(R.string.deletion_helper_upsell_summary))
.setPositiveButton(R.string.deletion_helper_upsell_activate, this)
.setNegativeButton(R.string.deletion_helper_upsell_cancel, this)
.create();
}
@Override
public void onClick(DialogInterface dialog, int buttonId) {
if (buttonId == DialogInterface.BUTTON_POSITIVE) {
// TODO: Activate the storage manager once the storage manager is landed.
} else {
SharedPreferences sp = getSharedPreferences(getContext());
int noThanksCount = sp.getInt(NO_THANKS_COUNT, 0) + 1;
SharedPreferences.Editor editor = sp.edit();
editor.putInt(NO_THANKS_COUNT, noThanksCount);
editor.putLong(NEXT_SHOW_TIME,
System.currentTimeMillis() + getNoThanksDelay(noThanksCount));
editor.apply();
}
}
@Override
public void onCancel(DialogInterface dialog) {
SharedPreferences sp = getSharedPreferences(getContext());
int dismissCount = sp.getInt(DISMISSED_COUNT, 0) + 1;
SharedPreferences.Editor editor = sp.edit();
editor.putInt(DISMISSED_COUNT, dismissCount);
editor.putLong(NEXT_SHOW_TIME,
System.currentTimeMillis() + getDismissDelay(dismissCount));
editor.apply();
}
/**
* Returns if the dialog should be shown, given the delays between when it is shown.
* @param context Context to get shared preferences for determining the next show time.
*/
public static boolean shouldShow(Context context) {
// TODO: If the Storage Manager is enabled, return false.
long nextTimeToShow = getSharedPreferences(context).getLong(NEXT_SHOW_TIME, 0);
if (nextTimeToShow == NEVER) {
return false;
}
return System.currentTimeMillis() > nextTimeToShow;
}
private static SharedPreferences getSharedPreferences(Context context) {
return context.getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE);
}
private static long getNoThanksDelay(int noThanksCount) {
return (noThanksCount > NO_THANKS_LONG_THRESHOLD)
? NO_THANKS_LONG_DELAY : NO_THANKS_SHORT_DELAY;
}
private static long getDismissDelay(int dismissCount) {
return (dismissCount > DISMISS_LONG_THRESHOLD)
? DISMISS_LONG_DELAY : DISMISS_SHORT_DELAY;
}
}

View File

@@ -38,6 +38,7 @@ import android.os.storage.StorageManager;
import android.os.storage.VolumeInfo;
import android.os.storage.VolumeRecord;
import android.provider.DocumentsContract;
import android.provider.Settings;
import android.support.v7.preference.Preference;
import android.support.v7.preference.PreferenceCategory;
import android.support.v7.preference.PreferenceGroup;
@@ -59,9 +60,7 @@ import com.android.settings.Settings.StorageUseActivity;
import com.android.settings.SettingsPreferenceFragment;
import com.android.settings.Utils;
import com.android.settings.applications.ManageApplications;
import com.android.settings.deletionhelper.DeletionHelperFragment;
import com.android.settings.deletionhelper.AutomaticStorageManagerSettings;
import com.android.settings.deletionhelper.StorageManagerUpsellDialog;
import com.android.settings.deviceinfo.StorageSettings.MountTask;
import com.android.settingslib.deviceinfo.StorageMeasurement;
import com.android.settingslib.deviceinfo.StorageMeasurement.MeasurementDetails;
@@ -445,8 +444,9 @@ public class PrivateVolumeSettings extends SettingsPreferenceFragment {
startActivity(intent);
return true;
case R.id.storage_free:
startFragment(this, DeletionHelperFragment.class.getCanonicalName(),
R.string.deletion_helper_title, DELETION_HELPER_SETTINGS, args);
final Intent deletion_helper_intent =
new Intent(Settings.ACTION_DELETION_HELPER_SETTINGS);
startActivity(deletion_helper_intent);
return true;
}
return super.onOptionsItemSelected(item);
@@ -540,18 +540,6 @@ public class PrivateVolumeSettings extends SettingsPreferenceFragment {
return super.onPreferenceTreeClick(pref);
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == DELETION_HELPER_SETTINGS && resultCode == DELETION_HELPER_CLEAR &&
StorageManagerUpsellDialog.shouldShow(getActivity())) {
long freedBytes = data.getLongExtra(DeletionHelperFragment.FREED_BYTES_KEY, 0);
StorageManagerUpsellDialog dialog =
StorageManagerUpsellDialog.newInstance(freedBytes);
dialog.show(getFragmentManager(), StorageManagerUpsellDialog.TAG);
}
}
private final MeasurementReceiver mReceiver = new MeasurementReceiver() {
@Override
public void onDetailsChanged(MeasurementDetails details) {

View File

@@ -1,31 +0,0 @@
/*
* 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.settings.overlay;
import android.content.Context;
import android.support.v7.preference.PreferenceGroup;
import com.android.settings.deletionhelper.DeletionType;
/**
* Feature provider for the manual deletion helper Settings page.
*/
public interface DeletionHelperFeatureProvider {
/**
* Creates a {@link DeletionType} for clearing out stored photos and videos on the device.
*/
DeletionType createPhotoVideoDeletionType(Context context);
}

View File

@@ -61,12 +61,6 @@ public abstract class FeatureFactory {
public abstract SupportFeatureProvider getSupportFeatureProvider(Context context);
/**
* Return a provider which adds additional deletion services to the Deletion Helper.
*/
public abstract DeletionHelperFeatureProvider getDeletionHelperFeatureProvider();
public abstract StorageManagementJobProvider getStorageManagementJobProvider();
public static final class FactoryNotFoundException extends RuntimeException {
public FactoryNotFoundException(Throwable throwable) {
super("Unable to create factory. Did you misconfigure Proguard?", throwable);

View File

@@ -28,14 +28,4 @@ public final class FeatureFactoryImpl extends FeatureFactory {
return null;
}
@Override
public DeletionHelperFeatureProvider getDeletionHelperFeatureProvider() {
return null;
}
@Override
public StorageManagementJobProvider getStorageManagementJobProvider() {
return null;
}
}

View File

@@ -1,40 +0,0 @@
/*
* 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.settings.overlay;
import android.app.job.JobParameters;
import android.content.Context;
/**
* Feature provider for automatic storage management jobs.
*/
public interface StorageManagementJobProvider {
/**
* Starts an asynchronous deletion job to clear out storage older than
* @param params Standard JobService parameters.
* @param daysToRetain Number of days of information to retain on the device.
* @return If the job needs to process the work on a separate thread.
*/
boolean onStartJob(Context context, JobParameters params, int daysToRetain);
/**
* Attempt to stop the execution of the job.
* @param params Parameters specifying info about this job.
* @return If the job should be rescheduled.
*/
boolean onStopJob(Context context, JobParameters params);
}