Misc tweaks to create shortcut handler

- Add restriction to only allow system apps provide shortcuts
- Load icon from target package when building shortcut icons
- Codestyle clean up

Change-Id: If9e5c5e60601efda216ce2cc2d4fd7beb44279e1
Fixes: 110393916
Test: visual, robotests
This commit is contained in:
Fan Zhang
2018-06-19 10:21:38 -07:00
parent 3df8494f08
commit 807dfbb0ad
4 changed files with 226 additions and 163 deletions

View File

@@ -21,6 +21,7 @@ import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.pm.ShortcutInfo;
@@ -33,6 +34,7 @@ import android.graphics.drawable.Icon;
import android.graphics.drawable.LayerDrawable;
import android.net.ConnectivityManager;
import android.os.AsyncTask;
import android.util.Log;
import android.view.ContextThemeWrapper;
import android.view.LayoutInflater;
import android.view.View;
@@ -52,6 +54,7 @@ import androidx.annotation.VisibleForTesting;
public class CreateShortcut extends LauncherActivity {
private static final String TAG = "CreateShortcut";
@VisibleForTesting
static final String SHORTCUT_ID_PREFIX = "component-shortcut-";
@@ -76,11 +79,12 @@ public class CreateShortcut extends LauncherActivity {
ShortcutManager sm = getSystemService(ShortcutManager.class);
ActivityInfo activityInfo = resolveInfo.activityInfo;
Icon maskableIcon = activityInfo.icon != 0 ? Icon.createWithAdaptiveBitmap(
createIcon(activityInfo.icon,
Icon maskableIcon = activityInfo.icon != 0 && activityInfo.applicationInfo != null
? Icon.createWithAdaptiveBitmap(
createIcon(activityInfo.applicationInfo, activityInfo.icon,
R.layout.shortcut_badge_maskable,
getResources().getDimensionPixelSize(R.dimen.shortcut_size_maskable))) :
Icon.createWithResource(this, R.drawable.ic_launcher_settings);
getResources().getDimensionPixelSize(R.dimen.shortcut_size_maskable)))
: Icon.createWithResource(this, R.drawable.ic_launcher_settings);
String shortcutId = SHORTCUT_ID_PREFIX +
shortcutIntent.getComponent().flattenToShortString();
ShortcutInfo info = new ShortcutInfo.Builder(this, shortcutId)
@@ -98,7 +102,9 @@ public class CreateShortcut extends LauncherActivity {
intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, label);
if (activityInfo.icon != 0) {
intent.putExtra(Intent.EXTRA_SHORTCUT_ICON, createIcon(activityInfo.icon,
intent.putExtra(Intent.EXTRA_SHORTCUT_ICON, createIcon(
activityInfo.applicationInfo,
activityInfo.icon,
R.layout.shortcut_badge,
getResources().getDimensionPixelSize(R.dimen.shortcut_size)));
}
@@ -114,20 +120,29 @@ public class CreateShortcut extends LauncherActivity {
info.activityInfo.name);
}
private Bitmap createIcon(int resource, int layoutRes, int size) {
Context context = new ContextThemeWrapper(this, android.R.style.Theme_Material);
View view = LayoutInflater.from(context).inflate(layoutRes, null);
Drawable iconDrawable = getDrawable(resource);
if (iconDrawable instanceof LayerDrawable) {
iconDrawable = ((LayerDrawable) iconDrawable).getDrawable(1);
}
((ImageView) view.findViewById(android.R.id.icon)).setImageDrawable(iconDrawable);
int spec = MeasureSpec.makeMeasureSpec(size, MeasureSpec.EXACTLY);
private Bitmap createIcon(ApplicationInfo app, int resource, int layoutRes, int size) {
final Context context = new ContextThemeWrapper(this, android.R.style.Theme_Material);
final View view = LayoutInflater.from(context).inflate(layoutRes, null);
final int spec = MeasureSpec.makeMeasureSpec(size, MeasureSpec.EXACTLY);
view.measure(spec, spec);
Bitmap bitmap = Bitmap.createBitmap(view.getMeasuredWidth(), view.getMeasuredHeight(),
final Bitmap bitmap = Bitmap.createBitmap(view.getMeasuredWidth(), view.getMeasuredHeight(),
Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
final Canvas canvas = new Canvas(bitmap);
Drawable iconDrawable = null;
try {
iconDrawable =
getPackageManager().getResourcesForApplication(app).getDrawable(resource);
if (iconDrawable instanceof LayerDrawable) {
iconDrawable = ((LayerDrawable) iconDrawable).getDrawable(1);
}
((ImageView) view.findViewById(android.R.id.icon)).setImageDrawable(iconDrawable);
} catch (PackageManager.NameNotFoundException e) {
Log.w(TAG, "Cannot load icon from app " + app + ", returning a default icon");
Icon icon = Icon.createWithResource(this, R.drawable.ic_launcher_settings);
((ImageView) view.findViewById(android.R.id.icon)).setImageIcon(icon);
}
view.layout(0, 0, view.getMeasuredWidth(), view.getMeasuredHeight());
view.draw(canvas);
return bitmap;
@@ -147,12 +162,15 @@ public class CreateShortcut extends LauncherActivity {
* Perform query on package manager for list items. The default
* implementation queries for activities.
*/
@Override
protected List<ResolveInfo> onQueryPackageManager(Intent queryIntent) {
List<ResolveInfo> activities = getPackageManager().queryIntentActivities(queryIntent,
PackageManager.GET_META_DATA);
final ConnectivityManager cm =
(ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
if (activities == null) return null;
if (activities == null) {
return null;
}
for (int i = activities.size() - 1; i >= 0; i--) {
ResolveInfo info = activities.get(i);
if (info.activityInfo.name.endsWith(TetherSettingsActivity.class.getSimpleName())) {
@@ -160,6 +178,10 @@ public class CreateShortcut extends LauncherActivity {
activities.remove(i);
}
}
if (!info.activityInfo.applicationInfo.isSystemApp()) {
Log.d(TAG, "Skipping non-system app: " + info.activityInfo);
activities.remove(i);
}
}
return activities;
}

View File

@@ -0,0 +1,179 @@
/*
* Copyright (C) 2018 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.shortcut;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.pm.ShortcutInfo;
import android.content.pm.ShortcutManager;
import com.android.settings.Settings;
import com.android.settings.testutils.SettingsRobolectricTestRunner;
import com.android.settings.testutils.shadow.ShadowConnectivityManager;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.Robolectric;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;
import org.robolectric.shadow.api.Shadow;
import org.robolectric.shadows.ShadowPackageManager;
import java.util.Arrays;
import java.util.List;
/**
* Tests for {@link CreateShortcutTest}
*/
@RunWith(SettingsRobolectricTestRunner.class)
@Config(shadows = ShadowConnectivityManager.class)
public class CreateShortcutTest {
private static final String SHORTCUT_ID_PREFIX = CreateShortcut.SHORTCUT_ID_PREFIX;
private Context mContext;
private ShadowConnectivityManager mShadowConnectivityManager;
private ShadowPackageManager mPackageManager;
@Mock
private ShortcutManager mShortcutManager;
@Captor
private ArgumentCaptor<List<ShortcutInfo>> mListCaptor;
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
mContext = RuntimeEnvironment.application;
mPackageManager = Shadow.extract(mContext.getPackageManager());
mShadowConnectivityManager = ShadowConnectivityManager.getShadow();
mShadowConnectivityManager.setTetheringSupported(true);
}
@Test
public void createResultIntent() {
CreateShortcut orgActivity = Robolectric.setupActivity(CreateShortcut.class);
CreateShortcut activity = spy(orgActivity);
doReturn(mShortcutManager).when(activity).getSystemService(eq(Context.SHORTCUT_SERVICE));
when(mShortcutManager.createShortcutResultIntent(any(ShortcutInfo.class)))
.thenReturn(new Intent().putExtra("d1", "d2"));
final Intent intent = CreateShortcut.getBaseIntent()
.setClass(activity, Settings.ManageApplicationsActivity.class);
final ResolveInfo ri = activity.getPackageManager().resolveActivity(intent, 0);
final Intent result = activity.createResultIntent(intent, ri, "dummy");
assertThat(result.getStringExtra("d1")).isEqualTo("d2");
assertThat((Object) result.getParcelableExtra(Intent.EXTRA_SHORTCUT_INTENT)).isNotNull();
ArgumentCaptor<ShortcutInfo> infoCaptor = ArgumentCaptor.forClass(ShortcutInfo.class);
verify(mShortcutManager, times(1))
.createShortcutResultIntent(infoCaptor.capture());
assertThat(infoCaptor.getValue().getId())
.isEqualTo(SHORTCUT_ID_PREFIX + intent.getComponent().flattenToShortString());
}
@Test
public void shortcutsUpdateTask() {
mContext = spy(RuntimeEnvironment.application);
doReturn(mShortcutManager).when(mContext).getSystemService(eq(Context.SHORTCUT_SERVICE));
final Intent shortcut1 = CreateShortcut.getBaseIntent().setComponent(
new ComponentName(mContext, Settings.ManageApplicationsActivity.class));
final ResolveInfo ri1 = mock(ResolveInfo.class);
final Intent shortcut2 = CreateShortcut.getBaseIntent().setComponent(
new ComponentName(mContext, Settings.SoundSettingsActivity.class));
final ResolveInfo ri2 = mock(ResolveInfo.class);
when(ri1.loadLabel(any(PackageManager.class))).thenReturn("label1");
when(ri2.loadLabel(any(PackageManager.class))).thenReturn("label2");
mPackageManager.addResolveInfoForIntent(shortcut1, ri1);
mPackageManager.addResolveInfoForIntent(shortcut2, ri2);
final List<ShortcutInfo> pinnedShortcuts = Arrays.asList(
makeShortcut("d1"),
makeShortcut("d2"),
makeShortcut(Settings.ManageApplicationsActivity.class),
makeShortcut("d3"),
makeShortcut(Settings.SoundSettingsActivity.class));
when(mShortcutManager.getPinnedShortcuts()).thenReturn(pinnedShortcuts);
new CreateShortcut.ShortcutsUpdateTask(mContext).doInBackground();
verify(mShortcutManager, times(1)).updateShortcuts(mListCaptor.capture());
final List<ShortcutInfo> updates = mListCaptor.getValue();
assertThat(updates).hasSize(2);
assertThat(pinnedShortcuts.get(2).getId()).isEqualTo(updates.get(0).getId());
assertThat(pinnedShortcuts.get(4).getId()).isEqualTo(updates.get(1).getId());
}
@Test
public void queryActivities_shouldOnlyIncludeSystemApp() {
final ResolveInfo ri1 = new ResolveInfo();
ri1.activityInfo = new ActivityInfo();
ri1.activityInfo.name = "activity1";
ri1.activityInfo.applicationInfo = new ApplicationInfo();
ri1.activityInfo.applicationInfo.flags = 0;
final ResolveInfo ri2 = new ResolveInfo();
ri2.activityInfo = new ActivityInfo();
ri2.activityInfo.name = "activity2";
ri2.activityInfo.applicationInfo = new ApplicationInfo();
ri2.activityInfo.applicationInfo.flags = ApplicationInfo.FLAG_SYSTEM;
mPackageManager.addResolveInfoForIntent(CreateShortcut.getBaseIntent(),
Arrays.asList(ri1, ri2));
TestClass orgActivity = Robolectric.setupActivity(TestClass.class);
TestClass activity = spy(orgActivity);
List<ResolveInfo> info = activity.onQueryPackageManager(CreateShortcut.getBaseIntent());
assertThat(info).hasSize(1);
assertThat(info.get(0)).isEqualTo(ri2);
}
private ShortcutInfo makeShortcut(Class<?> className) {
ComponentName cn = new ComponentName(mContext, className);
return makeShortcut(SHORTCUT_ID_PREFIX + cn.flattenToShortString());
}
private ShortcutInfo makeShortcut(String id) {
return new ShortcutInfo.Builder(mContext, id).build();
}
private static class TestClass extends CreateShortcut {
}
}

View File

@@ -19,8 +19,10 @@ package com.android.settings.testutils.shadow;
import android.net.ConnectivityManager;
import android.util.SparseBooleanArray;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.shadow.api.Shadow;
@Implements(value = ConnectivityManager.class, inheritImplementationMethods = true)
public class ShadowConnectivityManager extends org.robolectric.shadows.ShadowConnectivityManager {
@@ -45,4 +47,9 @@ public class ShadowConnectivityManager extends org.robolectric.shadows.ShadowCon
public boolean isTetheringSupported() {
return mTetheringSupported;
}
public static ShadowConnectivityManager getShadow() {
return Shadow.extract(
RuntimeEnvironment.application.getSystemService(ConnectivityManager.class));
}
}

View File

@@ -1,145 +0,0 @@
/*
* 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.shortcut;
import static android.support.test.espresso.Espresso.onView;
import static android.support.test.espresso.assertion.ViewAssertions.doesNotExist;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.app.Instrumentation;
import android.content.ComponentName;
import android.content.Context;
import android.content.ContextWrapper;
import android.content.Intent;
import android.content.pm.ResolveInfo;
import android.content.pm.ShortcutInfo;
import android.content.pm.ShortcutManager;
import android.support.test.InstrumentationRegistry;
import android.support.test.espresso.matcher.ViewMatchers;
import android.support.test.filters.SmallTest;
import android.support.test.runner.AndroidJUnit4;
import com.android.settings.R;
import com.android.settings.Settings;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.util.Arrays;
import java.util.List;
/**
* Tests for {@link CreateShortcutTest}
*/
@RunWith(AndroidJUnit4.class)
@SmallTest
public class CreateShortcutTest {
private static final String SHORTCUT_ID_PREFIX = CreateShortcut.SHORTCUT_ID_PREFIX;
private Instrumentation mInstrumentation;
private Context mContext;
@Mock
ShortcutManager mShortcutManager;
@Captor
ArgumentCaptor<List<ShortcutInfo>> mListCaptor;
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
mInstrumentation = InstrumentationRegistry.getInstrumentation();
mContext = mInstrumentation.getTargetContext();
}
@Test
public void test_layoutDoesNotHaveCancelButton() {
mInstrumentation.startActivitySync(new Intent(Intent.ACTION_CREATE_SHORTCUT)
.setClassName(mContext, CreateShortcut.class.getName())
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
onView(ViewMatchers.withText(R.string.cancel)).check(doesNotExist());
}
@Test
public void createResultIntent() {
CreateShortcut orgActivity = (CreateShortcut) mInstrumentation.startActivitySync(
new Intent(Intent.ACTION_CREATE_SHORTCUT)
.setClassName(mContext, CreateShortcut.class.getName())
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
CreateShortcut activity = spy(orgActivity);
doReturn(mShortcutManager).when(activity).getSystemService(eq(Context.SHORTCUT_SERVICE));
when(mShortcutManager.createShortcutResultIntent(any(ShortcutInfo.class)))
.thenReturn(new Intent().putExtra("d1", "d2"));
Intent intent = CreateShortcut.getBaseIntent()
.setClass(activity, Settings.ManageApplicationsActivity.class);
ResolveInfo ri = activity.getPackageManager().resolveActivity(intent, 0);
Intent result = activity.createResultIntent(intent, ri, "dummy");
assertEquals("d2", result.getStringExtra("d1"));
assertNotNull(result.getParcelableExtra(Intent.EXTRA_SHORTCUT_INTENT));
ArgumentCaptor<ShortcutInfo> infoCaptor = ArgumentCaptor.forClass(ShortcutInfo.class);
verify(mShortcutManager, times(1))
.createShortcutResultIntent(infoCaptor.capture());
String expectedId = SHORTCUT_ID_PREFIX + intent.getComponent().flattenToShortString();
assertEquals(expectedId, infoCaptor.getValue().getId());
}
@Test
public void shortcutsUpdateTask() {
mContext = spy(new ContextWrapper(mInstrumentation.getTargetContext()));
doReturn(mShortcutManager).when(mContext).getSystemService(eq(Context.SHORTCUT_SERVICE));
List<ShortcutInfo> pinnedShortcuts = Arrays.asList(
makeShortcut("d1"), makeShortcut("d2"),
makeShortcut(Settings.ManageApplicationsActivity.class),
makeShortcut("d3"),
makeShortcut(Settings.SoundSettingsActivity.class));
when(mShortcutManager.getPinnedShortcuts()).thenReturn(pinnedShortcuts);
new CreateShortcut.ShortcutsUpdateTask(mContext).doInBackground();
verify(mShortcutManager, times(1)).updateShortcuts(mListCaptor.capture());
List<ShortcutInfo> updates = mListCaptor.getValue();
assertEquals(2, updates.size());
assertEquals(pinnedShortcuts.get(2).getId(), updates.get(0).getId());
assertEquals(pinnedShortcuts.get(4).getId(), updates.get(1).getId());
}
private ShortcutInfo makeShortcut(Class<?> className) {
ComponentName cn = new ComponentName(mContext, className);
return makeShortcut(SHORTCUT_ID_PREFIX + cn.flattenToShortString());
}
private ShortcutInfo makeShortcut(String id) {
return new ShortcutInfo.Builder(mContext, id).build();
}
}