diff --git a/app/build.gradle b/app/build.gradle
index 7c51589..bd27cae 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -167,6 +167,14 @@ dependencies {
implementation "androidx.compose.material3:material3:1.3.2"
implementation "com.squareup.moshi:moshi:1.15.2"
implementation "com.squareup.moshi:moshi-adapters:1.15.2"
+
+ def camerax_version = "1.3.2"
+ implementation "androidx.camera:camera-core:$camerax_version"
+ implementation "androidx.camera:camera-camera2:$camerax_version"
+ implementation "androidx.camera:camera-lifecycle:$camerax_version"
+ implementation "androidx.camera:camera-view:$camerax_version"
+
+ implementation 'com.google.zxing:core:3.5.3'
}
configurations.implementation {
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 834cddb..375d196 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -18,6 +18,8 @@
+
+
diff --git a/app/src/main/java/de/fraunhofer/fokus/OpenMobileNetworkToolkit/Preferences/SharedPreferencesIOFragment.java b/app/src/main/java/de/fraunhofer/fokus/OpenMobileNetworkToolkit/Preferences/SharedPreferencesIOFragment.java
index 0928739..6f31ba9 100644
--- a/app/src/main/java/de/fraunhofer/fokus/OpenMobileNetworkToolkit/Preferences/SharedPreferencesIOFragment.java
+++ b/app/src/main/java/de/fraunhofer/fokus/OpenMobileNetworkToolkit/Preferences/SharedPreferencesIOFragment.java
@@ -24,6 +24,7 @@
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
+import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ScrollView;
@@ -35,6 +36,8 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
+import androidx.navigation.NavController;
+import androidx.navigation.fragment.NavHostFragment;
import java.io.BufferedReader;
import java.io.File;
@@ -48,6 +51,7 @@
import de.fraunhofer.fokus.OpenMobileNetworkToolkit.ClearPreferencesFragment;
import de.fraunhofer.fokus.OpenMobileNetworkToolkit.MultiSelectDialogFragment;
import de.fraunhofer.fokus.OpenMobileNetworkToolkit.R;
+import de.fraunhofer.fokus.OpenMobileNetworkToolkit.ScannerFragment;
import de.fraunhofer.fokus.OpenMobileNetworkToolkit.SettingPreferences.ClearPreferencesListener;
import org.json.JSONObject;
@@ -59,6 +63,7 @@ public class SharedPreferencesIOFragment extends Fragment implements ClearPrefer
private Uri uri;
private LinearLayout mainLayout;
private ScrollView scrollView;
+ private ImageButton qrButton;
private final ActivityResultLauncher exportPreferencesLauncher = registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(), result -> {
@@ -109,6 +114,19 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c
return view;
}
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ getParentFragmentManager().setFragmentResultListener("qr_scan_request", this, (requestKey, bundle) -> {
+ String scannedText = bundle.getString("scanned_qr");
+ if (scannedText != null) {
+ Log.d("OriginalFragment", "Got scanned QR: " + scannedText);
+ clearConfig();
+ importPreferenceFromText(scannedText);
+ }
+ });
+ }
+
private void clearConfig() {
ClearPreferencesFragment fragment = new ClearPreferencesFragment();
fragment.setClearPreferencesListener(this);
@@ -117,6 +135,16 @@ private void clearConfig() {
private void setupUI(View view) {
+
+ qrButton = view.findViewById(R.id.fragment_shared_preferences_io_button_scan_qr_code);
+ qrButton.setOnClickListener(v -> {
+ NavController navController = NavHostFragment.findNavController(this);
+ navController.navigate(R.id.fragment_scanner);
+
+ });
+
+
+
mainLayout = view.findViewById(R.id.fragment_shared_preferences_io);
Button exportButton = createButton("Export Config", v -> createFile());
@@ -209,7 +237,16 @@ private List getKeysFromJson(String jsonString) {
return keys;
}
-
+ private void importPreferenceFromText(String jsonString) {
+ try {
+ List keys = getKeysFromJson(jsonString);
+ MultiSelectDialogFragment dialogFragment = getMultiSelectDialogFragment(jsonString, keys);
+ dialogFragment.show(getParentFragmentManager(), "multiSelectDialog");
+ } catch (Exception e) {
+ Log.e(TAG, "Failed to import Config", e);
+ showToast("Failed to import Config");
+ }
+ }
private void importPreferencesFromFile(Uri uri) {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(context.getContentResolver().openInputStream(uri)))) {
StringBuilder stringBuilder = new StringBuilder();
diff --git a/app/src/main/java/de/fraunhofer/fokus/OpenMobileNetworkToolkit/ScannerFragment.java b/app/src/main/java/de/fraunhofer/fokus/OpenMobileNetworkToolkit/ScannerFragment.java
new file mode 100644
index 0000000..e113ddd
--- /dev/null
+++ b/app/src/main/java/de/fraunhofer/fokus/OpenMobileNetworkToolkit/ScannerFragment.java
@@ -0,0 +1,179 @@
+/*
+ * SPDX-FileCopyrightText: 2025 Peter Hasse
+ * SPDX-FileCopyrightText: 2025 Johann Hackler
+ * SPDX-FileCopyrightText: 2025 Fraunhofer FOKUS
+ *
+ * SPDX-License-Identifier: BSD-3-Clause-Clear
+ */
+
+package de.fraunhofer.fokus.OpenMobileNetworkToolkit;
+
+import android.Manifest;
+import android.annotation.SuppressLint;
+import android.content.pm.PackageManager;
+import android.graphics.ImageFormat;
+import android.media.Image;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Toast;
+
+import androidx.activity.result.ActivityResultLauncher;
+import androidx.activity.result.contract.ActivityResultContracts;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.camera.core.CameraSelector;
+import androidx.camera.core.ImageAnalysis;
+import androidx.camera.core.Preview;
+import androidx.camera.lifecycle.ProcessCameraProvider;
+import androidx.camera.view.PreviewView;
+import androidx.core.content.ContextCompat;
+import androidx.fragment.app.Fragment;
+import androidx.navigation.NavController;
+import androidx.navigation.fragment.NavHostFragment;
+
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.zxing.BarcodeFormat;
+import com.google.zxing.BinaryBitmap;
+import com.google.zxing.DecodeHintType;
+import com.google.zxing.MultiFormatReader;
+import com.google.zxing.NotFoundException;
+import com.google.zxing.PlanarYUVLuminanceSource;
+import com.google.zxing.Result;
+import com.google.zxing.common.HybridBinarizer;
+
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+import java.util.EnumMap;
+import java.util.Map;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+
+public class ScannerFragment extends Fragment {
+
+ private PreviewView previewView;
+ private Executor cameraExecutor;
+ private MultiFormatReader barcodeReader;
+ private static final int CAMERA_PERMISSION_REQUEST_CODE = 1001;
+ private ActivityResultLauncher requestPermissionLauncher;
+
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.fragment_scanner, container, false);
+ previewView = view.findViewById(R.id.previewView);
+ cameraExecutor = Executors.newSingleThreadExecutor();
+ setupBarcodeReader();
+
+
+ requestPermissionLauncher = registerForActivityResult(
+ new ActivityResultContracts.RequestPermission(),
+ isGranted -> {
+ if (isGranted) {
+ Log.d("ScannerFragment", "Camera permission granted. Starting camera...");
+ startCamera();
+ } else {
+ Log.e("ScannerFragment", "Camera permission denied.");
+ Toast.makeText(getContext(), "Camera permission is required to scan QR codes.", Toast.LENGTH_LONG).show();
+ }
+ });
+
+
+ if (ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.CAMERA)
+ == PackageManager.PERMISSION_GRANTED) {
+ startCamera();
+ } else {
+ requestPermissionLauncher.launch(Manifest.permission.CAMERA);
+ }
+
+ return view;
+ }
+
+ private void setupBarcodeReader() {
+ barcodeReader = new MultiFormatReader();
+ Map hints = new EnumMap<>(DecodeHintType.class);
+ hints.put(DecodeHintType.POSSIBLE_FORMATS,
+ Arrays.asList(BarcodeFormat.QR_CODE, BarcodeFormat.EAN_13, BarcodeFormat.CODE_128));
+ barcodeReader.setHints(hints);
+ }
+
+ private void startCamera() {
+
+ ListenableFuture cameraProviderFuture =
+ ProcessCameraProvider.getInstance(requireContext());
+
+ cameraProviderFuture.addListener(() -> {
+ try {
+ ProcessCameraProvider cameraProvider = cameraProviderFuture.get();
+
+ Preview preview = new Preview.Builder().build();
+ preview.setSurfaceProvider(previewView.getSurfaceProvider());
+
+ ImageAnalysis imageAnalysis = new ImageAnalysis.Builder()
+ .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
+ .build();
+
+ imageAnalysis.setAnalyzer(cameraExecutor, image -> {
+ if (image.getFormat() == ImageFormat.YUV_420_888) {
+ @SuppressLint("UnsafeOptInUsageError")
+ Image mediaImage = image.getImage();
+ if (mediaImage != null) {
+ int width = mediaImage.getWidth();
+ int height = mediaImage.getHeight();
+ ByteBuffer buffer = image.getPlanes()[0].getBuffer();
+ byte[] data = new byte[buffer.remaining()];
+ buffer.get(data);
+
+ PlanarYUVLuminanceSource source = new PlanarYUVLuminanceSource(
+ data, width, height,
+ 0, 0, width, height,
+ false
+ );
+
+ BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
+
+ try {
+ Result result = barcodeReader.decode(bitmap);
+ Log.d("ZXing", "Scanned: " + result.getText());
+
+ requireActivity().runOnUiThread(() -> {
+ Bundle bundleResult = new Bundle();
+ bundleResult.putString("scanned_qr", result.getText());
+ getParentFragmentManager().setFragmentResult("qr_scan_request", bundleResult);
+ NavHostFragment.findNavController(this).popBackStack();
+ });
+
+ } catch (NotFoundException e) {
+ // No QR code found, ignore
+ } finally {
+ image.close();
+ }
+ } else {
+ image.close();
+ }
+ } else {
+ image.close();
+ }
+ });
+
+
+ CameraSelector cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA;
+
+ cameraProvider.unbindAll();
+ cameraProvider.bindToLifecycle(this, cameraSelector, preview, imageAnalysis);
+
+ } catch (ExecutionException | InterruptedException e) {
+ e.printStackTrace();
+ }
+
+ }, ContextCompat.getMainExecutor(requireContext()));
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ }
+}
diff --git a/app/src/main/res/drawable/qr_code_scanner_24.xml b/app/src/main/res/drawable/qr_code_scanner_24.xml
new file mode 100644
index 0000000..06ad770
--- /dev/null
+++ b/app/src/main/res/drawable/qr_code_scanner_24.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/app/src/main/res/layout/fragment_scanner.xml b/app/src/main/res/layout/fragment_scanner.xml
new file mode 100644
index 0000000..17214ca
--- /dev/null
+++ b/app/src/main/res/layout/fragment_scanner.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_shared_preferences_io.xml b/app/src/main/res/layout/fragment_shared_preferences_io.xml
index 8188175..4240cb0 100644
--- a/app/src/main/res/layout/fragment_shared_preferences_io.xml
+++ b/app/src/main/res/layout/fragment_shared_preferences_io.xml
@@ -5,6 +5,16 @@
android:orientation="vertical"
android:id="@+id/fragment_shared_preferences_io"
android:padding="16dp">
+
+
+
+
diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml
index 246b554..b3636df 100644
--- a/app/src/main/res/navigation/nav_graph.xml
+++ b/app/src/main/res/navigation/nav_graph.xml
@@ -55,7 +55,13 @@
app:destination="@id/workProfileActivity" />
+
+