Support dynamic summary and icon for injected tiles

Bug: 63758074
Test: robotests
Change-Id: I2fa27812800c12606a613896ea2aa69dda393944
This commit is contained in:
Fan Zhang
2017-07-26 10:58:32 -07:00
parent 3eb22656eb
commit 26a1deffe6
8 changed files with 239 additions and 108 deletions

View File

@@ -16,16 +16,25 @@
package com.android.settings.dashboard;
import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_ICON_URI;
import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_SUMMARY;
import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_SUMMARY_URI;
import android.app.Activity;
import android.content.ComponentName;
import android.content.Context;
import android.content.IContentProvider;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.graphics.drawable.Icon;
import android.os.Bundle;
import android.provider.Settings;
import android.support.annotation.VisibleForTesting;
import android.support.v7.preference.Preference;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.Log;
import android.util.Pair;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
import com.android.settings.R;
@@ -37,9 +46,12 @@ import com.android.settingslib.drawer.DashboardCategory;
import com.android.settingslib.drawer.ProfileSelectDialog;
import com.android.settingslib.drawer.SettingsDrawerActivity;
import com.android.settingslib.drawer.Tile;
import com.android.settingslib.drawer.TileUtils;
import com.android.settingslib.utils.ThreadUtils;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* Impl for {@code DashboardFeatureProvider}.
@@ -125,14 +137,8 @@ public class DashboardFeatureProviderImpl implements DashboardFeatureProvider {
} else {
pref.setKey(getDashboardKeyForTile(tile));
}
if (tile.summary != null) {
pref.setSummary(tile.summary);
} else {
pref.setSummary(R.string.summary_placeholder);
}
if (tile.icon != null) {
pref.setIcon(tile.icon.loadDrawable(activity));
}
bindSummary(pref, tile);
bindIcon(pref, tile);
final Bundle metadata = tile.metaData;
String clsName = null;
String action = null;
@@ -206,6 +212,50 @@ public class DashboardFeatureProviderImpl implements DashboardFeatureProvider {
launchIntentOrSelectProfile(activity, tile, intent, MetricsEvent.DASHBOARD_SUMMARY);
}
private void bindSummary(Preference preference, Tile tile) {
if (tile.summary != null) {
preference.setSummary(tile.summary);
} else if (tile.metaData != null
&& tile.metaData.containsKey(META_DATA_PREFERENCE_SUMMARY_URI)) {
ThreadUtils.postOnBackgroundThread(() -> {
final Map<String, IContentProvider> providerMap = new ArrayMap<>();
final String uri = tile.metaData.getString(META_DATA_PREFERENCE_SUMMARY_URI);
final String summary = TileUtils.getTextFromUri(
mContext, uri, providerMap, META_DATA_PREFERENCE_SUMMARY);
ThreadUtils.postOnMainThread(() -> preference.setSummary(summary));
});
} else {
preference.setSummary(R.string.summary_placeholder);
}
}
@VisibleForTesting
void bindIcon(Preference preference, Tile tile) {
if (tile.icon != null) {
preference.setIcon(tile.icon.loadDrawable(preference.getContext()));
} else if (tile.metaData != null
&& tile.metaData.containsKey(META_DATA_PREFERENCE_ICON_URI))
ThreadUtils.postOnBackgroundThread(() -> {
String packageName = null;
if (tile.intent != null) {
Intent intent = tile.intent;
if (!TextUtils.isEmpty(intent.getPackage())) {
packageName = intent.getPackage();
} else if (intent.getComponent() != null) {
packageName = intent.getComponent().getPackageName();
}
}
final Map<String, IContentProvider> providerMap = new ArrayMap<>();
final String uri = tile.metaData.getString(META_DATA_PREFERENCE_ICON_URI);
final Pair<String, Integer> iconInfo = TileUtils.getIconFromUri(
mContext, packageName, uri, providerMap);
tile.icon = Icon.createWithResource(iconInfo.first, iconInfo.second);
ThreadUtils.postOnMainThread(() ->
preference.setIcon(tile.icon.loadDrawable(preference.getContext()))
);
});
}
private void launchIntentOrSelectProfile(Activity activity, Tile tile, Intent intent,
int sourceMetricCategory) {
if (!isIntentResolvable(intent)) {

View File

@@ -201,7 +201,7 @@ public abstract class DashboardFragment extends SettingsPreferenceFragment
/**
* Returns the CategoryKey for loading {@link DashboardCategory} for this fragment.
*/
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
@VisibleForTesting
public String getCategoryKey() {
return DashboardFragmentRegistry.PARENT_TO_CATEGORY_KEY_MAP.get(getClass().getName());
}

View File

@@ -22,8 +22,6 @@ import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.graphics.drawable.Drawable;
import android.os.Handler;
import android.os.Looper;
import android.support.annotation.VisibleForTesting;
import android.support.v7.preference.Preference;
import android.support.v7.preference.PreferenceScreen;
@@ -37,10 +35,10 @@ import com.android.settings.trustagent.TrustAgentManagerImpl;
import com.android.settingslib.drawer.DashboardCategory;
import com.android.settingslib.drawer.Tile;
import com.android.settingslib.drawer.TileUtils;
import com.android.settingslib.utils.ThreadUtils;
import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.Executors;
/** Implementation for {@code SecurityFeatureProvider}. */
public class SecurityFeatureProviderImpl implements SecurityFeatureProvider {
@@ -71,12 +69,8 @@ public class SecurityFeatureProviderImpl implements SecurityFeatureProvider {
// Fetching the summary and icon from the provider introduces latency, so do this on a
// separate thread.
Executors.newSingleThreadExecutor().execute(new Runnable() {
@Override
public void run() {
updatePreferencesToRunOnWorkerThread(context, preferenceScreen, dashboardCategory);
}
});
ThreadUtils.postOnBackgroundThread(() ->
updatePreferencesToRunOnWorkerThread(context, preferenceScreen, dashboardCategory));
}
@VisibleForTesting
@@ -162,9 +156,7 @@ public class SecurityFeatureProviderImpl implements SecurityFeatureProvider {
sIconCache.put(iconUri, icon);
// Icon is only returned if the icon belongs to Settings or the target app.
// setIcon must be called on the UI thread.
new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
ThreadUtils.postOnMainThread(() -> {
try {
matchingPref.setIcon(context.getPackageManager()
.getResourcesForApplication(icon.first /* package name */)
@@ -175,7 +167,6 @@ public class SecurityFeatureProviderImpl implements SecurityFeatureProvider {
// Intentionally ignored. If icon resources cannot be found, do not
// update.
}
}
});
}
}
@@ -184,9 +175,7 @@ public class SecurityFeatureProviderImpl implements SecurityFeatureProvider {
TileUtils.META_DATA_PREFERENCE_SUMMARY);
sSummaryCache.put(summaryUri, summary);
// setSummary must be called on UI thread.
new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
ThreadUtils.postOnMainThread(() -> {
// Only update the summary if it has actually changed.
if (summary == null) {
if (matchingPref.getSummary() != null) {
@@ -195,7 +184,6 @@ public class SecurityFeatureProviderImpl implements SecurityFeatureProvider {
} else if (!summary.equals(matchingPref.getSummary())) {
matchingPref.setSummary(summary);
}
}
});
}
}

View File

@@ -16,9 +16,17 @@
package com.android.settings;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import android.app.Activity;
import android.content.Context;
import android.content.IContentProvider;
import android.content.pm.PackageManager;
import android.hardware.fingerprint.FingerprintManager;
import android.os.UserManager;
@@ -33,7 +41,7 @@ import com.android.settings.testutils.FakeFeatureFactory;
import com.android.settings.testutils.SettingsRobolectricTestRunner;
import com.android.settings.testutils.XmlTestUtils;
import com.android.settings.testutils.shadow.ShadowLockPatternUtils;
import com.android.settingslib.drawer.DashboardCategory;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -42,47 +50,23 @@ import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.shadows.ShadowApplication;
import org.robolectric.util.ReflectionHelpers;
import java.util.List;
import java.util.Map;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
@RunWith(SettingsRobolectricTestRunner.class)
@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
public class SecuritySettingsTest {
private static final String MOCK_SUMMARY = "summary";
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
private Context mContext;
@Mock
private DashboardCategory mDashboardCategory;
@Mock
private SummaryLoader mSummaryLoader;
private SecuritySettings.SummaryProvider mSummaryProvider;
@Implements(com.android.settingslib.drawer.TileUtils.class)
public static class ShadowTileUtils {
@Implementation
public static String getTextFromUri(Context context, String uriString,
Map<String, IContentProvider> providerMap, String key) {
return MOCK_SUMMARY;
}
}
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);

View File

@@ -16,6 +16,15 @@
package com.android.settings.dashboard;
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.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.robolectric.Shadows.shadowOf;
import android.app.Activity;
import android.content.ComponentName;
import android.content.Context;
@@ -31,14 +40,17 @@ import android.support.v7.preference.Preference;
import com.android.internal.logging.nano.MetricsProto;
import com.android.settings.R;
import com.android.settings.SettingsActivity;
import com.android.settings.testutils.SettingsRobolectricTestRunner;
import com.android.settings.TestConfig;
import com.android.settings.testutils.FakeFeatureFactory;
import com.android.settings.testutils.SettingsRobolectricTestRunner;
import com.android.settings.testutils.shadow.ShadowThreadUtils;
import com.android.settings.testutils.shadow.ShadowTileUtils;
import com.android.settings.testutils.shadow.ShadowUserManager;
import com.android.settingslib.drawer.CategoryKey;
import com.android.settingslib.drawer.CategoryManager;
import com.android.settingslib.drawer.DashboardCategory;
import com.android.settingslib.drawer.Tile;
import com.android.settingslib.drawer.TileUtils;
import org.junit.Before;
import org.junit.Test;
@@ -55,15 +67,6 @@ import org.robolectric.util.ReflectionHelpers;
import java.util.ArrayList;
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.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.robolectric.Shadows.shadowOf;
@RunWith(SettingsRobolectricTestRunner.class)
@Config(manifest = TestConfig.MANIFEST_PATH,
sdk = TestConfig.SDK_VERSION,
@@ -245,6 +248,26 @@ public class DashboardFeatureProviderImplTest {
assertThat(preference.getSummary()).isEqualTo(tile.summary);
}
@Test
@Config(shadows = {
ShadowTileUtils.class,
ShadowThreadUtils.class
})
public void bindPreference_hasSummaryUri_shouldLoadSummaryFromContentProvider() {
final Preference preference = new Preference(RuntimeEnvironment.application);
final Tile tile = new Tile();
tile.intent = new Intent();
tile.intent.setComponent(new ComponentName("pkg", "class"));
tile.metaData = new Bundle();
tile.metaData.putString(TileUtils.META_DATA_PREFERENCE_SUMMARY_URI,
"content://com.android.settings/tile_summary");
mImpl.bindPreferenceToTile(mActivity, MetricsProto.MetricsEvent.VIEW_UNKNOWN,
preference, tile, null /*key */, Preference.DEFAULT_ORDER);
assertThat(preference.getSummary()).isEqualTo(ShadowTileUtils.MOCK_SUMMARY);
}
@Test
public void bindPreference_withNullKeyTileKey_shouldUseTileKey() {
final Preference preference = new Preference(RuntimeEnvironment.application);
@@ -258,6 +281,26 @@ public class DashboardFeatureProviderImplTest {
assertThat(preference.getKey()).isEqualTo(tile.key);
}
@Test
@Config(shadows = {
ShadowTileUtils.class,
ShadowThreadUtils.class
})
public void bindPreference_withIconUri_shouldLoadIconFromContentProvider() {
final Preference preference = new Preference(RuntimeEnvironment.application);
final Tile tile = new Tile();
tile.key = "key";
tile.intent = new Intent();
tile.intent.setComponent(
new ComponentName(RuntimeEnvironment.application.getPackageName(), "class"));
tile.metaData = new Bundle();
tile.metaData.putString(TileUtils.META_DATA_PREFERENCE_ICON_URI,
"content://com.android.settings/tile_icon");
mImpl.bindIcon(preference, tile);
assertThat(tile.icon).isNotNull();
}
@Test
public void bindPreference_withBaseOrder_shouldOffsetPriority() {
final int baseOrder = 100;

View File

@@ -16,8 +16,17 @@
package com.android.settings.security;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Matchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import android.content.Context;
import android.content.IContentProvider;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.res.Resources;
@@ -25,11 +34,11 @@ import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.support.v7.preference.Preference;
import android.support.v7.preference.PreferenceScreen;
import android.util.Pair;
import com.android.settings.testutils.SettingsRobolectricTestRunner;
import com.android.settings.R;
import com.android.settings.TestConfig;
import com.android.settings.testutils.SettingsRobolectricTestRunner;
import com.android.settings.testutils.shadow.ShadowTileUtils;
import com.android.settingslib.drawer.DashboardCategory;
import com.android.settingslib.drawer.Tile;
import com.android.settingslib.drawer.TileUtils;
@@ -41,22 +50,8 @@ import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.shadows.ShadowLooper;
import java.util.Map;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Matchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
@RunWith(SettingsRobolectricTestRunner.class)
@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
public class SecurityFeatureProviderImplTest {
@@ -77,21 +72,6 @@ public class SecurityFeatureProviderImplTest {
private SecurityFeatureProviderImpl mImpl;
@Implements(com.android.settingslib.drawer.TileUtils.class)
public static class ShadowTileUtils {
@Implementation
public static Pair getIconFromUri(Context context, String packageName, String uriString,
Map<String, IContentProvider> providerMap) {
return Pair.create("package", 161803);
}
@Implementation
public static String getTextFromUri(Context context, String uriString,
Map<String, IContentProvider> providerMap, String key) {
return MOCK_SUMMARY;
}
}
@Before
public void setUp() throws PackageManager.NameNotFoundException {
MockitoAnnotations.initMocks(this);

View File

@@ -0,0 +1,36 @@
/*
* 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.testutils.shadow;
import com.android.settingslib.utils.ThreadUtils;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
@Implements(ThreadUtils.class)
public class ShadowThreadUtils {
@Implementation
public static void postOnBackgroundThread(Runnable runnable) {
runnable.run();
}
@Implementation
public static void postOnMainThread(Runnable runnable) {
runnable.run();
}
}

View File

@@ -0,0 +1,50 @@
/*
* 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.testutils.shadow;
import android.content.Context;
import android.content.IContentProvider;
import android.util.Pair;
import com.android.settings.R;
import com.android.settingslib.drawer.TileUtils;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import java.util.Map;
@Implements(TileUtils.class)
public class ShadowTileUtils {
public static final int ICON_RES_ID = R.drawable.ic_settings;
public static final String MOCK_SUMMARY = "summary";
@Implementation
public static String getTextFromUri(Context context, String uriString,
Map<String, IContentProvider> providerMap, String key) {
return MOCK_SUMMARY;
}
@Implementation
public static Pair<String, Integer> getIconFromUri(Context context, String packageName,
String uriString, Map<String, IContentProvider> providerMap) {
return Pair.create(RuntimeEnvironment.application.getPackageName(), ICON_RES_ID);
}
}