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.CHANGE_APP_IDLE_STATE" />
|
||||||
<uses-permission android:name="android.permission.PEERS_MAC_ADDRESS"/>
|
<uses-permission android:name="android.permission.PEERS_MAC_ADDRESS"/>
|
||||||
<uses-permission android:name="android.permission.MANAGE_NOTIFICATIONS"/>
|
<uses-permission android:name="android.permission.MANAGE_NOTIFICATIONS"/>
|
||||||
|
<uses-permission android:name="android.permission.DELETE_PACKAGES"/>
|
||||||
|
|
||||||
<application android:label="@string/settings_label"
|
<application android:label="@string/settings_label"
|
||||||
android:icon="@mipmap/ic_launcher_settings"
|
android:icon="@mipmap/ic_launcher_settings"
|
||||||
|
@@ -60,6 +60,14 @@
|
|||||||
android:textColor="?android:attr/textColorSecondary"
|
android:textColor="?android:attr/textColorSecondary"
|
||||||
android:duplicateParentState="true" />
|
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
|
<FrameLayout
|
||||||
android:id="@android:id/widget_frame"
|
android:id="@android:id/widget_frame"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
|
@@ -30,4 +30,7 @@
|
|||||||
<item
|
<item
|
||||||
android:id="@+id/storage_migrate"
|
android:id="@+id/storage_migrate"
|
||||||
android:title="@string/storage_menu_migrate" />
|
android:title="@string/storage_menu_migrate" />
|
||||||
|
<item
|
||||||
|
android:id="@+id/storage_free"
|
||||||
|
android:title="@string/storage_menu_free" />
|
||||||
</menu>
|
</menu>
|
||||||
|
@@ -2441,6 +2441,8 @@
|
|||||||
<string name="storage_menu_set_up">Set up</string>
|
<string name="storage_menu_set_up">Set up</string>
|
||||||
<!-- Storage setting. Menu option for exploring a storage device [CHAR LIMIT=30]-->
|
<!-- Storage setting. Menu option for exploring a storage device [CHAR LIMIT=30]-->
|
||||||
<string name="storage_menu_explore">Explore</string>
|
<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]-->
|
<!-- Storage setting. Title for USB transfer settings [CHAR LIMIT=30]-->
|
||||||
<string name="storage_title_usb">USB computer connection</string>
|
<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>
|
@@ -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.SettingsPreferenceFragment;
|
||||||
import com.android.settings.Utils;
|
import com.android.settings.Utils;
|
||||||
import com.android.settings.applications.ManageApplications;
|
import com.android.settings.applications.ManageApplications;
|
||||||
|
import com.android.settings.deletionhelper.DeletionHelperFragment;
|
||||||
import com.android.settings.deviceinfo.StorageSettings.MountTask;
|
import com.android.settings.deviceinfo.StorageSettings.MountTask;
|
||||||
import com.android.settingslib.deviceinfo.StorageMeasurement;
|
import com.android.settingslib.deviceinfo.StorageMeasurement;
|
||||||
import com.android.settingslib.deviceinfo.StorageMeasurement.MeasurementDetails;
|
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 unmount = menu.findItem(R.id.storage_unmount);
|
||||||
final MenuItem format = menu.findItem(R.id.storage_format);
|
final MenuItem format = menu.findItem(R.id.storage_format);
|
||||||
final MenuItem migrate = menu.findItem(R.id.storage_migrate);
|
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
|
// Actions live in menu for non-internal private volumes; they're shown
|
||||||
// as preference items for public volumes.
|
// as preference items for public volumes.
|
||||||
@@ -369,11 +371,13 @@ public class PrivateVolumeSettings extends SettingsPreferenceFragment {
|
|||||||
mount.setVisible(false);
|
mount.setVisible(false);
|
||||||
unmount.setVisible(false);
|
unmount.setVisible(false);
|
||||||
format.setVisible(false);
|
format.setVisible(false);
|
||||||
|
manage.setVisible(true);
|
||||||
} else {
|
} else {
|
||||||
rename.setVisible(mVolume.getType() == VolumeInfo.TYPE_PRIVATE);
|
rename.setVisible(mVolume.getType() == VolumeInfo.TYPE_PRIVATE);
|
||||||
mount.setVisible(mVolume.getState() == VolumeInfo.STATE_UNMOUNTED);
|
mount.setVisible(mVolume.getState() == VolumeInfo.STATE_UNMOUNTED);
|
||||||
unmount.setVisible(mVolume.isMountedReadable());
|
unmount.setVisible(mVolume.isMountedReadable());
|
||||||
format.setVisible(true);
|
format.setVisible(true);
|
||||||
|
manage.setVisible(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
format.setTitle(R.string.storage_menu_format_public);
|
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());
|
intent.putExtra(VolumeInfo.EXTRA_VOLUME_ID, mVolume.getId());
|
||||||
startActivity(intent);
|
startActivity(intent);
|
||||||
return true;
|
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);
|
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