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;
+        }
+    }
+}