feat(android): QR pairing — ZXing scanner + ScanPairingActivity + strings PT-PT

- Adiciona dependência zxing-android-embedded:4.3.0
- Adiciona permissão CAMERA e regista ScanPairingActivity no Manifest
- Cria ScanPairingActivity: scan QR → parse JSON → POST claim-device
- Adiciona preferência "Emparelhar dispositivo" nas definições do servidor
- Adiciona handler de clique em WhatSmsServerSettingsFragment
- Strings PT-PT: scan_qr_to_pair, pairing_success/failed/cancelled/error
- Bump versionName para 3.2.0

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-04-23 00:55:46 +01:00
parent 05efaf185c
commit a3285cc4a2
6 changed files with 473 additions and 4 deletions
+5
View File
@@ -7,6 +7,7 @@
android:required="false" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE" />
@@ -84,6 +85,10 @@
android:foregroundServiceType="dataSync"
tools:node="merge" />
<activity
android:name=".ui.ScanPairingActivity"
android:exported="false" />
<activity
android:name=".MainActivity"
android:exported="true">
@@ -0,0 +1,99 @@
package me.capcom.smsgateway.ui
import android.app.Activity
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import com.google.zxing.integration.android.IntentIntegrator
import com.google.zxing.integration.android.IntentResult
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import me.capcom.smsgateway.R
import me.capcom.smsgateway.modules.gateway.GatewaySettings
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import org.json.JSONObject
import org.koin.android.ext.android.inject
class ScanPairingActivity : AppCompatActivity() {
private val settings: GatewaySettings by inject()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
IntentIntegrator(this).apply {
setPrompt(getString(R.string.scan_qr_to_pair))
setBeepEnabled(true)
setOrientationLocked(false)
initiateScan()
}
}
@Suppress("OVERRIDE_DEPRECATION")
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
val result: IntentResult =
IntentIntegrator.parseActivityResult(requestCode, resultCode, data)
?: return super.onActivityResult(requestCode, resultCode, data)
if (result.contents == null) {
Toast.makeText(this, R.string.pairing_cancelled, Toast.LENGTH_SHORT).show()
setResult(Activity.RESULT_CANCELED)
finish()
return
}
claimDevice(result.contents)
}
private fun claimDevice(qrContent: String) {
val deviceId = settings.deviceId
val username = settings.username
val password = settings.password
if (deviceId == null || username == null || password == null) {
Toast.makeText(this, R.string.pairing_not_registered, Toast.LENGTH_LONG).show()
setResult(Activity.RESULT_CANCELED)
finish()
return
}
CoroutineScope(Dispatchers.IO).launch {
try {
val qr = JSONObject(qrContent)
val claimUrl = qr.getString("claimUrl")
val pairingToken = qr.getString("pairingToken")
val body = JSONObject().apply {
put("pairingToken", pairingToken)
put("deviceId", deviceId)
put("username", username)
put("password", password)
put("deviceName", Build.MODEL)
}.toString()
val client = OkHttpClient()
val request = Request.Builder()
.url(claimUrl)
.post(body.toRequestBody("application/json".toMediaType()))
.build()
val response = client.newCall(request).execute()
val ok = response.isSuccessful
withContext(Dispatchers.Main) {
if (ok) {
Toast.makeText(this@ScanPairingActivity, R.string.pairing_success, Toast.LENGTH_SHORT).show()
setResult(Activity.RESULT_OK)
} else {
Toast.makeText(this@ScanPairingActivity, R.string.pairing_failed, Toast.LENGTH_LONG).show()
setResult(Activity.RESULT_CANCELED)
}
finish()
}
} catch (e: Exception) {
withContext(Dispatchers.Main) {
Toast.makeText(this@ScanPairingActivity, getString(R.string.pairing_error, e.message), Toast.LENGTH_LONG).show()
setResult(Activity.RESULT_CANCELED)
finish()
}
}
}
}
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +1,57 @@
PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPFByZWZlcmVuY2VTY3JlZW4geG1sbnM6YW5kcm9pZD0iaHR0cDovL3NjaGVtYXMuYW5kcm9pZC5jb20vYXBrL3Jlcy9hbmRyb2lkIgogICAgeG1sbnM6YXBwPSJodHRwOi8vc2NoZW1hcy5hbmRyb2lkLmNvbS9hcGsvcmVzLWF1dG8iPgogICAgPFByZWZlcmVuY2VDYXRlZ29yeSBhcHA6dGl0bGU9IkBzdHJpbmcvc2VydmVyIj4KICAgICAgICA8UHJlZmVyZW5jZQogICAgICAgICAgICBhcHA6ZW5hYmxlQ29weWluZz0idHJ1ZSIKICAgICAgICAgICAgYXBwOmljb249IkBkcmF3YWJsZS9pY19zZXJ2ZXIiCiAgICAgICAgICAgIGFwcDprZXk9InRyYW5zaWVudC5zZXJ2ZXJfdXJsIgogICAgICAgICAgICBhcHA6cGVyc2lzdGVudD0iZmFsc2UiCiAgICAgICAgICAgIGFwcDpzZWxlY3RhYmxlPSJmYWxzZSIKICAgICAgICAgICAgYXBwOnRpdGxlPSJAc3RyaW5nL2FwaV91cmwiIC8+CiAgICAgICAgPExpc3RQcmVmZXJlbmNlCiAgICAgICAgICAgIGFwcDppY29uPSJAZHJhd2FibGUvaWNfbm90aWZpY2F0aW9ucyIKICAgICAgICAgICAgYXBwOmtleT0iZ2F0ZXdheS5ub3RpZmljYXRpb25fY2hhbm5lbCIKICAgICAgICAgICAgYXBwOmRlZmF1bHRWYWx1ZT0iQVVUTyIKICAgICAgICAgICAgYXBwOmVudHJpZXM9IkBhcnJheS9ub3RpZmljYXRpb25fY2hhbm5lbHNfdGl0bGVzIgogICAgICAgICAgICBhcHA6ZW50cnlWYWx1ZXM9IkBhcnJheS9ub3RpZmljYXRpb25fY2hhbm5lbHNfdmFsdWVzIgogICAgICAgICAgICBhcHA6dGl0bGU9IkBzdHJpbmcvbm90aWZpY2F0aW9uX2NoYW5uZWwiCiAgICAgICAgICAgIGFwcDp1c2VTaW1wbGVTdW1tYXJ5UHJvdmlkZXI9InRydWUiIC8+CiAgICA8L1ByZWZlcmVuY2VDYXRlZ29yeT4KICAgIDxQcmVmZXJlbmNlQ2F0ZWdvcnkgYXBwOnRpdGxlPSJAc3RyaW5nL2NyZWRlbnRpYWxzIj4KICAgICAgICA8RWRpdFRleHRQcmVmZXJlbmNlCiAgICAgICAgICAgIGFwcDplbmFibGVDb3B5aW5nPSJ0cnVlIgogICAgICAgICAgICBhcHA6aWNvbj0iQGRyYXdhYmxlL2ljX3VzZXJuYW1lIgogICAgICAgICAgICBhcHA6a2V5PSJnYXRld2F5LnVzZXJuYW1lIgogICAgICAgICAgICBhcHA6cGVyc2lzdGVudD0iZmFsc2UiCiAgICAgICAgICAgIGFwcDpzZWxlY3RhYmxlPSJmYWxzZSIKICAgICAgICAgICAgYXBwOnRpdGxlPSJAc3RyaW5nL3VzZXJuYW1lIiAvPgogICAgICAgIDxFZGl0VGV4dFByZWZlcmVuY2UKICAgICAgICAgICAgYXBwOmVuYWJsZUNvcHlpbmc9InRydWUiCiAgICAgICAgICAgIGFwcDppY29uPSJAZHJhd2FibGUvaWNfcGFzc3dvcmQiCiAgICAgICAgICAgIGFwcDprZXk9ImdhdGV3YXkucGFzc3dvcmQiCiAgICAgICAgICAgIGFwcDpwZXJzaXN0ZW50PSJmYWxzZSIKICAgICAgICAgICAgYXBwOnRpdGxlPSJAc3RyaW5nL3Bhc3N3b3JkIiAvPgogICAgICAgIDxQcmVmZXJlbmNlCiAgICAgICAgICAgIGFuZHJvaWQ6aWNvbj0iQGRyYXdhYmxlL2ljX2NvZGUiCiAgICAgICAgICAgIGFuZHJvaWQ6a2V5PSJnYXRld2F5LmxvZ2luX2NvZGUiCiAgICAgICAgICAgIGFuZHJvaWQ6cGVyc2lzdGVudD0iZmFsc2UiCiAgICAgICAgICAgIGFuZHJvaWQ6c3VtbWFyeT0iQHN0cmluZy91c2VfdGhpc19jb2RlX3RvX3NpZ25faW5fb25fYW5vdGhlcl9kZXZpY2UiCiAgICAgICAgICAgIGFuZHJvaWQ6dGl0bGU9IkBzdHJpbmcvbG9naW5fY29kZSIKICAgICAgICAgICAgYXBwOmVuYWJsZUNvcHlpbmc9InRydWUiIC8+CiAgICA8L1ByZWZlcmVuY2VDYXRlZ29yeT4KICAgIDxQcmVmZXJlbmNlQ2F0ZWdvcnkgYXBwOnRpdGxlPSJAc3RyaW5nL2RldmljZSI+CiAgICAgICAgPFByZWZlcmVuY2UKICAgICAgICAgICAgYW5kcm9pZDppY29uPSJAZHJhd2FibGUvaWNfZGV2aWNlX2lkIgogICAgICAgICAgICBhbmRyb2lkOmtleT0idHJhbnNpZW50LmRldmljZV9pZCIKICAgICAgICAgICAgYW5kcm9pZDp0aXRsZT0iQHN0cmluZy9kZXZpY2VfaWQiCiAgICAgICAgICAgIGFwcDplbmFibGVDb3B5aW5nPSJ0cnVlIgogICAgICAgICAgICBhcHA6cGVyc2lzdGVudD0iZmFsc2UiIC8+CiAgICA8L1ByZWZlcmVuY2VDYXRlZ29yeT4KICAgIDxQcmVmZXJlbmNlIGFwcDpzdW1tYXJ5PSJAc3RyaW5nL3Jlc3RhcnRfcmVxdWlyZWRfdG9fYXBwbHlfY2hhbmdlcyIgLz4KPC9QcmVmZXJlbmNlU2NyZWVuPgo=
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<PreferenceCategory app:title="@string/server">
<Preference
app:enableCopying="true"
app:icon="@drawable/ic_server"
app:key="transient.server_url"
app:persistent="false"
app:selectable="false"
app:title="@string/api_url" />
<ListPreference
app:icon="@drawable/ic_notifications"
app:key="gateway.notification_channel"
app:defaultValue="AUTO"
app:entries="@array/notification_channels_titles"
app:entryValues="@array/notification_channels_values"
app:title="@string/notification_channel"
app:useSimpleSummaryProvider="true" />
</PreferenceCategory>
<PreferenceCategory app:title="@string/credentials">
<EditTextPreference
app:enableCopying="true"
app:icon="@drawable/ic_username"
app:key="gateway.username"
app:persistent="false"
app:selectable="false"
app:title="@string/username" />
<EditTextPreference
app:enableCopying="true"
app:icon="@drawable/ic_password"
app:key="gateway.password"
app:persistent="false"
app:title="@string/password" />
<Preference
android:icon="@drawable/ic_code"
android:key="gateway.login_code"
android:persistent="false"
android:summary="@string/use_this_code_to_sign_in_on_another_device"
android:title="@string/login_code"
app:enableCopying="true" />
</PreferenceCategory>
<PreferenceCategory app:title="@string/device">
<Preference
android:icon="@drawable/ic_device_id"
android:key="transient.device_id"
android:title="@string/device_id"
app:enableCopying="true"
app:persistent="false" />
<Preference
app:key="action.pair_device"
app:icon="@drawable/ic_cloud_server"
app:title="@string/pair_device"
app:summary="@string/pair_device_summary" />
</PreferenceCategory>
<Preference app:summary="@string/restart_required_to_apply_changes" />
</PreferenceScreen>