Merge "Add recent apps in app & notification"

This commit is contained in:
TreeHugger Robot
2017-05-03 22:52:22 +00:00
committed by Android (Google) Code Review
10 changed files with 596 additions and 14 deletions

View File

@@ -958,11 +958,6 @@
<category android:name="android.intent.category.VOICE_LAUNCH" />
<category android:name="com.android.settings.SHORTCUT" />
</intent-filter>
<intent-filter android:priority="200">
<action android:name="com.android.settings.action.SETTINGS" />
</intent-filter>
<meta-data android:name="com.android.settings.category"
android:value="com.android.settings.category.ia.apps" />
<meta-data android:name="com.android.settings.FRAGMENT_CLASS"
android:value="com.android.settings.applications.ManageApplications" />
<meta-data android:name="com.android.settings.PRIMARY_PROFILE_CONTROLLED"

View File

@@ -0,0 +1,27 @@
<!--
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.
-->
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:autoMirrored="true"
android:height="24dp"
android:width="24dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0"
android:tint="?android:attr/colorControlNormal">
<path android:fillColor="#FF000000"
android:pathData="M10,6L8.59,7.41 13.17,12l-4.58,4.59L10,18l6,-6z" />
</vector>

View File

@@ -18,16 +18,16 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="72dp"
android:paddingTop="16dp"
android:paddingBottom="16dp"
android:paddingTop="12dp"
android:paddingBottom="12dp"
android:gravity="top"
android:columnCount="3"
android:duplicateParentState="true">
<ImageView
android:id="@android:id/icon"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_gravity="center"
android:scaleType="fitXY"
android:layout_marginEnd="16dip"

View File

@@ -105,4 +105,7 @@
<!-- Whether or not we should tint the icon color on setting pages. -->
<bool name="config_tintSettingIcon">true</bool>
<!-- Whether or not App & Notification screen should display recently used apps -->
<bool name="config_display_recent_apps">false</bool>
</resources>

View File

@@ -3470,6 +3470,10 @@
<string name="install_applications">Unknown sources</string>
<!-- Applications settings screen, setting check box title. If checked, the system allows installation of applications that are downloaded from random places, such as web sites. [CHAR LIMIT=30] -->
<string name="install_applications_title">Allow all app sources</string>
<!-- Category title listing recently used apps [CHAR_LIMIT=50]-->
<string name="recent_app_category_title">Recently used apps</string>
<!-- Preference title for showing all apps on device [CHAR_LIMIT=50]-->
<string name="see_all_apps_title">See all apps</string>
<!-- Warning that appears below the unknown sources switch in settings -->
<string name="install_all_warning" product="tablet">

View File

@@ -20,6 +20,27 @@
xmlns:settings="http://schemas.android.com/apk/res/com.android.settings"
android:title="@string/app_and_notification_dashboard_title">
<PreferenceCategory
android:key="recent_apps_category"
android:title="@string/recent_app_category_title"
android:order="-200">
<!-- Placeholder for a list of recent apps -->
<!-- See all apps button -->
<Preference
android:title="@string/applications_settings"
android:key="all_app_info"
android:summary="@string/summary_placeholder"
android:order="20">
<intent
android:action="android.intent.action.MAIN"
android:targetPackage="com.android.settings"
android:targetClass="com.android.settings.Settings$ManageApplicationsActivity">
<extra android:name="show_drawer_menu" android:value="true" />
</intent>
</Preference>
</PreferenceCategory>
<Preference
android:key="manage_perms"
android:title="@string/app_permissions"

View File

@@ -16,6 +16,9 @@
package com.android.settings.applications;
import android.app.Activity;
import android.app.Application;
import android.app.Fragment;
import android.content.Context;
import android.provider.SearchIndexableResource;
@@ -56,13 +59,22 @@ public class AppAndNotificationDashboardFragment extends DashboardFragment {
@Override
protected List<PreferenceController> getPreferenceControllers(Context context) {
return buildPreferenceControllers(context);
final Activity activity = getActivity();
final Application app;
if (activity != null) {
app = activity.getApplication();
} else {
app = null;
}
return buildPreferenceControllers(context, app, this);
}
private static List<PreferenceController> buildPreferenceControllers(Context context) {
private static List<PreferenceController> buildPreferenceControllers(Context context,
Application app, Fragment host) {
final List<PreferenceController> controllers = new ArrayList<>();
controllers.add(new SpecialAppAccessPreferenceController(context));
controllers.add(new AppPermissionsPreferenceController(context));
controllers.add(new RecentAppsPreferenceController(context, app, host));
return controllers;
}
@@ -78,7 +90,7 @@ public class AppAndNotificationDashboardFragment extends DashboardFragment {
@Override
public List<PreferenceController> getPreferenceControllers(Context context) {
return buildPreferenceControllers(context);
return buildPreferenceControllers(context, null, null /* host */);
}
};
}

View File

@@ -14,7 +14,6 @@
package com.android.settings.applications;
import android.app.AppGlobals;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
@@ -32,7 +31,7 @@ public abstract class AppCounter extends AsyncTask<Void, Void, Integer> {
public AppCounter(Context context, PackageManagerWrapper packageManager) {
mPm = packageManager;
mUm = UserManager.get(context);
mUm = (UserManager) context.getSystemService(Context.USER_SERVICE);
}
@Override

View File

@@ -0,0 +1,312 @@
/*
* 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.app.Application;
import android.app.Fragment;
import android.app.usage.UsageStats;
import android.app.usage.UsageStatsManager;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.UserHandle;
import android.support.annotation.VisibleForTesting;
import android.support.v7.preference.Preference;
import android.support.v7.preference.PreferenceCategory;
import android.support.v7.preference.PreferenceScreen;
import android.text.TextUtils;
import android.text.format.DateUtils;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.IconDrawableFactory;
import android.util.Log;
import com.android.settings.R;
import com.android.settings.core.PreferenceController;
import com.android.settingslib.applications.ApplicationsState;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static com.android.internal.logging.nano.MetricsProto.MetricsEvent
.SETTINGS_APP_NOTIF_CATEGORY;
/**
* This controller displays a list of recently used apps and a "See all" button. If there is
* no recently used app, "See all" will be displayed as "App info".
*/
public class RecentAppsPreferenceController extends PreferenceController
implements Comparator<UsageStats> {
private static final String TAG = "RecentAppsCtrl";
private static final String KEY_PREF_CATEGORY = "recent_apps_category";
@VisibleForTesting
static final String KEY_SEE_ALL = "all_app_info";
private static final int SHOW_RECENT_APP_COUNT = 5;
private static final Set<String> SKIP_SYSTEM_PACKAGES = new ArraySet<>();
private final Fragment mHost;
private final PackageManager mPm;
private final UsageStatsManager mUsageStatsManager;
private final ApplicationsState mApplicationsState;
private final int mUserId;
private final IconDrawableFactory mIconDrawableFactory;
private Calendar mCal;
private List<UsageStats> mStats;
private PreferenceCategory mCategory;
private Preference mSeeAllPref;
static {
SKIP_SYSTEM_PACKAGES.addAll(Arrays.asList(
"android",
"com.android.phone",
"com.android.settings",
"com.android.systemui",
"com.android.providers.calendar",
"com.android.providers.media"
));
}
public RecentAppsPreferenceController(Context context, Application app, Fragment host) {
this(context, app == null ? null : ApplicationsState.getInstance(app), host);
}
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
RecentAppsPreferenceController(Context context, ApplicationsState appState, Fragment host) {
super(context);
mIconDrawableFactory = IconDrawableFactory.newInstance(context);
mUserId = UserHandle.myUserId();
mPm = context.getPackageManager();
mHost = host;
mUsageStatsManager =
(UsageStatsManager) context.getSystemService(Context.USAGE_STATS_SERVICE);
mApplicationsState = appState;
}
@Override
public boolean isAvailable() {
return true;
}
@Override
public String getPreferenceKey() {
return KEY_PREF_CATEGORY;
}
@Override
public void updateNonIndexableKeys(List<String> keys) {
super.updateNonIndexableKeys(keys);
// Don't index category name into search. It's not actionable.
keys.add(KEY_PREF_CATEGORY);
}
@Override
public void displayPreference(PreferenceScreen screen) {
mCategory = (PreferenceCategory) screen.findPreference(getPreferenceKey());
mSeeAllPref = screen.findPreference(KEY_SEE_ALL);
super.displayPreference(screen);
refreshUi(mCategory.getContext());
}
@Override
public void updateState(Preference preference) {
super.updateState(preference);
// Show total number of installed apps as See all's summary.
new InstalledAppCounter(mContext, InstalledAppCounter.IGNORE_INSTALL_REASON,
new PackageManagerWrapperImpl(mContext.getPackageManager())) {
@Override
protected void onCountComplete(int num) {
mSeeAllPref.setSummary(mContext.getString(R.string.apps_summary, num));
}
}.execute();
refreshUi(mCategory.getContext());
}
@Override
public final int compare(UsageStats a, UsageStats b) {
// return by descending order
return Long.compare(b.getLastTimeUsed(), a.getLastTimeUsed());
}
@VisibleForTesting
void refreshUi(Context prefContext) {
reloadData();
if (shouldDisplayRecentApps()) {
displayRecentApps(prefContext);
} else {
displayOnlyAppInfo();
}
}
@VisibleForTesting
void reloadData() {
mCal = Calendar.getInstance();
mCal.add(Calendar.DAY_OF_YEAR, -1);
mStats = mUsageStatsManager.queryUsageStats(
UsageStatsManager.INTERVAL_BEST, mCal.getTimeInMillis(),
System.currentTimeMillis());
}
private void displayOnlyAppInfo() {
mCategory.setTitle(null);
mSeeAllPref.setTitle(R.string.applications_settings);
mSeeAllPref.setIcon(null);
int prefCount = mCategory.getPreferenceCount();
for (int i = prefCount - 1; i >= 0; i--) {
final Preference pref = mCategory.getPreference(i);
if (!TextUtils.equals(pref.getKey(), KEY_SEE_ALL)) {
mCategory.removePreference(pref);
}
}
}
private void displayRecentApps(Context prefContext) {
mCategory.setTitle(R.string.recent_app_category_title);
mSeeAllPref.setTitle(R.string.see_all_apps_title);
mSeeAllPref.setIcon(R.drawable.ic_chevron_right_24dp);
final List<UsageStats> recentApps = getDisplayableRecentAppList();
// Rebind prefs/avoid adding new prefs if possible. Adding/removing prefs causes jank.
// Build a cached preference pool
final Map<String, Preference> appPreferences = new ArrayMap<>();
int prefCount = mCategory.getPreferenceCount();
for (int i = 0; i < prefCount; i++) {
final Preference pref = mCategory.getPreference(i);
final String key = pref.getKey();
if (!TextUtils.equals(key, KEY_SEE_ALL)) {
appPreferences.put(key, pref);
}
}
final int recentAppsCount = recentApps.size();
for (int i = 0; i < recentAppsCount; i++) {
final UsageStats stat = recentApps.get(i);
// Bind recent apps to existing prefs if possible, or create a new pref.
final String pkgName = stat.getPackageName();
final ApplicationsState.AppEntry appEntry =
mApplicationsState.getEntry(pkgName, mUserId);
if (appEntry == null) {
continue;
}
boolean rebindPref = true;
Preference pref = appPreferences.remove(pkgName);
if (pref == null) {
pref = new Preference(prefContext);
rebindPref = false;
}
pref.setKey(pkgName);
pref.setTitle(appEntry.label);
pref.setIcon(mIconDrawableFactory.getBadgedIcon(appEntry.info));
pref.setSummary(DateUtils.getRelativeTimeSpanString(stat.getLastTimeUsed(),
System.currentTimeMillis(),
DateUtils.MINUTE_IN_MILLIS,
DateUtils.FORMAT_ABBREV_RELATIVE));
pref.setOrder(i);
pref.setOnPreferenceClickListener(preference -> {
AppInfoBase.startAppInfoFragment(InstalledAppDetails.class,
R.string.application_info_label, pkgName, appEntry.info.uid, mHost,
1001 /*RequestCode*/, SETTINGS_APP_NOTIF_CATEGORY);
return true;
});
if (!rebindPref) {
mCategory.addPreference(pref);
}
}
// Remove unused prefs from pref cache pool
for (Preference unusedPrefs : appPreferences.values()) {
mCategory.removePreference(unusedPrefs);
}
}
private List<UsageStats> getDisplayableRecentAppList() {
final List<UsageStats> recentApps = new ArrayList<>();
final Map<String, UsageStats> map = new ArrayMap<>();
final int statCount = mStats.size();
for (int i = 0; i < statCount; i++) {
final UsageStats pkgStats = mStats.get(i);
if (!shouldIncludePkgInRecents(pkgStats)) {
continue;
}
final String pkgName = pkgStats.getPackageName();
final UsageStats existingStats = map.get(pkgName);
if (existingStats == null) {
map.put(pkgName, pkgStats);
} else {
existingStats.add(pkgStats);
}
}
final List<UsageStats> packageStats = new ArrayList<>();
packageStats.addAll(map.values());
Collections.sort(packageStats, this /* comparator */);
int count = 0;
for (UsageStats stat : packageStats) {
final ApplicationsState.AppEntry appEntry = mApplicationsState.getEntry(
stat.getPackageName(), mUserId);
if (appEntry == null) {
continue;
}
recentApps.add(stat);
count++;
if (count >= SHOW_RECENT_APP_COUNT) {
break;
}
}
return recentApps;
}
/**
* Whether or not we should show a list of recent apps, and a see all link.
*/
@VisibleForTesting
boolean shouldDisplayRecentApps() {
return mContext.getResources().getBoolean(R.bool.config_display_recent_apps)
&& mApplicationsState != null && mStats != null && !mStats.isEmpty();
}
/**
* Whether or not the app should be included in recent list.
*/
private boolean shouldIncludePkgInRecents(UsageStats stat) {
final String pkgName = stat.getPackageName();
if (stat.getLastTimeUsed() < mCal.getTimeInMillis()) {
Log.d(TAG, "Invalid timestamp, skipping " + pkgName);
return false;
}
if (SKIP_SYSTEM_PACKAGES.contains(pkgName)) {
Log.d(TAG, "System package, skipping " + pkgName);
return false;
}
final Intent launchIntent = new Intent().addCategory(Intent.CATEGORY_LAUNCHER)
.setPackage(pkgName);
if (mPm.resolveActivity(launchIntent, 0) == null) {
// Not visible on launcher -> likely not a user visible app, skip
Log.d(TAG, "Not a user visible app, skipping " + pkgName);
return false;
}
return true;
}
}

View File

@@ -0,0 +1,209 @@
/*
* 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.app.Application;
import android.app.usage.UsageStats;
import android.app.usage.UsageStatsManager;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ResolveInfo;
import android.os.UserHandle;
import android.os.UserManager;
import android.support.v7.preference.Preference;
import android.support.v7.preference.PreferenceCategory;
import android.support.v7.preference.PreferenceScreen;
import com.android.settings.R;
import com.android.settings.SettingsRobolectricTestRunner;
import com.android.settings.TestConfig;
import com.android.settingslib.applications.ApplicationsState;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Answers;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;
import java.util.ArrayList;
import java.util.List;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Matchers.anyLong;
import static org.mockito.Matchers.anyString;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@RunWith(SettingsRobolectricTestRunner.class)
@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
public class RecentAppsPreferenceControllerTest {
@Mock
private PreferenceScreen mScreen;
@Mock
private PreferenceCategory mCategory;
@Mock
private Preference mSeeAllPref;
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
private Context mMockContext;
@Mock
private UsageStatsManager mUsageStatsManager;
@Mock
private UserManager mUserManager;
@Mock
private ApplicationsState mAppState;
private Context mContext;
private RecentAppsPreferenceController mController;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
when(mMockContext.getSystemService(Context.USAGE_STATS_SERVICE))
.thenReturn(mUsageStatsManager);
when(mMockContext.getSystemService(Context.USER_SERVICE))
.thenReturn(mUserManager);
mContext = RuntimeEnvironment.application;
mController = new RecentAppsPreferenceController(mContext, mAppState, null);
when(mScreen.findPreference(anyString())).thenReturn(mCategory);
when(mScreen.findPreference(RecentAppsPreferenceController.KEY_SEE_ALL))
.thenReturn(mSeeAllPref);
when(mCategory.getContext()).thenReturn(mContext);
}
@Test
public void isAlwaysAvailable() {
assertThat(mController.isAvailable()).isTrue();
}
@Test
public void doNotIndexCategory() {
final List<String> nonIndexable = new ArrayList<>();
mController.updateNonIndexableKeys(nonIndexable);
assertThat(nonIndexable).containsExactly(mController.getPreferenceKey());
}
@Test
public void onDisplayAndUpdateState_shouldRefreshUi() {
mController = spy(
new RecentAppsPreferenceController(mMockContext, (Application) null, null));
doNothing().when(mController).refreshUi(mContext);
mController.displayPreference(mScreen);
mController.updateState(mCategory);
verify(mController, times(2)).refreshUi(mContext);
}
@Test
public void configOff_shouldNotDisplayRecentApps() {
mController = new RecentAppsPreferenceController(mMockContext, (Application) null, null);
when(mMockContext.getResources().getBoolean(R.bool.config_display_recent_apps))
.thenReturn(false);
assertThat(mController.shouldDisplayRecentApps()).isFalse();
}
@Test
public void configOn_shouldDisplayRecentAppsWhenHaveData() {
final List<UsageStats> stats = new ArrayList<>();
stats.add(mock(UsageStats.class));
when(mMockContext.getResources().getBoolean(R.bool.config_display_recent_apps))
.thenReturn(true);
when(mUsageStatsManager.queryUsageStats(anyInt(), anyLong(), anyLong()))
.thenReturn(stats);
mController = new RecentAppsPreferenceController(mMockContext, mAppState, null);
mController.reloadData();
assertThat(mController.shouldDisplayRecentApps()).isTrue();
}
@Test
public void display_shouldNotShowRecents_showAppInfoPreference() {
mController = new RecentAppsPreferenceController(mMockContext, mAppState, null);
when(mMockContext.getResources().getBoolean(R.bool.config_display_recent_apps))
.thenReturn(false);
mController.displayPreference(mScreen);
verify(mCategory, never()).addPreference(any(Preference.class));
verify(mCategory).setTitle(null);
verify(mSeeAllPref).setTitle(R.string.applications_settings);
verify(mSeeAllPref).setIcon(null);
}
@Test
public void display_showRecents() {
when(mMockContext.getResources().getBoolean(R.bool.config_display_recent_apps))
.thenReturn(true);
final List<UsageStats> stats = new ArrayList<>();
final UsageStats stat1 = new UsageStats();
final UsageStats stat2 = new UsageStats();
final UsageStats stat3 = new UsageStats();
stat1.mLastTimeUsed = System.currentTimeMillis();
stat1.mPackageName = "pkg.class";
stats.add(stat1);
stat2.mLastTimeUsed = System.currentTimeMillis();
stat2.mPackageName = "com.android.settings";
stats.add(stat2);
stat3.mLastTimeUsed = System.currentTimeMillis();
stat3.mPackageName = "pkg.class2";
stats.add(stat3);
// stat1, stat2 are valid apps. stat3 is invalid.
when(mAppState.getEntry(stat1.mPackageName, UserHandle.myUserId()))
.thenReturn(mock(ApplicationsState.AppEntry.class));
when(mAppState.getEntry(stat2.mPackageName, UserHandle.myUserId()))
.thenReturn(mock(ApplicationsState.AppEntry.class));
when(mAppState.getEntry(stat3.mPackageName, UserHandle.myUserId()))
.thenReturn(null);
when(mMockContext.getPackageManager().resolveActivity(any(Intent.class), anyInt()))
.thenReturn(new ResolveInfo());
when(mUsageStatsManager.queryUsageStats(anyInt(), anyLong(), anyLong()))
.thenReturn(stats);
mController = new RecentAppsPreferenceController(mMockContext, mAppState, null);
mController.displayPreference(mScreen);
verify(mCategory).setTitle(R.string.recent_app_category_title);
// Only add stat1. stat2 is skipped because of the package name, stat3 skipped because
// it's invalid app.
verify(mCategory, times(1)).addPreference(any(Preference.class));
verify(mSeeAllPref).setTitle(R.string.see_all_apps_title);
verify(mSeeAllPref).setIcon(R.drawable.ic_chevron_right_24dp);
}
}