diff --git a/res/values/strings.xml b/res/values/strings.xml index d2c8d5a0aba..c23d3995567 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -8607,8 +8607,11 @@ full - + Clear app + + Do you want to remove this instant app? Games diff --git a/src/com/android/settings/applications/ApplicationFeatureProvider.java b/src/com/android/settings/applications/ApplicationFeatureProvider.java index ef8cb232c6c..5e986db959c 100644 --- a/src/com/android/settings/applications/ApplicationFeatureProvider.java +++ b/src/com/android/settings/applications/ApplicationFeatureProvider.java @@ -37,7 +37,7 @@ public interface ApplicationFeatureProvider { * 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 124a8dea265..417185787dd 100644 --- a/src/com/android/settings/applications/ApplicationFeatureProviderImpl.java +++ b/src/com/android/settings/applications/ApplicationFeatureProviderImpl.java @@ -58,8 +58,8 @@ public class ApplicationFeatureProviderImpl implements ApplicationFeatureProvide @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 1fc5515e60c..47ea5b684f1 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.DefaultEmergencyPreferenceC 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 @@ public class InstalledAppDetails extends AppInfoBase protected ProcStatsData mStatsManager; protected ProcStatsPackageEntry mStats; + private InstantAppButtonsController mInstantAppButtonsController; + private AppStorageStats mLastResult; private boolean handleDisableable(Button button) { @@ -771,6 +774,9 @@ public class InstalledAppDetails extends AppInfoBase .setNegativeButton(R.string.dlg_cancel, null) .create(); } + if (mInstantAppButtonsController != null) { + return mInstantAppButtonsController.createDialog(id); + } return null; } @@ -1120,10 +1126,11 @@ public class InstalledAppDetails extends AppInfoBase 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 2be92ed3062..8dae417aecc 100644 --- a/src/com/android/settings/applications/PackageManagerWrapper.java +++ b/src/com/android/settings/applications/PackageManagerWrapper.java @@ -20,6 +20,7 @@ import android.content.ComponentName; 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 @@ public interface PackageManagerWrapper { */ 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 698c14cc679..a0d824fb732 100644 --- a/src/com/android/settings/applications/PackageManagerWrapperImpl.java +++ b/src/com/android/settings/applications/PackageManagerWrapperImpl.java @@ -20,6 +20,7 @@ import android.content.ComponentName; 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 @@ public class PackageManagerWrapperImpl implements PackageManagerWrapper { 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 aa7c418d198..16956dfbd3b 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 class InstantAppButtonsController { 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 -> 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()); } - clearDataButton.setOnClickListener(v -> { - FeatureFactory.getFactory(mContext).getMetricsFeatureProvider().action(mContext, - MetricsEvent.ACTION_SETTINGS_CLEAR_INSTANT_APP, mPackageName); - PackageManager pm = mContext.getPackageManager(); - pm.clearApplicationUserData(mPackageName, null); - }); } 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 209cdeb59a9..3d4b8403cbc 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 @@ package com.android.settings.applications; 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.annotation.Config; 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 @@ public final class InstalledAppDetailsTest { 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 @@ public final class InstalledAppDetailsTest { 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 00000000000..13040a2b1c6 --- /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; + } + } +}