Merge "Adds the Clear App dialog for Instant Apps" into oc-dev
diff --git a/res/values/strings.xml b/res/values/strings.xml
index d2c8d5a..c23d399 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -8607,8 +8607,11 @@
<string name="storage_percent_full">full</string>
- <!-- Label for button allow user to clear the data for an instant app -->
+ <!-- Label for button allow user to remove the instant app from the device. -->
<string name="clear_instant_app_data">Clear app</string>
+ <!-- Confirmation message displayed when the user taps Clear app, to ensure they want to remove
+ the instant app from the device. -->
+ <string name="clear_instant_app_confirmation">Do you want to remove this instant app?</string>
<!-- Title of games app storage screen [CHAR LIMIT=30] -->
<string name="game_storage_settings">Games</string>
diff --git a/src/com/android/settings/applications/ApplicationFeatureProvider.java b/src/com/android/settings/applications/ApplicationFeatureProvider.java
index ef8cb23..5e986db 100644
--- a/src/com/android/settings/applications/ApplicationFeatureProvider.java
+++ b/src/com/android/settings/applications/ApplicationFeatureProvider.java
@@ -37,7 +37,7 @@
* only relevant to instant apps.
*/
InstantAppButtonsController newInstantAppButtonsController(Fragment fragment,
- View view);
+ View view, InstantAppButtonsController.ShowDialogDelegate showDialogDelegate);
/**
* Calculates the total number of apps installed on the device via policy in the current user
diff --git a/src/com/android/settings/applications/ApplicationFeatureProviderImpl.java b/src/com/android/settings/applications/ApplicationFeatureProviderImpl.java
index 124a8de..4171857 100644
--- a/src/com/android/settings/applications/ApplicationFeatureProviderImpl.java
+++ b/src/com/android/settings/applications/ApplicationFeatureProviderImpl.java
@@ -58,8 +58,8 @@
@Override
public InstantAppButtonsController newInstantAppButtonsController(Fragment fragment,
- View view) {
- return new InstantAppButtonsController(mContext, fragment, view);
+ View view, InstantAppButtonsController.ShowDialogDelegate showDialogDelegate) {
+ return new InstantAppButtonsController(mContext, fragment, view, showDialogDelegate);
}
@Override
diff --git a/src/com/android/settings/applications/InstalledAppDetails.java b/src/com/android/settings/applications/InstalledAppDetails.java
index 1fc5515..47ea5b6 100755
--- a/src/com/android/settings/applications/InstalledAppDetails.java
+++ b/src/com/android/settings/applications/InstalledAppDetails.java
@@ -85,6 +85,7 @@
import com.android.settings.applications.defaultapps.DefaultHomePreferenceController;
import com.android.settings.applications.defaultapps.DefaultPhonePreferenceController;
import com.android.settings.applications.defaultapps.DefaultSmsPreferenceController;
+import com.android.settings.applications.instantapps.InstantAppButtonsController;
import com.android.settings.datausage.AppDataUsage;
import com.android.settings.datausage.DataUsageList;
import com.android.settings.datausage.DataUsageSummary;
@@ -190,6 +191,8 @@
protected ProcStatsData mStatsManager;
protected ProcStatsPackageEntry mStats;
+ private InstantAppButtonsController mInstantAppButtonsController;
+
private AppStorageStats mLastResult;
private boolean handleDisableable(Button button) {
@@ -771,6 +774,9 @@
.setNegativeButton(R.string.dlg_cancel, null)
.create();
}
+ if (mInstantAppButtonsController != null) {
+ return mInstantAppButtonsController.createDialog(id);
+ }
return null;
}
@@ -1120,10 +1126,11 @@
if (AppUtils.isInstant(mPackageInfo.applicationInfo)) {
LayoutPreference buttons = (LayoutPreference) findPreference(KEY_INSTANT_APP_BUTTONS);
final Activity activity = getActivity();
- FeatureFactory.getFactory(activity)
+ mInstantAppButtonsController = FeatureFactory.getFactory(activity)
.getApplicationFeatureProvider(activity)
.newInstantAppButtonsController(this,
- buttons.findViewById(R.id.instant_app_button_container))
+ buttons.findViewById(R.id.instant_app_button_container),
+ id -> showDialogInner(id, 0))
.setPackageName(mPackageName)
.show();
}
diff --git a/src/com/android/settings/applications/PackageManagerWrapper.java b/src/com/android/settings/applications/PackageManagerWrapper.java
index 2be92ed..8dae417 100644
--- a/src/com/android/settings/applications/PackageManagerWrapper.java
+++ b/src/com/android/settings/applications/PackageManagerWrapper.java
@@ -20,6 +20,7 @@
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ApplicationInfo;
+import android.content.pm.IPackageDeleteObserver;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.os.UserHandle;
@@ -98,4 +99,10 @@
*/
void replacePreferredActivity(IntentFilter homeFilter, int matchCategoryEmpty,
ComponentName[] componentNames, ComponentName component);
+
+ /**
+ * Calls {@code PackageManager.deletePackageAsUser}
+ */
+ void deletePackageAsUser(String packageName, IPackageDeleteObserver observer, int flags,
+ int userId);
}
diff --git a/src/com/android/settings/applications/PackageManagerWrapperImpl.java b/src/com/android/settings/applications/PackageManagerWrapperImpl.java
index 698c14c..a0d824f 100644
--- a/src/com/android/settings/applications/PackageManagerWrapperImpl.java
+++ b/src/com/android/settings/applications/PackageManagerWrapperImpl.java
@@ -20,6 +20,7 @@
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ApplicationInfo;
+import android.content.pm.IPackageDeleteObserver;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.os.UserHandle;
@@ -90,4 +91,10 @@
ComponentName[] componentNames, ComponentName component) {
mPm.replacePreferredActivity(homeFilter, matchCategoryEmpty, componentNames, component);
}
+
+ @Override
+ public void deletePackageAsUser(String packageName, IPackageDeleteObserver observer, int flags,
+ int userId) {
+ mPm.deletePackageAsUser(packageName, observer, flags, userId);
+ }
}
diff --git a/src/com/android/settings/applications/instantapps/InstantAppButtonsController.java b/src/com/android/settings/applications/instantapps/InstantAppButtonsController.java
index aa7c418..16956df 100644
--- a/src/com/android/settings/applications/instantapps/InstantAppButtonsController.java
+++ b/src/com/android/settings/applications/instantapps/InstantAppButtonsController.java
@@ -16,31 +16,53 @@
package com.android.settings.applications.instantapps;
+import android.app.AlertDialog;
+import android.app.Fragment;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.os.UserHandle;
+import android.view.View;
+import android.widget.Button;
+
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
import com.android.settings.R;
import com.android.settings.applications.AppStoreUtil;
+import com.android.settings.applications.PackageManagerWrapper;
+import com.android.settings.applications.PackageManagerWrapperImpl;
import com.android.settings.overlay.FeatureFactory;
-import android.app.Fragment;
-import android.content.Context;
-import android.content.Intent;
-import android.content.pm.PackageManager;
-import android.view.View;
-import android.view.View.OnClickListener;
-import android.widget.Button;
-
/** Encapsulates a container for buttons relevant to instant apps */
-public class InstantAppButtonsController {
+public class InstantAppButtonsController implements DialogInterface.OnClickListener {
+
+ public interface ShowDialogDelegate {
+ /**
+ * Delegate that should be called when this controller wants to show a dialog.
+ */
+ void showDialog(int id);
+ }
private final Context mContext;
private final Fragment mFragment;
private final View mView;
+ private final PackageManagerWrapper mPackageManagerWrapper;
+ private final ShowDialogDelegate mShowDialogDelegate;
private String mPackageName;
- public InstantAppButtonsController(Context context, Fragment fragment, View view) {
+ public static final int DLG_BASE = 0x5032;
+ public static final int DLG_CLEAR_APP = DLG_BASE + 1;
+
+ public InstantAppButtonsController(
+ Context context,
+ Fragment fragment,
+ View view,
+ ShowDialogDelegate showDialogDelegate) {
mContext = context;
mFragment = fragment;
mView = view;
+ mShowDialogDelegate = showDialogDelegate;
+ mPackageManagerWrapper = new PackageManagerWrapperImpl(context.getPackageManager());
}
public InstantAppButtonsController setPackageName(String packageName) {
@@ -51,17 +73,38 @@
public void bindButtons() {
Button installButton = (Button)mView.findViewById(R.id.install);
Button clearDataButton = (Button)mView.findViewById(R.id.clear_data);
- Intent installIntent = AppStoreUtil.getAppStoreLink(mContext, mPackageName);
- if (installIntent != null) {
+ Intent appStoreIntent = AppStoreUtil.getAppStoreLink(mContext, mPackageName);
+ if (appStoreIntent != null) {
installButton.setEnabled(true);
- installButton.setOnClickListener(v -> mFragment.startActivity(installIntent));
+ installButton.setOnClickListener(v -> mFragment.startActivity(appStoreIntent));
}
- clearDataButton.setOnClickListener(v -> {
- FeatureFactory.getFactory(mContext).getMetricsFeatureProvider().action(mContext,
- MetricsEvent.ACTION_SETTINGS_CLEAR_INSTANT_APP, mPackageName);
- PackageManager pm = mContext.getPackageManager();
- pm.clearApplicationUserData(mPackageName, null);
- });
+
+ clearDataButton.setOnClickListener(v -> mShowDialogDelegate.showDialog(DLG_CLEAR_APP));
+ }
+
+ public AlertDialog createDialog(int id) {
+ if (id == DLG_CLEAR_APP) {
+ AlertDialog dialog = new AlertDialog.Builder(mFragment.getActivity())
+ .setPositiveButton(R.string.clear_instant_app_data, this)
+ .setNegativeButton(R.string.cancel, null)
+ .setTitle(R.string.clear_instant_app_data)
+ .setMessage(mContext.getString(R.string.clear_instant_app_confirmation))
+ .create();
+ return dialog;
+ }
+ return null;
+ }
+
+ public void onClick(DialogInterface dialog, int which) {
+ if (which == DialogInterface.BUTTON_POSITIVE) {
+ FeatureFactory.getFactory(mContext)
+ .getMetricsFeatureProvider()
+ .action(mContext,
+ MetricsEvent.ACTION_SETTINGS_CLEAR_INSTANT_APP,
+ mPackageName);
+ mPackageManagerWrapper.deletePackageAsUser(
+ mPackageName, null, 0, UserHandle.myUserId());
+ }
}
public InstantAppButtonsController show() {
diff --git a/tests/robotests/src/com/android/settings/applications/InstalledAppDetailsTest.java b/tests/robotests/src/com/android/settings/applications/InstalledAppDetailsTest.java
index 209cdeb..3d4b840 100644
--- a/tests/robotests/src/com/android/settings/applications/InstalledAppDetailsTest.java
+++ b/tests/robotests/src/com/android/settings/applications/InstalledAppDetailsTest.java
@@ -18,6 +18,7 @@
import android.app.Activity;
+import android.app.AlertDialog;
import android.app.admin.DevicePolicyManager;
import android.content.Context;
import android.content.pm.ApplicationInfo;
@@ -48,7 +49,9 @@
import org.robolectric.util.ReflectionHelpers;
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.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
@@ -222,6 +225,20 @@
verify(forceStopButton).setVisibility(View.GONE);
}
+ @Test
+ public void instantApps_buttonControllerHandlesDialog() {
+ InstantAppButtonsController mockController = mock(InstantAppButtonsController.class);
+ ReflectionHelpers.setField(
+ mAppDetail, "mInstantAppButtonsController", mockController);
+ // Make sure first that button controller is not called for supported dialog id
+ AlertDialog mockDialog = mock(AlertDialog.class);
+ when(mockController.createDialog(InstantAppButtonsController.DLG_CLEAR_APP))
+ .thenReturn(mockDialog);
+ assertThat(mAppDetail.createDialog(InstantAppButtonsController.DLG_CLEAR_APP, 0))
+ .isEqualTo(mockDialog);
+ verify(mockController).createDialog(InstantAppButtonsController.DLG_CLEAR_APP);
+ }
+
// A helper class for testing the InstantAppButtonsController - it lets us look up the
// preference associated with a key for instant app buttons and get back a mock
// LayoutPreference (to avoid a null pointer exception).
@@ -261,8 +278,8 @@
FakeFeatureFactory.setupForTest(mContext);
FakeFeatureFactory factory =
(FakeFeatureFactory) FakeFeatureFactory.getFactory(mContext);
- when(factory.applicationFeatureProvider.newInstantAppButtonsController(any(),
- any())).thenReturn(buttonsController);
+ when(factory.applicationFeatureProvider.newInstantAppButtonsController(
+ any(), any(), any())).thenReturn(buttonsController);
fragment.maybeAddInstantAppButtons();
verify(buttonsController).setPackageName(anyString());
diff --git a/tests/robotests/src/com/android/settings/applications/instantapps/InstantAppButtonsControllerTest.java b/tests/robotests/src/com/android/settings/applications/instantapps/InstantAppButtonsControllerTest.java
new file mode 100644
index 0000000..13040a2
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/applications/instantapps/InstantAppButtonsControllerTest.java
@@ -0,0 +1,180 @@
+/*
+ * 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.instantapps;
+
+import static com.android.settings.applications.instantapps.InstantAppButtonsController
+ .ShowDialogDelegate;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Matchers.eq;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Matchers.isNull;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
+
+import android.annotation.SuppressLint;
+import android.app.Fragment;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.view.View;
+import android.widget.Button;
+
+import com.android.settings.R;
+import com.android.settings.SettingsRobolectricTestRunner;
+import com.android.settings.TestConfig;
+import com.android.settings.applications.PackageManagerWrapper;
+import com.android.settings.backup.BackupSettingsActivityTest;
+import com.android.settings.core.instrumentation.MetricsFeatureProvider;
+import com.android.settings.testutils.FakeFeatureFactory;
+
+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.annotation.Config;
+import org.robolectric.shadows.ShadowUserManager;
+import org.robolectric.util.ReflectionHelpers;
+
+/** Tests for the InstantAppButtonsController. */
+@RunWith(SettingsRobolectricTestRunner.class)
+@Config(manifest = TestConfig.MANIFEST_PATH, sdk = 23)
+public class InstantAppButtonsControllerTest {
+
+ private static final String TEST_INSTALLER_PACKAGE_NAME = "com.installer";
+ private static final String TEST_INSTALLER_ACTIVITY_NAME = "com.installer.InstallerActivity";
+ private static final ComponentName TEST_INSTALLER_COMPONENT =
+ new ComponentName(
+ TEST_INSTALLER_PACKAGE_NAME,
+ TEST_INSTALLER_ACTIVITY_NAME);
+ private static final String TEST_AIA_PACKAGE_NAME = "test.aia.package";
+
+ @Mock(answer = Answers.RETURNS_DEEP_STUBS)
+ Context mockContext;
+ @Mock
+ PackageManager mockPackageManager;
+ @Mock
+ PackageManagerWrapper mockPackageManagerWrapper;
+ @Mock
+ View mockView;
+ @Mock
+ ShowDialogDelegate mockShowDialogDelegate;
+ @Mock
+ Button mockInstallButton;
+ @Mock
+ Button mockClearButton;
+ @Mock
+ MetricsFeatureProvider mockMetricsFeatureProvider;
+ @Mock
+ ResolveInfo mockResolveInfo;
+ @Mock
+ ActivityInfo mockActivityInfo;
+
+ private PackageManager stubPackageManager;
+
+ private FakeFeatureFactory fakeFeatureFactory;
+ private TestFragment testFragment;
+ private InstantAppButtonsController controller;
+
+
+ private View.OnClickListener receivedListener;
+
+ @Before
+ public void init() {
+ MockitoAnnotations.initMocks(this);
+ testFragment = new TestFragment();
+ when(mockView.findViewById(R.id.install)).thenReturn(mockInstallButton);
+ when(mockView.findViewById(R.id.clear_data)).thenReturn(mockClearButton);
+ mockResolveInfo.activityInfo = mockActivityInfo;
+ mockActivityInfo.packageName = TEST_INSTALLER_PACKAGE_NAME;
+ mockActivityInfo.name = TEST_INSTALLER_ACTIVITY_NAME;
+ when(mockContext.getPackageManager()).thenReturn(mockPackageManager);
+ when(mockPackageManager.resolveActivity(any(), anyInt())).thenReturn(mockResolveInfo);
+ controller = new InstantAppButtonsController(
+ mockContext, testFragment, mockView, mockShowDialogDelegate);
+ controller.setPackageName(TEST_AIA_PACKAGE_NAME);
+ ReflectionHelpers.setField(
+ controller, "mPackageManagerWrapper", mockPackageManagerWrapper);
+ FakeFeatureFactory.setupForTest(mockContext);
+ }
+
+ @Test
+ public void testInstallListenerTriggersInstall() {
+ doAnswer(invocation -> {
+ receivedListener = (View.OnClickListener) invocation.getArguments()[0];
+ return null;
+ }).when(mockInstallButton).setOnClickListener(any());
+ controller.bindButtons();
+
+ assertThat(receivedListener).isNotNull();
+ receivedListener.onClick(mockInstallButton);
+ assertThat(testFragment.getStartActivityIntent()).isNotNull();
+ assertThat(testFragment.getStartActivityIntent().getComponent())
+ .isEqualTo(TEST_INSTALLER_COMPONENT);
+ }
+
+ @Test
+ public void testClearListenerShowsDialog() {
+ doAnswer(invocation -> {
+ receivedListener = (View.OnClickListener) invocation.getArguments()[0];
+ return null;
+ }).when(mockClearButton).setOnClickListener(any());
+ controller.bindButtons();
+ assertThat(receivedListener).isNotNull();
+ receivedListener.onClick(mockClearButton);
+ verify(mockShowDialogDelegate).showDialog(InstantAppButtonsController.DLG_CLEAR_APP);
+ }
+
+ @Test
+ public void testDialogInterfaceOnClick_positiveClearsApp() {
+ controller.onClick(mock(DialogInterface.class), DialogInterface.BUTTON_POSITIVE);
+ verify(mockPackageManagerWrapper)
+ .deletePackageAsUser(eq(TEST_AIA_PACKAGE_NAME), any(), anyInt(),anyInt());
+ }
+
+ @Test
+ public void testDialogInterfaceOnClick_nonPositiveDoesNothing() {
+ controller.onClick(mock(DialogInterface.class), DialogInterface.BUTTON_NEGATIVE);
+ controller.onClick(mock(DialogInterface.class), DialogInterface.BUTTON_NEUTRAL);
+ verifyZeroInteractions(mockPackageManagerWrapper);
+ }
+ @SuppressLint("ValidFragment")
+ private class TestFragment extends Fragment {
+
+ private Intent startActivityIntent;
+
+ public Intent getStartActivityIntent() {
+ return startActivityIntent;
+ }
+
+ @Override
+ public void startActivity(Intent intent) {
+ startActivityIntent = intent;
+ }
+ }
+}