diff --git a/CHANGELOG.md b/CHANGELOG.md index aab718fcb..23a49269e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - #974 - Added telemetry for bookmarks - #113 - Added QR code scanner - #975 - Added telemetry for preference switches +- #1955 - Added a confirmation dialog for QR code and barcode searches ### Changed - #1429 - Updated site permissions ui for MVP diff --git a/app/src/main/java/org/mozilla/fenix/ext/Resources.kt b/app/src/main/java/org/mozilla/fenix/ext/Resources.kt new file mode 100644 index 000000000..ffa1ca4a8 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/ext/Resources.kt @@ -0,0 +1,92 @@ +/* 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.ext + +import android.content.res.Resources +import android.os.Build +import android.text.SpannableString +import android.text.Spanned.SPAN_EXCLUSIVE_EXCLUSIVE +import androidx.annotation.StringRes +import java.util.Formatter +import kotlin.collections.ArrayList +import kotlin.collections.HashMap + +// Credit to Michael Spitsin https://medium.com/@programmerr47/working-with-spans-in-android-ca4ab1327bc4 +@Suppress("SpreadOperator") +fun Resources.getSpannable(@StringRes id: Int, spanParts: List>>): CharSequence { + val resultCreator = SpannableStringCreator() + Formatter( + SpannableAppendable(resultCreator, spanParts), + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + configuration.locales[0] + } else configuration.locale + ).format(getString(id), *spanParts.map { it.first }.toTypedArray()) + return resultCreator.toSpannableString() +} + +class SpannableStringCreator { + private val parts = ArrayList() + private var length = 0 + private val spanMap: MutableMap> = HashMap() + + fun append(newText: CharSequence, spans: Iterable) = apply { + val end = newText.length + parts.add(newText) + spanMap[(length..length + end)] = spans + length += end + } + + fun append(newText: CharSequence) = apply { + parts.add(newText) + length += newText.length + } + + fun toSpannableString() = SpannableString((parts.joinToString(""))).apply { + spanMap.forEach { entry -> + val range = entry.key + entry.value.forEach { + setSpan(it, range.start, range.endInclusive, SPAN_EXCLUSIVE_EXCLUSIVE) + } + } + } +} + +class SpannableAppendable( + private val creator: SpannableStringCreator, + spanParts: List>> +) : Appendable { + + private val spansMap = spanParts.toMap().mapKeys { entry -> entry.key.let { it as? CharSequence ?: it.toString() } } + + override fun append(csq: CharSequence?) = apply { creator.appendSmart(csq, spansMap) } + + override fun append(csq: CharSequence?, start: Int, end: Int) = apply { + if (csq != null) { + if (start in 0 until end && end <= csq.length) { + append(csq.subSequence(start, end)) + } else { + throw IndexOutOfBoundsException("start " + start + ", end " + end + ", s.length() " + csq.length) + } + } + } + + override fun append(c: Char) = apply { creator.append(c.toString()) } + + private fun SpannableStringCreator.appendSmart(csq: CharSequence?, spanDict: Map>) { + if (csq != null) { + if (csq in spanDict) { + append(csq, spanDict.getValue(csq)) + } else { + val possibleMatchDict = spanDict.filter { it.key.toString() == csq } + if (possibleMatchDict.isNotEmpty()) { + val spanDictEntry = possibleMatchDict.entries.toList()[0] + append(spanDictEntry.key, spanDictEntry.value) + } else { + append(csq) + } + } + } + } +} diff --git a/app/src/main/java/org/mozilla/fenix/search/SearchFragment.kt b/app/src/main/java/org/mozilla/fenix/search/SearchFragment.kt index 7311c2c3e..e5e6fe4f5 100644 --- a/app/src/main/java/org/mozilla/fenix/search/SearchFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/search/SearchFragment.kt @@ -6,13 +6,17 @@ package org.mozilla.fenix.search import android.Manifest import android.content.Context +import android.content.DialogInterface +import android.graphics.Typeface.BOLD +import android.graphics.Typeface.ITALIC import android.os.Bundle +import android.text.style.StyleSpan import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.Fragment -import kotlinx.android.synthetic.main.component_search.* import kotlinx.android.synthetic.main.fragment_search.* import kotlinx.android.synthetic.main.fragment_search.view.* import mozilla.components.browser.search.SearchEngine @@ -33,6 +37,7 @@ import org.mozilla.fenix.components.toolbar.SearchState import org.mozilla.fenix.components.toolbar.ToolbarComponent import org.mozilla.fenix.components.toolbar.ToolbarUIView import org.mozilla.fenix.ext.components +import org.mozilla.fenix.ext.getSpannable import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.mvi.ActionBusFactory import org.mozilla.fenix.mvi.getAutoDisposeObservable @@ -90,9 +95,28 @@ class SearchFragment : Fragment(), BackHandler { requestPermissions(permissions, REQUEST_CODE_CAMERA_PERMISSIONS) }, onScanResult = { result -> - (activity as HomeActivity) - .openToBrowserAndLoad(result, from = BrowserDirection.FromSearch) - // TODO add metrics, also should we have confirmation before going to a URL? + activity?.let { + AlertDialog.Builder(it).apply { + val spannable = resources.getSpannable( + R.string.qr_scanner_confirmation_dialog_message, + listOf( + getString(R.string.app_name) to listOf(StyleSpan(BOLD)), + result to listOf(StyleSpan(ITALIC)) + ) + ) + setMessage(spannable) + setNegativeButton("DENY") { dialog: DialogInterface, _ -> + dialog.cancel() + } + setPositiveButton("ALLOW") { dialog: DialogInterface, _ -> + (activity as HomeActivity) + .openToBrowserAndLoad(result, from = BrowserDirection.FromSearch) + dialog.dismiss() + // TODO add metrics + } + create() + }.show() + } }), owner = this, view = view diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 003bf6592..c9b23e24d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -421,4 +421,8 @@ Close + + + Allow %1$s to open %2$s