Move packages between candidate volumes.

We now allow moving packages between all possible candidate volumes,
as determined by the framework.  Moving now jumps through wizard to
help user understand what's going on.

Bug: 19993667
Change-Id: I5416ed2865f400b1d718c68a3f0e033080fefa0e
This commit is contained in:
Jeff Sharkey
2015-04-15 14:32:25 -07:00
parent 7bc9235aaa
commit 1f4efd7360
7 changed files with 246 additions and 182 deletions

View File

@@ -36,7 +36,7 @@
<ListView android:id="@android:id/list"
style="@style/PreferenceFragmentListSinglePane"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_height="match_parent"
android:paddingStart="@dimen/settings_side_margin"
android:paddingEnd="@dimen/settings_side_margin"
android:paddingTop="@dimen/dashboard_padding_top"

View File

@@ -23,13 +23,12 @@ import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.IPackageDataObserver;
import android.content.pm.IPackageMoveObserver;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.os.Message;
import android.os.RemoteException;
import android.os.storage.StorageManager;
import android.os.storage.VolumeInfo;
import android.text.format.Formatter;
import android.util.Log;
import android.view.View;
@@ -38,11 +37,16 @@ import android.widget.Button;
import android.widget.TextView;
import com.android.internal.logging.MetricsLogger;
import com.android.settings.DropDownPreference;
import com.android.settings.R;
import com.android.settings.Utils;
import com.android.settings.applications.ApplicationsState.AppEntry;
import com.android.settings.applications.ApplicationsState.Callbacks;
import com.android.settings.DropDownPreference;
import com.android.settings.deviceinfo.StorageWizardMoveConfirm;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
public class AppStorageSettings extends AppInfoWithHeader
implements OnClickListener, Callbacks, DropDownPreference.Callback {
@@ -53,7 +57,6 @@ public class AppStorageSettings extends AppInfoWithHeader
private static final int OP_FAILED = 2;
private static final int MSG_CLEAR_USER_DATA = 1;
private static final int MSG_CLEAR_CACHE = 3;
private static final int MSG_PACKAGE_MOVE = 4;
// invalid size value used initially and also when size retrieval through PackageManager
// fails for whatever reason
@@ -64,13 +67,11 @@ public class AppStorageSettings extends AppInfoWithHeader
private static final int DLG_CLEAR_DATA = DLG_BASE + 1;
private static final int DLG_CANNOT_CLEAR_DATA = DLG_BASE + 2;
private static final int DLG_MOVE_FAILED = DLG_BASE + 3;
private static final String KEY_MOVE_PREFERENCE = "app_location_setting";
private static final String KEY_STORAGE_SETTINGS = "storage_settings";
private static final String KEY_CACHE_SETTINGS = "cache_settings";
private CanBeOnSdCardChecker mCanBeOnSdCardChecker;
private TextView mTotalSize;
private TextView mAppSize;
private TextView mDataSize;
@@ -83,7 +84,6 @@ public class AppStorageSettings extends AppInfoWithHeader
private Button mClearCacheButton;
private DropDownPreference mMoveDropDown;
private boolean mMoveInProgress = false;
private boolean mCanClearData = true;
private boolean mHaveSizes = false;
@@ -97,7 +97,6 @@ public class AppStorageSettings extends AppInfoWithHeader
private ClearCacheObserver mClearCacheObserver;
private ClearUserDataObserver mClearDataObserver;
private PackageMoveObserver mPackageMoveObserver;
// Resource strings
private CharSequence mInvalidSizeStr;
@@ -107,7 +106,6 @@ public class AppStorageSettings extends AppInfoWithHeader
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mCanBeOnSdCardChecker = new CanBeOnSdCardChecker();
addPreferencesFromResource(R.xml.app_storage_settings);
setupViews();
}
@@ -166,18 +164,19 @@ public class AppStorageSettings extends AppInfoWithHeader
@Override
public boolean onItemSelected(int pos, Object value) {
boolean selectedExternal = (Boolean) value;
boolean isExternal = (mAppEntry.info.flags & ApplicationInfo.FLAG_EXTERNAL_STORAGE) != 0;
if (selectedExternal ^ isExternal) {
if (mPackageMoveObserver == null) {
mPackageMoveObserver = new PackageMoveObserver();
}
int moveFlags = selectedExternal ? PackageManager.MOVE_EXTERNAL_MEDIA
: PackageManager.MOVE_INTERNAL;
mMoveInProgress = true;
refreshButtons();
mPm.movePackage(mAppEntry.info.packageName, mPackageMoveObserver, moveFlags);
final Context context = getActivity();
// If not current volume, kick off move wizard
final VolumeInfo targetVol = (VolumeInfo) value;
final VolumeInfo currentVol = context.getPackageManager().getApplicationCurrentVolume(
mAppEntry.info);
if (!Objects.equals(targetVol, currentVol)) {
final Intent intent = new Intent(context, StorageWizardMoveConfirm.class);
intent.putExtra(VolumeInfo.EXTRA_VOLUME_ID, targetVol.getId());
intent.putExtra(Intent.EXTRA_PACKAGE_NAME, mAppEntry.info.packageName);
startActivity(intent);
}
return true;
}
@@ -260,27 +259,17 @@ public class AppStorageSettings extends AppInfoWithHeader
retrieveAppEntry();
refreshButtons();
refreshSizeInfo();
boolean isExternal = (mAppEntry.info.flags & ApplicationInfo.FLAG_EXTERNAL_STORAGE) != 0;
mMoveDropDown.setSelectedItem(isExternal ? 1 : 0);
final VolumeInfo currentVol = getActivity().getPackageManager()
.getApplicationCurrentVolume(mAppEntry.info);
mMoveDropDown.setSelectedValue(currentVol);
return true;
}
private void refreshButtons() {
if (!mMoveInProgress) {
initMoveDropDown();
initDataButtons();
} else {
mMoveDropDown.setSummary(R.string.moving);
mMoveDropDown.setSelectable(false);
}
}
private void updateMoveEnabled(boolean enabled) {
mMoveDropDown.clearItems();
mMoveDropDown.addItem(R.string.storage_type_internal, false);
if (enabled) {
mMoveDropDown.addItem(R.string.storage_type_external, true);
}
initMoveDropDown();
initDataButtons();
}
private void initDataButtons() {
@@ -310,20 +299,18 @@ public class AppStorageSettings extends AppInfoWithHeader
}
private void initMoveDropDown() {
if (Environment.isExternalStorageEmulated()) {
updateMoveEnabled(false);
return;
final Context context = getActivity();
final StorageManager storage = context.getSystemService(StorageManager.class);
final List<VolumeInfo> candidates = context.getPackageManager()
.getApplicationCandidateVolumes(mAppEntry.info);
Collections.sort(candidates, VolumeInfo.getDescriptionComparator());
mMoveDropDown.clearItems();
for (VolumeInfo vol : candidates) {
final String volDescrip = storage.getBestVolumeDescription(vol);
mMoveDropDown.addItem(volDescrip, vol);
}
boolean dataOnly = (mPackageInfo == null) && (mAppEntry != null);
boolean moveDisable = true;
if ((mAppEntry.info.flags & ApplicationInfo.FLAG_EXTERNAL_STORAGE) != 0) {
// Always let apps move to internal storage from sdcard.
moveDisable = false;
} else {
mCanBeOnSdCardChecker.init();
moveDisable = !mCanBeOnSdCardChecker.check(mAppEntry.info);
}
updateMoveEnabled(!moveDisable);
mMoveDropDown.setSelectable(!mAppControlRestricted);
}
@@ -351,21 +338,6 @@ public class AppStorageSettings extends AppInfoWithHeader
}
}
private void processMoveMsg(Message msg) {
int result = msg.arg1;
String packageName = mAppEntry.info.packageName;
// Refresh the button attributes.
mMoveInProgress = false;
if (result == PackageManager.MOVE_SUCCEEDED) {
Log.i(TAG, "Moved resources for " + packageName);
// Refresh size information again.
mState.requestSize(mPackageName, mUserId);
} else {
showDialogInner(DLG_MOVE_FAILED, result);
}
refreshUi();
}
/*
* Private method to handle clear message notification from observer when
* the async operation from PackageManager is complete
@@ -382,24 +354,6 @@ public class AppStorageSettings extends AppInfoWithHeader
}
}
private CharSequence getMoveErrMsg(int errCode) {
switch (errCode) {
case PackageManager.MOVE_FAILED_INSUFFICIENT_STORAGE:
return getActivity().getString(R.string.insufficient_storage);
case PackageManager.MOVE_FAILED_DOESNT_EXIST:
return getActivity().getString(R.string.does_not_exist);
case PackageManager.MOVE_FAILED_FORWARD_LOCKED:
return getActivity().getString(R.string.app_forward_locked);
case PackageManager.MOVE_FAILED_INVALID_LOCATION:
return getActivity().getString(R.string.invalid_location);
case PackageManager.MOVE_FAILED_SYSTEM_PACKAGE:
return getActivity().getString(R.string.system_package);
case PackageManager.MOVE_FAILED_INTERNAL_ERROR:
return "";
}
return "";
}
@Override
protected AlertDialog createDialog(int id, int errorCode) {
switch (id) {
@@ -427,14 +381,6 @@ public class AppStorageSettings extends AppInfoWithHeader
}
})
.create();
case DLG_MOVE_FAILED:
CharSequence msg = getActivity().getString(R.string.move_app_failed_dlg_text,
getMoveErrMsg(errorCode));
return new AlertDialog.Builder(getActivity())
.setTitle(getActivity().getText(R.string.move_app_failed_dlg_title))
.setMessage(msg)
.setNeutralButton(R.string.dlg_ok, null)
.create();
}
return null;
}
@@ -459,9 +405,6 @@ public class AppStorageSettings extends AppInfoWithHeader
// Refresh size info
mState.requestSize(mPackageName, mUserId);
break;
case MSG_PACKAGE_MOVE:
processMoveMsg(msg);
break;
}
}
};
@@ -508,13 +451,4 @@ public class AppStorageSettings extends AppInfoWithHeader
mHandler.sendMessage(msg);
}
}
class PackageMoveObserver extends IPackageMoveObserver.Stub {
public void packageMoved(String packageName, int returnCode) throws RemoteException {
final Message msg = mHandler.obtainMessage(MSG_PACKAGE_MOVE);
msg.arg1 = returnCode;
mHandler.sendMessage(msg);
}
}
}

View File

@@ -295,20 +295,6 @@ public class ApplicationsState {
}
};
public static final AppFilter FILTER_ON_SD_CARD = new AppFilter() {
final CanBeOnSdCardChecker mCanBeOnSdCardChecker
= new CanBeOnSdCardChecker();
public void init() {
mCanBeOnSdCardChecker.init();
}
@Override
public boolean filterApp(AppEntry entry) {
return mCanBeOnSdCardChecker.check(entry.info);
}
};
public static final AppFilter FILTER_DISABLED = new AppFilter() {
public void init() {
}

View File

@@ -20,15 +20,11 @@ import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.IPackageManager;
import android.content.pm.IntentFilterVerificationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.UserHandle;
import android.os.UserManager;
import android.preference.PreferenceFrameLayout;
@@ -53,7 +49,6 @@ import android.widget.Filterable;
import android.widget.ListView;
import android.widget.Spinner;
import com.android.internal.content.PackageHelper;
import com.android.internal.logging.MetricsLogger;
import com.android.settings.HelpUtils;
import com.android.settings.InstrumentedFragment;
@@ -74,47 +69,6 @@ import java.util.Collections;
import java.util.Comparator;
import java.util.List;
final class CanBeOnSdCardChecker {
final IPackageManager mPm;
int mInstallLocation;
CanBeOnSdCardChecker() {
mPm = IPackageManager.Stub.asInterface(
ServiceManager.getService("package"));
}
void init() {
try {
mInstallLocation = mPm.getInstallLocation();
} catch (RemoteException e) {
Log.e("CanBeOnSdCardChecker", "Is Package Manager running?");
return;
}
}
boolean check(ApplicationInfo info) {
boolean canBe = false;
if ((info.flags & ApplicationInfo.FLAG_EXTERNAL_STORAGE) != 0) {
canBe = true;
} else {
if ((info.flags & ApplicationInfo.FLAG_SYSTEM) == 0) {
if (info.installLocation == PackageInfo.INSTALL_LOCATION_PREFER_EXTERNAL ||
info.installLocation == PackageInfo.INSTALL_LOCATION_AUTO) {
canBe = true;
} else if (info.installLocation
== PackageInfo.INSTALL_LOCATION_UNSPECIFIED) {
if (mInstallLocation == PackageHelper.APP_INSTALL_EXTERNAL) {
// For apps with no preference and the default value set
// to install on sdcard.
canBe = true;
}
}
}
}
return canBe;
}
}
/**
* Activity to pick an application that will be used to display installation information and
* options to uninstall/delete user data for system applications. This activity

View File

@@ -41,7 +41,6 @@ import com.android.settings.search.SearchIndexableRaw;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
/**
@@ -91,21 +90,6 @@ public class StorageSettings extends SettingsPreferenceFragment implements Index
setHasOptionsMenu(true);
}
private static final Comparator<VolumeInfo> sVolumeComparator = new Comparator<VolumeInfo>() {
@Override
public int compare(VolumeInfo lhs, VolumeInfo rhs) {
if (VolumeInfo.ID_PRIVATE_INTERNAL.equals(lhs.getId())) {
return -1;
} else if (lhs.getDescription() == null) {
return 1;
} else if (rhs.getDescription() == null) {
return -1;
} else {
return lhs.getDescription().compareTo(rhs.getDescription());
}
}
};
private final StorageEventListener mStorageListener = new StorageEventListener() {
@Override
public void onVolumeStateChanged(VolumeInfo vol, int oldState, int newState) {
@@ -133,7 +117,7 @@ public class StorageSettings extends SettingsPreferenceFragment implements Index
mExternalCategory.removeAll();
final List<VolumeInfo> volumes = mStorageManager.getVolumes();
Collections.sort(volumes, sVolumeComparator);
Collections.sort(volumes, VolumeInfo.getDescriptionComparator());
for (VolumeInfo vol : volumes) {
if (vol.getType() == VolumeInfo.TYPE_PRIVATE) {

View File

@@ -0,0 +1,68 @@
/*
* Copyright (C) 2015 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.deviceinfo;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager.NameNotFoundException;
import android.os.Bundle;
import android.os.storage.VolumeInfo;
import com.android.internal.util.Preconditions;
import com.android.settings.R;
public class StorageWizardMoveConfirm extends StorageWizardBase {
private String mPackageName;
private ApplicationInfo mApp;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.storage_wizard_generic);
try {
mPackageName = getIntent().getStringExtra(Intent.EXTRA_PACKAGE_NAME);
mApp = getPackageManager().getApplicationInfo(mPackageName, 0);
} catch (NameNotFoundException e) {
throw new RuntimeException(e);
}
Preconditions.checkNotNull(mVolume);
Preconditions.checkNotNull(mApp);
// Sanity check that target volume is candidate
Preconditions.checkState(
getPackageManager().getApplicationCandidateVolumes(mApp).contains(mVolume));
final String appName = getPackageManager().getApplicationLabel(mApp).toString();
final String volumeName = mStorage.getBestVolumeDescription(mVolume);
setHeaderText(R.string.storage_wizard_move_confirm_title, appName);
setBodyText(R.string.storage_wizard_move_confirm_body, appName, volumeName);
getNextButton().setText(R.string.move_app);
}
@Override
public void onNavigateNext() {
final Intent intent = new Intent(this, StorageWizardMoveProgress.class);
intent.putExtra(VolumeInfo.EXTRA_VOLUME_ID, mVolume.getId());
intent.putExtra(Intent.EXTRA_PACKAGE_NAME, mPackageName);
startActivity(intent);
finishAffinity();
}
}

View File

@@ -0,0 +1,138 @@
/*
* Copyright (C) 2015 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.deviceinfo;
import static com.android.settings.deviceinfo.StorageSettings.TAG;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.IPackageMoveObserver;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.RemoteException;
import android.os.storage.VolumeInfo;
import android.util.Log;
import android.view.View;
import android.widget.Toast;
import com.android.internal.util.Preconditions;
import com.android.settings.R;
import java.util.concurrent.CountDownLatch;
public class StorageWizardMoveProgress extends StorageWizardBase {
private String mPackageName;
private ApplicationInfo mApp;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.storage_wizard_progress);
try {
mPackageName = getIntent().getStringExtra(Intent.EXTRA_PACKAGE_NAME);
mApp = getPackageManager().getApplicationInfo(mPackageName, 0);
} catch (NameNotFoundException e) {
throw new RuntimeException(e);
}
Preconditions.checkNotNull(mVolume);
Preconditions.checkNotNull(mApp);
final String appName = getPackageManager().getApplicationLabel(mApp).toString();
final String volumeName = mStorage.getBestVolumeDescription(mVolume);
setHeaderText(R.string.storage_wizard_move_progress_title, appName);
setBodyText(R.string.storage_wizard_move_progress_body, volumeName, appName);
setCurrentProgress(20);
getNextButton().setVisibility(View.GONE);
new MoveTask().execute();
}
private CharSequence moveStatusToMessage(int returnCode) {
switch (returnCode) {
case PackageManager.MOVE_FAILED_INSUFFICIENT_STORAGE:
return getString(R.string.insufficient_storage);
case PackageManager.MOVE_FAILED_DOESNT_EXIST:
return getString(R.string.does_not_exist);
case PackageManager.MOVE_FAILED_FORWARD_LOCKED:
return getString(R.string.app_forward_locked);
case PackageManager.MOVE_FAILED_INVALID_LOCATION:
return getString(R.string.invalid_location);
case PackageManager.MOVE_FAILED_SYSTEM_PACKAGE:
return getString(R.string.system_package);
case PackageManager.MOVE_FAILED_INTERNAL_ERROR:
default:
return getString(R.string.insufficient_storage);
}
}
private class LocalPackageMoveObserver extends IPackageMoveObserver.Stub {
public int returnCode;
public CountDownLatch finished = new CountDownLatch(1);
@Override
public void packageMoved(String packageName, int returnCode) throws RemoteException {
this.returnCode = returnCode;
this.finished.countDown();
}
}
public class MoveTask extends AsyncTask<Void, Void, Integer> {
@Override
protected Integer doInBackground(Void... params) {
try {
final LocalPackageMoveObserver observer = new LocalPackageMoveObserver();
if (mApp.isExternalAsec()) {
getPackageManager().movePackage(mPackageName, observer,
PackageManager.MOVE_INTERNAL);
} else if (mVolume.getType() == VolumeInfo.TYPE_PUBLIC) {
getPackageManager().movePackage(mPackageName, observer,
PackageManager.MOVE_EXTERNAL_MEDIA);
} else {
getPackageManager().movePackageAndData(mPackageName, mVolume.fsUuid, observer);
}
observer.finished.await();
return observer.returnCode;
} catch (Exception e) {
Log.e(TAG, "Failed to move", e);
return PackageManager.MOVE_FAILED_INTERNAL_ERROR;
}
}
@Override
protected void onPostExecute(Integer returnCode) {
final Context context = StorageWizardMoveProgress.this;
if (returnCode == PackageManager.MOVE_SUCCEEDED) {
finishAffinity();
} else {
Log.w(TAG, "Move failed with status " + returnCode);
Toast.makeText(context, moveStatusToMessage(returnCode), Toast.LENGTH_LONG).show();
finishAffinity();
}
}
}
}