diff --git a/src/com/android/settings/Settings.java b/src/com/android/settings/Settings.java index ecdf4ba2287..1ec4284c9fc 100644 --- a/src/com/android/settings/Settings.java +++ b/src/com/android/settings/Settings.java @@ -21,7 +21,6 @@ import android.os.Bundle; import com.android.settings.applications.AppOpsSummary; import com.android.settings.enterprise.EnterprisePrivacySettings; import com.android.settings.fingerprint.FingerprintEnrollIntroduction; -import com.android.settings.fingerprint.FingerprintSettings; import com.android.settings.password.ChooseLockGeneric; /** @@ -129,6 +128,9 @@ public class Settings extends SettingsActivity { public static class AutomaticStorageManagerSettingsActivity extends SettingsActivity { /* empty */ } public static class GamesStorageActivity extends SettingsActivity { /* empty */ } public static class MoviesStorageActivity extends SettingsActivity { /* empty */ } + public static class PhotosStorageActivity extends SettingsActivity { + /* empty */ + } public static class TopLevelSettings extends SettingsActivity { /* empty */ } public static class ApnSettingsActivity extends SettingsActivity { /* empty */ } diff --git a/src/com/android/settings/applications/ManageApplications.java b/src/com/android/settings/applications/ManageApplications.java index b93249664fb..11eb0ccefd1 100644 --- a/src/com/android/settings/applications/ManageApplications.java +++ b/src/com/android/settings/applications/ManageApplications.java @@ -59,6 +59,7 @@ import android.widget.TextView; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.settings.R; +import com.android.settings.Settings; import com.android.settings.Settings.AllApplicationsActivity; import com.android.settings.Settings.GamesStorageActivity; import com.android.settings.Settings.HighPowerApplicationsActivity; @@ -219,6 +220,7 @@ public class ManageApplications extends InstrumentedPreferenceFragment public static final int STORAGE_TYPE_DEFAULT = 0; // Show all apps that are not categorized. public static final int STORAGE_TYPE_MUSIC = 1; public static final int STORAGE_TYPE_LEGACY = 2; // Show apps even if they can be categorized. + public static final int STORAGE_TYPE_PHOTOS_VIDEOS = 3; // sort order private int mSortOrder = R.id.sort_order_alpha; @@ -262,6 +264,7 @@ public class ManageApplications extends InstrumentedPreferenceFragment public static final int LIST_TYPE_MANAGE_SOURCES = 8; public static final int LIST_TYPE_GAMES = 9; public static final int LIST_TYPE_MOVIES = 10; + public static final int LIST_TYPE_PHOTOGRAPHY = 11; // List types that should show instant apps. @@ -326,6 +329,10 @@ public class ManageApplications extends InstrumentedPreferenceFragment } else if (className.equals(MoviesStorageActivity.class.getName())) { mListType = LIST_TYPE_MOVIES; mSortOrder = R.id.sort_order_size; + } else if (className.equals(Settings.PhotosStorageActivity.class.getName())) { + mListType = LIST_TYPE_PHOTOGRAPHY; + mSortOrder = R.id.sort_order_size; + mStorageType = args.getInt(EXTRA_STORAGE_TYPE, STORAGE_TYPE_DEFAULT); } else { mListType = LIST_TYPE_MAIN; } @@ -378,6 +385,14 @@ public class ManageApplications extends InstrumentedPreferenceFragment new StorageStatsSource(context), mVolumeUuid, UserHandle.of(UserHandle.getUserId(mCurrentUid)))); + } else if (mStorageType == STORAGE_TYPE_PHOTOS_VIDEOS) { + Context context = getContext(); + mApplications.setExtraViewController( + new PhotosViewHolderController( + context, + new StorageStatsSource(context), + mVolumeUuid, + UserHandle.of(UserHandle.getUserId(mCurrentUid)))); } mListView.setAdapter(mApplications); mListView.setRecyclerListener(mApplications); @@ -450,6 +465,8 @@ public class ManageApplications extends InstrumentedPreferenceFragment return new CompoundFilter(ApplicationsState.FILTER_GAMES, filter); } else if (listType == LIST_TYPE_MOVIES) { return new CompoundFilter(ApplicationsState.FILTER_MOVIES, filter); + } else if (listType == LIST_TYPE_PHOTOGRAPHY) { + return new CompoundFilter(ApplicationsState.FILTER_PHOTOS, filter); } return null; @@ -479,6 +496,7 @@ public class ManageApplications extends InstrumentedPreferenceFragment case LIST_TYPE_STORAGE: case LIST_TYPE_GAMES: case LIST_TYPE_MOVIES: + case LIST_TYPE_PHOTOGRAPHY: return mSortOrder == R.id.sort_order_alpha; default: return false; @@ -501,6 +519,8 @@ public class ManageApplications extends InstrumentedPreferenceFragment return MetricsEvent.APPLICATIONS_STORAGE_GAMES; case LIST_TYPE_MOVIES: return MetricsEvent.APPLICATIONS_STORAGE_MOVIES; + case LIST_TYPE_PHOTOGRAPHY: + return MetricsEvent.APPLICATIONS_STORAGE_PHOTOS; case LIST_TYPE_USAGE_ACCESS: return MetricsEvent.USAGE_ACCESS; case LIST_TYPE_HIGH_POWER: @@ -604,6 +624,9 @@ public class ManageApplications extends InstrumentedPreferenceFragment case LIST_TYPE_MOVIES: startAppInfoFragment(AppStorageSettings.class, R.string.storage_movies_tv); break; + case LIST_TYPE_PHOTOGRAPHY: + startAppInfoFragment(AppStorageSettings.class, R.string.storage_photos_videos); + break; // TODO: Figure out if there is a way where we can spin up the profile's settings // process ahead of time, to avoid a long load of data when user clicks on a managed // app. Maybe when they load the list of apps that contains managed profile apps. diff --git a/src/com/android/settings/applications/PhotosViewHolderController.java b/src/com/android/settings/applications/PhotosViewHolderController.java new file mode 100644 index 00000000000..a652bb15159 --- /dev/null +++ b/src/com/android/settings/applications/PhotosViewHolderController.java @@ -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.applications; + +import android.app.Fragment; +import android.content.Context; +import android.content.Intent; +import android.graphics.drawable.InsetDrawable; +import android.os.UserHandle; +import android.support.annotation.WorkerThread; +import android.text.format.Formatter; +import android.util.Log; + +import com.android.settings.R; +import com.android.settings.Utils; +import com.android.settingslib.applications.StorageStatsSource; + +import java.io.IOException; + +/** PhotosViewHolderController controls an Audio/Music file view in the ManageApplications view. */ +public class PhotosViewHolderController implements FileViewHolderController { + private static final String TAG = "PhotosViewHolderController"; + + private static final String IMAGE_MIME_TYPE = "image/*"; + private static final int INSET_SIZE = 24; // dp + + private Context mContext; + private StorageStatsSource mSource; + private String mVolumeUuid; + private long mFilesSize; + private UserHandle mUser; + + public PhotosViewHolderController( + Context context, StorageStatsSource source, String volumeUuid, UserHandle user) { + mContext = context; + mSource = source; + mVolumeUuid = volumeUuid; + mUser = user; + } + + @Override + @WorkerThread + public void queryStats() { + try { + StorageStatsSource.ExternalStorageStats stats = + mSource.getExternalStorageStats(mVolumeUuid, mUser); + mFilesSize = stats.imageBytes + stats.videoBytes; + } catch (IOException e) { + mFilesSize = 0; + Log.w(TAG, e); + } + } + + @Override + public boolean shouldShow() { + return true; + } + + @Override + public void setupView(AppViewHolder holder) { + holder.appIcon.setImageDrawable( + new InsetDrawable(mContext.getDrawable(R.drawable.ic_photo_library), INSET_SIZE)); + holder.appName.setText(mContext.getText(R.string.storage_detail_images)); + holder.summary.setText(Formatter.formatFileSize(mContext, mFilesSize)); + } + + @Override + public void onClick(Fragment fragment) { + Intent intent = new Intent(); + intent.setAction(android.content.Intent.ACTION_VIEW); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT); + intent.setType(IMAGE_MIME_TYPE); + intent.putExtra(Intent.EXTRA_FROM_STORAGE, true); + Utils.launchIntent(fragment, intent); + } +} diff --git a/src/com/android/settings/deviceinfo/storage/StorageAsyncLoader.java b/src/com/android/settings/deviceinfo/storage/StorageAsyncLoader.java index 630df8576ab..f92a24e9f9d 100644 --- a/src/com/android/settings/deviceinfo/storage/StorageAsyncLoader.java +++ b/src/com/android/settings/deviceinfo/storage/StorageAsyncLoader.java @@ -18,6 +18,7 @@ package com.android.settings.deviceinfo.storage; import static android.content.pm.ApplicationInfo.CATEGORY_AUDIO; import static android.content.pm.ApplicationInfo.CATEGORY_GAME; +import static android.content.pm.ApplicationInfo.CATEGORY_IMAGE; import static android.content.pm.ApplicationInfo.CATEGORY_VIDEO; import android.content.Context; @@ -134,6 +135,9 @@ public class StorageAsyncLoader case CATEGORY_VIDEO: result.videoAppsSize += blamedSize; break; + case CATEGORY_IMAGE: + result.photosAppsSize += blamedSize; + break; default: // The deprecated game flag does not set the category. if ((app.flags & ApplicationInfo.FLAG_IS_GAME) != 0) { @@ -163,6 +167,7 @@ public class StorageAsyncLoader public static class AppsStorageResult { public long gamesSize; public long musicAppsSize; + public long photosAppsSize; public long videoAppsSize; public long otherAppsSize; public long cacheSize; diff --git a/src/com/android/settings/deviceinfo/storage/StorageItemPreferenceController.java b/src/com/android/settings/deviceinfo/storage/StorageItemPreferenceController.java index 99679d6f1a7..ca85f69d0be 100644 --- a/src/com/android/settings/deviceinfo/storage/StorageItemPreferenceController.java +++ b/src/com/android/settings/deviceinfo/storage/StorageItemPreferenceController.java @@ -59,7 +59,6 @@ public class StorageItemPreferenceController extends AbstractPreferenceControlle PreferenceControllerMixin { private static final String TAG = "StorageItemPreference"; - private static final String IMAGE_MIME_TYPE = "image/*"; private static final String SYSTEM_FRAGMENT_TAG = "SystemInfo"; @VisibleForTesting @@ -93,9 +92,9 @@ public class StorageItemPreferenceController extends AbstractPreferenceControlle private StorageItemPreference mAppPreference; private StorageItemPreference mFilePreference; private StorageItemPreference mSystemPreference; + private boolean mIsWorkProfile; private static final String AUTHORITY_MEDIA = "com.android.providers.media.documents"; - private boolean mIsWorkProfile; public StorageItemPreferenceController( Context context, Fragment hostFragment, VolumeInfo volume, StorageVolumeProvider svp) { @@ -259,7 +258,8 @@ public class StorageItemPreferenceController extends AbstractPreferenceControlle // TODO(b/35927909): Figure out how to split out apps which are only installed for work // profiles in order to attribute those app's code bytes only to that profile. mPhotoPreference.setStorageSize( - data.externalStats.imageBytes + data.externalStats.videoBytes, mTotalSize); + data.photosAppsSize + data.externalStats.imageBytes + data.externalStats.videoBytes, + mTotalSize); mAudioPreference.setStorageSize( data.musicAppsSize + data.externalStats.audioBytes, mTotalSize); mGamePreference.setStorageSize(data.gamesSize, mTotalSize); @@ -280,10 +280,12 @@ public class StorageItemPreferenceController extends AbstractPreferenceControlle long attributedSize = 0; for (int i = 0; i < result.size(); i++) { final StorageAsyncLoader.AppsStorageResult otherData = result.valueAt(i); - attributedSize += otherData.gamesSize - + otherData.musicAppsSize - + otherData.videoAppsSize - + otherData.otherAppsSize; + attributedSize += + otherData.gamesSize + + otherData.musicAppsSize + + otherData.videoAppsSize + + otherData.photosAppsSize + + otherData.otherAppsSize; attributedSize += otherData.externalStats.totalBytes - otherData.externalStats.appBytes; } @@ -317,12 +319,21 @@ public class StorageItemPreferenceController extends AbstractPreferenceControlle } private Intent getPhotosIntent() { - Intent intent = new Intent(); - intent.setAction(android.content.Intent.ACTION_VIEW); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT); - intent.setType(IMAGE_MIME_TYPE); - intent.putExtra(Intent.EXTRA_FROM_STORAGE, true); - return intent; + Bundle args = new Bundle(2); + args.putString( + ManageApplications.EXTRA_CLASSNAME, Settings.PhotosStorageActivity.class.getName()); + args.putInt( + ManageApplications.EXTRA_STORAGE_TYPE, + ManageApplications.STORAGE_TYPE_PHOTOS_VIDEOS); + return Utils.onBuildStartFragmentIntent( + mContext, + ManageApplications.class.getName(), + args, + null, + R.string.storage_photos_videos, + null, + false, + mMetricsFeatureProvider.getMetricsCategory(mFragment)); } private Intent getAudioIntent() { diff --git a/tests/robotests/src/com/android/settings/applications/PhotosViewHolderControllerTest.java b/tests/robotests/src/com/android/settings/applications/PhotosViewHolderControllerTest.java new file mode 100644 index 00000000000..7eacba2e285 --- /dev/null +++ b/tests/robotests/src/com/android/settings/applications/PhotosViewHolderControllerTest.java @@ -0,0 +1,88 @@ +package com.android.settings.applications; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Matchers.nullable; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.Fragment; +import android.content.Context; +import android.content.Intent; +import android.os.UserHandle; +import android.os.storage.VolumeInfo; +import android.view.LayoutInflater; + +import com.android.settings.TestConfig; +import com.android.settings.testutils.SettingsRobolectricTestRunner; +import com.android.settingslib.applications.StorageStatsSource; +import com.android.settingslib.deviceinfo.StorageVolumeProvider; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Answers; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; + +@RunWith(SettingsRobolectricTestRunner.class) +@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION) +public class PhotosViewHolderControllerTest { + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + private Fragment mFragment; + + @Mock private StorageVolumeProvider mSvp; + @Mock private StorageStatsSource mSource; + + private Context mContext; + private PhotosViewHolderController mController; + private VolumeInfo mVolume; + private AppViewHolder mHolder; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + mContext = RuntimeEnvironment.application; + mVolume = new VolumeInfo("id", 0, null, "id"); + mController = + new PhotosViewHolderController( + mContext, mSource, mVolume.fsUuid, new UserHandle(0)); + + LayoutInflater inflater = LayoutInflater.from(mContext); + mHolder = AppViewHolder.createOrRecycle(inflater, null); + } + + @Test + public void storageShouldBeZeroBytesIfQueriedBeforeStorageQueryFinishes() { + mController.setupView(mHolder); + + assertThat(mHolder.summary.getText().toString()).isEqualTo("0.00B"); + } + + @Test + public void storageShouldRepresentStorageStatsQuery() throws Exception { + when(mSource.getExternalStorageStats(nullable(String.class), nullable(UserHandle.class))) + .thenReturn(new StorageStatsSource.ExternalStorageStats(1, 0, 1, 10, 0)); + + mController.queryStats(); + mController.setupView(mHolder); + + assertThat(mHolder.summary.getText().toString()).isEqualTo("11.00B"); + } + + @Test + public void clickingShouldIntentIntoFilesApp() { + mController.onClick(mFragment); + + final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Intent.class); + verify(mFragment).startActivity(argumentCaptor.capture()); + Intent intent = argumentCaptor.getValue(); + + assertThat(intent.getType()).isEqualTo("image/*"); + assertThat(intent.getAction()).isEqualTo(android.content.Intent.ACTION_VIEW); + assertThat(intent.getBooleanExtra(Intent.EXTRA_FROM_STORAGE, false)).isTrue(); + } +} diff --git a/tests/robotests/src/com/android/settings/deviceinfo/storage/StorageItemPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/deviceinfo/storage/StorageItemPreferenceControllerTest.java index 107e4f31df8..d3b5cbbea29 100644 --- a/tests/robotests/src/com/android/settings/deviceinfo/storage/StorageItemPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/deviceinfo/storage/StorageItemPreferenceControllerTest.java @@ -119,9 +119,12 @@ public class StorageItemPreferenceControllerTest { nullable(UserHandle.class)); Intent intent = argumentCaptor.getValue(); - assertThat(intent.getType()).isEqualTo("image/*"); - assertThat(intent.getAction()).isEqualTo(android.content.Intent.ACTION_VIEW); - assertThat(intent.getBooleanExtra(Intent.EXTRA_FROM_STORAGE, false)).isTrue(); + assertThat(intent.getAction()).isEqualTo(Intent.ACTION_MAIN); + assertThat(intent.getComponent().getClassName()).isEqualTo(SubSettings.class.getName()); + assertThat(intent.getStringExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT)) + .isEqualTo(ManageApplications.class.getName()); + assertThat(intent.getIntExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT_TITLE_RESID, 0)) + .isEqualTo(R.string.storage_photos_videos); } @Test