From abbb5c4dc36e226a6e8d9a98cc53252b78540281 Mon Sep 17 00:00:00 2001 From: Sudheer Shanka Date: Sun, 27 Aug 2023 04:45:45 +0000 Subject: [PATCH] Add controls in dev options to quarantine apps. Bug: 297934650 Test: m -j RunSettingsRoboTests Test: atest SettingsUnitTests Change-Id: Ic0f35b370c04dd4ed3baaf3610b46ff1b37a0463 --- Android.bp | 1 + res/values/strings.xml | 3 + res/xml/development_settings.xml | 6 + res/xml/quarantined_apps.xml | 25 ++ .../settings/development/quarantine/OWNERS | 4 + .../quarantine/QuarantinedAppPreference.java | 76 ++++++ .../quarantine/QuarantinedAppStateBridge.java | 61 +++++ .../quarantine/QuarantinedAppsFragment.java | 172 ++++++++++++++ .../QuarantinedAppsPreferenceController.java | 33 +++ .../QuarantinedAppsScreenController.java | 221 ++++++++++++++++++ .../QuarantinedAppsScreenControllerTest.java | 95 ++++++++ tests/unit/Android.bp | 2 +- .../QuarantinedAppStateBridgeTest.java | 90 +++++++ ...arantinedAppsPreferenceControllerTest.java | 69 ++++++ 14 files changed, 857 insertions(+), 1 deletion(-) create mode 100644 res/xml/quarantined_apps.xml create mode 100644 src/com/android/settings/development/quarantine/OWNERS create mode 100644 src/com/android/settings/development/quarantine/QuarantinedAppPreference.java create mode 100644 src/com/android/settings/development/quarantine/QuarantinedAppStateBridge.java create mode 100644 src/com/android/settings/development/quarantine/QuarantinedAppsFragment.java create mode 100644 src/com/android/settings/development/quarantine/QuarantinedAppsPreferenceController.java create mode 100644 src/com/android/settings/development/quarantine/QuarantinedAppsScreenController.java create mode 100644 tests/robotests/src/com/android/settings/development/quarantine/QuarantinedAppsScreenControllerTest.java create mode 100644 tests/unit/src/com/android/settings/development/quarantine/QuarantinedAppStateBridgeTest.java create mode 100644 tests/unit/src/com/android/settings/development/quarantine/QuarantinedAppsPreferenceControllerTest.java diff --git a/Android.bp b/Android.bp index ca4971e2dcd..0271b2f10fb 100644 --- a/Android.bp +++ b/Android.bp @@ -112,6 +112,7 @@ android_library { "androidx.room_room-runtime", "SystemUIUnfoldLib", "aconfig_settings_flags_lib", + "android.content.pm.flags-aconfig-java", ], plugins: [ diff --git a/res/values/strings.xml b/res/values/strings.xml index e4b68d032e1..7de1068f353 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -12434,4 +12434,7 @@ Grammatical gender Select Grammatical gender + + + Quarantined Apps diff --git a/res/xml/development_settings.xml b/res/xml/development_settings.xml index ab1ee418223..4135750b397 100644 --- a/res/xml/development_settings.xml +++ b/res/xml/development_settings.xml @@ -736,6 +736,12 @@ android:title="@string/enable_notes_role_title" android:summary="@string/enable_notes_role_summary" /> + + + + + + \ No newline at end of file diff --git a/src/com/android/settings/development/quarantine/OWNERS b/src/com/android/settings/development/quarantine/OWNERS new file mode 100644 index 00000000000..d4de31a9042 --- /dev/null +++ b/src/com/android/settings/development/quarantine/OWNERS @@ -0,0 +1,4 @@ +# Bug component: 316234 + +sudheersai@google.com +yamasani@google.com \ No newline at end of file diff --git a/src/com/android/settings/development/quarantine/QuarantinedAppPreference.java b/src/com/android/settings/development/quarantine/QuarantinedAppPreference.java new file mode 100644 index 00000000000..6ad1f86fbcf --- /dev/null +++ b/src/com/android/settings/development/quarantine/QuarantinedAppPreference.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2023 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.development.quarantine; + +import android.content.Context; +import android.graphics.drawable.Drawable; + +import androidx.preference.PreferenceViewHolder; + +import com.android.settings.R; +import com.android.settingslib.applications.AppUtils; +import com.android.settingslib.applications.ApplicationsState.AppEntry; +import com.android.settingslib.utils.ThreadUtils; +import com.android.settingslib.widget.AppSwitchPreference; + +public class QuarantinedAppPreference extends AppSwitchPreference { + private final AppEntry mEntry; + private Drawable mCacheIcon; + + public QuarantinedAppPreference(Context context, AppEntry entry) { + super(context); + mEntry = entry; + mCacheIcon = AppUtils.getIconFromCache(mEntry); + + mEntry.ensureLabel(context); + setKey(generateKey(mEntry)); + if (mCacheIcon != null) { + setIcon(mCacheIcon); + } else { + setIcon(R.drawable.empty_icon); + } + updateState(); + } + + static String generateKey(AppEntry entry) { + return entry.info.packageName + "|" + entry.info.uid; + } + + public AppEntry getEntry() { + return mEntry; + } + + @Override + public void onBindViewHolder(PreferenceViewHolder holder) { + if (mCacheIcon == null) { + ThreadUtils.postOnBackgroundThread(() -> { + final Drawable icon = AppUtils.getIcon(getContext(), mEntry); + ThreadUtils.postOnMainThread(() -> { + setIcon(icon); + mCacheIcon = icon; + }); + }); + } + super.onBindViewHolder(holder); + } + + void updateState() { + setTitle(mEntry.label); + setChecked((boolean) mEntry.extraInfo); + notifyChanged(); + } +} diff --git a/src/com/android/settings/development/quarantine/QuarantinedAppStateBridge.java b/src/com/android/settings/development/quarantine/QuarantinedAppStateBridge.java new file mode 100644 index 00000000000..206b922a23f --- /dev/null +++ b/src/com/android/settings/development/quarantine/QuarantinedAppStateBridge.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2023 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.development.quarantine; + +import android.content.Context; +import android.content.pm.PackageManager; +import android.os.UserHandle; + +import com.android.settings.applications.AppStateBaseBridge; +import com.android.settingslib.applications.ApplicationsState; +import com.android.settingslib.applications.ApplicationsState.AppEntry; + +import java.util.ArrayList; + +public class QuarantinedAppStateBridge extends AppStateBaseBridge { + private Context mContext; + + public QuarantinedAppStateBridge(Context context, + ApplicationsState appState, Callback callback) { + super(appState, callback); + mContext = context; + } + + @Override + protected void loadAllExtraInfo() { + final ArrayList apps = mAppSession.getAllApps(); + for (int i = 0; i < apps.size(); i++) { + final AppEntry app = apps.get(i); + updateExtraInfo(app, app.info.packageName, app.info.uid); + } + } + + @Override + protected void updateExtraInfo(AppEntry app, String pkg, int uid) { + app.extraInfo = isPackageQuarantined(pkg, uid); + } + + private boolean isPackageQuarantined(String pkg, int uid) { + final PackageManager pm = mContext.createContextAsUser( + UserHandle.getUserHandleForUid(uid), 0).getPackageManager(); + try { + return pm.isPackageQuarantined(pkg); + } catch (PackageManager.NameNotFoundException e) { + return false; + } + } +} diff --git a/src/com/android/settings/development/quarantine/QuarantinedAppsFragment.java b/src/com/android/settings/development/quarantine/QuarantinedAppsFragment.java new file mode 100644 index 00000000000..985e96287de --- /dev/null +++ b/src/com/android/settings/development/quarantine/QuarantinedAppsFragment.java @@ -0,0 +1,172 @@ +/* + * Copyright (C) 2023 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.development.quarantine; + +import static android.view.MenuItem.SHOW_AS_ACTION_ALWAYS; +import static android.view.MenuItem.SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW; + +import android.app.settings.SettingsEnums; +import android.os.Bundle; +import android.text.TextUtils; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.widget.SearchView; + +import com.android.settings.R; +import com.android.settings.dashboard.DashboardFragment; +import com.android.settings.search.BaseSearchIndexProvider; +import com.android.settingslib.applications.AppIconCacheManager; +import com.android.settingslib.applications.ApplicationsState; +import com.android.settingslib.applications.ApplicationsState.AppFilter; +import com.android.settingslib.search.SearchIndexable; + +import com.google.android.material.appbar.AppBarLayout; + +// TODO: b/297934650 - Update this to use SPA framework +@SearchIndexable +public class QuarantinedAppsFragment extends DashboardFragment implements + SearchView.OnQueryTextListener, SearchView.OnCloseListener, + MenuItem.OnActionExpandListener { + private static final String TAG = "QuarantinedApps"; + + private static final int MENU_SEARCH_APPS = Menu.FIRST + 42; + private static final int MENU_SHOW_SYSTEM = Menu.FIRST + 43; + private static final String EXTRA_SHOW_SYSTEM = "show_system"; + + private boolean mShowSystem; + private SearchView mSearchView; + private String mCurQuery; + + @Override + public void onCreate(Bundle icicle) { + super.onCreate(icicle); + mShowSystem = icicle != null && icicle.getBoolean(EXTRA_SHOW_SYSTEM); + use(QuarantinedAppsScreenController.class).setFilter(mCustomFilter); + use(QuarantinedAppsScreenController.class).setSession(getSettingsLifecycle()); + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + mSearchView = new SearchView(getContext()); + mSearchView.setOnQueryTextListener(this); + mSearchView.setOnCloseListener(this); + mSearchView.setIconifiedByDefault(true); + + menu.add(Menu.NONE, MENU_SEARCH_APPS, Menu.NONE, R.string.search_settings) + .setIcon(R.drawable.ic_find_in_page_24px) + .setActionView(mSearchView) + .setOnActionExpandListener(this) + .setShowAsAction(SHOW_AS_ACTION_ALWAYS | SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW); + menu.add(Menu.NONE, MENU_SHOW_SYSTEM, Menu.NONE, + mShowSystem ? R.string.menu_hide_system : R.string.menu_show_system); + super.onCreateOptionsMenu(menu, inflater); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == MENU_SHOW_SYSTEM) { + mShowSystem = !mShowSystem; + item.setTitle(mShowSystem ? R.string.menu_hide_system : R.string.menu_show_system); + use(QuarantinedAppsScreenController.class).setFilter(mCustomFilter); + use(QuarantinedAppsScreenController.class).rebuild(); + return true; + } + return super.onOptionsItemSelected(item); + } + + @Override + public boolean onQueryTextChange(String newText) { + mCurQuery = !TextUtils.isEmpty(newText) ? newText : null; + use(QuarantinedAppsScreenController.class).rebuild(); + return true; + } + + @Override + public boolean onQueryTextSubmit(String query) { + // Don't care about this. + return true; + } + + @Override + public boolean onClose() { + if (!TextUtils.isEmpty(mSearchView.getQuery())) { + mSearchView.setQuery(null, true); + } + return true; + } + + public final AppFilter mCustomFilter = new AppFilter() { + @Override + public void init() { + } + + @Override + public boolean filterApp(ApplicationsState.AppEntry entry) { + final AppFilter defaultFilter = mShowSystem ? ApplicationsState.FILTER_ALL_ENABLED + : ApplicationsState.FILTER_DOWNLOADED_AND_LAUNCHER; + return defaultFilter.filterApp(entry) && (mCurQuery == null + || entry.label.toLowerCase().contains(mCurQuery.toLowerCase())); + } + }; + + @Override + public boolean onMenuItemActionExpand(MenuItem item) { + final AppBarLayout mAppBarLayout = getActivity().findViewById(R.id.app_bar); + // To prevent a large space on tool bar. + mAppBarLayout.setExpanded(false /*expanded*/, false /*animate*/); + return true; + } + + @Override + public boolean onMenuItemActionCollapse(MenuItem item) { + final AppBarLayout mAppBarLayout = getActivity().findViewById(R.id.app_bar); + // To prevent a large space on tool bar. + mAppBarLayout.setExpanded(false /*expanded*/, false /*animate*/); + return true; + } + + @Override + public int getPreferenceScreenResId() { + return R.xml.quarantined_apps; + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putBoolean(EXTRA_SHOW_SYSTEM, mShowSystem); + } + + @Override + protected String getLogTag() { + return TAG; + } + + @Override + public int getMetricsCategory() { + return SettingsEnums.QUARANTINED_APPS_DEV_CONTROL; + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + AppIconCacheManager.getInstance().release(); + } + + public static final BaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = + new BaseSearchIndexProvider(R.xml.quarantined_apps); +} diff --git a/src/com/android/settings/development/quarantine/QuarantinedAppsPreferenceController.java b/src/com/android/settings/development/quarantine/QuarantinedAppsPreferenceController.java new file mode 100644 index 00000000000..de3b18bb3a6 --- /dev/null +++ b/src/com/android/settings/development/quarantine/QuarantinedAppsPreferenceController.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2023 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.development.quarantine; + +import android.content.Context; +import android.content.pm.Flags; + +import com.android.settings.core.BasePreferenceController; + +public class QuarantinedAppsPreferenceController extends BasePreferenceController { + public QuarantinedAppsPreferenceController(Context context, String preferenceKey) { + super(context, preferenceKey); + } + + @Override + public int getAvailabilityStatus() { + return Flags.quarantinedEnabled() ? AVAILABLE : CONDITIONALLY_UNAVAILABLE; + } +} diff --git a/src/com/android/settings/development/quarantine/QuarantinedAppsScreenController.java b/src/com/android/settings/development/quarantine/QuarantinedAppsScreenController.java new file mode 100644 index 00000000000..e5373fd0bee --- /dev/null +++ b/src/com/android/settings/development/quarantine/QuarantinedAppsScreenController.java @@ -0,0 +1,221 @@ +/* + * Copyright (C) 2023 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.development.quarantine; + +import android.app.Application; +import android.content.Context; +import android.content.pm.PackageManager; +import android.os.UserHandle; + +import androidx.annotation.VisibleForTesting; +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; + +import com.android.settings.R; +import com.android.settings.applications.AppStateBaseBridge; +import com.android.settings.core.BasePreferenceController; +import com.android.settingslib.applications.AppUtils; +import com.android.settingslib.applications.ApplicationsState; +import com.android.settingslib.applications.ApplicationsState.AppEntry; +import com.android.settingslib.applications.ApplicationsState.AppFilter; +import com.android.settingslib.core.lifecycle.Lifecycle; +import com.android.settingslib.core.lifecycle.LifecycleObserver; +import com.android.settingslib.core.lifecycle.events.OnDestroy; +import com.android.settingslib.core.lifecycle.events.OnStart; +import com.android.settingslib.core.lifecycle.events.OnStop; + +import java.util.ArrayList; +import java.util.Set; +import java.util.TreeSet; + +public class QuarantinedAppsScreenController extends BasePreferenceController implements + LifecycleObserver, OnStart, OnStop, OnDestroy, + ApplicationsState.Callbacks, Preference.OnPreferenceChangeListener, + AppStateBaseBridge.Callback { + private final ApplicationsState mApplicationsState; + private final QuarantinedAppStateBridge mQuarantinedAppStateBridge; + private ApplicationsState.Session mSession; + private PreferenceScreen mScreen; + private AppFilter mFilter; + private boolean mExtraLoaded; + + public QuarantinedAppsScreenController(Context context, String preferenceKey) { + super(context, preferenceKey); + mApplicationsState = ApplicationsState.getInstance( + (Application) context.getApplicationContext()); + mQuarantinedAppStateBridge = new QuarantinedAppStateBridge(context, + mApplicationsState, this); + } + + @Override + public void onStart() { + mQuarantinedAppStateBridge.resume(true /* forceLoadAllApps */); + } + + @Override + public void onStop() { + mQuarantinedAppStateBridge.pause(); + } + + @Override + public void onDestroy() { + mQuarantinedAppStateBridge.release(); + } + + @Override + public void displayPreference(PreferenceScreen screen) { + super.displayPreference(screen); + mScreen = screen; + } + + public void setFilter(AppFilter filter) { + mFilter = filter; + } + + public void setSession(Lifecycle lifecycle) { + mSession = mApplicationsState.newSession(this, lifecycle); + } + + @Override + public void onExtraInfoUpdated() { + mExtraLoaded = true; + rebuild(); + } + + public void rebuild() { + if (!mExtraLoaded || mSession == null) { + return; + } + + final ArrayList apps = mSession.rebuild(mFilter, + ApplicationsState.ALPHA_COMPARATOR); + if (apps != null) { + onRebuildComplete(apps); + } + } + + @Override + public void onRebuildComplete(ArrayList apps) { + if (apps == null) { + return; + } + + // Preload top visible icons of app list. + AppUtils.preloadTopIcons(mContext, apps, + mContext.getResources().getInteger(R.integer.config_num_visible_app_icons)); + + // Create apps key set for removing useless preferences + final Set appsKeySet = new TreeSet<>(); + // Add or update preferences + final int count = apps.size(); + for (int i = 0; i < count; i++) { + final AppEntry entry = apps.get(i); + if (!shouldAddPreference(entry)) { + continue; + } + final String prefkey = QuarantinedAppPreference.generateKey(entry); + appsKeySet.add(prefkey); + QuarantinedAppPreference preference = mScreen.findPreference(prefkey); + if (preference == null) { + preference = new QuarantinedAppPreference(mScreen.getContext(), entry); + preference.setOnPreferenceChangeListener(this); + mScreen.addPreference(preference); + } else { + preference.updateState(); + } + preference.setOrder(i); + } + + // Remove useless preferences + removeUselessPrefs(appsKeySet); + } + + private void removeUselessPrefs(final Set appsKeySet) { + final int prefCount = mScreen.getPreferenceCount(); + String prefKey; + if (prefCount > 0) { + for (int i = prefCount - 1; i >= 0; i--) { + final Preference pref = mScreen.getPreference(i); + prefKey = pref.getKey(); + if (!appsKeySet.isEmpty() && appsKeySet.contains(prefKey)) { + continue; + } + mScreen.removePreference(pref); + } + } + } + + @VisibleForTesting + static boolean shouldAddPreference(AppEntry app) { + return app != null && UserHandle.isApp(app.info.uid); + } + + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + if (preference instanceof QuarantinedAppPreference) { + final QuarantinedAppPreference quarantinedPreference = + (QuarantinedAppPreference) preference; + final boolean quarantined = newValue == Boolean.TRUE; + setPackageQuarantined(quarantinedPreference.getEntry().info.packageName, + quarantinedPreference.getEntry().info.uid, quarantined); + quarantinedPreference.getEntry().extraInfo = quarantined; + return true; + } + return false; + } + + private void setPackageQuarantined(String pkg, int uid, boolean quarantined) { + final PackageManager pm = mContext.createContextAsUser( + UserHandle.getUserHandleForUid(uid), 0).getPackageManager(); + pm.setPackagesSuspended(new String[] {pkg}, quarantined, null /* appExtras */, + null /* launcherExtras */, null /* dialogInfo */, + PackageManager.FLAG_SUSPEND_QUARANTINED); + } + + @Override + public int getAvailabilityStatus() { + return AVAILABLE; + } + + @Override + public void onRunningStateChanged(boolean running) { + } + + @Override + public void onPackageListChanged() { + } + + @Override + public void onPackageIconChanged() { + } + + @Override + public void onPackageSizeChanged(String packageName) { + } + + @Override + public void onAllSizesComputed() { + } + + @Override + public void onLauncherInfoChanged() { + } + + @Override + public void onLoadEntriesCompleted() { + } +} diff --git a/tests/robotests/src/com/android/settings/development/quarantine/QuarantinedAppsScreenControllerTest.java b/tests/robotests/src/com/android/settings/development/quarantine/QuarantinedAppsScreenControllerTest.java new file mode 100644 index 00000000000..32ad0ad8ab3 --- /dev/null +++ b/tests/robotests/src/com/android/settings/development/quarantine/QuarantinedAppsScreenControllerTest.java @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2023 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.development.quarantine; + +import static org.mockito.AdditionalMatchers.aryEq; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.os.UserHandle; + +import androidx.test.core.app.ApplicationProvider; + +import com.android.settingslib.applications.ApplicationsState.AppEntry; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; + +@RunWith(RobolectricTestRunner.class) +public class QuarantinedAppsScreenControllerTest { + private static final String PREF_KEY = "quarantined_apps_screen"; + private static final String TEST_PACKAGE = "com.example.test.pkg"; + private static final int TEST_APP_ID = 1234; + private static final int TEST_USER_ID = 10; + + private Context mContext; + private QuarantinedAppsScreenController mController; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mContext = spy(ApplicationProvider.getApplicationContext()); + mController = new QuarantinedAppsScreenController(mContext, PREF_KEY); + } + + @Test + public void testOnPreferenceChange() { + final Context userContext = mock(Context.class); + doReturn(userContext).when(mContext).createContextAsUser( + eq(UserHandle.of(TEST_USER_ID)), anyInt()); + final PackageManager packageManager = mock(PackageManager.class); + doReturn(packageManager).when(userContext).getPackageManager(); + + final AppEntry entry = createAppEntry(TEST_PACKAGE, TEST_APP_ID, TEST_USER_ID); + final QuarantinedAppPreference preference = new QuarantinedAppPreference(mContext, entry); + + mController.onPreferenceChange(preference, true); + verify(packageManager).setPackagesSuspended(aryEq(new String[] {TEST_PACKAGE}), eq(true), + any(), any(), any(), + eq(PackageManager.FLAG_SUSPEND_QUARANTINED)); + + mController.onPreferenceChange(preference, false); + verify(packageManager).setPackagesSuspended(aryEq(new String[] {TEST_PACKAGE}), eq(false), + any(), any(), any(), + eq(PackageManager.FLAG_SUSPEND_QUARANTINED)); + } + + private AppEntry createAppEntry(String packageName, int appId, int userId) { + final AppEntry entry = mock(AppEntry.class); + entry.info = createApplicationInfo(packageName, appId, userId); + entry.extraInfo = false; + return entry; + } + + private ApplicationInfo createApplicationInfo(String packageName, int appId, int userId) { + final ApplicationInfo info = new ApplicationInfo(); + info.packageName = packageName; + info.uid = UserHandle.getUid(userId, appId); + return info; + } +} diff --git a/tests/unit/Android.bp b/tests/unit/Android.bp index c0d63e1012b..196b80914c7 100644 --- a/tests/unit/Android.bp +++ b/tests/unit/Android.bp @@ -27,7 +27,7 @@ android_test { "platform-test-annotations", "truth-prebuilt", "kotlinx_coroutines_test", - "flag-junit-base", + "flag-junit", // Don't add SettingsLib libraries here - you can use them directly as they are in the // instrumented Settings app. ], diff --git a/tests/unit/src/com/android/settings/development/quarantine/QuarantinedAppStateBridgeTest.java b/tests/unit/src/com/android/settings/development/quarantine/QuarantinedAppStateBridgeTest.java new file mode 100644 index 00000000000..707d2b9f215 --- /dev/null +++ b/tests/unit/src/com/android/settings/development/quarantine/QuarantinedAppStateBridgeTest.java @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2023 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.development.quarantine; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.content.pm.PackageManager; +import android.os.UserHandle; + +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import com.android.settingslib.applications.ApplicationsState.AppEntry; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@RunWith(AndroidJUnit4.class) +public class QuarantinedAppStateBridgeTest { + private static final String TEST_PACKAGE = "com.example.test.pkg"; + private static final int TEST_APP_ID = 1234; + private static final int TEST_USER_ID_1 = 0; + private static final int TEST_USER_ID_2 = 10; + + @Mock + private Context mContext; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + } + + @Test + public void updateExtraInfo_packageQuarantined() throws Exception { + setPackageQuarantined(TEST_PACKAGE, TEST_USER_ID_1, false); + setPackageQuarantined(TEST_PACKAGE, TEST_USER_ID_2, true); + + final QuarantinedAppStateBridge bridge = + new QuarantinedAppStateBridge(mContext, null, null); + final AppEntry entry = mock(AppEntry.class); + + bridge.updateExtraInfo(entry, TEST_PACKAGE, UserHandle.getUid(TEST_USER_ID_2, TEST_APP_ID)); + assertThat(entry.extraInfo).isEqualTo(true); + } + + @Test + public void updateExtraInfo_packageNotQuarantined() throws Exception { + setPackageQuarantined(TEST_PACKAGE, TEST_USER_ID_1, false); + setPackageQuarantined(TEST_PACKAGE, TEST_USER_ID_2, false); + + final QuarantinedAppStateBridge bridge = + new QuarantinedAppStateBridge(mContext, null, null); + final AppEntry entry = mock(AppEntry.class); + + bridge.updateExtraInfo(entry, TEST_PACKAGE, UserHandle.getUid(TEST_USER_ID_2, TEST_APP_ID)); + assertThat(entry.extraInfo).isEqualTo(false); + } + + private void setPackageQuarantined(String packageName, int userId, boolean quarantined) + throws Exception { + final Context userContext = mock(Context.class); + when(mContext.createContextAsUser(eq(UserHandle.of(userId)), anyInt())) + .thenReturn(userContext); + final PackageManager packageManager = mock(PackageManager.class); + when(userContext.getPackageManager()).thenReturn(packageManager); + when(packageManager.isPackageQuarantined(packageName)).thenReturn(quarantined); + } +} diff --git a/tests/unit/src/com/android/settings/development/quarantine/QuarantinedAppsPreferenceControllerTest.java b/tests/unit/src/com/android/settings/development/quarantine/QuarantinedAppsPreferenceControllerTest.java new file mode 100644 index 00000000000..33e43929715 --- /dev/null +++ b/tests/unit/src/com/android/settings/development/quarantine/QuarantinedAppsPreferenceControllerTest.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2023 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.development.quarantine; + +import static com.android.settings.core.BasePreferenceController.AVAILABLE; +import static com.android.settings.core.BasePreferenceController.CONDITIONALLY_UNAVAILABLE; + +import static org.junit.Assert.assertEquals; + +import android.content.Context; +import android.content.pm.Flags; +import android.platform.test.annotations.RequiresFlagsDisabled; +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; + +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@RunWith(AndroidJUnit4.class) +public class QuarantinedAppsPreferenceControllerTest { + + private static final String PREF_KEY = "quarantined_apps"; + + @Rule + public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); + + @Mock + private Context mContext; + private QuarantinedAppsPreferenceController mController; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mController = new QuarantinedAppsPreferenceController(mContext, PREF_KEY); + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_QUARANTINED_ENABLED) + public void testAvailabilityStatus_flagEnabled() { + assertEquals(mController.getAvailabilityStatus(), AVAILABLE); + } + + @Test + @RequiresFlagsDisabled(Flags.FLAG_QUARANTINED_ENABLED) + public void testAvailabilityStatus_flagDisabled() { + assertEquals(mController.getAvailabilityStatus(), CONDITIONALLY_UNAVAILABLE); + } +}