Merge "Add a deletion helper view for clearing space." into nyc-mr1-dev
am: 179103258e
* commit '179103258e4e664fbdd76b8440aff3edae86b093':
Add a deletion helper view for clearing space.
Change-Id: If02b00b3e35e717508ae69894a36ac71a1c3c30f
This commit is contained in:
@@ -81,6 +81,7 @@
|
||||
<uses-permission android:name="android.permission.CHANGE_APP_IDLE_STATE" />
|
||||
<uses-permission android:name="android.permission.PEERS_MAC_ADDRESS"/>
|
||||
<uses-permission android:name="android.permission.MANAGE_NOTIFICATIONS"/>
|
||||
<uses-permission android:name="android.permission.DELETE_PACKAGES"/>
|
||||
|
||||
<application android:label="@string/settings_label"
|
||||
android:icon="@mipmap/ic_launcher_settings"
|
||||
|
@@ -60,6 +60,14 @@
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:duplicateParentState="true" />
|
||||
|
||||
<Switch
|
||||
android:id="@android:id/switch_widget"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentRight="true"
|
||||
android:background="@null"
|
||||
android:visibility="gone"/>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@android:id/widget_frame"
|
||||
android:layout_width="wrap_content"
|
||||
|
@@ -30,4 +30,7 @@
|
||||
<item
|
||||
android:id="@+id/storage_migrate"
|
||||
android:title="@string/storage_menu_migrate" />
|
||||
<item
|
||||
android:id="@+id/storage_free"
|
||||
android:title="@string/storage_menu_free" />
|
||||
</menu>
|
||||
|
@@ -2441,6 +2441,8 @@
|
||||
<string name="storage_menu_set_up">Set up</string>
|
||||
<!-- Storage setting. Menu option for exploring a storage device [CHAR LIMIT=30]-->
|
||||
<string name="storage_menu_explore">Explore</string>
|
||||
<!-- Storage setting. Menu option for using the deletion helper. [CHAR LIMIT=30] -->
|
||||
<string name="storage_menu_free">Free up space</string>
|
||||
|
||||
<!-- Storage setting. Title for USB transfer settings [CHAR LIMIT=30]-->
|
||||
<string name="storage_title_usb">USB computer connection</string>
|
||||
|
24
res/xml/deletion_helper_list.xml
Normal file
24
res/xml/deletion_helper_list.xml
Normal file
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- 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.
|
||||
-->
|
||||
|
||||
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:title="@string/deletion_helper_title">
|
||||
|
||||
<PreferenceCategory
|
||||
android:key="apps_group"
|
||||
android:title="@string/deletion_helper_apps_title" />
|
||||
|
||||
</PreferenceScreen>
|
@@ -81,7 +81,7 @@ import com.android.settingslib.applications.ApplicationsState.VolumeFilter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.Locale;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* Activity to pick an application that will be used to display installation information and
|
||||
|
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* 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.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;
|
||||
}
|
||||
|
||||
long daysSinceLastUse = (long) mEntry.extraInfo;
|
||||
String fileSize = Formatter.formatFileSize(mContext, mEntry.size);
|
||||
if (daysSinceLastUse == AppStateUsageStatsBridge.NEVER_USED) {
|
||||
summary.setText(mContext.getString(R.string.deletion_helper_app_summary_never_used,
|
||||
fileSize));
|
||||
} else if (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,
|
||||
daysSinceLastUse));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
* 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 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 UsageStatsManager mUsageStatsManager;
|
||||
public static final long NEVER_USED = -1;
|
||||
public static final long UNKNOWN_LAST_USE = -2;
|
||||
|
||||
public AppStateUsageStatsBridge(Context context, ApplicationsState appState,
|
||||
Callback callback) {
|
||||
super(appState, callback);
|
||||
mUsageStatsManager =
|
||||
(UsageStatsManager) context.getSystemService(Context.USAGE_STATS_SERVICE);
|
||||
}
|
||||
|
||||
@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 = getDaysSinceLastUse(usageStats);
|
||||
}
|
||||
}
|
||||
|
||||
@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 = getDaysSinceLastUse(usageStats);
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 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() {
|
||||
private long UNUSED_DAYS_DELETION_THRESHOLD = 60;
|
||||
|
||||
@Override
|
||||
public void init() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean filterApp(AppEntry info) {
|
||||
if (info == null) return false;
|
||||
boolean isBundled = (info.info.flags & ApplicationInfo.FLAG_SYSTEM) != 0;
|
||||
return isExtraInfoValid(info.extraInfo) && !isBundled;
|
||||
}
|
||||
|
||||
private boolean isExtraInfoValid(Object extraInfo) {
|
||||
if (extraInfo == null || !(extraInfo instanceof Long)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
long daysSinceLastUse = (long) extraInfo;
|
||||
return daysSinceLastUse >= UNUSED_DAYS_DELETION_THRESHOLD ||
|
||||
daysSinceLastUse == NEVER_USED;
|
||||
}
|
||||
};
|
||||
}
|
@@ -0,0 +1,262 @@
|
||||
package com.android.settings.deletionhelper;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.support.v7.preference.Preference;
|
||||
import android.support.v7.preference.PreferenceGroup;
|
||||
import android.text.format.Formatter;
|
||||
import android.util.ArraySet;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.CompoundButton;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.RelativeLayout;
|
||||
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.deletionhelper.AppStateUsageStatsBridge;
|
||||
import com.android.settings.deletionhelper.AppDeletionPreference;
|
||||
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.Comparator;
|
||||
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 {
|
||||
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 Button mCancel, mFree;
|
||||
private PreferenceGroup mApps;
|
||||
|
||||
private ApplicationsState mState;
|
||||
private Session mSession;
|
||||
private HashSet<String> mUncheckedApplications;
|
||||
private AppStateUsageStatsBridge mDataUsageBridge;
|
||||
private ArrayList<AppEntry> mAppEntries;
|
||||
private boolean mHasReceivedAppEntries, mHasReceivedBridgeCallback, mFinishedLoading;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
mState = ApplicationsState.getInstance(getActivity().getApplication());
|
||||
mSession = mState.newSession(this);
|
||||
mUncheckedApplications = new HashSet<>();
|
||||
mDataUsageBridge = new AppStateUsageStatsBridge(getActivity(), mState, this);
|
||||
|
||||
addPreferencesFromResource(R.xml.deletion_helper_list);
|
||||
mApps = (PreferenceGroup) findPreference(KEY_APPS_GROUP);
|
||||
|
||||
if (savedInstanceState != null) {
|
||||
mHasReceivedAppEntries =
|
||||
savedInstanceState.getBoolean(EXTRA_HAS_SIZES, false);
|
||||
mHasReceivedBridgeCallback =
|
||||
savedInstanceState.getBoolean(EXTRA_HAS_BRIDGE, false);
|
||||
mUncheckedApplications =
|
||||
(HashSet<String>) savedInstanceState.getSerializable(EXTRA_CHECKED_SET);
|
||||
}
|
||||
}
|
||||
|
||||
private void initializeButtons(View v) {
|
||||
mCancel = (Button) v.findViewById(R.id.back_button);
|
||||
mCancel.setText(R.string.cancel);
|
||||
mCancel.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
finishFragment();
|
||||
}
|
||||
});
|
||||
|
||||
mFree = (Button) v.findViewById(R.id.next_button);
|
||||
mFree.setText(R.string.storage_menu_free);
|
||||
mFree.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
ArraySet<String> apps = new ArraySet<>();
|
||||
for (AppEntry entry : mAppEntries) {
|
||||
if (!mUncheckedApplications.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.");
|
||||
}
|
||||
});
|
||||
finishFragment();
|
||||
task.run();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View v, Bundle savedInstanceState) {
|
||||
super.onViewCreated(v, savedInstanceState);
|
||||
initializeButtons(v);
|
||||
setLoading(true, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
mSession.resume();
|
||||
mDataUsageBridge.resume();
|
||||
}
|
||||
|
||||
|
||||
@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, mUncheckedApplications);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
mDataUsageBridge.pause();
|
||||
mSession.pause();
|
||||
}
|
||||
|
||||
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);
|
||||
mAppEntries = apps;
|
||||
cacheRemoveAllPrefs(mApps);
|
||||
int entryCount = apps.size();
|
||||
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.setChecked(!mUncheckedApplications.contains(packageName));
|
||||
preference.setOnPreferenceChangeListener(this);
|
||||
mApps.addPreference(preference);
|
||||
}
|
||||
preference.setOrder(i);
|
||||
}
|
||||
removeCachedPrefs(mApps);
|
||||
|
||||
// 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;
|
||||
String packageName = ((AppDeletionPreference) preference).getPackageName();
|
||||
if (checked) {
|
||||
mUncheckedApplications.remove(packageName);
|
||||
} else {
|
||||
mUncheckedApplications.add(packageName);
|
||||
}
|
||||
updateFreeButtonText();
|
||||
return true;
|
||||
}
|
||||
|
||||
private long getTotalFreeableSpace() {
|
||||
long freeableSpace = 0;
|
||||
for (int i = 0; i < mAppEntries.size(); i++) {
|
||||
final AppEntry entry = mAppEntries.get(i);
|
||||
if (!mUncheckedApplications.contains(entry.label)) {
|
||||
freeableSpace += mAppEntries.get(i).size;
|
||||
}
|
||||
}
|
||||
return freeableSpace;
|
||||
}
|
||||
}
|
@@ -0,0 +1,58 @@
|
||||
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();
|
||||
}
|
||||
}
|
@@ -58,6 +58,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.deviceinfo.StorageSettings.MountTask;
|
||||
import com.android.settingslib.deviceinfo.StorageMeasurement;
|
||||
import com.android.settingslib.deviceinfo.StorageMeasurement.MeasurementDetails;
|
||||
@@ -361,6 +362,7 @@ public class PrivateVolumeSettings extends SettingsPreferenceFragment {
|
||||
final MenuItem unmount = menu.findItem(R.id.storage_unmount);
|
||||
final MenuItem format = menu.findItem(R.id.storage_format);
|
||||
final MenuItem migrate = menu.findItem(R.id.storage_migrate);
|
||||
final MenuItem manage = menu.findItem(R.id.storage_free);
|
||||
|
||||
// Actions live in menu for non-internal private volumes; they're shown
|
||||
// as preference items for public volumes.
|
||||
@@ -369,11 +371,13 @@ public class PrivateVolumeSettings extends SettingsPreferenceFragment {
|
||||
mount.setVisible(false);
|
||||
unmount.setVisible(false);
|
||||
format.setVisible(false);
|
||||
manage.setVisible(true);
|
||||
} else {
|
||||
rename.setVisible(mVolume.getType() == VolumeInfo.TYPE_PRIVATE);
|
||||
mount.setVisible(mVolume.getState() == VolumeInfo.STATE_UNMOUNTED);
|
||||
unmount.setVisible(mVolume.isMountedReadable());
|
||||
format.setVisible(true);
|
||||
manage.setVisible(false);
|
||||
}
|
||||
|
||||
format.setTitle(R.string.storage_menu_format_public);
|
||||
@@ -412,6 +416,10 @@ public class PrivateVolumeSettings extends SettingsPreferenceFragment {
|
||||
intent.putExtra(VolumeInfo.EXTRA_VOLUME_ID, mVolume.getId());
|
||||
startActivity(intent);
|
||||
return true;
|
||||
case R.id.storage_free:
|
||||
startFragment(this, DeletionHelperFragment.class.getCanonicalName(),
|
||||
R.string.deletion_helper_title, 0, args);
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
* 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.test.AndroidTestCase;
|
||||
import android.content.pm.IPackageDeleteObserver;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.RemoteException;
|
||||
import android.test.mock.MockPackageManager;
|
||||
import android.test.suitebuilder.annotation.SmallTest;
|
||||
|
||||
import com.android.settings.deletionhelper.PackageDeletionTask;
|
||||
import com.android.settings.deletionhelper.PackageDeletionTask.Callback;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.HashSet;
|
||||
|
||||
public class PackageDeletionTaskTest extends AndroidTestCase {
|
||||
private FakePackageManager mPackageManager;
|
||||
private Set<String> mDeletedApps;
|
||||
|
||||
@Override
|
||||
protected void setUp() throws Exception {
|
||||
mPackageManager = new FakePackageManager();
|
||||
mDeletedApps = new HashSet<String>();
|
||||
}
|
||||
|
||||
@SmallTest
|
||||
public void testDeleteNoApps() throws Exception {
|
||||
runTask(new HashSet<String>(), false);
|
||||
}
|
||||
|
||||
@SmallTest
|
||||
public void testDeleteOneApp() throws Exception {
|
||||
HashSet<String> appsToDelete = new HashSet<String>();
|
||||
appsToDelete.add("app.test1");
|
||||
runTask(appsToDelete, false);
|
||||
}
|
||||
|
||||
@SmallTest
|
||||
public void testDeleteManyApps() throws Exception {
|
||||
HashSet<String> appsToDelete = new HashSet<String>();
|
||||
appsToDelete.add("app.test1");
|
||||
appsToDelete.add("app.test2");
|
||||
runTask(appsToDelete, false);
|
||||
}
|
||||
|
||||
@SmallTest
|
||||
public void testDeleteFails() throws Exception {
|
||||
HashSet<String> appsToDelete = new HashSet<String>();
|
||||
appsToDelete.add("app.test1");
|
||||
mPackageManager.deletionSucceeds = false;
|
||||
runTask(appsToDelete, true);
|
||||
}
|
||||
|
||||
private void runTask(HashSet<String> appsToDelete, boolean shouldFail) {
|
||||
PackageDeletionTask task = new PackageDeletionTask(mPackageManager, appsToDelete,
|
||||
new VerifierCallback(appsToDelete, shouldFail));
|
||||
task.run();
|
||||
}
|
||||
|
||||
class FakePackageManager extends MockPackageManager {
|
||||
public boolean deletionSucceeds = true;
|
||||
|
||||
@Override
|
||||
public void deletePackageAsUser(String packageName, IPackageDeleteObserver observer,
|
||||
int flags, int userId) {
|
||||
int resultCode;
|
||||
if (deletionSucceeds) {
|
||||
resultCode = PackageManager.DELETE_SUCCEEDED;
|
||||
mDeletedApps.add(packageName);
|
||||
} else {
|
||||
resultCode = PackageManager.DELETE_FAILED_INTERNAL_ERROR;
|
||||
}
|
||||
|
||||
try {
|
||||
observer.packageDeleted(packageName, resultCode);
|
||||
} catch (RemoteException e) {
|
||||
fail(e.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class VerifierCallback extends Callback {
|
||||
private Set<String> mExpectedDeletedApps;
|
||||
private boolean mShouldFail;
|
||||
|
||||
public VerifierCallback(HashSet<String> expectedDeletedApps, boolean shouldFail) {
|
||||
mExpectedDeletedApps = expectedDeletedApps;
|
||||
mShouldFail = shouldFail;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSuccess() {
|
||||
System.out.println("lol");
|
||||
assertFalse(mShouldFail);
|
||||
assertEquals(mExpectedDeletedApps, mDeletedApps);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError() {
|
||||
assertTrue(mShouldFail);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
Reference in New Issue
Block a user