Add QR code scanning for server configuration

Adds QR code scanning functionality to both ServerSetupActivity and
SettingsActivity, allowing users to auto-fill server configuration by
scanning a QR code from the server's admin dashboard.

Changes:
- Add ZXing Android Embedded library (v4.3.0) for QR scanning
- Add CAMERA permission to AndroidManifest
- Create QrServerConfig model to parse and validate QR JSON data
- Create QrScannerUtil for shared scanning functionality
- Extend PermissionUtil with camera permission handling
- Add "Scan QR Code" button to ServerSetupActivity with hint text
- Add "Scan QR Code" button to SettingsActivity
- Add QR scan icon and string resources
- Configure Gradle Java toolchain auto-detection

The QR code contains JSON with server address, port, and client key
which are automatically populated into the input fields when scanned.
This commit is contained in:
2025-12-27 14:49:01 -05:00
parent a86c7b74aa
commit 0e08718566
12 changed files with 456 additions and 8 deletions

View File

@@ -56,6 +56,9 @@ dependencies {
implementation 'com.android.support:support-annotations:28.0.0' implementation 'com.android.support:support-annotations:28.0.0'
implementation 'androidx.palette:palette:1.0.0' implementation 'androidx.palette:palette:1.0.0'
// QR Code scanning
implementation 'com.journeyapps:zxing-android-embedded:4.3.0'
// Room // Room
def room_version = "2.6.1" def room_version = "2.6.1"
implementation "androidx.room:room-runtime:$room_version" implementation "androidx.room:room-runtime:$room_version"

View File

@@ -9,6 +9,7 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission android:name="android.permission.CAMERA" />
<application <application
android:allowBackup="false" android:allowBackup="false"

View File

@@ -0,0 +1,88 @@
package com.mattintech.simplelogupload.model;
import org.json.JSONException;
import org.json.JSONObject;
/**
* Model class for QR code server configuration
*/
public class QrServerConfig {
private final String server;
private final int port;
private final String key;
private QrServerConfig(String server, int port, String key) {
this.server = server;
this.port = port;
this.key = key;
}
public String getServer() {
return server;
}
public int getPort() {
return port;
}
public String getKey() {
return key;
}
/**
* Parse QR code JSON data
*
* @param qrData The QR code string data
* @return QrServerConfig object
* @throws QrConfigException if parsing fails or data is invalid
*/
public static QrServerConfig fromQrData(String qrData) throws QrConfigException {
if (qrData == null || qrData.trim().isEmpty()) {
throw new QrConfigException("QR code data is empty");
}
try {
JSONObject json = new JSONObject(qrData);
// Extract fields
if (!json.has("server")) {
throw new QrConfigException("Missing 'server' field in QR code");
}
if (!json.has("port")) {
throw new QrConfigException("Missing 'port' field in QR code");
}
if (!json.has("key")) {
throw new QrConfigException("Missing 'key' field in QR code");
}
String server = json.getString("server");
int port = json.getInt("port");
String key = json.getString("key");
// Validate
if (server.trim().isEmpty()) {
throw new QrConfigException("Server address cannot be empty");
}
if (port <= 0 || port > 65535) {
throw new QrConfigException("Port must be between 1 and 65535");
}
if (key.trim().isEmpty()) {
throw new QrConfigException("Client key cannot be empty");
}
return new QrServerConfig(server.trim(), port, key.trim());
} catch (JSONException e) {
throw new QrConfigException("Invalid QR code format: " + e.getMessage());
}
}
/**
* Custom exception for QR configuration errors
*/
public static class QrConfigException extends Exception {
public QrConfigException(String message) {
super(message);
}
}
}

View File

@@ -24,7 +24,8 @@ public class PermissionUtil {
public static final int REQUEST_MANAGE_EXTERNAL_STORAGE = 101; public static final int REQUEST_MANAGE_EXTERNAL_STORAGE = 101;
public static final int REQUEST_NOTIFICATION_PERMISSION = 102; public static final int REQUEST_NOTIFICATION_PERMISSION = 102;
public static final int REQUEST_CAMERA_PERMISSION = 103;
/** /**
* Checks if the app has notification permission * Checks if the app has notification permission
* *
@@ -93,4 +94,31 @@ public class PermissionUtil {
} }
} }
} }
/**
* Checks if the app has camera permission
*
* @param context The context
* @return true if the app has camera permission, false otherwise
*/
public static boolean hasCameraPermission(Context context) {
return ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA)
== PackageManager.PERMISSION_GRANTED;
}
/**
* Requests camera permission
*
* @param activity The activity
*/
public static void requestCameraPermission(Activity activity) {
if (!hasCameraPermission(activity)) {
Log.d(TAG, "Requesting camera permission");
ActivityCompat.requestPermissions(
activity,
new String[]{Manifest.permission.CAMERA},
REQUEST_CAMERA_PERMISSION
);
}
}
} }

View File

@@ -0,0 +1,54 @@
package com.mattintech.simplelogupload.util;
import com.journeyapps.barcodescanner.ScanOptions;
import com.mattintech.simplelogupload.model.QrServerConfig;
/**
* Utility class for QR code scanning
*/
public class QrScannerUtil {
/**
* Callback interface for QR scan results
*/
public interface QrScanCallback {
void onQrScanned(QrServerConfig config);
void onQrScanCancelled();
void onQrScanError(String errorMessage);
}
/**
* Create scan options for QR code scanning
*
* @return ScanOptions configured for QR codes
*/
public static ScanOptions createScanOptions() {
ScanOptions options = new ScanOptions();
options.setDesiredBarcodeFormats(ScanOptions.QR_CODE);
options.setPrompt("Scan server configuration QR code");
options.setBeepEnabled(true);
options.setBarcodeImageEnabled(false);
options.setOrientationLocked(true);
return options;
}
/**
* Process scan result
*
* @param contents The scanned QR code contents
* @param callback The callback to handle the result
*/
public static void processScanResult(String contents, QrScanCallback callback) {
if (contents == null) {
callback.onQrScanCancelled();
return;
}
try {
QrServerConfig config = QrServerConfig.fromQrData(contents);
callback.onQrScanned(config);
} catch (QrServerConfig.QrConfigException e) {
callback.onQrScanError(e.getMessage());
}
}
}

View File

@@ -1,28 +1,38 @@
package com.mattintech.simplelogupload.view; package com.mattintech.simplelogupload.view;
import android.content.Intent; import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Bundle; import android.os.Bundle;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.Log; import android.util.Log;
import android.widget.Button; import android.widget.Button;
import android.widget.Toast; import android.widget.Toast;
import androidx.activity.result.ActivityResultLauncher;
import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
import com.google.android.material.textfield.TextInputEditText; import com.google.android.material.textfield.TextInputEditText;
import com.journeyapps.barcodescanner.ScanContract;
import com.journeyapps.barcodescanner.ScanOptions;
import com.journeyapps.barcodescanner.ScanIntentResult;
import com.mattintech.simplelogupload.R; import com.mattintech.simplelogupload.R;
import com.mattintech.simplelogupload.constant.AppConstants; import com.mattintech.simplelogupload.constant.AppConstants;
import com.mattintech.simplelogupload.model.QrServerConfig;
import com.mattintech.simplelogupload.util.PermissionUtil;
import com.mattintech.simplelogupload.util.PreferencesUtil; import com.mattintech.simplelogupload.util.PreferencesUtil;
import com.mattintech.simplelogupload.util.QrScannerUtil;
/** /**
* Activity for server setup * Activity for server setup
*/ */
public class ServerSetupActivity extends AppCompatActivity { public class ServerSetupActivity extends AppCompatActivity {
private static final String TAG = AppConstants.APP_TAG + "ServerSetupActivity"; private static final String TAG = AppConstants.APP_TAG + "ServerSetupActivity";
private TextInputEditText serverAddressInput; private TextInputEditText serverAddressInput;
private TextInputEditText serverPortInput; private TextInputEditText serverPortInput;
private TextInputEditText clientKeyInput; private TextInputEditText clientKeyInput;
private ActivityResultLauncher<ScanOptions> qrScannerLauncher;
private Button scanQrButton;
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
@@ -34,7 +44,17 @@ public class ServerSetupActivity extends AppCompatActivity {
serverPortInput = findViewById(R.id.serverPortInput); serverPortInput = findViewById(R.id.serverPortInput);
clientKeyInput = findViewById(R.id.clientKeyInput); clientKeyInput = findViewById(R.id.clientKeyInput);
Button finishSetupButton = findViewById(R.id.finishSetupButton); Button finishSetupButton = findViewById(R.id.finishSetupButton);
scanQrButton = findViewById(R.id.scanQrButton);
// Initialize QR scanner launcher
qrScannerLauncher = registerForActivityResult(
new ScanContract(),
result -> handleQrScanResult(result)
);
// Set up QR scan button click listener
scanQrButton.setOnClickListener(v -> initiateQrScan());
// Set up action bar // Set up action bar
if (getSupportActionBar() != null) { if (getSupportActionBar() != null) {
getSupportActionBar().setTitle("Server Setup"); getSupportActionBar().setTitle("Server Setup");
@@ -103,7 +123,81 @@ public class ServerSetupActivity extends AppCompatActivity {
Toast.makeText(this, "Error saving settings: " + e.getMessage(), Toast.LENGTH_LONG).show(); Toast.makeText(this, "Error saving settings: " + e.getMessage(), Toast.LENGTH_LONG).show();
} }
} }
/**
* Initiate QR code scan
*/
private void initiateQrScan() {
// Check camera permission
if (!PermissionUtil.hasCameraPermission(this)) {
PermissionUtil.requestCameraPermission(this);
return;
}
// Launch scanner
ScanOptions options = QrScannerUtil.createScanOptions();
qrScannerLauncher.launch(options);
}
/**
* Handle QR scan result
*
* @param result The scan result
*/
private void handleQrScanResult(ScanIntentResult result) {
QrScannerUtil.processScanResult(
result.getContents(),
new QrScannerUtil.QrScanCallback() {
@Override
public void onQrScanned(QrServerConfig config) {
// Auto-fill the input fields
serverAddressInput.setText(config.getServer());
serverPortInput.setText(String.valueOf(config.getPort()));
clientKeyInput.setText(config.getKey());
Toast.makeText(ServerSetupActivity.this,
R.string.qr_scan_success,
Toast.LENGTH_SHORT).show();
Log.d(TAG, "QR code scanned successfully");
}
@Override
public void onQrScanCancelled() {
Toast.makeText(ServerSetupActivity.this,
R.string.qr_scan_cancelled,
Toast.LENGTH_SHORT).show();
}
@Override
public void onQrScanError(String errorMessage) {
Toast.makeText(ServerSetupActivity.this,
getString(R.string.qr_scan_error, errorMessage),
Toast.LENGTH_LONG).show();
Log.e(TAG, "QR scan error: " + errorMessage);
}
}
);
}
/**
* Handle permission request results
*/
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == PermissionUtil.REQUEST_CAMERA_PERMISSION) {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// Permission granted, initiate scan
initiateQrScan();
} else {
// Permission denied
Toast.makeText(this, R.string.camera_permission_denied, Toast.LENGTH_LONG).show();
}
}
}
@Override @Override
public boolean onSupportNavigateUp() { public boolean onSupportNavigateUp() {
onBackPressed(); onBackPressed();

View File

@@ -1,28 +1,38 @@
package com.mattintech.simplelogupload.view; package com.mattintech.simplelogupload.view;
import android.content.pm.PackageManager;
import android.os.Bundle; import android.os.Bundle;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.Log; import android.util.Log;
import android.widget.Button; import android.widget.Button;
import android.widget.Toast; import android.widget.Toast;
import androidx.activity.result.ActivityResultLauncher;
import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
import com.google.android.material.textfield.TextInputEditText; import com.google.android.material.textfield.TextInputEditText;
import com.journeyapps.barcodescanner.ScanContract;
import com.journeyapps.barcodescanner.ScanOptions;
import com.journeyapps.barcodescanner.ScanIntentResult;
import com.mattintech.simplelogupload.R; import com.mattintech.simplelogupload.R;
import com.mattintech.simplelogupload.constant.AppConstants; import com.mattintech.simplelogupload.constant.AppConstants;
import com.mattintech.simplelogupload.model.QrServerConfig;
import com.mattintech.simplelogupload.util.PermissionUtil;
import com.mattintech.simplelogupload.util.PreferencesUtil; import com.mattintech.simplelogupload.util.PreferencesUtil;
import com.mattintech.simplelogupload.util.QrScannerUtil;
/** /**
* Activity for application settings * Activity for application settings
*/ */
public class SettingsActivity extends AppCompatActivity { public class SettingsActivity extends AppCompatActivity {
private static final String TAG = AppConstants.APP_TAG + "SettingsActivity"; private static final String TAG = AppConstants.APP_TAG + "SettingsActivity";
private TextInputEditText serverAddressInput; private TextInputEditText serverAddressInput;
private TextInputEditText serverPortInput; private TextInputEditText serverPortInput;
private TextInputEditText clientKeyInput; private TextInputEditText clientKeyInput;
private ActivityResultLauncher<ScanOptions> qrScannerLauncher;
private Button scanQrButton;
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
@@ -34,7 +44,17 @@ public class SettingsActivity extends AppCompatActivity {
clientKeyInput = findViewById(R.id.clientKeyInput); clientKeyInput = findViewById(R.id.clientKeyInput);
Button saveButton = findViewById(R.id.saveSettingsButton); Button saveButton = findViewById(R.id.saveSettingsButton);
Button resetButton = findViewById(R.id.resetSettingsButton); Button resetButton = findViewById(R.id.resetSettingsButton);
scanQrButton = findViewById(R.id.scanQrButton);
// Initialize QR scanner launcher
qrScannerLauncher = registerForActivityResult(
new ScanContract(),
result -> handleQrScanResult(result)
);
// Set up QR scan button click listener
scanQrButton.setOnClickListener(v -> initiateQrScan());
// Set up action bar // Set up action bar
if (getSupportActionBar() != null) { if (getSupportActionBar() != null) {
getSupportActionBar().setDisplayHomeAsUpEnabled(true); getSupportActionBar().setDisplayHomeAsUpEnabled(true);
@@ -127,7 +147,81 @@ public class SettingsActivity extends AppCompatActivity {
loadSettings(); loadSettings();
Toast.makeText(this, "Settings reset to defaults", Toast.LENGTH_SHORT).show(); Toast.makeText(this, "Settings reset to defaults", Toast.LENGTH_SHORT).show();
} }
/**
* Initiate QR code scan
*/
private void initiateQrScan() {
// Check camera permission
if (!PermissionUtil.hasCameraPermission(this)) {
PermissionUtil.requestCameraPermission(this);
return;
}
// Launch scanner
ScanOptions options = QrScannerUtil.createScanOptions();
qrScannerLauncher.launch(options);
}
/**
* Handle QR scan result
*
* @param result The scan result
*/
private void handleQrScanResult(ScanIntentResult result) {
QrScannerUtil.processScanResult(
result.getContents(),
new QrScannerUtil.QrScanCallback() {
@Override
public void onQrScanned(QrServerConfig config) {
// Auto-fill the input fields
serverAddressInput.setText(config.getServer());
serverPortInput.setText(String.valueOf(config.getPort()));
clientKeyInput.setText(config.getKey());
Toast.makeText(SettingsActivity.this,
R.string.qr_scan_success,
Toast.LENGTH_SHORT).show();
Log.d(TAG, "QR code scanned successfully");
}
@Override
public void onQrScanCancelled() {
Toast.makeText(SettingsActivity.this,
R.string.qr_scan_cancelled,
Toast.LENGTH_SHORT).show();
}
@Override
public void onQrScanError(String errorMessage) {
Toast.makeText(SettingsActivity.this,
getString(R.string.qr_scan_error, errorMessage),
Toast.LENGTH_LONG).show();
Log.e(TAG, "QR scan error: " + errorMessage);
}
}
);
}
/**
* Handle permission request results
*/
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == PermissionUtil.REQUEST_CAMERA_PERMISSION) {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// Permission granted, initiate scan
initiateQrScan();
} else {
// Permission denied
Toast.makeText(this, R.string.camera_permission_denied, Toast.LENGTH_LONG).show();
}
}
}
@Override @Override
public boolean onSupportNavigateUp() { public boolean onSupportNavigateUp() {
onBackPressed(); onBackPressed();

View File

@@ -0,0 +1,39 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M3,11h8V3H3V11zM5,5h4v4H5V5z"/>
<path
android:fillColor="@android:color/white"
android:pathData="M3,21h8v-8H3V21zM5,15h4v4H5V15z"/>
<path
android:fillColor="@android:color/white"
android:pathData="M13,3v8h8V3H13zM19,9h-4V5h4V9z"/>
<path
android:fillColor="@android:color/white"
android:pathData="M19,19h2v2h-2z"/>
<path
android:fillColor="@android:color/white"
android:pathData="M13,13h2v2h-2z"/>
<path
android:fillColor="@android:color/white"
android:pathData="M15,15h2v2h-2z"/>
<path
android:fillColor="@android:color/white"
android:pathData="M13,17h2v2h-2z"/>
<path
android:fillColor="@android:color/white"
android:pathData="M15,19h2v2h-2z"/>
<path
android:fillColor="@android:color/white"
android:pathData="M17,17h2v2h-2z"/>
<path
android:fillColor="@android:color/white"
android:pathData="M17,13h2v2h-2z"/>
<path
android:fillColor="@android:color/white"
android:pathData="M19,15h2v2h-2z"/>
</vector>

View File

@@ -88,6 +88,27 @@
</com.google.android.material.textfield.TextInputLayout> </com.google.android.material.textfield.TextInputLayout>
<!-- QR Code Scan Button -->
<Button
android:id="@+id/scanQrButton"
style="?attr/materialButtonOutlinedStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="@string/scan_qr_code_button"
android:drawableLeft="@drawable/ic_qr_scan"
android:drawablePadding="8dp"
android:paddingVertical="12dp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/scan_qr_code_hint"
android:textAlignment="center"
android:textSize="12sp"
android:textStyle="italic" />
<TextView <TextView
android:id="@+id/testConnectionInfo" android:id="@+id/testConnectionInfo"
android:layout_width="match_parent" android:layout_width="match_parent"

View File

@@ -67,6 +67,18 @@
android:inputType="textPassword" /> android:inputType="textPassword" />
</com.google.android.material.textfield.TextInputLayout> </com.google.android.material.textfield.TextInputLayout>
<!-- QR Code Scan Button -->
<Button
android:id="@+id/scanQrButton"
style="?attr/materialButtonOutlinedStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginBottom="8dp"
android:text="@string/scan_qr_code_button"
android:drawableLeft="@drawable/ic_qr_scan"
android:drawablePadding="8dp" />
<Button <Button
android:id="@+id/saveSettingsButton" android:id="@+id/saveSettingsButton"
android:layout_width="match_parent" android:layout_width="match_parent"

View File

@@ -67,4 +67,13 @@
<string name="expiry_time_note">Maximum 24 hours (1 day)</string> <string name="expiry_time_note">Maximum 24 hours (1 day)</string>
<string name="cancel_button">Cancel</string> <string name="cancel_button">Cancel</string>
<string name="upload_button">Upload</string> <string name="upload_button">Upload</string>
<!-- QR Code Scanning -->
<string name="scan_qr_code_button">Scan QR Code</string>
<string name="scan_qr_code_hint">Scan a QR code from your server admin panel to auto-fill settings</string>
<string name="qr_scan_success">Configuration loaded from QR code</string>
<string name="qr_scan_cancelled">QR code scan cancelled</string>
<string name="qr_scan_error">QR code error: %1$s</string>
<string name="camera_permission_required">Camera permission is required to scan QR codes</string>
<string name="camera_permission_denied">Camera permission denied. Please enable it in Settings to scan QR codes.</string>
</resources> </resources>

View File

@@ -1,6 +1,11 @@
android.suppressUnsupportedCompileSdk=36 android.suppressUnsupportedCompileSdk=36
android.useAndroidX=true android.useAndroidX=true
# Enable Gradle toolchain auto-detection and auto-download
org.gradle.java.installations.auto-detect=true
org.gradle.java.installations.auto-download=true
org.gradle.java.installations.paths=/opt/homebrew/opt/openjdk@17/libexec/openjdk.jdk/Contents/Home
# JVM arguments for kapt with JDK 9+ # JVM arguments for kapt with JDK 9+
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 \ org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 \
--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED \ --add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED \