Fix a crash when transferring an app.

This crash occurs because the background loader is loading
the storage for an app which is currently being moved off of
the device. In the native code which is calculating the storage,
it throws an InstallerException which is caught and rethrown as an
IllegalStateException.

We handle this in the view by reporting that we could not calculate
the size of the app.

Change-Id: I109b1be60ae60f8ef31b08cb4392b576261fa806
Fixes: 35922033
Test: Robotest
This commit is contained in:
Daniel Nishi
2017-03-02 12:52:40 -08:00
parent 8e12df9d62
commit 6a256e77d6
6 changed files with 285 additions and 71 deletions

View File

@@ -50,24 +50,12 @@
android:selectable="false"
android:layout="@layout/horizontal_preference" />
<Preference
android:key="external_code_size"
android:title="@string/external_code_size_label"
android:selectable="false"
android:layout="@layout/horizontal_preference" />
<Preference
android:key="data_size"
android:title="@string/data_size_label"
android:selectable="false"
android:layout="@layout/horizontal_preference" />
<Preference
android:key="external_data_size"
android:title="@string/external_data_size_label"
android:selectable="false"
android:layout="@layout/horizontal_preference" />
<com.android.settings.applications.LayoutPreference
android:key="clear_data_button"
android:selectable="false"

View File

@@ -105,12 +105,6 @@ public class AppStorageSettings extends AppInfoWithHeader
private static final String KEY_URI_CATEGORY = "uri_category";
private static final String KEY_CLEAR_URI = "clear_uri_button";
private Preference mTotalSize;
private Preference mAppSize;
private Preference mDataSize;
private Preference mExternalCodeSize;
private Preference mExternalDataSize;
// Views related to cache info
private Preference mCacheSize;
private Button mClearDataButton;
@@ -125,15 +119,9 @@ public class AppStorageSettings extends AppInfoWithHeader
private PreferenceCategory mUri;
private boolean mCanClearData = true;
private boolean mHaveSizes = false;
private AppStorageStats mLastResult;
private long mLastCodeSize = -1;
private long mLastDataSize = -1;
private long mLastExternalCodeSize = -1;
private long mLastExternalDataSize = -1;
private long mLastCacheSize = -1;
private long mLastTotalSize = -1;
private AppStorageSizesController mSizeController;
private ClearCacheObserver mClearCacheObserver;
private ClearUserDataObserver mClearDataObserver;
@@ -166,17 +154,15 @@ public class AppStorageSettings extends AppInfoWithHeader
mInvalidSizeStr = getActivity().getText(R.string.invalid_size_value);
// Set default values on sizes
mTotalSize = findPreference(KEY_TOTAL_SIZE);
mAppSize = findPreference(KEY_APP_SIZE);
mDataSize = findPreference(KEY_DATA_SIZE);
mExternalCodeSize = findPreference(KEY_EXTERNAL_CODE_SIZE);
mExternalDataSize = findPreference(KEY_EXTERNAL_DATA_SIZE);
mSizeController = new AppStorageSizesController.Builder()
.setTotalSizePreference(findPreference(KEY_TOTAL_SIZE))
.setAppSizePreference(findPreference(KEY_APP_SIZE))
.setDataSizePreference(findPreference(KEY_DATA_SIZE))
.setCacheSizePreference(findPreference(KEY_CACHE_SIZE))
.setComputingString(R.string.computing_size)
.setErrorString(R.string.invalid_size_value)
.build();
if (Environment.isExternalStorageEmulated()) {
PreferenceCategory category = (PreferenceCategory) findPreference(KEY_STORAGE_CATEGORY);
category.removePreference(mExternalCodeSize);
category.removePreference(mExternalDataSize);
}
mClearDataButton = (Button) ((LayoutPreference) findPreference(KEY_CLEAR_DATA))
.findViewById(R.id.button);
@@ -265,13 +251,6 @@ public class AppStorageSettings extends AppInfoWithHeader
dialog.dismiss();
}
private String getSizeStr(long size) {
if (size == SIZE_INVALID) {
return mInvalidSizeStr.toString();
}
return Formatter.formatFileSize(getActivity(), size);
}
@Override
protected boolean refreshUi() {
retrieveAppEntry();
@@ -521,7 +500,7 @@ public class AppStorageSettings extends AppInfoWithHeader
@Override
public void onLoadFinished(Loader<AppStorageStats> loader, AppStorageStats result) {
mLastResult = result;
mSizeController.setResult(result);
updateUiWithSize(result);
}
@@ -545,39 +524,15 @@ public class AppStorageSettings extends AppInfoWithHeader
}
private void updateUiWithSize(AppStorageStats result) {
mSizeController.updateUi(getContext());
if (result == null) {
mLastCodeSize = mLastDataSize = mLastCacheSize = mLastTotalSize = -1;
if (!mHaveSizes) {
mAppSize.setSummary(mComputingStr);
mDataSize.setSummary(mComputingStr);
mCacheSize.setSummary(mComputingStr);
mTotalSize.setSummary(mComputingStr);
}
mClearDataButton.setEnabled(false);
mClearCacheButton.setEnabled(false);
} else {
mHaveSizes = true;
long codeSize = result.getCodeBytes();
long dataSize = result.getDataBytes();
if (mLastCodeSize != codeSize) {
mLastCodeSize = codeSize;
mAppSize.setSummary(getSizeStr(codeSize));
}
if (mLastDataSize != dataSize) {
mLastDataSize = dataSize;
mDataSize.setSummary(getSizeStr(dataSize));
}
long cacheSize = result.getCacheBytes();
if (mLastCacheSize != cacheSize) {
mLastCacheSize = cacheSize;
mCacheSize.setSummary(getSizeStr(cacheSize));
}
long totalSize = codeSize + dataSize + cacheSize;
if (mLastTotalSize != totalSize) {
mLastTotalSize = totalSize;
mTotalSize.setSummary(getSizeStr(totalSize));
}
if (dataSize <= 0 || !mCanClearData) {
mClearDataButton.setEnabled(false);

View File

@@ -0,0 +1,155 @@
/*
* Copyright (C) 2017 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.applications;
import android.content.Context;
import android.support.annotation.Nullable;
import android.support.annotation.StringRes;
import android.support.v7.preference.Preference;
import android.text.format.Formatter;
import com.android.internal.util.Preconditions;
import com.android.settingslib.applications.StorageStatsSource;
/**
* Handles setting the sizes for the app info screen.
*/
public class AppStorageSizesController {
private final Preference mTotalSize;
private final Preference mAppSize;
private final Preference mDataSize;
private final Preference mCacheSize;
private final @StringRes int mComputing;
private final @StringRes int mError;
@Nullable
private StorageStatsSource.AppStorageStats mLastResult;
private boolean mLastResultFailed;
private long mLastCodeSize = -1;
private long mLastDataSize = -1;
private long mLastCacheSize = -1;
private long mLastTotalSize = -1;
private AppStorageSizesController(Preference total, Preference app,
Preference data, Preference cache, @StringRes int computing, @StringRes int error) {
mTotalSize = total;
mAppSize = app;
mDataSize = data;
mCacheSize = cache;
mComputing = computing;
mError = error;
}
/**
* Updates the UI using storage stats.
* @param context Context to use to fetch strings
*/
public void updateUi(Context context) {
if (mLastResult == null) {
int errorRes = mLastResultFailed ? mError : mComputing;
mAppSize.setSummary(errorRes);
mDataSize.setSummary(errorRes);
mCacheSize.setSummary(errorRes);
mTotalSize.setSummary(errorRes);
} else {
long codeSize = mLastResult.getCodeBytes();
long dataSize = mLastResult.getDataBytes();
if (mLastCodeSize != codeSize) {
mLastCodeSize = codeSize;
mAppSize.setSummary(getSizeStr(context, codeSize));
}
if (mLastDataSize != dataSize) {
mLastDataSize = dataSize;
mDataSize.setSummary(getSizeStr(context, dataSize));
}
long cacheSize = mLastResult.getCacheBytes();
if (mLastCacheSize != cacheSize) {
mLastCacheSize = cacheSize;
mCacheSize.setSummary(getSizeStr(context, cacheSize));
}
long totalSize = codeSize + dataSize + cacheSize;
if (mLastTotalSize != totalSize) {
mLastTotalSize = totalSize;
mTotalSize.setSummary(getSizeStr(context, totalSize));
}
}
}
/**
* Sets a result for the controller to use to update the UI.
* @param result A result for the UI. If null, count as a failed calculation.
*/
public void setResult(StorageStatsSource.AppStorageStats result) {
mLastResult = result;
mLastResultFailed = result == null;
}
private String getSizeStr(Context context, long size) {
return Formatter.formatFileSize(context, size);
}
public static class Builder {
private Preference mTotalSize;
private Preference mAppSize;
private Preference mDataSize;
private Preference mCacheSize;
private @StringRes int mComputing;
private @StringRes int mError;
public Builder setAppSizePreference(Preference preference) {
mAppSize = preference;
return this;
}
public Builder setDataSizePreference(Preference preference) {
mDataSize = preference;
return this;
}
public Builder setCacheSizePreference(Preference preference) {
mCacheSize = preference;
return this;
}
public Builder setTotalSizePreference(Preference preference) {
mTotalSize = preference;
return this;
}
public Builder setComputingString(@StringRes int sequence) {
mComputing = sequence;
return this;
}
public Builder setErrorString(@StringRes int sequence) {
mError = sequence;
return this;
}
public AppStorageSizesController build() {
return new AppStorageSizesController(
Preconditions.checkNotNull(mTotalSize),
Preconditions.checkNotNull(mAppSize),
Preconditions.checkNotNull(mDataSize),
Preconditions.checkNotNull(mCacheSize),
mComputing,
mError);
}
}
}

View File

@@ -20,6 +20,7 @@ import android.annotation.NonNull;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.os.UserHandle;
import android.util.Log;
import com.android.internal.util.Preconditions;
import com.android.settings.utils.AsyncLoader;
@@ -30,6 +31,7 @@ import com.android.settingslib.applications.StorageStatsSource.AppStorageStats;
* Fetches the storage stats using the StorageStatsManager for a given package and user tuple.
*/
public class FetchPackageStorageAsyncLoader extends AsyncLoader<AppStorageStats> {
private static final String TAG = "FetchPackageStorage";
private final StorageStatsSource mSource;
private final ApplicationInfo mInfo;
private final UserHandle mUser;
@@ -44,7 +46,13 @@ public class FetchPackageStorageAsyncLoader extends AsyncLoader<AppStorageStats>
@Override
public AppStorageStats loadInBackground() {
return mSource.getStatsForPackage(mInfo.volumeUuid, mInfo.packageName, mUser);
AppStorageStats result = null;
try {
result = mSource.getStatsForPackage(mInfo.volumeUuid, mInfo.packageName, mUser);
} catch (IllegalStateException e) {
Log.w(TAG, "Package may have been removed during query, failing gracefully", e);
}
return result;
}
@Override

View File

@@ -0,0 +1,95 @@
package com.android.settings.applications;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import android.content.Context;
import android.support.v7.preference.Preference;
import com.android.settings.SettingsRobolectricTestRunner;
import com.android.settings.TestConfig;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;
import org.robolectric.shadows.ShadowApplication;
import com.android.settings.R;
import com.android.settingslib.applications.StorageStatsSource.AppStorageStats;
@RunWith(SettingsRobolectricTestRunner.class)
@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
public class AppStorageSizesControllerTest {
private static final String COMPUTING = "Computing…";
private static final String INVALID_SIZE = "Couldnt compute package size.";
private AppStorageSizesController mController;
private Context mContext;
private Preference mAppPreference;
private Preference mCachePreference;
private Preference mDataPreference;
private Preference mTotalPreference;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
mContext = RuntimeEnvironment.application;
mAppPreference = new Preference(mContext);
mCachePreference = new Preference(mContext);
mDataPreference = new Preference(mContext);
mTotalPreference = new Preference(mContext);
mController = new AppStorageSizesController.Builder()
.setAppSizePreference(mAppPreference)
.setCacheSizePreference(mCachePreference)
.setDataSizePreference(mDataPreference)
.setTotalSizePreference(mTotalPreference)
.setErrorString(R.string.invalid_size_value)
.setComputingString(R.string.computing_size)
.build();
}
@Test
public void requestingUpdateBeforeValuesSetIsComputing() {
mController.updateUi(mContext);
assertThat(mAppPreference.getSummary()).isEqualTo(COMPUTING);
assertThat(mCachePreference.getSummary()).isEqualTo(COMPUTING);
assertThat(mDataPreference.getSummary()).isEqualTo(COMPUTING);
assertThat(mTotalPreference.getSummary()).isEqualTo(COMPUTING);
}
@Test
public void requestingUpdateAfterFailureHasErrorText() {
mController.setResult(null);
mController.updateUi(mContext);
assertThat(mAppPreference.getSummary()).isEqualTo(INVALID_SIZE);
assertThat(mCachePreference.getSummary()).isEqualTo(INVALID_SIZE);
assertThat(mDataPreference.getSummary()).isEqualTo(INVALID_SIZE);
assertThat(mTotalPreference.getSummary()).isEqualTo(INVALID_SIZE);
}
@Test
public void properlyPopulatedAfterValidEntry() {
AppStorageStats result = mock(AppStorageStats.class);
when(result.getCodeBytes()).thenReturn(1L);
when(result.getCacheBytes()).thenReturn(10L);
when(result.getDataBytes()).thenReturn(100L);
when(result.getTotalBytes()).thenReturn(111L);
mController.setResult(result);
mController.updateUi(mContext);
assertThat(mAppPreference.getSummary()).isEqualTo("1.00B");
assertThat(mCachePreference.getSummary()).isEqualTo("10.00B");
assertThat(mDataPreference.getSummary()).isEqualTo("100B");
assertThat(mTotalPreference.getSummary()).isEqualTo("111B");
}
}

View File

@@ -43,6 +43,7 @@ import org.robolectric.annotation.Config;
@RunWith(SettingsRobolectricTestRunner.class)
@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
public class FetchPackageStorageAsyncLoaderTest {
private static final String PACKAGE_NAME = "com.test.package";
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
private Context mContext;
@Mock
@@ -63,10 +64,22 @@ public class FetchPackageStorageAsyncLoaderTest {
when(mSource.getStatsForPackage(anyString(), anyString(), any(UserHandle.class)))
.thenReturn(stats);
ApplicationInfo info = new ApplicationInfo();
info.packageName = "com.test.package";
info.packageName = PACKAGE_NAME;
FetchPackageStorageAsyncLoader task = new FetchPackageStorageAsyncLoader(
mContext, mSource, info, new UserHandle(0));
assertThat(task.loadInBackground()).isEqualTo(stats);
}
@Test
public void installerExceptionHandledCleanly() {
when(mSource.getStatsForPackage(anyString(), anyString(), any(UserHandle.class))).
thenThrow(new IllegalStateException("intentional failure"));
ApplicationInfo info = new ApplicationInfo();
info.packageName = PACKAGE_NAME;
FetchPackageStorageAsyncLoader task = new FetchPackageStorageAsyncLoader(
mContext, mSource, info, new UserHandle(0));
assertThat(task.loadInBackground()).isNull();
}
}