parent
e6c34f7045
commit
5b7e297adf
|
@ -40,6 +40,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
- #974 - Added telemetry for bookmarks
|
- #974 - Added telemetry for bookmarks
|
||||||
- #113 - Added QR code scanner
|
- #113 - Added QR code scanner
|
||||||
- #975 - Added telemetry for preference switches
|
- #975 - Added telemetry for preference switches
|
||||||
|
- #1955 - Added a confirmation dialog for QR code and barcode searches
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- #1429 - Updated site permissions ui for MVP
|
- #1429 - Updated site permissions ui for MVP
|
||||||
|
|
|
@ -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<Pair<Any, Iterable<Any>>>): 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<CharSequence>()
|
||||||
|
private var length = 0
|
||||||
|
private val spanMap: MutableMap<IntRange, Iterable<Any>> = HashMap()
|
||||||
|
|
||||||
|
fun append(newText: CharSequence, spans: Iterable<Any>) = 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<Pair<Any, Iterable<Any>>>
|
||||||
|
) : 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<CharSequence, Iterable<Any>>) {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,13 +6,17 @@ package org.mozilla.fenix.search
|
||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.DialogInterface
|
||||||
|
import android.graphics.Typeface.BOLD
|
||||||
|
import android.graphics.Typeface.ITALIC
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.text.style.StyleSpan
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.fragment.app.Fragment
|
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.*
|
||||||
import kotlinx.android.synthetic.main.fragment_search.view.*
|
import kotlinx.android.synthetic.main.fragment_search.view.*
|
||||||
import mozilla.components.browser.search.SearchEngine
|
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.ToolbarComponent
|
||||||
import org.mozilla.fenix.components.toolbar.ToolbarUIView
|
import org.mozilla.fenix.components.toolbar.ToolbarUIView
|
||||||
import org.mozilla.fenix.ext.components
|
import org.mozilla.fenix.ext.components
|
||||||
|
import org.mozilla.fenix.ext.getSpannable
|
||||||
import org.mozilla.fenix.ext.requireComponents
|
import org.mozilla.fenix.ext.requireComponents
|
||||||
import org.mozilla.fenix.mvi.ActionBusFactory
|
import org.mozilla.fenix.mvi.ActionBusFactory
|
||||||
import org.mozilla.fenix.mvi.getAutoDisposeObservable
|
import org.mozilla.fenix.mvi.getAutoDisposeObservable
|
||||||
|
@ -90,9 +95,28 @@ class SearchFragment : Fragment(), BackHandler {
|
||||||
requestPermissions(permissions, REQUEST_CODE_CAMERA_PERMISSIONS)
|
requestPermissions(permissions, REQUEST_CODE_CAMERA_PERMISSIONS)
|
||||||
},
|
},
|
||||||
onScanResult = { result ->
|
onScanResult = { result ->
|
||||||
(activity as HomeActivity)
|
activity?.let {
|
||||||
.openToBrowserAndLoad(result, from = BrowserDirection.FromSearch)
|
AlertDialog.Builder(it).apply {
|
||||||
// TODO add metrics, also should we have confirmation before going to a URL?
|
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,
|
owner = this,
|
||||||
view = view
|
view = view
|
||||||
|
|
|
@ -421,4 +421,8 @@
|
||||||
|
|
||||||
<!-- Content description (not visible, for screen readers etc.): button to close the collection creator -->
|
<!-- Content description (not visible, for screen readers etc.): button to close the collection creator -->
|
||||||
<string name="create_collection_close">Close</string>
|
<string name="create_collection_close">Close</string>
|
||||||
|
|
||||||
|
<!-- QR code scanner prompt which appears after scanning a code, but before navigating to it
|
||||||
|
First parameter is the name of the app, second parameter is the URL or text scanned-->
|
||||||
|
<string name="qr_scanner_confirmation_dialog_message">Allow %1$s to open %2$s</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
Loading…
Reference in New Issue