Add custom Activity to show libraries licenses.
As Google's library for showing licences isn't open-source, this commit reimplements its main Activity. This is in prevision to having an OSS flavor of fenix. We chose to not introduce dependencies to third-party libraries such as AboutLibraries for now, and we'll stick to using Google's gradle plugin for the dependencies extraction. Fixes #7584 See also #162pull/21/head
parent
a6481cc897
commit
2743c37b40
@ -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<ListView>(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<LibraryItem> {
|
||||
/*
|
||||
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<TextView>(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
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- 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/. -->
|
||||
|
||||
<RelativeLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/about_libraries"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fillViewport="true"
|
||||
tools:context="org.mozilla.fenix.settings.about.AboutLibrariesActivity">
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:background="?attr/colorPrimary"
|
||||
android:elevation="4dp"
|
||||
android:theme="@style/ThemeOverlay.AppCompat.ActionBar" />
|
||||
<ListView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:id="@+id/about_libraries_listview" />
|
||||
</RelativeLayout>
|
@ -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<ListView>(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<ListView>(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<TextView>(android.R.id.message)
|
||||
.text
|
||||
.toString()
|
||||
assertTrue(alertDialogText.contains("MPL"))
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue