diff --git a/AndroidManifest.xml b/AndroidManifest.xml index cf1ae94d4c7..c620f58164c 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -986,6 +986,18 @@ android:value="true" /> + + + + + + + + + @@ -2663,6 +2675,12 @@ android:resource="@xml/file_paths" /> + + diff --git a/res/xml/about_legal.xml b/res/xml/about_legal.xml index c86276a393f..90b19855df6 100644 --- a/res/xml/about_legal.xml +++ b/res/xml/about_legal.xml @@ -42,6 +42,13 @@ android:title="@string/terms_title" settings:controller="com.android.settings.deviceinfo.legal.TermsPreferenceController" /> + + + + + + + + diff --git a/src/com/android/settings/Settings.java b/src/com/android/settings/Settings.java index b4ebc5759cf..ab59da46008 100644 --- a/src/com/android/settings/Settings.java +++ b/src/com/android/settings/Settings.java @@ -56,6 +56,7 @@ public class Settings extends SettingsActivity { public static class NightDisplaySettingsActivity extends SettingsActivity { /* empty */ } public static class NightDisplaySuggestionActivity extends NightDisplaySettingsActivity { /* empty */ } public static class MyDeviceInfoActivity extends SettingsActivity { /* empty */ } + public static class ModuleLicensesActivity extends SettingsActivity { /* empty */ } public static class ApplicationSettingsActivity extends SettingsActivity { /* empty */ } public static class ManageApplicationsActivity extends SettingsActivity { /* empty */ } public static class ManageAssistActivity extends SettingsActivity { /* empty */ } diff --git a/src/com/android/settings/core/gateway/SettingsGateway.java b/src/com/android/settings/core/gateway/SettingsGateway.java index b35a9747a43..5a81e71efb7 100644 --- a/src/com/android/settings/core/gateway/SettingsGateway.java +++ b/src/com/android/settings/core/gateway/SettingsGateway.java @@ -72,6 +72,7 @@ import com.android.settings.deviceinfo.StorageDashboardFragment; import com.android.settings.deviceinfo.StorageSettings; import com.android.settings.deviceinfo.aboutphone.MyDeviceInfoFragment; import com.android.settings.deviceinfo.firmwareversion.FirmwareVersionSettings; +import com.android.settings.deviceinfo.legal.ModuleLicensesDashboard; import com.android.settings.display.NightDisplaySettings; import com.android.settings.dream.DreamSettings; import com.android.settings.enterprise.EnterprisePrivacySettings; @@ -175,6 +176,7 @@ public class SettingsGateway { UserDictionarySettings.class.getName(), DisplaySettings.class.getName(), MyDeviceInfoFragment.class.getName(), + ModuleLicensesDashboard.class.getName(), ManageApplications.class.getName(), FirmwareVersionSettings.class.getName(), ManageAssist.class.getName(), @@ -318,6 +320,7 @@ public class SettingsGateway { Settings.DateTimeSettingsActivity.class.getName(), Settings.EnterprisePrivacySettingsActivity.class.getName(), Settings.MyDeviceInfoActivity.class.getName(), + Settings.ModuleLicensesActivity.class.getName(), UserBackupSettingsActivity.class.getName(), }; } diff --git a/src/com/android/settings/deviceinfo/legal/ModuleLicensePreference.java b/src/com/android/settings/deviceinfo/legal/ModuleLicensePreference.java new file mode 100644 index 00000000000..e012275798e --- /dev/null +++ b/src/com/android/settings/deviceinfo/legal/ModuleLicensePreference.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2019 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.deviceinfo.legal; + +import android.content.ActivityNotFoundException; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ModuleInfo; +import android.util.Log; +import android.widget.Toast; + +import androidx.preference.Preference; + +import com.android.settings.R; + +/** + * Preference in a list that represents a mainline module that has a licenses file. + */ +public class ModuleLicensePreference extends Preference { + private static final String TAG = "ModuleLicensePreference"; + private final ModuleInfo mModule; + + public ModuleLicensePreference(Context context, ModuleInfo module) { + super(context); + mModule = module; + setKey(module.getPackageName()); + setTitle(module.getName()); + } + + @Override + protected void onClick() { + // Kick off external viewer due to WebView security restrictions (Settings cannot use + // WebView because it is UID 1000). + Intent intent = new Intent(Intent.ACTION_VIEW) + .setDataAndType( + ModuleLicenseProvider.getUriForPackage(mModule.getPackageName()), + ModuleLicenseProvider.LICENSE_FILE_MIME_TYPE) + .putExtra(Intent.EXTRA_TITLE, mModule.getName()) + .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + .addCategory(Intent.CATEGORY_DEFAULT) + .setPackage("com.android.htmlviewer"); + try { + getContext().startActivity(intent); + } catch (ActivityNotFoundException e) { + Log.e(TAG, "Failed to find viewer", e); + showError(); + } + } + + private void showError() { + Toast.makeText( + getContext(), R.string.settings_license_activity_unavailable, Toast.LENGTH_LONG) + .show(); + } +} diff --git a/src/com/android/settings/deviceinfo/legal/ModuleLicenseProvider.java b/src/com/android/settings/deviceinfo/legal/ModuleLicenseProvider.java new file mode 100644 index 00000000000..6731c695ab9 --- /dev/null +++ b/src/com/android/settings/deviceinfo/legal/ModuleLicenseProvider.java @@ -0,0 +1,194 @@ +/* + * Copyright (C) 2019 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.deviceinfo.legal; + +import android.content.ContentProvider; +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.content.SharedPreferences; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.res.AssetManager; +import android.database.Cursor; +import android.net.Uri; +import android.os.ParcelFileDescriptor; +import android.util.Log; + +import androidx.annotation.VisibleForTesting; +import androidx.core.util.Preconditions; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; +import java.util.List; +import java.util.zip.GZIPInputStream; + +public class ModuleLicenseProvider extends ContentProvider { + private static final String TAG = "ModuleLicenseProvider"; + + public static final String AUTHORITY = "com.android.settings.module_licenses"; + static final String GZIPPED_LICENSE_FILE_NAME = "NOTICE.html.gz"; + static final String LICENSE_FILE_NAME = "NOTICE.html"; + static final String LICENSE_FILE_MIME_TYPE = "text/html"; + static final String PREFS_NAME = "ModuleLicenseProvider"; + + @Override + public boolean onCreate() { + return true; + } + + @Override + public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, + String sortOrder) { + throw new UnsupportedOperationException(); + } + + @Override + public String getType(Uri uri) { + checkUri(getContext(), uri); + return LICENSE_FILE_MIME_TYPE; + } + + @Override + public Uri insert(Uri uri, ContentValues values) { + throw new UnsupportedOperationException(); + } + + @Override + public int delete(Uri uri, String selection, String[] selectionArgs) { + throw new UnsupportedOperationException(); + } + + @Override + public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { + throw new UnsupportedOperationException(); + } + + @Override + public ParcelFileDescriptor openFile(Uri uri, String mode) { + final Context context = getContext(); + checkUri(context, uri); + Preconditions.checkArgument("r".equals(mode), "Read is the only supported mode"); + + try { + String packageName = uri.getPathSegments().get(0); + File cachedFile = getCachedHtmlFile(context, packageName); + if (isCachedHtmlFileOutdated(context, packageName)) { + try (InputStream in = new GZIPInputStream( + getPackageAssetManager(context.getPackageManager(), packageName) + .open(GZIPPED_LICENSE_FILE_NAME))) { + File directory = getCachedFileDirectory(context, packageName); + if (!directory.exists()) { + directory.mkdir(); + } + Files.copy(in, cachedFile.toPath(), StandardCopyOption.REPLACE_EXISTING); + } + // Now that the file is saved, write the package's version code to shared prefs + SharedPreferences.Editor editor = getPrefs(context).edit(); + editor.putLong( + packageName, + getPackageInfo(context, packageName).getLongVersionCode()) + .commit(); + } + return ParcelFileDescriptor.open(cachedFile, ParcelFileDescriptor.MODE_READ_ONLY); + } catch (PackageManager.NameNotFoundException e) { + Log.wtf(TAG, "checkUri should have already caught this error", e); + } catch (IOException e) { + Log.e(TAG, "Could not open file descriptor", e); + } + return null; + } + + /** + * Returns true if the cached file for the given package is outdated. A cached file is + * outdated if one of the following are true: + * 1. the shared prefs does not contain a version code for this package + * 2. The version code does not match the package's version code + * 3. There is no file or the file is empty. + */ + @VisibleForTesting + static boolean isCachedHtmlFileOutdated(Context context, String packageName) + throws PackageManager.NameNotFoundException { + SharedPreferences prefs = getPrefs(context); + File file = getCachedHtmlFile(context, packageName); + return !prefs.contains(packageName) + || prefs.getLong(packageName, 0L) + != getPackageInfo(context, packageName).getLongVersionCode() + || !file.exists() || file.length() == 0; + } + + static AssetManager getPackageAssetManager(PackageManager packageManager, String packageName) + throws PackageManager.NameNotFoundException { + return packageManager.getResourcesForApplication( + packageManager.getPackageInfo(packageName, PackageManager.MATCH_APEX) + .applicationInfo) + .getAssets(); + } + + static Uri getUriForPackage(String packageName) { + return new Uri.Builder() + .scheme(ContentResolver.SCHEME_CONTENT) + .authority(AUTHORITY) + .appendPath(packageName) + .appendPath(LICENSE_FILE_NAME) + .build(); + } + + private static void checkUri(Context context, Uri uri) { + List pathSegments = uri.getPathSegments(); + // A URI is valid iff it: + // 1. is a content URI + // 2. uses the correct authority + // 3. has exactly 2 segments and the last one is NOTICE.html + // 4. (checked below) first path segment is the package name of a module + if (!ContentResolver.SCHEME_CONTENT.equals(uri.getScheme()) + || !AUTHORITY.equals(uri.getAuthority()) + || pathSegments == null + || pathSegments.size() != 2 + || !LICENSE_FILE_NAME.equals(pathSegments.get(1))) { + throw new IllegalArgumentException(uri + "is not a valid URI"); + } + // Grab the first path segment, which is the package name of the module and make sure that + // there's actually a module for that package. getModuleInfo will throw if it does not + // exist. + try { + context.getPackageManager().getModuleInfo(pathSegments.get(0), 0 /* flags */); + } catch (PackageManager.NameNotFoundException e) { + throw new IllegalArgumentException(uri + "is not a valid URI", e); + } + } + + private static File getCachedFileDirectory(Context context, String packageName) { + return new File(context.getCacheDir(), packageName); + } + + private static File getCachedHtmlFile(Context context, String packageName) { + return new File(context.getCacheDir() + "/" + packageName, LICENSE_FILE_NAME); + } + + private static PackageInfo getPackageInfo(Context context, String packageName) + throws PackageManager.NameNotFoundException { + return context.getPackageManager().getPackageInfo(packageName, PackageManager.MATCH_APEX); + } + + private static SharedPreferences getPrefs(Context context) { + return context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); + } +} diff --git a/src/com/android/settings/deviceinfo/legal/ModuleLicensesDashboard.java b/src/com/android/settings/deviceinfo/legal/ModuleLicensesDashboard.java new file mode 100644 index 00000000000..f74b68f923e --- /dev/null +++ b/src/com/android/settings/deviceinfo/legal/ModuleLicensesDashboard.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2019 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.deviceinfo.legal; + +import android.app.settings.SettingsEnums; + +import com.android.settings.R; +import com.android.settings.dashboard.DashboardFragment; + +public class ModuleLicensesDashboard extends DashboardFragment { + private static final String TAG = "ModuleLicensesDashboard"; + + @Override + public int getMetricsCategory() { + return SettingsEnums.MODULE_LICENSES_DASHBOARD; + } + + @Override + protected String getLogTag() { + return TAG; + } + + @Override + protected int getPreferenceScreenResId() { + return R.xml.module_licenses; + } + + @Override + public int getHelpResource() { + return 0; + } +} diff --git a/src/com/android/settings/deviceinfo/legal/ModuleLicensesListPreferenceController.java b/src/com/android/settings/deviceinfo/legal/ModuleLicensesListPreferenceController.java new file mode 100644 index 00000000000..9faff856074 --- /dev/null +++ b/src/com/android/settings/deviceinfo/legal/ModuleLicensesListPreferenceController.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2019 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.deviceinfo.legal; + +import android.content.Context; +import android.content.pm.ModuleInfo; +import android.content.pm.PackageManager; + +import com.android.settings.core.BasePreferenceController; + +import java.util.List; + +public class ModuleLicensesListPreferenceController extends BasePreferenceController { + public ModuleLicensesListPreferenceController(Context context, + String preferenceKey) { + super(context, preferenceKey); + } + + @Override + public int getAvailabilityStatus() { + PackageManager packageManager = mContext.getPackageManager(); + List modules = packageManager.getInstalledModules(0 /* flags */); + return modules.stream().anyMatch(new ModuleLicensesPreferenceController.Predicate(mContext)) + ? AVAILABLE + : CONDITIONALLY_UNAVAILABLE; + } +} diff --git a/src/com/android/settings/deviceinfo/legal/ModuleLicensesPreferenceController.java b/src/com/android/settings/deviceinfo/legal/ModuleLicensesPreferenceController.java new file mode 100644 index 00000000000..dd5edbb0f6f --- /dev/null +++ b/src/com/android/settings/deviceinfo/legal/ModuleLicensesPreferenceController.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2019 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.deviceinfo.legal; + +import android.content.Context; +import android.content.pm.ModuleInfo; +import android.content.pm.PackageManager; + +import androidx.preference.PreferenceGroup; +import androidx.preference.PreferenceScreen; + +import com.android.internal.util.ArrayUtils; +import com.android.settings.core.BasePreferenceController; + +import java.io.IOException; +import java.util.Comparator; +import java.util.List; + +public class ModuleLicensesPreferenceController extends BasePreferenceController { + public ModuleLicensesPreferenceController(Context context, String preferenceKey) { + super(context, preferenceKey); + } + + @Override + public int getAvailabilityStatus() { + return AVAILABLE_UNSEARCHABLE; + } + + @Override + public void displayPreference(PreferenceScreen screen) { + super.displayPreference(screen); + + PackageManager packageManager = mContext.getPackageManager(); + List modules = packageManager.getInstalledModules(0 /* flags */); + PreferenceGroup group = screen.findPreference(getPreferenceKey()); + modules.stream() + .sorted(Comparator.comparing(o -> o.getName().toString())) + .filter(new Predicate(mContext)) + .forEach(module -> + group.addPreference( + new ModuleLicensePreference(group.getContext(), module))); + } + + static class Predicate implements java.util.function.Predicate { + private final Context mContext; + + public Predicate(Context context) { + mContext = context; + } + @Override + public boolean test(ModuleInfo module) { + try { + return ArrayUtils.contains( + ModuleLicenseProvider.getPackageAssetManager( + mContext.getPackageManager(), + module.getPackageName()) + .list(""), + ModuleLicenseProvider.GZIPPED_LICENSE_FILE_NAME); + } catch (IOException | PackageManager.NameNotFoundException e) { + return false; + } + } + } +} diff --git a/tests/robotests/assets/grandfather_not_implementing_index_provider b/tests/robotests/assets/grandfather_not_implementing_index_provider index b950f30ffdb..061a81ece70 100644 --- a/tests/robotests/assets/grandfather_not_implementing_index_provider +++ b/tests/robotests/assets/grandfather_not_implementing_index_provider @@ -33,6 +33,7 @@ com.android.settings.datetime.timezone.TimeZoneSettings com.android.settings.deviceinfo.PrivateVolumeSettings com.android.settings.deviceinfo.PublicVolumeSettings com.android.settings.deviceinfo.StorageProfileFragment +com.android.settings.deviceinfo.legal.ModuleLicensesDashboard com.android.settings.enterprise.ApplicationListFragment$AdminGrantedPermissionCamera com.android.settings.enterprise.ApplicationListFragment$AdminGrantedPermissionLocation com.android.settings.enterprise.ApplicationListFragment$AdminGrantedPermissionMicrophone diff --git a/tests/robotests/src/com/android/settings/deviceinfo/legal/ModuleLicensePreferenceTest.java b/tests/robotests/src/com/android/settings/deviceinfo/legal/ModuleLicensePreferenceTest.java new file mode 100644 index 00000000000..cdf082f3d93 --- /dev/null +++ b/tests/robotests/src/com/android/settings/deviceinfo/legal/ModuleLicensePreferenceTest.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2019 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.deviceinfo.legal; + +import static com.google.common.truth.Truth.assertThat; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ModuleInfo; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.MockitoAnnotations; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.shadows.ShadowApplication; + +@RunWith(RobolectricTestRunner.class) +public class ModuleLicensePreferenceTest { + public static final String PACKAGE_NAME = "com.android.test_package"; + public static final String NAME = "Test Package"; + private Context mContext; + private ModuleInfo mModuleInfo; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + mContext = Robolectric.setupActivity(Activity.class); + mModuleInfo = new ModuleInfo(); + mModuleInfo.setPackageName(PACKAGE_NAME); + mModuleInfo.setName(NAME); + } + + @Test + public void ctor_properKeyAndTitle() { + ModuleLicensePreference pref = new ModuleLicensePreference(mContext, mModuleInfo); + + assertThat(pref.getKey()).isEqualTo(PACKAGE_NAME); + assertThat(pref.getTitle()).isEqualTo(NAME); + } + + @Test + public void onClick_sendsCorrectIntent() { + ModuleLicensePreference pref = new ModuleLicensePreference(mContext, mModuleInfo); + + pref.onClick(); + + Intent intent = ShadowApplication.getInstance().getNextStartedActivity(); + assertThat(intent.getAction()).isEqualTo(Intent.ACTION_VIEW); + assertThat(intent.getData()) + .isEqualTo(ModuleLicenseProvider.getUriForPackage(PACKAGE_NAME)); + assertThat(intent.getType()).isEqualTo(ModuleLicenseProvider.LICENSE_FILE_MIME_TYPE); + assertThat(intent.getCharSequenceExtra(Intent.EXTRA_TITLE)).isEqualTo(NAME); + assertThat(intent.getFlags()).isEqualTo(Intent.FLAG_GRANT_READ_URI_PERMISSION); + assertThat(intent.getCategories()).contains(Intent.CATEGORY_DEFAULT); + assertThat(intent.getPackage()).isEqualTo("com.android.htmlviewer"); + } +} diff --git a/tests/robotests/src/com/android/settings/deviceinfo/legal/ModuleLicenseProviderTest.java b/tests/robotests/src/com/android/settings/deviceinfo/legal/ModuleLicenseProviderTest.java new file mode 100644 index 00000000000..b65137b34a8 --- /dev/null +++ b/tests/robotests/src/com/android/settings/deviceinfo/legal/ModuleLicenseProviderTest.java @@ -0,0 +1,393 @@ +/* + * Copyright (C) 2019 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.deviceinfo.legal; + +import static com.android.settings.deviceinfo.legal.ModuleLicenseProvider.LICENSE_FILE_NAME; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +import android.content.ContentResolver; +import android.content.Context; +import android.content.SharedPreferences; +import android.content.pm.ApplicationInfo; +import android.content.pm.ModuleInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.res.AssetFileDescriptor; +import android.content.res.AssetManager; +import android.content.res.Resources; +import android.net.Uri; +import android.os.ParcelFileDescriptor; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; + +@RunWith(RobolectricTestRunner.class) +public class ModuleLicenseProviderTest { + public static final String PACKAGE_NAME = "com.android.test_package"; + @Test + public void onCreate_returnsTrue() { + ModuleLicenseProvider provider = new ModuleLicenseProvider(); + assertThat(provider.onCreate()).isTrue(); + } + + @Test(expected = UnsupportedOperationException.class) + public void query_throwsUnsupportedOperationException() { + ModuleLicenseProvider provider = new ModuleLicenseProvider(); + provider.query(null, null, null, null, null); + } + + @Test(expected = UnsupportedOperationException.class) + public void insert_throwsUnsupportedOperationException() { + ModuleLicenseProvider provider = new ModuleLicenseProvider(); + provider.insert(null, null); + } + + @Test(expected = UnsupportedOperationException.class) + public void delete_throwsUnsupportedOperationException() { + ModuleLicenseProvider provider = new ModuleLicenseProvider(); + provider.delete(null, null, null); + } + + @Test(expected = UnsupportedOperationException.class) + public void update_throwsUnsupportedOperationException() { + ModuleLicenseProvider provider = new ModuleLicenseProvider(); + provider.update(null, null, null, null); + } + + @Test(expected = IllegalArgumentException.class) + public void getType_notContentScheme_throwsIllegalArgumentException() { + ModuleLicenseProvider provider = new ModuleLicenseProvider(); + provider.getType(new Uri.Builder() + .scheme("badscheme") + .authority(ModuleLicenseProvider.AUTHORITY) + .appendPath(PACKAGE_NAME) + .appendPath(LICENSE_FILE_NAME) + .build()); + } + + @Test(expected = IllegalArgumentException.class) + public void getType_invalidAuthority_throwsIllegalArgumentException() { + ModuleLicenseProvider provider = new ModuleLicenseProvider(); + provider.getType(new Uri.Builder() + .scheme(ContentResolver.SCHEME_CONTENT) + .authority("notmyauthority") + .appendPath(PACKAGE_NAME) + .appendPath(LICENSE_FILE_NAME) + .build()); + } + + @Test(expected = IllegalArgumentException.class) + public void getType_emptyPath_throwsIllegalArgumentException() { + ModuleLicenseProvider provider = new ModuleLicenseProvider(); + provider.getType(new Uri.Builder() + .scheme(ContentResolver.SCHEME_CONTENT) + .authority(ModuleLicenseProvider.AUTHORITY) + .build()); + } + + @Test(expected = IllegalArgumentException.class) + public void getType_missingPackageName_throwsIllegalArgumentException() { + ModuleLicenseProvider provider = new ModuleLicenseProvider(); + provider.getType(new Uri.Builder() + .scheme(ContentResolver.SCHEME_CONTENT) + .authority(ModuleLicenseProvider.AUTHORITY) + .appendPath(LICENSE_FILE_NAME) + .build()); + } + + @Test(expected = IllegalArgumentException.class) + public void getType_missingFileName_throwsIllegalArgumentException() { + ModuleLicenseProvider provider = new ModuleLicenseProvider(); + provider.getType(new Uri.Builder() + .scheme(ContentResolver.SCHEME_CONTENT) + .authority(ModuleLicenseProvider.AUTHORITY) + .appendPath(PACKAGE_NAME) + .build()); + } + + @Test(expected = IllegalArgumentException.class) + public void getType_incorrectFileName_throwsIllegalArgumentException() { + ModuleLicenseProvider provider = new ModuleLicenseProvider(); + provider.getType(new Uri.Builder() + .scheme(ContentResolver.SCHEME_CONTENT) + .authority(ModuleLicenseProvider.AUTHORITY) + .appendPath(PACKAGE_NAME) + .appendPath("badname.txt") + .build()); + } + + @Test(expected = IllegalArgumentException.class) + public void getType_packageNotAModule_throwsIllegalArgumentException() + throws PackageManager.NameNotFoundException { + ModuleLicenseProvider provider = spy(new ModuleLicenseProvider()); + Context context = mock(Context.class); + PackageManager packageManager = mock(PackageManager.class); + when(provider.getContext()).thenReturn(context); + when(context.getPackageManager()).thenReturn(packageManager); + when(packageManager.getModuleInfo(PACKAGE_NAME, 0)) + .thenThrow(new PackageManager.NameNotFoundException()); + + provider.getType(new Uri.Builder() + .scheme(ContentResolver.SCHEME_CONTENT) + .authority(ModuleLicenseProvider.AUTHORITY) + .appendPath(PACKAGE_NAME) + .appendPath(LICENSE_FILE_NAME) + .build()); + } + + @Test + public void getType_validUri_returnsHtmlMimeType() + throws PackageManager.NameNotFoundException { + ModuleLicenseProvider provider = spy(new ModuleLicenseProvider()); + Context context = mock(Context.class); + PackageManager packageManager = mock(PackageManager.class); + when(provider.getContext()).thenReturn(context); + when(context.getPackageManager()).thenReturn(packageManager); + when(packageManager.getModuleInfo(PACKAGE_NAME, 0)) + .thenReturn(new ModuleInfo()); + + assertThat(provider.getType(new Uri.Builder() + .scheme(ContentResolver.SCHEME_CONTENT) + .authority(ModuleLicenseProvider.AUTHORITY) + .appendPath(PACKAGE_NAME) + .appendPath(LICENSE_FILE_NAME) + .build())).isEqualTo(ModuleLicenseProvider.LICENSE_FILE_MIME_TYPE); + } + + @Test(expected = IllegalArgumentException.class) + public void openFile_notContentScheme_throwsIllegalArgumentException() { + ModuleLicenseProvider provider = new ModuleLicenseProvider(); + provider.openFile(new Uri.Builder() + .scheme("badscheme") + .authority(ModuleLicenseProvider.AUTHORITY) + .appendPath(PACKAGE_NAME) + .appendPath(LICENSE_FILE_NAME) + .build(), "r"); + } + + @Test(expected = IllegalArgumentException.class) + public void openFile_invalidAuthority_throwsIllegalArgumentException() { + ModuleLicenseProvider provider = new ModuleLicenseProvider(); + provider.openFile(new Uri.Builder() + .scheme(ContentResolver.SCHEME_CONTENT) + .authority("notmyauthority") + .appendPath(PACKAGE_NAME) + .appendPath(LICENSE_FILE_NAME) + .build(), "r"); + } + + @Test(expected = IllegalArgumentException.class) + public void openFile_emptyPath_throwsIllegalArgumentException() { + ModuleLicenseProvider provider = new ModuleLicenseProvider(); + provider.openFile(new Uri.Builder() + .scheme(ContentResolver.SCHEME_CONTENT) + .authority(ModuleLicenseProvider.AUTHORITY) + .build(), "r"); + } + + @Test(expected = IllegalArgumentException.class) + public void openFile_missingPackageName_throwsIllegalArgumentException() { + ModuleLicenseProvider provider = new ModuleLicenseProvider(); + provider.openFile(new Uri.Builder() + .scheme(ContentResolver.SCHEME_CONTENT) + .authority(ModuleLicenseProvider.AUTHORITY) + .appendPath(LICENSE_FILE_NAME) + .build(), "r"); + } + + @Test(expected = IllegalArgumentException.class) + public void openFile_missingFileName_throwsIllegalArgumentException() { + ModuleLicenseProvider provider = new ModuleLicenseProvider(); + provider.openFile(new Uri.Builder() + .scheme(ContentResolver.SCHEME_CONTENT) + .authority(ModuleLicenseProvider.AUTHORITY) + .appendPath(PACKAGE_NAME) + .build(), "r"); + } + + @Test(expected = IllegalArgumentException.class) + public void openFile_incorrectFileName_throwsIllegalArgumentException() { + ModuleLicenseProvider provider = new ModuleLicenseProvider(); + provider.openFile(new Uri.Builder() + .scheme(ContentResolver.SCHEME_CONTENT) + .authority(ModuleLicenseProvider.AUTHORITY) + .appendPath(PACKAGE_NAME) + .appendPath("badname.txt") + .build(), "r"); + } + + @Test(expected = IllegalArgumentException.class) + public void openFile_packageNotAModule_throwsIllegalArgumentException() + throws PackageManager.NameNotFoundException { + ModuleLicenseProvider provider = spy(new ModuleLicenseProvider()); + Context context = mock(Context.class); + PackageManager packageManager = mock(PackageManager.class); + when(provider.getContext()).thenReturn(context); + when(context.getPackageManager()).thenReturn(packageManager); + when(packageManager.getModuleInfo(PACKAGE_NAME, 0)) + .thenThrow(new PackageManager.NameNotFoundException()); + + provider.openFile(new Uri.Builder() + .scheme(ContentResolver.SCHEME_CONTENT) + .authority(ModuleLicenseProvider.AUTHORITY) + .appendPath(PACKAGE_NAME) + .appendPath(LICENSE_FILE_NAME) + .build(), "r"); + } + + @Test(expected = IllegalArgumentException.class) + public void openFile_validUri_notReadMode_throwsIllegalArgumentException() + throws PackageManager.NameNotFoundException { + ModuleLicenseProvider provider = spy(new ModuleLicenseProvider()); + Context context = mock(Context.class); + PackageManager packageManager = mock(PackageManager.class); + when(provider.getContext()).thenReturn(context); + when(context.getPackageManager()).thenReturn(packageManager); + when(packageManager.getModuleInfo(PACKAGE_NAME, 0)) + .thenReturn(new ModuleInfo()); + + provider.openFile(new Uri.Builder() + .scheme(ContentResolver.SCHEME_CONTENT) + .authority(ModuleLicenseProvider.AUTHORITY) + .appendPath(PACKAGE_NAME) + .appendPath(LICENSE_FILE_NAME) + .build(), "badmode"); + } + + @Test + public void isCachedHtmlFileOutdated_packageNotInSharedPrefs_returnTrue() + throws PackageManager.NameNotFoundException { + Context context = RuntimeEnvironment.application; + context.getSharedPreferences(ModuleLicenseProvider.PREFS_NAME, Context.MODE_PRIVATE) + .edit().clear().commit(); + + assertThat(ModuleLicenseProvider.isCachedHtmlFileOutdated(context, PACKAGE_NAME)).isTrue(); + } + + @Test + public void isCachedHtmlFileOutdated_versionCodeDiffersFromSharedPref_returnTrue() + throws PackageManager.NameNotFoundException { + Context context = spy(RuntimeEnvironment.application); + SharedPreferences.Editor editor = context.getSharedPreferences( + ModuleLicenseProvider.PREFS_NAME, Context.MODE_PRIVATE) + .edit(); + editor.clear().commit(); + editor.putLong(PACKAGE_NAME, 900L).commit(); + PackageManager packageManager = mock(PackageManager.class); + doReturn(packageManager).when(context).getPackageManager(); + PackageInfo packageInfo = new PackageInfo(); + packageInfo.setLongVersionCode(1000L); + when(packageManager.getPackageInfo(PACKAGE_NAME, PackageManager.MATCH_APEX)) + .thenReturn(packageInfo); + + assertThat(ModuleLicenseProvider.isCachedHtmlFileOutdated(context, PACKAGE_NAME)).isTrue(); + } + + @Test + public void isCachedHtmlFileOutdated_fileDoesNotExist_returnTrue() + throws PackageManager.NameNotFoundException { + Context context = spy(RuntimeEnvironment.application); + context.getSharedPreferences(ModuleLicenseProvider.PREFS_NAME, Context.MODE_PRIVATE) + .edit().clear().commit(); + SharedPreferences.Editor editor = context.getSharedPreferences( + ModuleLicenseProvider.PREFS_NAME, Context.MODE_PRIVATE) + .edit(); + editor.clear().commit(); + editor.putLong(PACKAGE_NAME, 1000L).commit(); + PackageManager packageManager = mock(PackageManager.class); + doReturn(packageManager).when(context).getPackageManager(); + PackageInfo packageInfo = new PackageInfo(); + packageInfo.setLongVersionCode(1000L); + when(packageManager.getPackageInfo(PACKAGE_NAME, PackageManager.MATCH_APEX)) + .thenReturn(packageInfo); + new File(context.getCacheDir() + "/" + PACKAGE_NAME, LICENSE_FILE_NAME).delete(); + + assertThat(ModuleLicenseProvider.isCachedHtmlFileOutdated(context, PACKAGE_NAME)).isTrue(); + } + + @Test + public void isCachedHtmlFileOutdated_fileIsEmpty_returnTrue() + throws PackageManager.NameNotFoundException, IOException { + Context context = spy(RuntimeEnvironment.application); + context.getSharedPreferences(ModuleLicenseProvider.PREFS_NAME, Context.MODE_PRIVATE) + .edit().clear().commit(); + SharedPreferences.Editor editor = context.getSharedPreferences( + ModuleLicenseProvider.PREFS_NAME, Context.MODE_PRIVATE) + .edit(); + editor.clear().commit(); + editor.putLong(PACKAGE_NAME, 1000L).commit(); + PackageManager packageManager = mock(PackageManager.class); + doReturn(packageManager).when(context).getPackageManager(); + PackageInfo packageInfo = new PackageInfo(); + packageInfo.setLongVersionCode(1000L); + when(packageManager.getPackageInfo(PACKAGE_NAME, PackageManager.MATCH_APEX)) + .thenReturn(packageInfo); + new File(context.getCacheDir(), PACKAGE_NAME).mkdir(); + File file = new File(context.getCacheDir() + "/" + PACKAGE_NAME, LICENSE_FILE_NAME); + file.delete(); + file.createNewFile(); + + assertThat(ModuleLicenseProvider.isCachedHtmlFileOutdated(context, PACKAGE_NAME)).isTrue(); + } + + @Test + public void isCachedHtmlFileOutdated_notOutdated_returnFalse() + throws PackageManager.NameNotFoundException, IOException { + Context context = spy(RuntimeEnvironment.application); + context.getSharedPreferences(ModuleLicenseProvider.PREFS_NAME, Context.MODE_PRIVATE) + .edit().clear().commit(); + SharedPreferences.Editor editor = context.getSharedPreferences( + ModuleLicenseProvider.PREFS_NAME, Context.MODE_PRIVATE) + .edit(); + editor.clear().commit(); + editor.putLong(PACKAGE_NAME, 1000L).commit(); + PackageManager packageManager = mock(PackageManager.class); + doReturn(packageManager).when(context).getPackageManager(); + PackageInfo packageInfo = new PackageInfo(); + packageInfo.setLongVersionCode(1000L); + when(packageManager.getPackageInfo(PACKAGE_NAME, PackageManager.MATCH_APEX)) + .thenReturn(packageInfo); + new File(context.getCacheDir(), PACKAGE_NAME).mkdir(); + File file = new File(context.getCacheDir() + "/" + PACKAGE_NAME, LICENSE_FILE_NAME); + file.delete(); + file.createNewFile(); + try (BufferedWriter writer = new BufferedWriter(new FileWriter(file))) { + writer.write("test"); + } + + assertThat(ModuleLicenseProvider.isCachedHtmlFileOutdated(context, PACKAGE_NAME)).isFalse(); + } + + @Test + public void getUriForPackage_returnsProperlyFormattedUri() { + assertThat(ModuleLicenseProvider.getUriForPackage(PACKAGE_NAME)) + .isEqualTo(Uri.parse("content://com.android.settings.module_licenses/com.android.test_package/NOTICE.html")); + } +} diff --git a/tests/robotests/src/com/android/settings/deviceinfo/legal/ModuleLicensesListPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/deviceinfo/legal/ModuleLicensesListPreferenceControllerTest.java new file mode 100644 index 00000000000..0ca22fb8d08 --- /dev/null +++ b/tests/robotests/src/com/android/settings/deviceinfo/legal/ModuleLicensesListPreferenceControllerTest.java @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2019 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.deviceinfo.legal; + +import static com.android.settings.deviceinfo.legal.ModuleLicenseProvider.GZIPPED_LICENSE_FILE_NAME; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.ModuleInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.res.AssetManager; +import android.content.res.Resources; + +import com.android.settings.core.BasePreferenceController; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +import java.io.IOException; +import java.util.Collections; + +@RunWith(RobolectricTestRunner.class) +public class ModuleLicensesListPreferenceControllerTest { + private static final String PREFERENCE_KEY = "key"; + private static final String PACKAGE_NAME = "com.android.test_package"; + + @Test + public void getAvailabilityStatus_validLicenses_returnsAvailable() + throws PackageManager.NameNotFoundException, IOException { + Context context = mock(Context.class); + PackageManager packageManager = mock(PackageManager.class); + when(context.getPackageManager()).thenReturn(packageManager); + ModuleInfo moduleInfo = new ModuleInfo(); + moduleInfo.setPackageName(PACKAGE_NAME); + when(packageManager.getInstalledModules(0)) + .thenReturn(Collections.singletonList(moduleInfo)); + PackageInfo packageInfo = new PackageInfo(); + ApplicationInfo applicationInfo = new ApplicationInfo(); + packageInfo.applicationInfo = applicationInfo; + when(packageManager.getPackageInfo(PACKAGE_NAME, PackageManager.MATCH_APEX)).thenReturn( + packageInfo); + Resources resources = mock(Resources.class); + when(packageManager.getResourcesForApplication(applicationInfo)).thenReturn(resources); + AssetManager manager = mock(AssetManager.class); + when(resources.getAssets()).thenReturn(manager); + when(manager.list("")).thenReturn(new String[]{GZIPPED_LICENSE_FILE_NAME}); + + ModuleLicensesListPreferenceController controller = + new ModuleLicensesListPreferenceController(context, PREFERENCE_KEY); + assertThat(controller.getAvailabilityStatus()) + .isEqualTo(BasePreferenceController.AVAILABLE); + } + + @Test + public void getAvailabilityStatus_noModules_returnsConditionallyUnavailable() { + Context context = mock(Context.class); + PackageManager packageManager = mock(PackageManager.class); + when(context.getPackageManager()).thenReturn(packageManager); + when(packageManager.getInstalledModules(0)) + .thenReturn(Collections.emptyList()); + + ModuleLicensesListPreferenceController controller = + new ModuleLicensesListPreferenceController(context, PREFERENCE_KEY); + assertThat(controller.getAvailabilityStatus()) + .isEqualTo(BasePreferenceController.CONDITIONALLY_UNAVAILABLE); + } + + @Test + public void getAvailabilityStatus_noLicenses_returnsConditionallyUnavailable() + throws PackageManager.NameNotFoundException, IOException { + Context context = mock(Context.class); + PackageManager packageManager = mock(PackageManager.class); + when(context.getPackageManager()).thenReturn(packageManager); + ModuleInfo moduleInfo = new ModuleInfo(); + moduleInfo.setPackageName(PACKAGE_NAME); + when(packageManager.getInstalledModules(0)) + .thenReturn(Collections.singletonList(moduleInfo)); + PackageInfo packageInfo = new PackageInfo(); + ApplicationInfo applicationInfo = new ApplicationInfo(); + packageInfo.applicationInfo = applicationInfo; + when(packageManager.getPackageInfo(PACKAGE_NAME, PackageManager.MATCH_APEX)).thenReturn( + packageInfo); + Resources resources = mock(Resources.class); + when(packageManager.getResourcesForApplication(applicationInfo)).thenReturn(resources); + AssetManager manager = mock(AssetManager.class); + when(resources.getAssets()).thenReturn(manager); + when(manager.list("")).thenReturn(new String[]{}); + + ModuleLicensesListPreferenceController controller = + new ModuleLicensesListPreferenceController(context, PREFERENCE_KEY); + assertThat(controller.getAvailabilityStatus()) + .isEqualTo(BasePreferenceController.CONDITIONALLY_UNAVAILABLE); + } +} diff --git a/tests/robotests/src/com/android/settings/deviceinfo/legal/ModuleLicensesPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/deviceinfo/legal/ModuleLicensesPreferenceControllerTest.java new file mode 100644 index 00000000000..bf1b01ef24f --- /dev/null +++ b/tests/robotests/src/com/android/settings/deviceinfo/legal/ModuleLicensesPreferenceControllerTest.java @@ -0,0 +1,171 @@ +/* + * Copyright (C) 2019 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.deviceinfo.legal; + +import static com.android.settings.deviceinfo.legal.ModuleLicenseProvider.GZIPPED_LICENSE_FILE_NAME; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.ModuleInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.res.AssetManager; +import android.content.res.Resources; +import android.util.AttributeSet; + +import androidx.preference.Preference; +import androidx.preference.PreferenceGroup; +import androidx.preference.PreferenceManager; +import androidx.preference.PreferenceScreen; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +@RunWith(RobolectricTestRunner.class) +public class ModuleLicensesPreferenceControllerTest { + + private static final String PREFERENCE_KEY = "key"; + private static final String MODULE_1_NAME = "Module 1"; + private static final String MODULE_1_PACKAGE_NAME = "com.android.module_one"; + private static final String MODULE_2_NAME = "Module 2"; + private static final String MODULE_2_PACKAGE_NAME = "com.android.module_two"; + private ModuleInfo mModuleOne; + private ModuleInfo mModuleTwo; + + @Before + public void setUp() { + mModuleOne = new ModuleInfo(); + mModuleOne.setName(MODULE_1_NAME); + mModuleOne.setPackageName(MODULE_1_PACKAGE_NAME); + mModuleTwo = new ModuleInfo(); + mModuleTwo.setName(MODULE_2_NAME); + mModuleTwo.setPackageName(MODULE_2_PACKAGE_NAME); + } + + @Test + public void displayPreference_alphabeticalOrder() + throws PackageManager.NameNotFoundException, IOException { + Context context = mock(Context.class); + ModuleLicensesPreferenceController controller = + new ModuleLicensesPreferenceController(context, PREFERENCE_KEY); + PackageManager packageManager = mock(PackageManager.class); + when(context.getPackageManager()).thenReturn(packageManager); + PreferenceScreen screen = mock(PreferenceScreen.class); + PreferenceGroup group = spy(new MockPreferenceGroup(RuntimeEnvironment.application, null)); + when(screen.findPreference(PREFERENCE_KEY)).thenReturn(group); + when(group.getPreferenceManager()).thenReturn(mock(PreferenceManager.class)); + when(packageManager.getInstalledModules(0)) + .thenReturn(Arrays.asList(mModuleTwo, mModuleOne)); + PackageInfo packageInfo = new PackageInfo(); + ApplicationInfo applicationInfo = new ApplicationInfo(); + packageInfo.applicationInfo = applicationInfo; + when(packageManager.getPackageInfo(MODULE_1_PACKAGE_NAME, PackageManager.MATCH_APEX)) + .thenReturn(packageInfo); + when(packageManager.getPackageInfo(MODULE_2_PACKAGE_NAME, PackageManager.MATCH_APEX)) + .thenReturn(packageInfo); + Resources resources = mock(Resources.class); + when(packageManager.getResourcesForApplication(applicationInfo)).thenReturn(resources); + AssetManager manager = mock(AssetManager.class); + when(resources.getAssets()).thenReturn(manager); + when(manager.list("")).thenReturn(new String[]{GZIPPED_LICENSE_FILE_NAME}); + + controller.displayPreference(screen); + + assertThat(group.getPreferenceCount()).isEqualTo(2); + assertThat(group.getPreference(0).getTitle()).isEqualTo(MODULE_1_NAME); + assertThat(group.getPreference(1).getTitle()).isEqualTo(MODULE_2_NAME); + } + + @Test + public void displayPreference_includeOnlyModulesWithLicenseFile() + throws PackageManager.NameNotFoundException, IOException { + Context context = mock(Context.class); + ModuleLicensesPreferenceController controller = + new ModuleLicensesPreferenceController(context, PREFERENCE_KEY); + PackageManager packageManager = mock(PackageManager.class); + when(context.getPackageManager()).thenReturn(packageManager); + PreferenceScreen screen = mock(PreferenceScreen.class); + PreferenceGroup group = spy(new MockPreferenceGroup(RuntimeEnvironment.application, null)); + when(screen.findPreference(PREFERENCE_KEY)).thenReturn(group); + when(group.getPreferenceManager()).thenReturn(mock(PreferenceManager.class)); + when(packageManager.getInstalledModules(0)) + .thenReturn(Arrays.asList(mModuleTwo, mModuleOne)); + PackageInfo packageInfo = new PackageInfo(); + ApplicationInfo applicationInfo = new ApplicationInfo(); + packageInfo.applicationInfo = applicationInfo; + when(packageManager.getPackageInfo(MODULE_1_PACKAGE_NAME, PackageManager.MATCH_APEX)) + .thenReturn(packageInfo); + Resources resources = mock(Resources.class); + when(packageManager.getResourcesForApplication(applicationInfo)).thenReturn(resources); + AssetManager manager = mock(AssetManager.class); + when(resources.getAssets()).thenReturn(manager); + when(manager.list("")).thenReturn(new String[]{GZIPPED_LICENSE_FILE_NAME}); + PackageInfo packageInfo2 = new PackageInfo(); + ApplicationInfo applicationInfo2 = new ApplicationInfo(); + packageInfo2.applicationInfo = applicationInfo2; + when(packageManager.getPackageInfo(MODULE_2_PACKAGE_NAME, PackageManager.MATCH_APEX)) + .thenReturn(packageInfo2); + Resources resources2 = mock(Resources.class); + when(packageManager.getResourcesForApplication(applicationInfo2)).thenReturn(resources2); + AssetManager manager2 = mock(AssetManager.class); + when(resources2.getAssets()).thenReturn(manager2); + when(manager2.list("")).thenReturn(new String[]{}); + + controller.displayPreference(screen); + + assertThat(group.getPreferenceCount()).isEqualTo(1); + assertThat(group.getPreference(0).getTitle()).isEqualTo(MODULE_1_NAME); + } + + private static class MockPreferenceGroup extends PreferenceGroup { + List mList = new ArrayList<>(); + + public MockPreferenceGroup(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + public boolean addPreference(Preference preference) { + mList.add(preference); + return true; + } + + @Override + public int getPreferenceCount() { + return mList.size(); + } + + @Override + public Preference getPreference(int index) { + return mList.get(index); + } + } +}