diff --git a/app/build.gradle b/app/build.gradle index 2035810be..0fc4e940c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -330,7 +330,6 @@ dependencies { implementation Deps.androidx_coordinatorlayout implementation Deps.sentry - implementation Deps.osslicenses_library implementation Deps.leanplum_core implementation Deps.leanplum_fcm diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 825889326..554e11126 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -228,13 +228,8 @@ - - - + diff --git a/app/src/main/java/org/mozilla/fenix/settings/about/AboutFragment.kt b/app/src/main/java/org/mozilla/fenix/settings/about/AboutFragment.kt index ece022c02..3fd152943 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/about/AboutFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/about/AboutFragment.kt @@ -13,7 +13,6 @@ import android.view.ViewGroup import androidx.core.content.pm.PackageInfoCompat import androidx.fragment.app.Fragment import androidx.recyclerview.widget.DividerItemDecoration -import com.google.android.gms.oss.licenses.OssLicensesMenuActivity import kotlinx.android.synthetic.main.fragment_about.* import org.mozilla.fenix.BrowserDirection import org.mozilla.fenix.BuildConfig @@ -168,13 +167,8 @@ class AboutFragment : Fragment(), AboutPageListener { } private fun openLibrariesPage() { - startActivity(Intent(context, OssLicensesMenuActivity::class.java)) - OssLicensesMenuActivity.setActivityTitle( - getString( - R.string.open_source_licenses_title, - appName - ) - ) + val intent = Intent(requireContext(), AboutLibrariesActivity::class.java) + startActivity(intent) } override fun onAboutItemClicked(item: AboutItem) { diff --git a/app/src/main/java/org/mozilla/fenix/settings/about/AboutLibrariesActivity.kt b/app/src/main/java/org/mozilla/fenix/settings/about/AboutLibrariesActivity.kt new file mode 100644 index 000000000..bf69fb2f3 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/settings/about/AboutLibrariesActivity.kt @@ -0,0 +1,120 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.settings.about + +import android.graphics.Typeface +import android.os.Bundle +import android.text.util.Linkify +import android.widget.ArrayAdapter +import android.widget.ListView +import android.widget.TextView +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.app.AppCompatActivity +import org.mozilla.fenix.R +import java.nio.charset.Charset +import java.util.Locale + +/** + * Displays the licenses of all the libraries used by Fenix. + * + * This is a re-implementation of play-services-oss-licenses library. + * We can't use the official implementation in the OSS flavor of Fenix + * because it is proprietary and closed-source. + * + * There are popular FLOSS alternatives to Google's plugin and library + * such as AboutLibraries (https://github.com/mikepenz/AboutLibraries) + * but we considered the risk of introducing such third-party dependency + * to Fenix too high. Therefore, we use Google's gradle plugin to + * extract the dependencies and their licenses, and this activity + * to show the extracted licenses to the end-user. + */ +class AboutLibrariesActivity : AppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val appName = getString(R.string.app_name) + title = getString(R.string.open_source_licenses_title, appName) + setContentView(R.layout.about_libraries_activity) + + setSupportActionBar(findViewById(R.id.toolbar)) + supportActionBar?.setDisplayHomeAsUpEnabled(true) + supportActionBar?.setDisplayShowHomeEnabled(true) + + setupLibrariesListView() + } + + override fun onSupportNavigateUp(): Boolean { + onBackPressed() + return true + } + + private fun setupLibrariesListView() { + val libraries = parseLibraries() + val listView = findViewById(R.id.about_libraries_listview) + listView.adapter = ArrayAdapter(this, android.R.layout.simple_list_item_1, libraries) + listView.setOnItemClickListener { _, _, position, _ -> + showLicenseDialog(libraries[position]) + } + } + + private fun parseLibraries(): List { + /* + The gradle plugin "oss-licenses-plugin" creates two "raw" resources: + + - third_party_licenses which is the binary concatenation of all the licenses text for + all the libraries. License texts can either be an URL to a license file or just the + raw text of the license. + + - third_party_licenses_metadata which contains one dependency per line formatted in + the following way: "[start_offset]:[length] [name]" + + [start_offset] : first byte in third_party_licenses that contains the license + text for this library. + [length] : length of the license text for this library in + third_party_licenses. + [name] : either the name of the library, or its artifact name. + + See https://github.com/google/play-services-plugins/tree/master/oss-licenses-plugin + */ + val licensesData = resources + .openRawResource(R.raw.third_party_licenses) + .readBytes() + val licensesMetadataReader = resources + .openRawResource(R.raw.third_party_license_metadata) + .bufferedReader() + + return licensesMetadataReader.use { reader -> reader.readLines() }.map { line -> + val (section, name) = line.split(" ", limit = 2) + val (startOffset, length) = section.split(":", limit = 2).map(String::toInt) + val licenseData = licensesData.sliceArray(startOffset until startOffset + length) + val licenseText = licenseData.toString(Charset.forName("UTF-8")) + LibraryItem(name, licenseText) + }.sortedBy { item -> item.name.toLowerCase(Locale.ROOT) } + } + + private fun showLicenseDialog(libraryItem: LibraryItem) { + val dialog = AlertDialog.Builder(this) + .setTitle(libraryItem.name) + .setMessage(libraryItem.license) + .create() + dialog.show() + + val textView = dialog.findViewById(android.R.id.message)!! + Linkify.addLinks(textView, Linkify.ALL) + textView.linksClickable = true + textView.textSize = LICENSE_TEXT_SIZE + textView.typeface = Typeface.MONOSPACE + } + + companion object { + private const val LICENSE_TEXT_SIZE = 10F + } +} + +private class LibraryItem(val name: String, val license: String) { + override fun toString(): String { + return name + } +} diff --git a/app/src/main/res/layout/about_libraries_activity.xml b/app/src/main/res/layout/about_libraries_activity.xml new file mode 100644 index 000000000..bf23bb437 --- /dev/null +++ b/app/src/main/res/layout/about_libraries_activity.xml @@ -0,0 +1,25 @@ + + + + + + + diff --git a/app/src/test/java/org/mozilla/fenix/settings/about/AboutLibrariesActivityTest.kt b/app/src/test/java/org/mozilla/fenix/settings/about/AboutLibrariesActivityTest.kt new file mode 100644 index 000000000..140dc44ae --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/settings/about/AboutLibrariesActivityTest.kt @@ -0,0 +1,45 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.settings.about + +import android.widget.ListView +import android.widget.TextView +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith +import org.mozilla.fenix.R +import org.mozilla.fenix.helpers.FenixRobolectricTestRunner +import org.robolectric.Robolectric +import org.robolectric.Shadows.shadowOf +import org.robolectric.shadows.ShadowAlertDialog + +@RunWith(FenixRobolectricTestRunner::class) +class AboutLibrariesActivityTest { + @Test + fun `activity should display licenses`() { + val activity = Robolectric.buildActivity(AboutLibrariesActivity::class.java).create().get() + val listView = activity.findViewById(R.id.about_libraries_listview) + + assertTrue(0 < listView.count) + } + + @Test + fun `item click should open license dialog`() { + val activity = Robolectric.buildActivity(AboutLibrariesActivity::class.java).create().get() + + val listView = activity.findViewById(R.id.about_libraries_listview) + val listViewShadow = shadowOf(listView) + listViewShadow.clickFirstItemContainingText("org.mozilla.geckoview:geckoview") + + val alertDialogShadow = ShadowAlertDialog.getLatestDialog() + assertTrue(alertDialogShadow.isShowing) + + val alertDialogText = alertDialogShadow + .findViewById(android.R.id.message) + .text + .toString() + assertTrue(alertDialogText.contains("MPL")) + } +} diff --git a/buildSrc/src/main/java/Dependencies.kt b/buildSrc/src/main/java/Dependencies.kt index 876e537dd..40933b3c0 100644 --- a/buildSrc/src/main/java/Dependencies.kt +++ b/buildSrc/src/main/java/Dependencies.kt @@ -10,7 +10,6 @@ object Versions { const val leakcanary = "2.4" const val leanplum = "5.4.0" const val osslicenses_plugin = "0.9.5" - const val osslicenses_library = "17.0.0" const val detekt = "1.9.1" const val androidx_appcompat = "1.2.0-rc01" @@ -59,7 +58,6 @@ object Deps { const val allopen = "org.jetbrains.kotlin:kotlin-allopen:${Versions.kotlin}" const val osslicenses_plugin = "com.google.android.gms:oss-licenses-plugin:${Versions.osslicenses_plugin}" - const val osslicenses_library = "com.google.android.gms:play-services-oss-licenses:${Versions.osslicenses_library}" const val mozilla_concept_engine = "org.mozilla.components:concept-engine:${Versions.mozilla_android_components}" const val mozilla_concept_menu = "org.mozilla.components:concept-menu:${Versions.mozilla_android_components}"