Standardise HTTP action

* Rename IAction uses to simplify code

* Renamed SimpleIntentIAction to IntentAction
* Renamed IntentAction#executeForIntent to createIntent
* Added suspend modifier to IAction#execute to simplify later work
* Added parseAndNormalizeUri
* Optimized usage of Coroutines in WifiAction
* Replaced one java Pattern instance with Kotlin Regex

* Simplify DecodeFragment#openUrl by moving stuff into it's own action

* Added OpenOrSearchAction
* Added WebAction
* Added DEFAULT_ACTION

* IgnoreCase in SchemeAction
This commit is contained in:
molikuner
2019-09-30 13:36:18 +02:00
committed by Markus Fisch
parent ca619efa14
commit 3ae7e561c4
26 changed files with 226 additions and 139 deletions
@@ -2,18 +2,22 @@ package de.markusfisch.android.binaryeye.actions
import de.markusfisch.android.binaryeye.actions.mail.MailAction
import de.markusfisch.android.binaryeye.actions.otpauth.OtpauthAction
import de.markusfisch.android.binaryeye.actions.search.OpenOrSearchAction
import de.markusfisch.android.binaryeye.actions.sms.SmsAction
import de.markusfisch.android.binaryeye.actions.tel.TelAction
import de.markusfisch.android.binaryeye.actions.vtype.vcard.VCardAction
import de.markusfisch.android.binaryeye.actions.vtype.vevent.VEventAction
import de.markusfisch.android.binaryeye.actions.web.WebAction
import de.markusfisch.android.binaryeye.actions.wifi.WifiAction
object ActionRegistry {
val DEFAULT_ACTION: IAction = OpenOrSearchAction
val REGISTRY: Set<IAction> = setOf(
MailAction, OtpauthAction, SmsAction, TelAction, VCardAction, VEventAction, WifiAction
MailAction, OtpauthAction, SmsAction, TelAction, VCardAction, VEventAction, WebAction, WifiAction
)
fun getAction(data: ByteArray): IAction? = REGISTRY.find {
fun getAction(data: ByteArray): IAction = REGISTRY.find {
it.canExecuteOn(data)
}
} ?: DEFAULT_ACTION
}
@@ -4,21 +4,22 @@ import android.content.Context
import android.content.Intent
import android.widget.Toast
import de.markusfisch.android.binaryeye.app.execShareIntent
import de.markusfisch.android.binaryeye.app.parseAndNormalizeUri
interface IAction {
val iconResId: Int
val titleResId: Int
fun canExecuteOn(data: ByteArray): Boolean
fun execute(context: Context, data: ByteArray)
suspend fun execute(context: Context, data: ByteArray)
}
abstract class SimpleIntentIAction : IAction {
abstract class IntentAction : IAction {
abstract val errorMsg: Int
final override fun execute(context: Context, data: ByteArray) {
final override suspend fun execute(context: Context, data: ByteArray) {
val intent =
executeForIntent(context, data) ?: return Toast.makeText(
createIntent(context, data) ?: return Toast.makeText(
context,
errorMsg,
Toast.LENGTH_LONG
@@ -26,11 +27,25 @@ abstract class SimpleIntentIAction : IAction {
execShareIntent(context, intent)
}
abstract fun executeForIntent(context: Context, data: ByteArray): Intent?
abstract suspend fun createIntent(context: Context, data: ByteArray): Intent?
}
fun IAction?.validateOrGetNew(data: ByteArray): IAction? {
return this?.takeIf {
canExecuteOn(data)
} ?: ActionRegistry.getAction(data)
abstract class SchemeAction : IAction {
abstract val scheme: String
open val intentAction: String = Intent.ACTION_VIEW
open val buildRegex: Boolean = false
final override fun canExecuteOn(data: ByteArray): Boolean {
val content = String(data)
return if (buildRegex) {
content.matches("""^$scheme://[\w\W]+$""".toRegex(RegexOption.IGNORE_CASE))
} else {
content.startsWith("$scheme://", ignoreCase = true)
}
}
final override suspend fun execute(context: Context, data: ByteArray) {
val uri = parseAndNormalizeUri(String(data))
execShareIntent(context, Intent(intentAction, uri))
}
}
@@ -4,9 +4,9 @@ import android.content.Context
import android.content.Intent
import android.net.Uri
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.actions.SimpleIntentIAction
import de.markusfisch.android.binaryeye.actions.IntentAction
object MailAction : SimpleIntentIAction() {
object MailAction : IntentAction() {
private val mailRegex =
"""^mail(?:to)?:([\w.%+-]+@[A-Za-z\d.-]+\.[A-Za-z]{2,6}(?:[?&](?:subject|body)=[\S\s]*?){0,2})$""".toRegex()
@@ -18,7 +18,7 @@ object MailAction : SimpleIntentIAction() {
return String(data).matches(mailRegex)
}
override fun executeForIntent(context: Context, data: ByteArray): Intent? {
override suspend fun createIntent(context: Context, data: ByteArray): Intent? {
val mailWithMessage = mailRegex.matchEntire(
String(data)
)?.groupValues?.get(1) ?: return null
@@ -3,9 +3,9 @@ package de.markusfisch.android.binaryeye.actions.otpauth
import android.content.Context
import android.content.Intent
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.actions.SimpleIntentIAction
import de.markusfisch.android.binaryeye.actions.IntentAction
object OtpauthAction : SimpleIntentIAction() {
object OtpauthAction : IntentAction() {
override val iconResId: Int = R.drawable.ic_action_otpauth
override val titleResId: Int = R.string.otpauth_add
override val errorMsg: Int = R.string.otpauth_error
@@ -14,7 +14,7 @@ object OtpauthAction : SimpleIntentIAction() {
return OtpauthParser(String(data)) != null
}
override fun executeForIntent(context: Context, data: ByteArray): Intent? {
override suspend fun createIntent(context: Context, data: ByteArray): Intent? {
return OtpauthParser(String(data))?.let {
Intent(Intent.ACTION_VIEW, it.uri)
}
@@ -0,0 +1,66 @@
package de.markusfisch.android.binaryeye.actions.search
import android.app.AlertDialog
import android.content.Context
import android.content.Intent
import android.widget.Toast
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.actions.IAction
import de.markusfisch.android.binaryeye.app.execShareIntent
import de.markusfisch.android.binaryeye.app.parseAndNormalizeUri
import de.markusfisch.android.binaryeye.app.prefs
import java.net.URLEncoder
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
object OpenOrSearchAction : IAction {
override val iconResId: Int = R.drawable.ic_action_search
override val titleResId: Int = R.string.search_web
override fun canExecuteOn(data: ByteArray): Boolean = false
override suspend fun execute(context: Context, data: ByteArray) {
val intent = openUri(context, String(data)) ?: return
execShareIntent(context, intent)
}
private suspend fun openUri(context: Context, data: String, search: Boolean = true): Intent? {
val uri = parseAndNormalizeUri(data)
val intent = Intent(Intent.ACTION_VIEW, uri)
when {
intent.resolveActivity(context.packageManager) != null -> return intent
search -> return getSearchIntent(context, data)
else -> Toast.makeText(
context,
R.string.cannot_resolve_action,
Toast.LENGTH_SHORT
).show()
}
return null
}
private suspend fun getSearchIntent(context: Context, query: String): Intent? {
val names = context.resources.getStringArray(
R.array.search_engines_names
).toMutableList()
val urls = context.resources.getStringArray(
R.array.search_engines_values
).toMutableList()
if (prefs.openWithUrl.isNotEmpty()) {
names.add(prefs.openWithUrl)
urls.add(prefs.openWithUrl)
}
val queryUri = suspendCoroutine<String?> { continuation ->
AlertDialog.Builder(context)
.setTitle(R.string.pick_search_engine)
.setItems(names.toTypedArray()) { _, which ->
continuation.resume(urls[which] + URLEncoder.encode(query, "utf-8"))
}
.setOnCancelListener {
continuation.resume(null)
}
.show()
} ?: return null
return openUri(context, queryUri, false)
}
}
@@ -4,9 +4,9 @@ import android.content.Context
import android.content.Intent
import android.net.Uri
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.actions.SimpleIntentIAction
import de.markusfisch.android.binaryeye.actions.IntentAction
object SmsAction : SimpleIntentIAction() {
object SmsAction : IntentAction() {
private val smsRegex =
"""^sms(?:to)?:(\+?[0-9]+)(?::([\S\s]*))?$""".toRegex(RegexOption.IGNORE_CASE)
@@ -18,7 +18,7 @@ object SmsAction : SimpleIntentIAction() {
return String(data).matches(smsRegex)
}
override fun executeForIntent(context: Context, data: ByteArray): Intent? {
override suspend fun createIntent(context: Context, data: ByteArray): Intent? {
val (number: String, message: String) = smsRegex.matchEntire(
String(data)
)?.let {
@@ -2,11 +2,11 @@ package de.markusfisch.android.binaryeye.actions.tel
import android.content.Context
import android.content.Intent
import android.net.Uri
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.actions.SimpleIntentIAction
import de.markusfisch.android.binaryeye.actions.IntentAction
import de.markusfisch.android.binaryeye.app.parseAndNormalizeUri
object TelAction : SimpleIntentIAction() {
object TelAction : IntentAction() {
private val telRegex = """^tel:(\+?[0-9]+)$""".toRegex(RegexOption.IGNORE_CASE)
override val iconResId: Int = R.drawable.ic_action_tel
@@ -17,7 +17,7 @@ object TelAction : SimpleIntentIAction() {
return String(data).matches(telRegex)
}
override fun executeForIntent(context: Context, data: ByteArray): Intent {
return Intent(Intent.ACTION_DIAL, Uri.parse(String(data)))
override suspend fun createIntent(context: Context, data: ByteArray): Intent {
return Intent(Intent.ACTION_DIAL, parseAndNormalizeUri(String(data)))
}
}
@@ -6,10 +6,10 @@ import android.content.Intent
import android.os.Build
import android.provider.ContactsContract
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.actions.SimpleIntentIAction
import de.markusfisch.android.binaryeye.actions.IntentAction
import de.markusfisch.android.binaryeye.actions.vtype.VTypeParser
object VCardAction : SimpleIntentIAction() {
object VCardAction : IntentAction() {
override val iconResId: Int
get() = R.drawable.ic_action_vcard
override val titleResId: Int
@@ -21,7 +21,7 @@ object VCardAction : SimpleIntentIAction() {
return VTypeParser.parseVType(String(data)) == "VCARD"
}
override fun executeForIntent(context: Context, data: ByteArray): Intent? {
override suspend fun createIntent(context: Context, data: ByteArray): Intent? {
val info = VTypeParser.parseMap(String(data))
return Intent(Intent.ACTION_INSERT_OR_EDIT).apply {
@@ -7,12 +7,12 @@ import android.os.Build
import android.provider.CalendarContract
import android.support.annotation.RequiresApi
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.actions.SimpleIntentIAction
import de.markusfisch.android.binaryeye.actions.IntentAction
import de.markusfisch.android.binaryeye.actions.vtype.VTypeParser
import java.text.SimpleDateFormat
import java.util.*
import java.util.Date
object VEventAction : SimpleIntentIAction() {
object VEventAction : IntentAction() {
override val iconResId: Int
get() = R.drawable.ic_action_vevent
override val titleResId: Int
@@ -26,7 +26,7 @@ object VEventAction : SimpleIntentIAction() {
}
@RequiresApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
override fun executeForIntent(context: Context, data: ByteArray): Intent? {
override suspend fun createIntent(context: Context, data: ByteArray): Intent? {
val info = VTypeParser.parseMap(String(data))
return Intent(Intent.ACTION_EDIT).apply {
@@ -0,0 +1,11 @@
package de.markusfisch.android.binaryeye.actions.web
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.actions.SchemeAction
object WebAction : SchemeAction() {
override val iconResId: Int = R.drawable.ic_action_open
override val titleResId: Int = R.string.open_url
override val scheme: String = "https?"
override val buildRegex: Boolean = true
}
@@ -7,7 +7,10 @@ import android.net.wifi.WifiManager
import android.widget.Toast
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.actions.IAction
import kotlinx.coroutines.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeoutOrNull
object WifiAction : IAction {
override val iconResId = R.drawable.ic_action_wifi
@@ -16,31 +19,29 @@ object WifiAction : IAction {
override fun canExecuteOn(data: ByteArray): Boolean =
WifiConfigurationFactory.parse(String(data)) != null
override fun execute(context: Context, data: ByteArray) {
CoroutineScope(Dispatchers.IO).launch {
val wifiConfig = WifiConfigurationFactory.parse(
String(data)
) ?: return@launch
val wifiManager = context.applicationContext.getSystemService(
WIFI_SERVICE
) as WifiManager
override suspend fun execute(context: Context, data: ByteArray) = withContext(Dispatchers.IO) {
val wifiConfig = WifiConfigurationFactory.parse(
String(data)
) ?: return@withContext
val wifiManager = context.applicationContext.getSystemService(
WIFI_SERVICE
) as WifiManager
wifiManager.enableWifi(context)
wifiManager.mayRemoveOldNetwork(wifiConfig)
wifiManager.enableNewNetwork(wifiConfig)
withContext(Dispatchers.Main) {
Toast.makeText(
context,
R.string.wifi_added,
Toast.LENGTH_LONG
).show()
}
wifiManager.enableWifi(context)
wifiManager.mayRemoveOldNetwork(wifiConfig)
wifiManager.enableNewNetwork(wifiConfig)
withContext(Dispatchers.Main) {
Toast.makeText(
context,
R.string.wifi_added,
Toast.LENGTH_LONG
).show()
}
}
private suspend fun WifiManager.enableWifi(context: Context): Boolean {
if (!this.isWifiEnabled) {
if (!this.setWifiEnabled(true)) {
if (!isWifiEnabled) {
if (!setWifiEnabled(true)) {
withContext(Dispatchers.Main) {
Toast.makeText(
context,
@@ -50,20 +51,18 @@ object WifiAction : IAction {
}
return false
}
var i = 0
while (!this.isWifiEnabled) {
if (i >= 10) {
withContext(Dispatchers.Main) {
Toast.makeText(
context,
R.string.wifi_config_failed,
Toast.LENGTH_LONG
).show()
}
return false
// wait for wifi to really be enabled, takes some time
withTimeoutOrNull(10000) {
while (!isWifiEnabled) delay(500)
} ?: if (!isWifiEnabled) {
withContext(Dispatchers.Main) {
Toast.makeText(
context,
R.string.wifi_config_failed,
Toast.LENGTH_LONG
).show()
}
delay(1000)
i++
return false
}
}
return true
@@ -74,7 +73,7 @@ object WifiAction : IAction {
) {
configuredNetworks?.firstOrNull {
it.SSID == wifiConfig.SSID &&
it.allowedKeyManagement == wifiConfig.allowedKeyManagement
it.allowedKeyManagement == wifiConfig.allowedKeyManagement
}?.networkId?.also {
removeNetwork(it)
}
@@ -1,7 +1,12 @@
package de.markusfisch.android.binaryeye.app
import java.util.regex.Pattern
import android.net.Uri
import android.os.Build
private val nonPrintable = Pattern.compile("[\\x00-\\x08\\x0e-\\x1f]")
private val nonPrintable = "[\\x00-\\x08\\x0e-\\x1f]".toRegex()
fun hasNonPrintableCharacters(s: String) = nonPrintable.matcher(s).find()
fun hasNonPrintableCharacters(s: String) = nonPrintable.containsMatchIn(s)
fun parseAndNormalizeUri(input: String): Uri = Uri.parse(input).let {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) it.normalizeScheme() else it
}
@@ -3,28 +3,35 @@ package de.markusfisch.android.binaryeye.fragment
import android.annotation.SuppressLint
import android.app.AlertDialog
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Environment
import android.support.v4.app.Fragment
import android.text.ClipboardManager
import android.text.Editable
import android.text.TextWatcher
import android.view.*
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.EditText
import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast
import com.google.zxing.BarcodeFormat
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.actions.IAction
import de.markusfisch.android.binaryeye.actions.validateOrGetNew
import de.markusfisch.android.binaryeye.app.*
import de.markusfisch.android.binaryeye.actions.ActionRegistry
import de.markusfisch.android.binaryeye.app.addFragment
import de.markusfisch.android.binaryeye.app.hasNonPrintableCharacters
import de.markusfisch.android.binaryeye.app.hasWritePermission
import de.markusfisch.android.binaryeye.app.shareText
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import java.io.File
import java.io.IOException
import java.net.URLEncoder
class DecodeFragment : Fragment() {
private lateinit var contentView: EditText
@@ -33,11 +40,14 @@ class DecodeFragment : Fragment() {
private lateinit var format: BarcodeFormat
private lateinit var actionMenuItem: MenuItem
private var action: IAction? = null
private var action = ActionRegistry.DEFAULT_ACTION
private var isBinary = false
private val content: String
get() = contentView.text.toString()
private val parentJob = Job()
private val scope: CoroutineScope = CoroutineScope(Dispatchers.Main + parentJob)
override fun onCreate(state: Bundle?) {
super.onCreate(state)
setHasOptionsMenu(true)
@@ -108,7 +118,10 @@ class DecodeFragment : Fragment() {
}
private fun updateViewsAndAction(bytes: ByteArray) {
action = action.validateOrGetNew(bytes)
val prevAction = action
if (!prevAction.canExecuteOn(bytes)) {
action = ActionRegistry.getAction(bytes)
}
formatView.text = resources.getQuantityString(
R.plurals.barcode_info,
bytes.size,
@@ -116,19 +129,17 @@ class DecodeFragment : Fragment() {
bytes.size
)
hexView.text = hexDump(bytes, 33)
if (::actionMenuItem.isInitialized) {
actionMenuItem.setIcon(action?.iconResId ?: R.drawable.ic_action_open)
actionMenuItem.setTitle(action?.titleResId ?: R.string.open_url)
if (::actionMenuItem.isInitialized && prevAction !== action) {
actionMenuItem.setIcon(action.iconResId)
actionMenuItem.setTitle(action.titleResId)
}
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.fragment_decode, menu)
actionMenuItem = menu.findItem(R.id.open_url)
action?.also { action ->
actionMenuItem.setIcon(action.iconResId)
actionMenuItem.setTitle(action.titleResId)
}
actionMenuItem.setIcon(action.iconResId)
actionMenuItem.setTitle(action.titleResId)
if (isBinary) {
menu.findItem(R.id.copy_to_clipboard).isVisible = false
menu.findItem(R.id.open_url).isVisible = false
@@ -143,7 +154,7 @@ class DecodeFragment : Fragment() {
true
}
R.id.open_url -> {
openUrl(content)
executeAction(content.toByteArray())
true
}
R.id.create -> {
@@ -171,56 +182,10 @@ class DecodeFragment : Fragment() {
).show()
}
private fun openUrl(
url: String,
executeCustomAction: Boolean = true,
searchIfNoUrl: Boolean = true
) {
if (activity == null || url.isEmpty()) {
return
private fun executeAction(content: ByteArray) {
if (activity != null && content.isNotEmpty()) scope.launch {
action.execute(activity, content)
}
var uri = Uri.parse(url)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
uri = uri.normalizeScheme()
}
val intent = Intent(Intent.ACTION_VIEW, uri)
when {
executeCustomAction && action != null -> action?.also { action ->
action.execute(activity, url.toByteArray())
}
intent.resolveActivity(activity.packageManager) != null -> {
startActivity(intent)
}
searchIfNoUrl -> pickSearchEngineAndSearch(activity, url)
else -> Toast.makeText(
activity,
R.string.cannot_resolve_action,
Toast.LENGTH_SHORT
).show()
}
}
private fun pickSearchEngineAndSearch(context: Context, query: String) {
val names = context.resources.getStringArray(
R.array.search_engines_names
).toMutableList()
val urls = context.resources.getStringArray(
R.array.search_engines_values
).toMutableList()
if (prefs.openWithUrl.isNotEmpty()) {
names.add(prefs.openWithUrl)
urls.add(prefs.openWithUrl)
}
AlertDialog.Builder(context)
.setTitle(R.string.pick_search_engine)
.setItems(names.toTypedArray()) { _, which ->
openUrl(
urls[which] + URLEncoder.encode(query, "utf-8"),
executeCustomAction = false,
searchIfNoUrl = false
)
}
.show()
}
// dialogs don't have a parent layout and must therefore be
@@ -249,6 +214,11 @@ class DecodeFragment : Fragment() {
.show()
}
override fun onDestroyView() {
parentJob.cancel()
super.onDestroyView()
}
companion object {
private const val CONTENT = "content"
private const val FORMAT = "format"
@@ -0,0 +1,6 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp"
android:tint="#FFFFFF" android:viewportHeight="24.0"
android:viewportWidth="24.0" android:width="24dp">
<path android:fillColor="#FF000000"
android:pathData="M15.5,14h-0.79l-0.28,-0.27C15.41,12.59 16,11.11 16,9.5 16,5.91 13.09,3 9.5,3S3,5.91 3,9.5 5.91,16 9.5,16c1.61,0 3.09,-0.59 4.23,-1.57l0.27,0.28v0.79l5,4.99L20.49,19l-4.99,-5zM9.5,14C7.01,14 5,11.99 5,9.5S7.01,5 9.5,5 14,7.01 14,9.5 11.99,14 9.5,14z"/>
</vector>
+1
View File
@@ -57,4 +57,5 @@
<string name="vevent_failed">Konnte nicht zum Kalender hinzufügen</string>
<string name="otpauth_add">2FA hinzufügen</string>
<string name="otpauth_error">Konnte 2FA nicht hinzufügen</string>
<string name="search_web">Suche im Internet</string>
</resources>
+1
View File
@@ -57,4 +57,5 @@
<string name="vevent_failed">Could not add to calendar</string>
<string name="otpauth_add">Add 2FA</string>
<string name="otpauth_error">Could not add 2FA</string>
<string name="search_web">Search the web</string>
</resources>
+1
View File
@@ -57,4 +57,5 @@
<string name="vevent_failed">Could not add to calendar</string>
<string name="otpauth_add">Add 2FA</string>
<string name="otpauth_error">Could not add 2FA</string>
<string name="search_web">Search the web</string>
</resources>
+1
View File
@@ -57,4 +57,5 @@
<string name="vevent_failed">Tidak bisa menambahkan ke kalender</string>
<string name="otpauth_add">Add 2FA</string>
<string name="otpauth_error">Could not add 2FA</string>
<string name="search_web">Search the web</string>
</resources>
+1
View File
@@ -57,4 +57,5 @@
<string name="vevent_failed">Could not add to calendar</string>
<string name="otpauth_add">Add 2FA</string>
<string name="otpauth_error">Could not add 2FA</string>
<string name="search_web">Search the web</string>
</resources>
+1
View File
@@ -57,4 +57,5 @@
<string name="vevent_failed">Could not add to calendar</string>
<string name="otpauth_add">Add 2FA</string>
<string name="otpauth_error">Could not add 2FA</string>
<string name="search_web">Search the web</string>
</resources>
@@ -57,4 +57,5 @@
<string name="vevent_failed">Não foi possível adicionar ao calendário</string>
<string name="otpauth_add">Add 2FA</string>
<string name="otpauth_error">Could not add 2FA</string>
<string name="search_web">Search the web</string>
</resources>
@@ -55,4 +55,5 @@
<string name="otpauth_add">Добавить 2FA</string>
<string name="otpauth_error">Не удалось добавить 2FA</string>
<string name="app_name">Binary Eye</string>
<string name="search_web">Search the web</string>
</resources>
@@ -57,4 +57,5 @@
<string name="vevent_failed">无法添加到日历</string>
<string name="otpauth_add">Add 2FA</string>
<string name="otpauth_error">Could not add 2FA</string>
<string name="search_web">Search the web</string>
</resources>
+1
View File
@@ -57,4 +57,5 @@
<string name="open_with_url">Preferences</string>
<string name="otpauth_add">Add 2FA</string>
<string name="otpauth_error">Could not add 2FA</string>
<string name="search_web">Search the web</string>
</resources>
+1
View File
@@ -57,4 +57,5 @@
<string name="vevent_failed">Could not add to calendar</string>
<string name="otpauth_add">Add 2FA</string>
<string name="otpauth_error">Could not add 2FA</string>
<string name="search_web">Search the web</string>
</resources>
+1 -1
View File
@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.1-all.zip