Cleanup Android project (Minor refactorings, etc.) (#1244)

* (Android) Get rid of double bangs by using Kotlin view binding

Instead of holding a nullable reference to the WebView, we are now
accessing the WebView using the view binding utility of Kotlin's
Android Extensions.

Further reading:
https://kotlinlang.org/docs/tutorials/android-plugin.html

* (Android) Enable WebView debugging in debug builds

This enables debugging the app's WebView using Chrome's DevTools.
https://developers.google.com/web/tools/chrome-devtools/remote-debugging/webviews

* (Android) Make MainActivity.kt adhere to common Kotlin conventions

* (Android) Update dependencies and improve formatting of Gradle files

This updates the Kotlin plugin to 1.3.21 and the Gradle plugin to 3.3.2

* (Android) Remove unnecessary ConstraintLayout container

Layout files should generally have as few nested layers as possible,
because every layer affects the performance.

* (Android) Use JSONObject class to construct a JSON string

It is way safer to construct a JSON string using classes that are
meant for doing that, instead of concatenating raw strings.

* (Android) Suppress JavaScript lint warning

* (Android) Use Kotlin string templates instead of concatenating strings

* (Android) Add missing SuppressLint import
This commit is contained in:
Christoph Kührer 2019-04-04 21:25:25 +02:00 committed by Donovan Preston
parent 373da3f090
commit 48b5d85904
4 changed files with 110 additions and 123 deletions

View file

@ -1,7 +1,5 @@
apply plugin: 'com.android.application' apply plugin: 'com.android.application'
apply plugin: 'kotlin-android' apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions' apply plugin: 'kotlin-android-extensions'
android { android {
@ -31,7 +29,7 @@ dependencies {
androidTestImplementation 'com.android.support.test:runner:1.0.2' androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
implementation 'com.github.delight-im:Android-AdvancedWebView:v3.0.0' implementation 'com.github.delight-im:Android-AdvancedWebView:v3.0.0'
implementation "org.mozilla.components:service-firefox-accounts:${rootProject.ext.android_components_version}" implementation "org.mozilla.components:service-firefox-accounts:$android_components_version"
} }
task generateAndLinkBundle(type: Exec, description: 'Generate the android.js bundle and link it into the assets directory') { task generateAndLinkBundle(type: Exec, description: 'Generate the android.js bundle and link it into the assets directory') {

View file

@ -1,39 +1,39 @@
package org.mozilla.firefoxsend package org.mozilla.firefoxsend
import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import im.delight.android.webview.AdvancedWebView
import android.graphics.Bitmap
import android.content.Intent
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.ComponentName import android.content.ComponentName
import android.content.Intent
import android.graphics.Bitmap
import android.net.Uri import android.net.Uri
import android.webkit.WebView import android.os.Bundle
import android.webkit.WebMessage import android.support.v7.app.AppCompatActivity
import android.util.Log
import android.util.Base64 import android.util.Base64
import android.util.Log
import android.view.View import android.view.View
import android.webkit.ConsoleMessage import android.webkit.*
import android.webkit.JavascriptInterface import im.delight.android.webview.AdvancedWebView
import android.webkit.WebChromeClient import kotlinx.android.synthetic.main.activity_main.*
import mozilla.components.service.fxa.Config import mozilla.components.service.fxa.Config
import mozilla.components.service.fxa.FirefoxAccount import mozilla.components.service.fxa.FirefoxAccount
import mozilla.components.service.fxa.Profile
import mozilla.components.service.fxa.FxaResult import mozilla.components.service.fxa.FxaResult
import org.json.JSONObject
internal class LoggingWebChromeClient : WebChromeClient() { internal class LoggingWebChromeClient : WebChromeClient() {
override fun onConsoleMessage(cm: ConsoleMessage): Boolean { override fun onConsoleMessage(cm: ConsoleMessage): Boolean {
Log.w("CONTENT", String.format("%s @ %d: %s", Log.d(TAG, String.format("%s @ %d: %s",
cm.message(), cm.lineNumber(), cm.sourceId())) cm.message(), cm.lineNumber(), cm.sourceId()))
return true return true
} }
companion object {
private const val TAG = "CONTENT"
}
} }
class WebAppInterface(private val mContext: MainActivity) { class WebAppInterface(private val mContext: MainActivity) {
@JavascriptInterface @JavascriptInterface
fun beginOAuthFlow() { fun beginOAuthFlow() {
mContext.beginOAuthFlow(); mContext.beginOAuthFlow()
} }
@JavascriptInterface @JavascriptInterface
@ -43,176 +43,176 @@ class WebAppInterface(private val mContext: MainActivity) {
} }
class MainActivity : AppCompatActivity(), AdvancedWebView.Listener { class MainActivity : AppCompatActivity(), AdvancedWebView.Listener {
private var mWebView: AdvancedWebView? = null
private var mToShare: String? = null private var mToShare: String? = null
private var mToCall: String? = null private var mToCall: String? = null
private var mAccount: FirefoxAccount? = null private var mAccount: FirefoxAccount? = null
@SuppressLint("SetJavaScriptEnabled")
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main) setContentView(R.layout.activity_main)
// https://developers.google.com/web/tools/chrome-devtools/remote-debugging/webviews WebView.setWebContentsDebuggingEnabled(BuildConfig.DEBUG)
// WebView.setWebContentsDebuggingEnabled(true); // TODO only dev builds webView.apply {
setListener(this@MainActivity, this@MainActivity)
addJavascriptInterface(WebAppInterface(this@MainActivity), JS_INTERFACE_NAME)
setLayerType(View.LAYER_TYPE_HARDWARE, null)
webChromeClient = LoggingWebChromeClient()
mWebView = findViewById<WebView>(R.id.webview) as AdvancedWebView settings.apply {
mWebView!!.setListener(this, this) userAgentString = "Send Android"
mWebView!!.setWebChromeClient(LoggingWebChromeClient()) allowUniversalAccessFromFileURLs = true
mWebView!!.addJavascriptInterface(WebAppInterface(this), "Android") javaScriptEnabled = true
mWebView!!.setLayerType(View.LAYER_TYPE_HARDWARE, null); }
}
val webSettings = mWebView!!.getSettings() val type = intent.type
webSettings.setUserAgentString("Send Android") if (Intent.ACTION_SEND == intent.action && type != null) {
webSettings.setAllowUniversalAccessFromFileURLs(true) if (type == "text/plain") {
webSettings.setJavaScriptEnabled(true)
val intent = getIntent()
val action = intent.getAction()
val type = intent.getType()
if (Intent.ACTION_SEND.equals(action) && type != null) {
if (type.equals("text/plain")) {
val sharedText = intent.getStringExtra(Intent.EXTRA_TEXT) val sharedText = intent.getStringExtra(Intent.EXTRA_TEXT)
Log.w("INTENT", "text/plain " + sharedText) Log.d(TAG_INTENT, "text/plain $sharedText")
mToShare = "data:text/plain;base64," + Base64.encodeToString(sharedText.toByteArray(), 16).trim() mToShare = "data:text/plain;base64," + Base64.encodeToString(sharedText.toByteArray(), 16).trim()
} else if (type.startsWith("image/")) { } else if (type.startsWith("image/")) {
val imageUri = intent.getParcelableExtra(Intent.EXTRA_STREAM) as Uri val imageUri = intent.getParcelableExtra(Intent.EXTRA_STREAM) as Uri
Log.w("INTENT", "image/ " + imageUri) Log.d(TAG_INTENT, "image/ $imageUri")
mToShare = "data:text/plain;base64," + Base64.encodeToString(imageUri.path.toByteArray(), 16).trim() mToShare = "data:text/plain;base64," + Base64.encodeToString(imageUri.path.toByteArray(), 16).trim()
} }
} }
mWebView!!.loadUrl("file:///android_asset/android.html") webView.loadUrl("file:///android_asset/android.html")
} }
fun beginOAuthFlow() { fun beginOAuthFlow() {
Config.release().then(fun (value: Config): FxaResult<Unit> { Config.release().then { value ->
mAccount = FirefoxAccount(value, "20f7931c9054d833", "https://send.firefox.com/fxa/android-redirect.html") mAccount = FirefoxAccount(value, "20f7931c9054d833", "https://send.firefox.com/fxa/android-redirect.html")
mAccount?.beginOAuthFlow(arrayOf("profile", "https://identity.mozilla.com/apps/send"), true)?.then(fun (url: String): FxaResult<Unit> { mAccount?.beginOAuthFlow(arrayOf("profile", "https://identity.mozilla.com/apps/send"), true)
Log.w("CONFIG", "GOT A URL " + url) ?.then { url ->
this@MainActivity.runOnUiThread({ Log.d(TAG_CONFIG, "GOT A URL $url")
mWebView!!.loadUrl(url) this@MainActivity.runOnUiThread {
}) webView.loadUrl(url)
return FxaResult.fromValue(Unit) }
}) FxaResult.fromValue(Unit)
Log.w("CONFIG", "CREATED FIREFOXACCOUNT") }
return FxaResult.fromValue(Unit) Log.d(TAG_CONFIG, "CREATED FIREFOXACCOUNT")
}) FxaResult.fromValue(Unit)
}
} }
fun shareUrl(url: String) { fun shareUrl(url: String) {
val shareIntent = Intent() val shareIntent = Intent().apply {
shareIntent.action = Intent.ACTION_SEND action = Intent.ACTION_SEND
shareIntent.type = "text/plain" type = "text/plain"
shareIntent.putExtra(Intent.EXTRA_TEXT, url) putExtra(Intent.EXTRA_TEXT, url)
}
val components = arrayOf(ComponentName(applicationContext, MainActivity::class.java))
val chooser = Intent.createChooser(shareIntent, "") val chooser = Intent.createChooser(shareIntent, "")
chooser.putExtra(Intent.EXTRA_EXCLUDE_COMPONENTS, arrayOf(ComponentName(applicationContext, MainActivity::class.java))) .putExtra(Intent.EXTRA_EXCLUDE_COMPONENTS, components)
startActivity(chooser) startActivity(chooser)
} }
@SuppressLint("NewApi")
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
mWebView!!.onResume() webView.onResume()
// ...
} }
@SuppressLint("NewApi")
override fun onPause() { override fun onPause() {
mWebView!!.onPause() webView.onPause()
// ...
super.onPause() super.onPause()
} }
override fun onDestroy() { override fun onDestroy() {
mWebView!!.onDestroy() webView.onDestroy()
// ...
super.onDestroy() super.onDestroy()
} }
override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) { override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) {
super.onActivityResult(requestCode, resultCode, intent) super.onActivityResult(requestCode, resultCode, intent)
mWebView!!.onActivityResult(requestCode, resultCode, intent) webView.onActivityResult(requestCode, resultCode, intent)
// ...
} }
override fun onBackPressed() { override fun onBackPressed() {
if (!mWebView!!.onBackPressed()) { if (!webView.onBackPressed()) {
return return
} }
// ...
super.onBackPressed() super.onBackPressed()
} }
override fun onPageStarted(url: String, favicon: Bitmap?) { override fun onPageStarted(url: String, favicon: Bitmap?) {
if (url.startsWith("https://send.firefox.com/fxa/android-redirect.html")) { if (url.startsWith("https://send.firefox.com/fxa/android-redirect.html")) {
// We load this here so the user doesn't see the android-redirect.html page // We load this here so the user doesn't see the android-redirect.html page
mWebView!!.loadUrl("file:///android_asset/android.html") webView.loadUrl("file:///android_asset/android.html")
val parsed = Uri.parse(url) val uri = Uri.parse(url)
val code = parsed.getQueryParameter("code") uri.getQueryParameter("code")?.let { code ->
val state = parsed.getQueryParameter("state") uri.getQueryParameter("state")?.let { state ->
code?.let { code ->
state?.let { state ->
mAccount?.completeOAuthFlow(code, state)?.whenComplete { info -> mAccount?.completeOAuthFlow(code, state)?.whenComplete { info ->
//displayAndPersistProfile(code, state) mAccount?.getProfile(false)?.then { profile ->
val profile = mAccount?.getProfile(false)?.then(fun (profile: Profile): FxaResult<Unit> { val profileJsonPayload = JSONObject()
val accessToken = info.accessToken .put("accessToken", info.accessToken)
val keys = info.keys .put("keys", info.keys)
val avatar = profile.avatar .put("avatar", profile.avatar)
val displayName = profile.displayName .put("displayName", profile.displayName)
val email = profile.email .put("email", profile.email)
val uid = profile.uid .put("uid", profile.uid).toString()
val toPass = "{\"accessToken\": \"${accessToken}\", \"keys\": '${keys}', \"avatar\": \"${avatar}\", \"displayName\": \"${displayName}\", \"email\": \"${email}\", \"uid\": \"${uid}\"}" mToCall = "finishLogin($profileJsonPayload)"
mToCall = "finishLogin(${toPass})" this@MainActivity.runOnUiThread {
this@MainActivity.runOnUiThread({
// Clear the history so that the user can't use the back button to see broken pages // Clear the history so that the user can't use the back button to see broken pages
// that were inserted into the history by the login process. // that were inserted into the history by the login process.
mWebView!!.clearHistory() webView.clearHistory()
// We also reload this here because we need to make sure onPageFinished runs after mToCall has been set. // We also reload this here because we need to make sure onPageFinished runs after mToCall has been set.
// We can't guarantee that onPageFinished wasn't already called at this point. // We can't guarantee that onPageFinished wasn't already called at this point.
mWebView!!.loadUrl("file:///android_asset/android.html") webView.loadUrl("file:///android_asset/android.html")
}) }
FxaResult.fromValue(Unit)
return FxaResult.fromValue(Unit)
})
} }
} }
} }
} }
Log.w("MAIN", "onPageStarted"); }
Log.d(TAG_MAIN, "onPageStarted")
} }
override fun onPageFinished(url: String) { override fun onPageFinished(url: String) {
Log.w("MAIN", "onPageFinished") Log.d(TAG_MAIN, "onPageFinished")
if (mToShare != null) { if (mToShare != null) {
Log.w("INTENT", mToShare) Log.d(TAG_INTENT, mToShare)
mWebView?.postWebMessage(WebMessage(mToShare), Uri.EMPTY) webView.postWebMessage(WebMessage(mToShare), Uri.EMPTY)
mToShare = null mToShare = null
} }
if (mToCall != null) { if (mToCall != null) {
this@MainActivity.runOnUiThread({ this@MainActivity.runOnUiThread {
mWebView?.evaluateJavascript(mToCall, fun (value: String) { webView.evaluateJavascript(mToCall) {
mToCall = null mToCall = null
}) }
}) }
} }
} }
override fun onPageError(errorCode: Int, description: String, failingUrl: String) { override fun onPageError(errorCode: Int, description: String, failingUrl: String) {
Log.w("MAIN", "onPageError " + description) Log.d(TAG_MAIN, "onPageError($errorCode, $description, $failingUrl)")
} }
override fun onDownloadRequested(url: String, suggestedFilename: String, mimeType: String, contentLength: Long, contentDisposition: String, userAgent: String) { override fun onDownloadRequested(url: String,
Log.w("MAIN", "onDownloadRequested") suggestedFilename: String,
mimeType: String,
contentLength: Long,
contentDisposition: String,
userAgent: String) {
Log.d(TAG_MAIN, "onDownloadRequested")
} }
override fun onExternalPageRequest(url: String) { override fun onExternalPageRequest(url: String) {
Log.w("MAIN", "onExternalPageRequest") Log.d(TAG_MAIN, "onExternalPageRequest($url)")
} }
companion object {
private const val TAG_MAIN = "MAIN"
private const val TAG_INTENT = "INTENT"
private const val TAG_CONFIG = "CONFIG"
private const val JS_INTERFACE_NAME = "Android"
}
} }

View file

@ -1,13 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <im.delight.android.webview.AdvancedWebView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/webView"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
tools:context=".MainActivity"> tools:context=".MainActivity" />
<im.delight.android.webview.AdvancedWebView
android:id="@+id/webview"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</android.support.constraint.ConstraintLayout>

View file

@ -8,20 +8,15 @@ buildscript {
jcenter() jcenter()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:3.3.1' classpath 'com.android.tools.build:gradle:3.3.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.3.20" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.3.21"
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
} }
} }
allprojects { allprojects {
repositories { repositories {
google() google()
maven { maven { url "https://maven.mozilla.org/maven2" }
url "https://maven.mozilla.org/maven2"
}
jcenter() jcenter()
maven { url "https://jitpack.io" } maven { url "https://jitpack.io" }
} }