OSDN Git Service

Changes to installed app details screen for instant apps
authorAntony Sargent <asargent@google.com>
Wed, 8 Mar 2017 22:34:10 +0000 (14:34 -0800)
committerAntony Sargent <asargent@google.com>
Mon, 27 Mar 2017 21:34:56 +0000 (14:34 -0700)
Bug: 35098444
Test: make RunSettingsRoboTests

This implements the following changes to the app details screen:

-Suppresses the "Uninstall" and "Force Stop" buttons
-Adds "Install app" and "Clear app" buttons
-Adds a mechanism for showing a link to the store's app details
 page that can be different from the default for installed apps.

Change-Id: Icea83f7d1fde62d4101cb0c8a6d03849f6c56bca

res/layout/instant_app_buttons.xml [new file with mode: 0644]
res/values/strings.xml
res/xml/installed_app_details_ia.xml
src/com/android/settings/applications/AppStoreUtil.java [new file with mode: 0644]
src/com/android/settings/applications/ApplicationFeatureProvider.java
src/com/android/settings/applications/ApplicationFeatureProviderImpl.java
src/com/android/settings/applications/InstalledAppDetails.java
src/com/android/settings/applications/instantapps/InstantAppButtonsController.java [new file with mode: 0644]
tests/robotests/src/com/android/settings/applications/InstalledAppDetailsTest.java

diff --git a/res/layout/instant_app_buttons.xml b/res/layout/instant_app_buttons.xml
new file mode 100644 (file)
index 0000000..b267361
--- /dev/null
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  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.
+  -->
+
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/instant_app_button_container"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:paddingTop="4dp"
+    android:paddingStart="8dp"
+    android:paddingEnd="8dp"
+    android:visibility="gone">
+    <Button
+        android:id="@+id/install"
+        style="@style/AppActionPrimaryButton"
+        android:enabled="false"
+        android:layout_width="0dp"
+        android:layout_weight="1"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center"
+        android:text="@string/install_text"/>
+    <Button
+        android:id="@+id/clear_data"
+        android:layout_width="0dp"
+        android:layout_weight="1"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center"
+        android:text="@string/clear_instant_app_data"/>
+</LinearLayout>
index 4be267f..a194442 100644 (file)
     <string name="storage_percent_full">full</string>
 
 
+    <!-- Label for button allow user to clear the data for an instant app -->
+    <string name="clear_instant_app_data">Clear app</string>
+
     <!-- Title of games app storage screen [CHAR LIMIT=30] -->
     <string name="game_storage_settings">Games</string>
 
index 3e06e39..50d183d 100644 (file)
         android:order="-10000"/>
 
     <com.android.settings.applications.LayoutPreference
+        android:key="instant_app_buttons"
+        android:layout="@layout/instant_app_buttons"
+        android:selectable="false"
+        android:order="-9999"/>
+
+    <com.android.settings.applications.LayoutPreference
       android:key="action_buttons"
       android:layout="@layout/app_action_buttons"
       android:selectable="false"
-      android:order="-9999"/>
+      android:order="-9998"/>
 
     <Preference
         android:key="notification_settings"
diff --git a/src/com/android/settings/applications/AppStoreUtil.java b/src/com/android/settings/applications/AppStoreUtil.java
new file mode 100644 (file)
index 0000000..f9b95b0
--- /dev/null
@@ -0,0 +1,70 @@
+/*
+ * 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;
+
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ResolveInfo;
+import android.net.Uri;
+import android.util.Log;
+
+// This class provides methods that help dealing with app stores.
+public class AppStoreUtil {
+    private static final String LOG_TAG = "AppStoreUtil";
+
+    private static Intent resolveIntent(Context context, Intent i) {
+        ResolveInfo result = context.getPackageManager().resolveActivity(i, 0);
+        return result != null ? new Intent(i.getAction())
+                .setClassName(result.activityInfo.packageName, result.activityInfo.name) : null;
+    }
+
+    // Returns the package name of the app which installed a given packageName, if one is
+    // available.
+    public static String getInstallerPackageName(Context context, String packageName) {
+        String installerPackageName = null;
+        try {
+            installerPackageName =
+                    context.getPackageManager().getInstallerPackageName(packageName);
+        } catch (IllegalArgumentException e) {
+            Log.e(LOG_TAG, "Exception while retrieving the package installer of " + packageName, e);
+        }
+        if (installerPackageName == null) {
+            return null;
+        }
+        return installerPackageName;
+    }
+
+    // Returns a link to the installer app store for a given package name.
+    public static Intent getAppStoreLink(Context context, String installerPackageName,
+            String packageName) {
+        Intent intent = new Intent(Intent.ACTION_SHOW_APP_INFO)
+                .setPackage(installerPackageName);
+        final Intent result = resolveIntent(context, intent);
+        if (result != null) {
+            result.putExtra(Intent.EXTRA_PACKAGE_NAME, packageName);
+            return result;
+        }
+        return null;
+    }
+
+    // Convenience method that looks up the installerPackageName for you.
+    public static Intent getAppStoreLink(Context context, String packageName) {
+      String installerPackageName = getInstallerPackageName(context, packageName);
+      return getAppStoreLink(context, installerPackageName, packageName);
+    }
+}
index 1855308..1816793 100644 (file)
@@ -16,6 +16,8 @@
 
 package com.android.settings.applications;
 
+import com.android.settings.applications.instantapps.InstantAppButtonsController;
+
 import android.app.Fragment;
 import android.content.Intent;
 import android.view.View;
@@ -30,6 +32,14 @@ public interface ApplicationFeatureProvider {
     AppHeaderController newAppHeaderController(Fragment fragment, View appHeader);
 
     /**
+     *
+     *  Returns a new {@link InstantAppButtonsController} instance for showing buttons
+     *  only relevant to instant apps.
+     */
+    InstantAppButtonsController newInstantAppButtonsController(Fragment fragment,
+            View view);
+
+    /**
      * Calculates the total number of apps installed on the device via policy across all users
      * and managed profiles.
      *
index ca19dca..7cc899b 100644 (file)
@@ -29,6 +29,7 @@ import android.os.UserManager;
 import android.util.ArraySet;
 import android.view.View;
 
+import com.android.settings.applications.instantapps.InstantAppButtonsController;
 import com.android.settings.enterprise.DevicePolicyManagerWrapper;
 
 import java.util.List;
@@ -57,6 +58,12 @@ public class ApplicationFeatureProviderImpl implements ApplicationFeatureProvide
     }
 
     @Override
+    public InstantAppButtonsController newInstantAppButtonsController(Fragment fragment,
+            View view) {
+        return new InstantAppButtonsController(mContext, fragment, view);
+    }
+
+    @Override
     public void calculateNumberOfPolicyInstalledApps(boolean async, NumberOfAppsCallback callback) {
         final AllUserPolicyInstalledAppCounter counter =
                 new AllUserPolicyInstalledAppCounter(mContext, mPm, callback);
index e00ba92..2ab007e 100755 (executable)
@@ -146,6 +146,7 @@ public class InstalledAppDetails extends AppInfoBase
     private static final int DLG_SPECIAL_DISABLE = DLG_BASE + 3;
 
     private static final String KEY_HEADER = "header_view";
+    private static final String KEY_INSTANT_APP_BUTTONS = "instant_app_buttons";
     private static final String KEY_ACTION_BUTTONS = "action_buttons";
     private static final String KEY_NOTIFICATION = "notification_settings";
     private static final String KEY_STORAGE = "storage_settings";
@@ -222,13 +223,7 @@ public class InstalledAppDetails extends AppInfoBase
         if (isBundled) {
             enabled = handleDisableable(mUninstallButton);
         } else {
-            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;
-            }
-            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<UserInfo> 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 (file)
index 0000000..aa7c418
--- /dev/null
@@ -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;
+    }
+}
index b0cd8d5..b457422 100644 (file)
@@ -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();
+    }
 }