Add auth key activity

This commit is contained in:
José Rebelo
2025-07-26 16:25:50 +02:00
committed by José Rebelo
parent 3cc6eafc49
commit f6316777a1
14 changed files with 271 additions and 20 deletions
+4
View File
@@ -664,6 +664,10 @@
android:name=".activities.discovery.DiscoveryActivityV2"
android:label="@string/title_activity_discovery"
android:parentActivityName=".activities.ControlCenterv2" />
<activity
android:name=".activities.AuthKeyActivity"
android:label="@string/pref_title_authkey"
android:parentActivityName=".activities.discovery.DiscoveryActivityV2" />
<activity
android:name=".activities.AndroidPairingActivity"
android:label="@string/title_activity_android_pairing" />
@@ -0,0 +1,128 @@
package nodomain.freeyourgadget.gadgetbridge.activities
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.widget.Toast
import androidx.core.content.edit
import androidx.core.net.toUri
import nodomain.freeyourgadget.gadgetbridge.GBApplication
import nodomain.freeyourgadget.gadgetbridge.R
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst
import nodomain.freeyourgadget.gadgetbridge.databinding.ActivityAuthKeyBinding
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate
import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper
import nodomain.freeyourgadget.gadgetbridge.util.GB
class AuthKeyActivity : AbstractGBActivity() {
private lateinit var binding: ActivityAuthKeyBinding
private var deviceCandidate: GBDeviceCandidate? = null
private var coordinator: DeviceCoordinator? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityAuthKeyBinding.inflate(layoutInflater)
setContentView(binding.root)
@Suppress("DEPRECATION")
deviceCandidate = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU) {
intent.getParcelableExtra(EXTRA_DEVICE_CANDIDATE, GBDeviceCandidate::class.java)
} else {
intent.getParcelableExtra(EXTRA_DEVICE_CANDIDATE)
}
deviceCandidate?.let { candidate ->
coordinator = DeviceHelper.getInstance().resolveDeviceType(candidate).deviceCoordinator
}
if (deviceCandidate == null || coordinator == null) {
Toast.makeText(this, "Device info missing", Toast.LENGTH_LONG).show()
finish()
return
}
title = getString(
R.string.auth_key_activity_title_for,
deviceCandidate?.name ?: getString(R.string.devicetype_unknown)
)
deviceCandidate?.macAddress?.let { macAddress ->
val sharedPrefs = GBApplication.getDeviceSpecificSharedPrefs(macAddress)
val authKey = sharedPrefs.getString(DeviceSettingsPreferenceConst.PREF_AUTH_KEY, "")
if (authKey?.isNotEmpty() == true) {
binding.authKeyEditText.setText(authKey)
}
}
binding.submitAuthKeyButton.setOnClickListener {
val authKey = binding.authKeyEditText.text.toString().trim()
if (authKey.isEmpty()) {
binding.authKeyInputLayout.error = getString(R.string.auth_key_required_message)
return@setOnClickListener
} else {
// Clear error
binding.authKeyInputLayout.error = null
}
if (coordinator?.validateAuthKey(authKey) == true) {
// Save the auth key
deviceCandidate?.macAddress?.let { macAddress ->
val sharedPrefs = GBApplication.getDeviceSpecificSharedPrefs(macAddress)
sharedPrefs.edit { putString(DeviceSettingsPreferenceConst.PREF_AUTH_KEY, authKey) }
}
// Return the auth key and candidate to the calling activity
val resultIntent = Intent().apply {
putExtra(EXTRA_AUTH_KEY_RESULT, authKey)
putExtra(EXTRA_DEVICE_CANDIDATE_RESULT, deviceCandidate)
}
setResult(RESULT_OK, resultIntent)
finish()
} else {
binding.authKeyInputLayout.error = getString(R.string.invalid_auth_key_message)
}
}
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.menu_auth_key, menu)
val helpItem = menu.findItem(R.id.auth_key_help)
helpItem?.isVisible = coordinator?.authHelp != null
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.auth_key_help -> {
coordinator?.authHelp?.let { url ->
val intent = Intent(Intent.ACTION_VIEW, coordinator?.authHelp!!.toUri())
try {
startActivity(intent)
} catch (e: Exception) {
GB.toast(this, getString(R.string.cannot_open_help_url), Toast.LENGTH_SHORT, GB.ERROR, e)
}
} ?: GB.toast(this, "No help URL available.", Toast.LENGTH_SHORT, GB.ERROR)
true
}
else -> super.onOptionsItemSelected(item)
}
}
companion object {
const val EXTRA_DEVICE_CANDIDATE = "EXTRA_DEVICE_CANDIDATE_FOR_AUTH"
const val EXTRA_AUTH_KEY_RESULT = "EXTRA_AUTH_KEY_RESULT"
const val EXTRA_DEVICE_CANDIDATE_RESULT = "EXTRA_DEVICE_CANDIDATE_RESULT"
fun newIntent(context: Context, deviceCandidate: GBDeviceCandidate): Intent {
return Intent(context, AuthKeyActivity::class.java).apply {
putExtra(EXTRA_DEVICE_CANDIDATE, deviceCandidate)
}
}
}
}
@@ -85,6 +85,7 @@ import java.util.Set;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBActivity;
import nodomain.freeyourgadget.gadgetbridge.activities.AuthKeyActivity;
import nodomain.freeyourgadget.gadgetbridge.activities.DebugActivity;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsActivity;
import nodomain.freeyourgadget.gadgetbridge.adapter.DeviceCandidateAdapter;
@@ -128,6 +129,8 @@ public class DiscoveryActivityV2 extends AbstractGBActivity implements AdapterVi
private Button startButton;
private boolean scanning;
private ActivityResultLauncher<Intent> authKeyLauncher;
private long selectedUnsupportedDeviceKey = DebugActivity.SELECT_DEVICE;
private final Runnable stopRunnable = () -> {
@@ -194,6 +197,22 @@ public class DiscoveryActivityV2 extends AbstractGBActivity implements AdapterVi
checkAndRequestLocationPermission();
authKeyLauncher = registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(),
result -> {
if (result.getResultCode() == Activity.RESULT_OK) {
final Intent data = result.getData();
if (data == null) {
// Should never happen
GB.toast(this, "Auth data is null", Toast.LENGTH_LONG, GB.ERROR);
return;
}
final GBDeviceCandidate deviceCandidate = data.getParcelableExtra(AuthKeyActivity.EXTRA_DEVICE_CANDIDATE_RESULT);
final DeviceType deviceType = DeviceHelper.getInstance().resolveDeviceType(deviceCandidate);
startPair(deviceCandidate, deviceType.getDeviceCoordinator());
}
});
if (!startDiscovery()) {
/* if we couldn't start scanning, go back to the main page.
A toast will have been shown explaining what's wrong */
@@ -297,7 +316,7 @@ public class DiscoveryActivityV2 extends AbstractGBActivity implements AdapterVi
try {
final Method isConnectedMethod = device.getClass().getMethod("isConnected");
final Boolean isConnected = (Boolean) isConnectedMethod.invoke(device);
if (isConnected!= null && isConnected) {
if (isConnected != null && isConnected) {
LOG.debug("Pre-adding already bonded device {}", device.getAddress());
deviceFoundProcessor.scheduleProcessing(new GBScanEvent(device, (short) -1, null));
}
@@ -528,7 +547,8 @@ public class DiscoveryActivityV2 extends AbstractGBActivity implements AdapterVi
private void showWarnDialog(@StringRes final int message) {
new MaterialAlertDialogBuilder(getContext())
.setMessage(message)
.setPositiveButton(R.string.ok, (dialog, whichButton) -> {})
.setPositiveButton(R.string.ok, (dialog, whichButton) -> {
})
.show();
}
@@ -630,29 +650,25 @@ public class DiscoveryActivityV2 extends AbstractGBActivity implements AdapterVi
final DeviceCoordinator coordinator = deviceType.getDeviceCoordinator();
LOG.info("Using device candidate {} with coordinator {}", deviceCandidate, coordinator.getClass());
if (coordinator.getBondingStyle() == DeviceCoordinator.BONDING_STYLE_REQUIRE_KEY) {
final SharedPreferences sharedPrefs = GBApplication.getDeviceSpecificSharedPrefs(deviceCandidate.getMacAddress());
final String authKey = sharedPrefs.getString("authkey", null);
if (authKey == null || authKey.isEmpty()) {
showWarnDialog(R.string.discovery_need_to_enter_authkey);
return;
} else if (!coordinator.validateAuthKey(authKey)) {
showWarnDialog(R.string.discovery_entered_invalid_authkey);
return;
}
}
if (coordinator.suggestUnbindBeforePair() && deviceCandidate.isBonded()) {
new MaterialAlertDialogBuilder(getContext())
.setTitle(R.string.unbind_before_pair_title)
.setMessage(R.string.unbind_before_pair_message)
.setIcon(R.drawable.ic_warning_gray)
.setPositiveButton(R.string.ok, (dialog, whichButton) -> {
startPair(deviceCandidate, coordinator);
checkAuthKeyAndPair(deviceCandidate, coordinator);
})
.setNegativeButton(android.R.string.cancel, null)
.show();
} else {
checkAuthKeyAndPair(deviceCandidate, coordinator);
}
}
private void checkAuthKeyAndPair(final GBDeviceCandidate deviceCandidate, final DeviceCoordinator coordinator) {
if (coordinator.getBondingStyle() == DeviceCoordinator.BONDING_STYLE_REQUIRE_KEY) {
final Intent authIntent = AuthKeyActivity.Companion.newIntent(this, deviceCandidate);
authKeyLauncher.launch(authIntent);
} else {
startPair(deviceCandidate, coordinator);
}
@@ -83,9 +83,6 @@ public class DeviceCandidateAdapter extends ArrayAdapter<GBDeviceCandidate> {
if (coordinator.isExperimental()) {
statusLines.add(getContext().getString(R.string.device_experimental));
}
if (coordinator.getBondingStyle() == DeviceCoordinator.BONDING_STYLE_REQUIRE_KEY) {
statusLines.add(getContext().getString(R.string.device_requires_key));
}
deviceStatus.setText(TextUtils.join("\n", statusLines));
return view;
@@ -1004,6 +1004,11 @@ public abstract class AbstractDeviceCoordinator implements DeviceCoordinator {
return !(authKey.getBytes().length < 34 || !authKey.startsWith("0x"));
}
@Nullable
public String getAuthHelp() {
return null;
}
@Override
public List<DeviceCardAction> getCustomActions() {
return Collections.emptyList();
@@ -861,5 +861,8 @@ public interface DeviceCoordinator {
boolean validateAuthKey(String authKey);
@Nullable
String getAuthHelp();
List<DeviceCardAction> getCustomActions();
}
@@ -155,6 +155,12 @@ public class CmfWatchProCoordinator extends AbstractBLEDeviceCoordinator {
return authKeyBytes.length == 32 || (authKey.startsWith("0x") && authKeyBytes.length == 34);
}
@Nullable
@Override
public String getAuthHelp() {
return "https://gadgetbridge.org/basics/pairing/nothing-cmf-server/";
}
@Override
public int[] getSupportedDeviceSpecificAuthenticationSettings() {
return new int[]{R.xml.devicesettings_pairingkey};
@@ -98,6 +98,12 @@ public abstract class HuamiCoordinator extends AbstractBLEDeviceCoordinator {
return false;
}
@Nullable
@Override
public String getAuthHelp() {
return "https://gadgetbridge.org/basics/pairing/huami-xiaomi-server/";
}
@Override
protected void deleteDevice(@NonNull final GBDevice gbDevice,
@NonNull final Device device,
@@ -21,6 +21,7 @@ import android.content.Context;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.apache.commons.lang3.ArrayUtils;
@@ -700,4 +701,10 @@ public abstract class ZeppOsCoordinator extends HuamiCoordinator {
final byte[] authKeyBytes = authKey.trim().getBytes();
return authKeyBytes.length == 32 || (authKey.trim().startsWith("0x") && authKeyBytes.length == 34);
}
@Nullable
@Override
public String getAuthHelp() {
return "https://gadgetbridge.org/basics/pairing/huami-xiaomi-server/";
}
}
@@ -315,6 +315,12 @@ public class QHybridCoordinator extends AbstractBLEDeviceCoordinator {
};
}
@Nullable
@Override
public String getAuthHelp() {
return "https://gadgetbridge.org/basics/pairing/fossil-server/";
}
@Deprecated // we should use the isHybridHR(GBDevice) instead of iterating every single device
private boolean isHybridHR() {
List<GBDevice> devices = GBApplication.app().getDeviceManager().getSelectedDevices();
@@ -99,6 +99,12 @@ public abstract class XiaomiCoordinator extends AbstractBLEDeviceCoordinator {
|| AUTH_KEY_PATTERN.matcher(authKey.trim()).matches();
}
@Nullable
@Override
public String getAuthHelp() {
return "https://gadgetbridge.org/basics/pairing/huami-xiaomi-server/";
}
@Nullable
@Override
public InstallHandler findInstallHandler(final Uri uri, final Context context) {
@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp"
tools:context=".activities.AuthKeyActivity">
<TextView
android:id="@+id/auth_key_explanation_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:autoLink="all"
android:linksClickable="true"
android:text="@string/auth_key_explanation"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/auth_key_input_layout"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/auth_key_explanation_text">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/auth_key_edit_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/pref_title_authkey"
android:inputType="textVisiblePassword" />
</com.google.android.material.textfield.TextInputLayout>
<Button
android:id="@+id/submit_auth_key_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="@string/submit_button_text"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/auth_key_input_layout" />
</androidx.constraintlayout.widget.ConstraintLayout>
+10
View File
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/auth_key_help"
android:icon="@drawable/ic_help_outline"
android:title="@string/help"
app:iconTint="?attr/actionmenu_icon_color"
app:showAsAction="always" />
</menu>
+7 -1
View File
@@ -2445,7 +2445,6 @@
<string name="app_crash_notification_title">%1s has crashed</string>
<string name="app_crash_share_stacktrace">Share error</string>
<string name="device_is_currently_bonded">ALREADY BONDED</string>
<string name="device_requires_key">KEY REQUIRED, LONG PRESS TO ENTER</string>
<string name="device_unsupported">UNSUPPORTED</string>
<string name="device_experimental">EXPERIMENTAL</string>
<string name="error_background_service_reason">Starting the background service failed because of an exception - click here for more information.\n\nError:</string>
@@ -4189,4 +4188,11 @@
<string name="error_queue_is_dead">GB\'s processing queue is dead</string>
<string name="error_sender_is_dead">GB\'s sender thread is dead</string>
<string name="error_receiver_is_dead">GB\'s receiver thread is dead</string>
<string name="submit_button_text">Submit</string>
<string name="auth_key_required_message">Auth key cannot be empty</string>
<string name="invalid_auth_key_message">Invalid auth key - please try again</string>
<string name="auth_key_activity_title_for">Auth key for %1$s</string>
<string name="auth_key_explanation">This device requires an authentication key for pairing.</string>
<string name="help">Help</string>
<string name="cannot_open_help_url">Could not open URL</string>
</resources>