diff --git a/res/layout/instant_app_buttons.xml b/res/layout/instant_app_buttons.xml
new file mode 100644
index 00000000000..b267361f5bc
--- /dev/null
+++ b/res/layout/instant_app_buttons.xml
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 4be267fec4a..a19444224d6 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -8491,6 +8491,9 @@
full
+
+ Clear app
+
Games
diff --git a/res/xml/installed_app_details_ia.xml b/res/xml/installed_app_details_ia.xml
index 3e06e392bee..50d183d37b6 100644
--- a/res/xml/installed_app_details_ia.xml
+++ b/res/xml/installed_app_details_ia.xml
@@ -23,11 +23,17 @@
android:selectable="false"
android:order="-10000"/>
+
+
+ android:order="-9998"/>
= 2) {
- // When we have multiple users, there is a separate menu
- // to uninstall for all users.
- enabled = false;
- }
- mUninstallButton.setText(R.string.uninstall_text);
+ enabled = initUnintsallButtonForUserApp();
}
// If this is a device admin, it can't be uninstalled or disabled.
// We do this here so the text of the button is still set correctly.
@@ -298,6 +293,22 @@ public class InstalledAppDetails extends AppInfoBase
}
}
+ @VisibleForTesting
+ boolean initUnintsallButtonForUserApp() {
+ boolean enabled = true;
+ if ((mPackageInfo.applicationInfo.flags & ApplicationInfo.FLAG_INSTALLED) == 0
+ && mUserManager.getUsers().size() >= 2) {
+ // When we have multiple users, there is a separate menu
+ // to uninstall for all users.
+ enabled = false;
+ } else if (AppUtils.isInstant(mPackageInfo.applicationInfo)) {
+ enabled = false;
+ mUninstallButton.setVisibility(View.GONE);
+ }
+ mUninstallButton.setText(R.string.uninstall_text);
+ return enabled;
+ }
+
/** Returns if the supplied package is device owner or profile owner of at least one user */
private boolean isProfileOrDeviceOwner(String packageName) {
List userInfos = mUserManager.getUsers();
@@ -455,12 +466,6 @@ public class InstalledAppDetails extends AppInfoBase
mForceStopButton.setEnabled(false);
}
- private Intent resolveIntent(Intent i) {
- ResolveInfo result = getContext().getPackageManager().resolveActivity(i, 0);
- return result != null ? new Intent(i.getAction())
- .setClassName(result.activityInfo.packageName, result.activityInfo.name) : null;
- }
-
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
menu.add(0, UNINSTALL_UPDATES, 0, R.string.app_factory_reset)
@@ -570,6 +575,8 @@ public class InstalledAppDetails extends AppInfoBase
} else if (PackageUtil.countPackageInUsers(mPm, mUserManager, mPackageName) < 2
&& (appEntry.info.flags & ApplicationInfo.FLAG_INSTALLED) != 0) {
showIt = false;
+ } else if (AppUtils.isInstant(appEntry.info)) {
+ showIt = false;
}
return showIt;
}
@@ -808,11 +815,15 @@ public class InstalledAppDetails extends AppInfoBase
}
}
- private void checkForceStop() {
+ @VisibleForTesting
+ void checkForceStop() {
if (mDpm.packageHasActiveAdmins(mPackageInfo.packageName)) {
// User can't force stop device admin.
Log.w(LOG_TAG, "User can't force stop device admin");
updateForceStopButton(false);
+ } else if (AppUtils.isInstant(mPackageInfo.applicationInfo)) {
+ updateForceStopButton(false);
+ mForceStopButton.setVisibility(View.GONE);
} else if ((mAppEntry.info.flags & ApplicationInfo.FLAG_STOPPED) == 0) {
// If the app isn't explicitly stopped, then always show the
// force stop button.
@@ -1072,6 +1083,7 @@ public class InstalledAppDetails extends AppInfoBase
}
addAppInstallerInfoPref(screen);
+ maybeAddInstantAppButtons();
}
private boolean isPotentialAppSource() {
@@ -1082,16 +1094,9 @@ public class InstalledAppDetails extends AppInfoBase
}
private void addAppInstallerInfoPref(PreferenceScreen screen) {
- String installerPackageName = null;
- try {
- installerPackageName =
- getContext().getPackageManager().getInstallerPackageName(mPackageName);
- } catch (IllegalArgumentException e) {
- Log.e(TAG, "Exception while retrieving the package installer of " + mPackageName, e);
- }
- if (installerPackageName == null) {
- return;
- }
+ String installerPackageName =
+ AppStoreUtil.getInstallerPackageName(getContext(), mPackageName);
+
final CharSequence installerLabel = Utils.getApplicationLabel(getContext(),
installerPackageName);
if (installerLabel == null) {
@@ -1104,18 +1109,31 @@ public class InstalledAppDetails extends AppInfoBase
pref.setTitle(R.string.app_install_details_title);
pref.setKey("app_info_store");
pref.setSummary(getString(R.string.app_install_details_summary, installerLabel));
- final Intent intent = new Intent(Intent.ACTION_SHOW_APP_INFO)
- .setPackage(installerPackageName);
- final Intent result = resolveIntent(intent);
- if (result != null) {
- result.putExtra(Intent.EXTRA_PACKAGE_NAME, mPackageName);
- pref.setIntent(result);
+
+ Intent intent =
+ AppStoreUtil.getAppStoreLink(getContext(), installerPackageName, mPackageName);
+ if (intent != null) {
+ pref.setIntent(intent);
} else {
pref.setEnabled(false);
}
category.addPreference(pref);
}
+ @VisibleForTesting
+ void maybeAddInstantAppButtons() {
+ if (AppUtils.isInstant(mPackageInfo.applicationInfo)) {
+ LayoutPreference buttons = (LayoutPreference) findPreference(KEY_INSTANT_APP_BUTTONS);
+ final Activity activity = getActivity();
+ FeatureFactory.getFactory(activity)
+ .getApplicationFeatureProvider(activity)
+ .newInstantAppButtonsController(this,
+ buttons.findViewById(R.id.instant_app_button_container))
+ .setPackageName(mPackageName)
+ .show();
+ }
+ }
+
private boolean hasPermission(String permission) {
if (mPackageInfo == null || mPackageInfo.requestedPermissions == null) {
return false;
diff --git a/src/com/android/settings/applications/instantapps/InstantAppButtonsController.java b/src/com/android/settings/applications/instantapps/InstantAppButtonsController.java
new file mode 100644
index 00000000000..aa7c418d198
--- /dev/null
+++ b/src/com/android/settings/applications/instantapps/InstantAppButtonsController.java
@@ -0,0 +1,72 @@
+/*
+ * 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 com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+import com.android.settings.R;
+import com.android.settings.applications.AppStoreUtil;
+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 {
+
+ private final Context mContext;
+ private final Fragment mFragment;
+ private final View mView;
+ private String mPackageName;
+
+ public InstantAppButtonsController(Context context, Fragment fragment, View view) {
+ mContext = context;
+ mFragment = fragment;
+ mView = view;
+ }
+
+ public InstantAppButtonsController setPackageName(String packageName) {
+ mPackageName = packageName;
+ return this;
+ }
+
+ 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) {
+ installButton.setEnabled(true);
+ installButton.setOnClickListener(v -> mFragment.startActivity(installIntent));
+ }
+ 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() {
+ bindButtons();
+ mView.setVisibility(View.VISIBLE);
+ return this;
+ }
+}
diff --git a/tests/robotests/src/com/android/settings/applications/InstalledAppDetailsTest.java b/tests/robotests/src/com/android/settings/applications/InstalledAppDetailsTest.java
index b0cd8d5b9ef..b457422bb1d 100644
--- a/tests/robotests/src/com/android/settings/applications/InstalledAppDetailsTest.java
+++ b/tests/robotests/src/com/android/settings/applications/InstalledAppDetailsTest.java
@@ -23,11 +23,18 @@ import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.os.UserManager;
+import android.support.v7.preference.Preference;
+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.instantapps.InstantAppButtonsController;
+import com.android.settings.testutils.FakeFeatureFactory;
+import com.android.settingslib.applications.AppUtils;
import com.android.settingslib.applications.ApplicationsState.AppEntry;
+import com.android.settingslib.applications.instantapps.InstantAppDataProvider;
import com.android.settingslib.applications.StorageStatsSource.AppStorageStats;
import org.junit.Before;
@@ -41,6 +48,7 @@ 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.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
@@ -51,6 +59,11 @@ import static org.mockito.Mockito.when;
@RunWith(SettingsRobolectricTestRunner.class)
@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
public final class InstalledAppDetailsTest {
+ @Mock(answer = Answers.RETURNS_DEEP_STUBS)
+ private Context mContext;
+
+ @Mock
+ ApplicationFeatureProvider mApplicationFeatureProvider;
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
private UserManager mUserManager;
@@ -65,6 +78,10 @@ public final class InstalledAppDetailsTest {
public void setUp() {
MockitoAnnotations.initMocks(this);
mAppDetail = new InstalledAppDetails();
+
+ // Default to not considering any apps to be instant (individual tests can override this).
+ ReflectionHelpers.setStaticField(AppUtils.class, "sInstantAppDataProvider",
+ (InstantAppDataProvider) (i -> false));
}
@Test
@@ -161,4 +178,119 @@ public final class InstalledAppDetailsTest {
assertThat(mAppDetail.ensurePackageInfoAvailable(mActivity)).isTrue();
verify(mActivity, never()).finishAndRemoveTask();
}
+
+ // Tests that we don't show the "uninstall for all users" button for instant apps.
+ @Test
+ public void instantApps_noUninstallForAllButton() {
+ // Make this app appear to be instant.
+ ReflectionHelpers.setStaticField(AppUtils.class, "sInstantAppDataProvider",
+ (InstantAppDataProvider) (i -> true));
+ when(mDevicePolicyManager.packageHasActiveAdmins(anyString())).thenReturn(false);
+ when(mUserManager.getUsers().size()).thenReturn(2);
+
+ final ApplicationInfo info = new ApplicationInfo();
+ info.enabled = true;
+ final AppEntry appEntry = mock(AppEntry.class);
+ appEntry.info = info;
+ final PackageInfo packageInfo = mock(PackageInfo.class);
+
+ ReflectionHelpers.setField(mAppDetail, "mDpm", mDevicePolicyManager);
+ ReflectionHelpers.setField(mAppDetail, "mUserManager", mUserManager);
+ ReflectionHelpers.setField(mAppDetail, "mPackageInfo", packageInfo);
+
+ assertThat(mAppDetail.shouldShowUninstallForAll(appEntry)).isFalse();
+ }
+
+ // Tests that we don't show the uninstall button for instant apps"
+ @Test
+ public void instantApps_noUninstallButton() {
+ // Make this app appear to be instant.
+ ReflectionHelpers.setStaticField(AppUtils.class, "sInstantAppDataProvider",
+ (InstantAppDataProvider) (i -> true));
+ final ApplicationInfo info = new ApplicationInfo();
+ info.flags = ApplicationInfo.FLAG_INSTALLED;
+ info.enabled = true;
+ final AppEntry appEntry = mock(AppEntry.class);
+ appEntry.info = info;
+ final PackageInfo packageInfo = mock(PackageInfo.class);
+ packageInfo.applicationInfo = info;
+ final Button uninstallButton = mock(Button.class);
+
+ ReflectionHelpers.setField(mAppDetail, "mUserManager", mUserManager);
+ ReflectionHelpers.setField(mAppDetail, "mAppEntry", appEntry);
+ ReflectionHelpers.setField(mAppDetail, "mPackageInfo", packageInfo);
+ ReflectionHelpers.setField(mAppDetail, "mUninstallButton", uninstallButton);
+
+ mAppDetail.initUnintsallButtonForUserApp();
+ verify(uninstallButton).setVisibility(View.GONE);
+ }
+
+ // Tests that we don't show the force stop button for instant apps (they aren't allowed to run
+ // when they aren't in the foreground).
+ @Test
+ public void instantApps_noForceStop() {
+ // Make this app appear to be instant.
+ ReflectionHelpers.setStaticField(AppUtils.class, "sInstantAppDataProvider",
+ (InstantAppDataProvider) (i -> true));
+ final PackageInfo packageInfo = mock(PackageInfo.class);
+ final AppEntry appEntry = mock(AppEntry.class);
+ final ApplicationInfo info = new ApplicationInfo();
+ appEntry.info = info;
+ final Button forceStopButton = mock(Button.class);
+
+ ReflectionHelpers.setField(mAppDetail, "mDpm", mDevicePolicyManager);
+ ReflectionHelpers.setField(mAppDetail, "mPackageInfo", packageInfo);
+ ReflectionHelpers.setField(mAppDetail, "mAppEntry", appEntry);
+ ReflectionHelpers.setField(mAppDetail, "mForceStopButton", forceStopButton);
+
+ mAppDetail.checkForceStop();
+ verify(forceStopButton).setVisibility(View.GONE);
+ }
+
+ // 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).
+ public static class InstalledAppDetailsWithMockInstantButtons extends InstalledAppDetails {
+ @Mock
+ private LayoutPreference mInstantButtons;
+
+ public InstalledAppDetailsWithMockInstantButtons() {
+ super();
+ MockitoAnnotations.initMocks(this);
+ }
+
+ @Override
+ public Preference findPreference(CharSequence key) {
+ if (key == "instant_app_buttons") {
+ return mInstantButtons;
+ }
+ return super.findPreference(key);
+ }
+ }
+
+ @Test
+ public void instantApps_instantSpecificButtons() {
+ // Make this app appear to be instant.
+ ReflectionHelpers.setStaticField(AppUtils.class, "sInstantAppDataProvider",
+ (InstantAppDataProvider) (i -> true));
+ final PackageInfo packageInfo = mock(PackageInfo.class);
+
+ final InstalledAppDetailsWithMockInstantButtons
+ fragment = new InstalledAppDetailsWithMockInstantButtons();
+ ReflectionHelpers.setField(fragment, "mPackageInfo", packageInfo);
+
+ final InstantAppButtonsController buttonsController =
+ mock(InstantAppButtonsController.class);
+ when(buttonsController.setPackageName(anyString())).thenReturn(buttonsController);
+
+ FakeFeatureFactory.setupForTest(mContext);
+ FakeFeatureFactory factory =
+ (FakeFeatureFactory) FakeFeatureFactory.getFactory(mContext);
+ when(factory.applicationFeatureProvider.newInstantAppButtonsController(any(),
+ any())).thenReturn(buttonsController);
+
+ fragment.maybeAddInstantAppButtons();
+ verify(buttonsController).setPackageName(anyString());
+ verify(buttonsController).show();
+ }
}