--- /dev/null
+<?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>
<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>
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"
--- /dev/null
+/*
+ * 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);
+ }
+}
package com.android.settings.applications;
+import com.android.settings.applications.instantapps.InstantAppButtonsController;
+
import android.app.Fragment;
import android.content.Intent;
import android.view.View;
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.
*
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;
}
@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);
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";
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.
}
}
+ @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();
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)
} 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;
}
}
}
- 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.
}
addAppInstallerInfoPref(screen);
+ maybeAddInstantAppButtons();
}
private boolean isPotentialAppSource() {
}
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) {
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;
--- /dev/null
+/*
+ * 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;
+ }
+}
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;
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;
@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;
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
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();
+ }
}