Populate Enterprise Privacy Settings page - batch 1

This CL adds the first batch of items to the Privacy Settings page.
These are all the items that fall into the "What types of
information can your organization see?" category and do not require
deeper Framework changes. Further batches are to come in separate
CLs.

Test: make RunSettingsRoboTests
Bug: 32692748
Change-Id: I460093bc45ed0e5baab2a5cdf9833e654d436cc9
This commit is contained in:
Bartosz Fabianowski
2016-11-18 00:09:52 +01:00
parent d12b97f9e3
commit 62b96811c1
18 changed files with 415 additions and 24 deletions

View File

@@ -134,6 +134,7 @@
<declare-styleable name="DividerPreference">
<attr name="allowDividerAbove" format="boolean" />
<attr name="allowDividerBelow" format="boolean" />
<attr name="multiLine" format="boolean" />
</declare-styleable>
<!-- For GesturePreference -->

View File

@@ -7982,4 +7982,21 @@
<string name="enterprise_privacy_settings_title">Privacy</string>
<!-- Enterprise Privacy settings activity header, summarizing the powers that the admin has. [CHAR LIMIT=NONE] -->
<string name="enterprise_privacy_header">To provide access to your work data, your organization may change settings and install software on your device, which could cause some of your personal content to be visible to your admin. Contact your organization\'s admin for more details.</string>
<!-- Title for the 'What types of information can your organization see?' preference category. [CHAR LIMIT=60] -->
<string name="exposure_category">What types of information can your organization see?</string>
<!-- Title for the 'What changes affect what your organization can see?' preference category. [CHAR LIMIT=60] -->
<string name="exposure_changes_category">What changes affect what your organization can see?</string>
<!-- Title for the 'What actions may impact your access to this device?' preference category. [CHAR LIMIT=60] -->
<string name="device_access_category">What actions may impact your access to this device?</string>
<!-- Label explaining that the admin can see data associated with his/her work account. [CHAR LIMIT=NONE] -->
<string name="enterprise_data">Data associated with your work account, such as email and calendar</string>
<!-- Label explaining that the admin can see all apps installed on the device. [CHAR LIMIT=NONE] -->
<string name="number_installed_packages_default">List of all apps on your device</string>
<!-- Label explaining that the admin can see all apps installed on the device. [CHAR LIMIT=NONE] -->
<plurals name="number_installed_packages">
<item quantity="one">List of all <xliff:g id="count">%d</xliff:g> app on your device</item>
<item quantity="other">List of all <xliff:g id="count">%d</xliff:g> apps on your device</item>
</plurals>
<!-- Label explaining that the admin can see app usage statistics. [CHAR LIMIT=NONE] -->
<string name="usage_stats">Usage (time spent and amount of data used) of each app on your device</string>
</resources>

View File

@@ -23,4 +23,29 @@
<Preference android:key="enterprise_privacy_header"
android:summary="@string/enterprise_privacy_header"
android:selectable="false"/>
<PreferenceCategory android:title="@string/exposure_category">
<com.android.settings.DividerPreference
android:key="enterprise_data"
android:layout_height="wrap_content"
android:title="@string/enterprise_data"
settings:allowDividerBelow="true"
settings:multiLine="true"/>
<com.android.settings.DividerPreference
android:key="number_installed_packages"
android:title="@string/number_installed_packages_default"
settings:allowDividerBelow="true"
settings:multiLine="true"/>
<com.android.settings.DividerPreference
android:key="usage_stats"
android:title="@string/usage_stats"
settings:allowDividerBelow="true"
settings:multiLine="true"/>
</PreferenceCategory>
<PreferenceCategory android:title="@string/exposure_changes_category">
</PreferenceCategory>
<PreferenceCategory android:title="@string/device_access_category">
</PreferenceCategory>
</PreferenceScreen>

View File

@@ -19,11 +19,13 @@ import android.content.res.TypedArray;
import android.support.v7.preference.Preference;
import android.support.v7.preference.PreferenceViewHolder;
import android.util.AttributeSet;
import android.widget.TextView;
public class DividerPreference extends Preference {
private Boolean mAllowAbove;
private Boolean mAllowBelow;
private Boolean mMultiLine;
public DividerPreference(Context context, AttributeSet attrs) {
super(context, attrs);
@@ -34,6 +36,9 @@ public class DividerPreference extends Preference {
if (a.hasValue(R.styleable.DividerPreference_allowDividerBelow)) {
mAllowBelow = a.getBoolean(R.styleable.DividerPreference_allowDividerBelow, false);
}
if (a.hasValue(R.styleable.DividerPreference_multiLine)) {
mMultiLine = a.getBoolean(R.styleable.DividerPreference_multiLine, false);
}
}
public DividerPreference(Context context) {
@@ -59,5 +64,12 @@ public class DividerPreference extends Preference {
if (mAllowBelow != null) {
holder.setDividerAllowedBelow(mAllowBelow);
}
if (mMultiLine != null && mMultiLine) {
TextView textView = (TextView)holder.findViewById(android.R.id.title);
if (textView != null) {
textView.setSingleLine(false);
}
}
}
}

View File

@@ -25,5 +25,17 @@ public interface ApplicationFeatureProvider {
* Returns a new {@link AppHeaderController} instance to customize app header.
*/
AppHeaderController newAppHeaderController(Fragment fragment, View appHeader);
}
/**
* Asynchronously calculates the total number of apps installed on the device, across all users
* and managed profiles.
*/
void calculateNumberOfInstalledApps(NumberOfInstalledAppsCallback callback);
/**
* Callback that receives the total number of packages installed on the device.
*/
public interface NumberOfInstalledAppsCallback {
void onNumberOfInstalledAppsResult(int num);
}
}

View File

@@ -18,17 +18,50 @@ package com.android.settings.applications;
import android.app.Fragment;
import android.content.Context;
import android.content.pm.UserInfo;
import android.os.UserManager;
import android.view.View;
import java.util.List;
public class ApplicationFeatureProviderImpl implements ApplicationFeatureProvider {
private final Context mContext;
private final PackageManagerWrapper mPm;
private final UserManager mUm;
public ApplicationFeatureProviderImpl(Context context) {
public ApplicationFeatureProviderImpl(Context context, PackageManagerWrapper pm) {
mContext = context.getApplicationContext();
mPm = pm;
mUm = UserManager.get(mContext);
}
@Override
public AppHeaderController newAppHeaderController(Fragment fragment, View appHeader) {
return new AppHeaderController(mContext, fragment, appHeader);
}
@Override
public void calculateNumberOfInstalledApps(NumberOfInstalledAppsCallback callback) {
new AllUserInstalledAppCounter(callback).execute();
}
private class AllUserInstalledAppCounter extends InstalledAppCounter {
private NumberOfInstalledAppsCallback mCallback;
AllUserInstalledAppCounter(NumberOfInstalledAppsCallback callback) {
super(mContext, ApplicationFeatureProviderImpl.this.mPm);
mCallback = callback;
}
@Override
protected void onCountComplete(int num) {
mCallback.onNumberOfInstalledAppsResult(num);
}
@Override
protected List<UserInfo> getUsersToCount() {
return mUm.getUsers(true /* excludeDying */);
}
}
}

View File

@@ -36,6 +36,13 @@ public interface PackageManagerWrapper {
*/
List<ApplicationInfo> getInstalledApplicationsAsUser(int flags, int userId);
/**
* Calls {@code PackageManager.hasSystemFeature()}.
*
* @see android.content.pm.PackageManager.PackageManager#hasSystemFeature
*/
boolean hasSystemFeature(String name);
/**
* Calls {@code PackageManager.queryIntentActivitiesAsUser()}.
*

View File

@@ -35,6 +35,11 @@ public class PackageManagerWrapperImpl implements PackageManagerWrapper {
return mPm.getInstalledApplicationsAsUser(flags, userId);
}
@Override
public boolean hasSystemFeature(String name) {
return mPm.hasSystemFeature(name);
}
@Override
public List<ResolveInfo> queryIntentActivitiesAsUser(Intent intent, int flags, int userId) {
return mPm.queryIntentActivitiesAsUser(intent, flags, userId);

View File

@@ -18,10 +18,17 @@ package com.android.settings.enterprise;
import android.content.ComponentName;
// This interface replicates a subset of the android.app.admin.DevicePolicyManager (DPM). The
// interface exists so that we can use a thin wrapper around the DPM in production code and a mock
// in tests. We cannot directly mock or shadow the DPM, because some of the methods we rely on are
// newer than the API version supported by Robolectric.
/**
* This interface replicates a subset of the android.app.admin.DevicePolicyManager (DPM). The
* interface exists so that we can use a thin wrapper around the DPM in production code and a mock
* in tests. We cannot directly mock or shadow the DPM, because some of the methods we rely on are
* newer than the API version supported by Robolectric.
*/
public interface DevicePolicyManagerWrapper {
public ComponentName getDeviceOwnerComponentOnAnyUser();
/**
* Calls {@code DevicePolicyManager.getDeviceOwnerComponentOnAnyUser()}.
*
* @see android.app.admin.DevicePolicyManager#getDeviceOwnerComponentOnAnyUser
*/
ComponentName getDeviceOwnerComponentOnAnyUser();
}

View File

@@ -26,6 +26,7 @@ public class DevicePolicyManagerWrapperImpl implements DevicePolicyManagerWrappe
mDpm = dpm;
}
@Override
public ComponentName getDeviceOwnerComponentOnAnyUser() {
return mDpm.getDeviceOwnerComponentOnAnyUser();
}

View File

@@ -17,21 +17,23 @@
package com.android.settings.enterprise;
import android.content.pm.PackageManager;
import android.content.Context;
import com.android.settings.applications.PackageManagerWrapper;
public class EnterprisePrivacyFeatureProviderImpl implements EnterprisePrivacyFeatureProvider {
private final Context mContext;
private final DevicePolicyManagerWrapper mDpm;
private final PackageManagerWrapper mPm;
public EnterprisePrivacyFeatureProviderImpl(Context context, DevicePolicyManagerWrapper dpm) {
mContext = context.getApplicationContext();
public EnterprisePrivacyFeatureProviderImpl(DevicePolicyManagerWrapper dpm,
PackageManagerWrapper pm) {
mDpm = dpm;
mPm = pm;
}
@Override
public boolean hasDeviceOwner() {
if (!mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_DEVICE_ADMIN)) {
if (!mPm.hasSystemFeature(PackageManager.FEATURE_DEVICE_ADMIN)) {
return false;
}
return mDpm.getDeviceOwnerComponentOnAnyUser() != null;

View File

@@ -17,7 +17,6 @@
package com.android.settings.enterprise;
import android.content.Context;
import android.os.Bundle;
import android.provider.SearchIndexableResource;
import com.android.settings.R;
@@ -25,7 +24,9 @@ import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
import com.android.settings.core.PreferenceController;
import com.android.settings.dashboard.DashboardFragment;
import com.android.settings.search.BaseSearchIndexProvider;
import com.android.settings.search.Indexable.SearchIndexProvider;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@@ -55,7 +56,9 @@ public class EnterprisePrivacySettings extends DashboardFragment {
@Override
protected List<PreferenceController> getPreferenceControllers(Context context) {
return null;
final List controllers = new ArrayList<PreferenceController>();
controllers.add(new InstalledPackagesPreferenceController(context));
return controllers;
}
public static final SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER =

View File

@@ -0,0 +1,62 @@
/*
* Copyright (C) 2016 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.enterprise;
import android.content.Context;
import android.content.res.Resources;
import android.support.v7.preference.Preference;
import com.android.settings.R;
import com.android.settings.applications.ApplicationFeatureProvider;
import com.android.settings.core.PreferenceController;
import com.android.settings.overlay.FeatureFactory;
public class InstalledPackagesPreferenceController extends PreferenceController {
private static final String KEY_NUMBER_INSTALLED_PACKAGES = "number_installed_packages";
private final ApplicationFeatureProvider mFeatureProvider;
public InstalledPackagesPreferenceController(Context context) {
super(context);
mFeatureProvider = FeatureFactory.getFactory(context)
.getApplicationFeatureProvider(context);
}
@Override
public void updateState(Preference preference) {
mFeatureProvider.calculateNumberOfInstalledApps(
new ApplicationFeatureProvider.NumberOfInstalledAppsCallback() {
@Override
public void onNumberOfInstalledAppsResult(int num) {
preference.setTitle(mContext.getResources().getQuantityString(
R.plurals.number_installed_packages, num, num));
}
});
}
@Override
public boolean isAvailable() {
return true;
}
@Override
public boolean handlePreferenceTreeClick(Preference preference) {
return false;
}
@Override
public String getPreferenceKey() {
return KEY_NUMBER_INSTALLED_PACKAGES;
}
}

View File

@@ -22,6 +22,7 @@ import android.support.annotation.Keep;
import com.android.settings.applications.ApplicationFeatureProvider;
import com.android.settings.applications.ApplicationFeatureProviderImpl;
import com.android.settings.applications.PackageManagerWrapperImpl;
import com.android.settings.core.instrumentation.MetricsFeatureProvider;
import com.android.settings.core.instrumentation.MetricsFeatureProviderImpl;
import com.android.settings.dashboard.DashboardFeatureProvider;
@@ -77,7 +78,8 @@ public final class FeatureFactoryImpl extends FeatureFactory {
@Override
public ApplicationFeatureProvider getApplicationFeatureProvider(Context context) {
if (mApplicationFeatureProvider == null) {
mApplicationFeatureProvider = new ApplicationFeatureProviderImpl(context);
mApplicationFeatureProvider = new ApplicationFeatureProviderImpl(context,
new PackageManagerWrapperImpl(context.getPackageManager()));
}
return mApplicationFeatureProvider;
}
@@ -93,9 +95,10 @@ public final class FeatureFactoryImpl extends FeatureFactory {
@Override
public EnterprisePrivacyFeatureProvider getEnterprisePrivacyFeatureProvider(Context context) {
if (mEnterprisePrivacyFeatureProvider == null) {
mEnterprisePrivacyFeatureProvider = new EnterprisePrivacyFeatureProviderImpl(context,
mEnterprisePrivacyFeatureProvider = new EnterprisePrivacyFeatureProviderImpl(
new DevicePolicyManagerWrapperImpl((DevicePolicyManager) context
.getSystemService(Context.DEVICE_POLICY_SERVICE)));
.getSystemService(Context.DEVICE_POLICY_SERVICE)),
new PackageManagerWrapperImpl(context.getPackageManager()));
}
return mEnterprisePrivacyFeatureProvider;
}

View File

@@ -0,0 +1,99 @@
/*
* Copyright (C) 2016 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.pm.PackageManager;
import android.content.pm.UserInfo;
import android.os.UserManager;
import com.android.settings.SettingsRobolectricTestRunner;
import com.android.settings.TestConfig;
import com.android.settings.testutils.ApplicationTestUtils;
import com.android.settings.testutils.shadow.ShadowUserManager;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.annotation.Config;
import org.robolectric.shadows.ShadowApplication;
import java.util.Arrays;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.when;
import org.robolectric.shadows.ShadowApplication;
/**
* Tests for {@link ApplicationFeatureProviderImpl}.
*/
@RunWith(SettingsRobolectricTestRunner.class)
@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION,
shadows = {ShadowUserManager.class})
public final class ApplicationFeatureProviderImplTest {
private final int MAIN_USER_ID = 0;
private final int MANAGED_PROFILE_ID = 10;
private @Mock UserManager mUserManager;
private @Mock Context mContext;
private @Mock PackageManagerWrapper mPackageManager;
private ApplicationFeatureProvider mProvider;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
when(mContext.getApplicationContext()).thenReturn(mContext);
when(mContext.getSystemService(Context.USER_SERVICE)).thenReturn(mUserManager);
mProvider = new ApplicationFeatureProviderImpl(mContext, mPackageManager);
}
@Test
public void testCalculateNumberOfInstalledApps() {
final Integer[] numberOfInstalledApps = new Integer[1];
numberOfInstalledApps[0] = null;
when(mUserManager.getUsers(true)).thenReturn(Arrays.asList(
new UserInfo(MAIN_USER_ID, "main", UserInfo.FLAG_ADMIN),
new UserInfo(MANAGED_PROFILE_ID, "managed profile", 0)));
when(mPackageManager.getInstalledApplicationsAsUser(PackageManager.GET_DISABLED_COMPONENTS
| PackageManager.GET_DISABLED_UNTIL_USED_COMPONENTS
| PackageManager.GET_UNINSTALLED_PACKAGES,
MAIN_USER_ID)).thenReturn(Arrays.asList(
ApplicationTestUtils.buildInfo(MAIN_USER_ID, "app1", 0 /* flags */)));
when(mPackageManager.getInstalledApplicationsAsUser(PackageManager.GET_DISABLED_COMPONENTS
| PackageManager.GET_DISABLED_UNTIL_USED_COMPONENTS,
MANAGED_PROFILE_ID)).thenReturn(Arrays.asList(
ApplicationTestUtils.buildInfo(MANAGED_PROFILE_ID, "app2", 0 /* flags */)));
mProvider.calculateNumberOfInstalledApps(
new ApplicationFeatureProvider.NumberOfInstalledAppsCallback() {
@Override
public void onNumberOfInstalledAppsResult(int num) {
numberOfInstalledApps[0] = num;
}
});
ShadowApplication.runBackgroundTasks();
assertThat(numberOfInstalledApps[0]).isEqualTo(2);
}
}

View File

@@ -22,6 +22,7 @@ import android.content.pm.PackageManager;
import com.android.settings.SettingsRobolectricTestRunner;
import com.android.settings.TestConfig;
import com.android.settings.applications.PackageManagerWrapper;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -41,9 +42,8 @@ public final class EnterprisePrivacyFeatureProviderImplTest {
private final ComponentName DEVICE_OWNER = new ComponentName("dummy", "component");
private @Mock PackageManager mPackageManager;
private @Mock Context mContext;
private @Mock DevicePolicyManagerWrapper mDevicePolicyManager;
private @Mock PackageManagerWrapper mPackageManager;
private EnterprisePrivacyFeatureProvider mProvider;
@@ -51,12 +51,10 @@ public final class EnterprisePrivacyFeatureProviderImplTest {
public void setUp() {
MockitoAnnotations.initMocks(this);
when(mContext.getApplicationContext()).thenReturn(mContext);
when(mContext.getPackageManager()).thenReturn(mPackageManager);
when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_DEVICE_ADMIN))
.thenReturn(true);
mProvider = new EnterprisePrivacyFeatureProviderImpl(mContext, mDevicePolicyManager);
mProvider = new EnterprisePrivacyFeatureProviderImpl(mDevicePolicyManager, mPackageManager);
}
@Test

View File

@@ -20,6 +20,8 @@ import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
import com.android.settings.R;
import com.android.settings.SettingsRobolectricTestRunner;
import com.android.settings.TestConfig;
import com.android.settings.core.PreferenceController;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -28,6 +30,8 @@ import org.robolectric.shadows.ShadowApplication;
import static com.google.common.truth.Truth.assertThat;
import java.util.List;
/**
* Tests for {@link EnterprisePrivacySettings}.
*/
@@ -66,7 +70,10 @@ public final class EnterprisePrivacySettingsTest {
@Test
public void getPreferenceControllers() {
assertThat(mSettings.getPreferenceControllers(
ShadowApplication.getInstance().getApplicationContext())).isNull();
final List<PreferenceController> controllers = mSettings.getPreferenceControllers(
ShadowApplication.getInstance().getApplicationContext());
assertThat(controllers).isNotNull();
assertThat(controllers.size()).isEqualTo(1);
assertThat(controllers.get(0)).isInstanceOf(InstalledPackagesPreferenceController.class);
}
}

View File

@@ -0,0 +1,97 @@
/*
* Copyright (C) 2016 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.enterprise;
import android.content.Context;
import android.content.res.Resources;
import android.support.v7.preference.Preference;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
import com.android.settings.R;
import com.android.settings.SettingsRobolectricTestRunner;
import com.android.settings.TestConfig;
import com.android.settings.applications.ApplicationFeatureProvider;
import com.android.settings.testutils.FakeFeatureFactory;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Answers;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.robolectric.annotation.Config;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.anyObject;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.when;
/**
* Tests for {@link InstalledPackagesPreferenceController}.
*/
@RunWith(SettingsRobolectricTestRunner.class)
@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
public final class InstalledPackagesPreferenceControllerTest {
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
private Context mContext;
private FakeFeatureFactory mFeatureFactory;
private InstalledPackagesPreferenceController mController;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
FakeFeatureFactory.setupForTest(mContext);
mFeatureFactory = (FakeFeatureFactory) FakeFeatureFactory.getFactory(mContext);
mController = new InstalledPackagesPreferenceController(mContext);
}
@Test
public void testUpdateState() {
final Preference preference = new Preference(mContext, null, 0, 0);
doAnswer(new Answer() {
public Object answer(InvocationOnMock invocation) {
((ApplicationFeatureProvider.NumberOfInstalledAppsCallback)
invocation.getArguments()[0]).onNumberOfInstalledAppsResult(20);
return null;
}}).when(mFeatureFactory.applicationFeatureProvider)
.calculateNumberOfInstalledApps(anyObject());
when(mContext.getResources().getQuantityString(R.plurals.number_installed_packages, 20, 20))
.thenReturn("20 packages");
mController.updateState(preference);
assertThat(preference.getTitle()).isEqualTo("20 packages");
}
@Test
public void testIsAvailable() {
assertThat(mController.isAvailable()).isTrue();
}
@Test
public void testHandlePreferenceTreeClick() {
assertThat(mController.handlePreferenceTreeClick(new Preference(mContext, null, 0, 0)))
.isFalse();
}
@Test
public void testGetPreferenceKey() {
assertThat(mController.getPreferenceKey()).isEqualTo("number_installed_packages");
}
}