From 6552391b7bcb66c422237cd01340ac1f576bb240 Mon Sep 17 00:00:00 2001 From: hajoha Date: Tue, 5 Aug 2025 15:44:26 +0200 Subject: [PATCH 1/3] add first example for QR Code Reading in App --- app/build.gradle | 9 + app/src/main/AndroidManifest.xml | 2 + .../SharedPreferencesIOFragment.java | 15 ++ .../ScannerFragment.java | 169 ++++++++++++++++++ .../main/res/drawable/qr_code_scanner_24.xml | 5 + app/src/main/res/layout/fragment_scanner.xml | 18 ++ .../layout/fragment_shared_preferences_io.xml | 10 ++ app/src/main/res/navigation/nav_graph.xml | 6 + 8 files changed, 234 insertions(+) create mode 100644 app/src/main/java/de/fraunhofer/fokus/OpenMobileNetworkToolkit/ScannerFragment.java create mode 100644 app/src/main/res/drawable/qr_code_scanner_24.xml create mode 100644 app/src/main/res/layout/fragment_scanner.xml diff --git a/app/build.gradle b/app/build.gradle index 7c51589..87e4050 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -167,6 +167,15 @@ 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" + + // ZXing core (you'll copy this manually later for F-Droid compatibility) + implementation 'com.google.zxing:core:3.5.3' // only for local dev } 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..8cc3078 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 -> { @@ -117,6 +122,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()); 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..f18f0b2 --- /dev/null +++ b/app/src/main/java/de/fraunhofer/fokus/OpenMobileNetworkToolkit/ScannerFragment.java @@ -0,0 +1,169 @@ +/* + * 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 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(); // call your camera setup here + } 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()); + // TODO: Handle result on main thread + } catch (NotFoundException e) { + // No QR code found + } 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" /> + + Date: Tue, 5 Aug 2025 16:18:49 +0200 Subject: [PATCH 2/3] add permission asking --- .../SharedPreferencesIOFragment.java | 24 ++++++++++++++++++- .../ScannerFragment.java | 14 +++++++++-- 2 files changed, 35 insertions(+), 3 deletions(-) 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 8cc3078..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 @@ -114,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); @@ -224,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 index f18f0b2..2eb0d59 100644 --- a/app/src/main/java/de/fraunhofer/fokus/OpenMobileNetworkToolkit/ScannerFragment.java +++ b/app/src/main/java/de/fraunhofer/fokus/OpenMobileNetworkToolkit/ScannerFragment.java @@ -31,6 +31,8 @@ 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; @@ -136,9 +138,16 @@ private void startCamera() { try { Result result = barcodeReader.decode(bitmap); Log.d("ZXing", "Scanned: " + result.getText()); - // TODO: Handle result on main thread + + 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 + // No QR code found, ignore } finally { image.close(); } @@ -150,6 +159,7 @@ private void startCamera() { } }); + CameraSelector cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA; cameraProvider.unbindAll(); From f1b2efb0e90a32f1fe8685e71aa8fe52b0deda70 Mon Sep 17 00:00:00 2001 From: hajoha Date: Tue, 5 Aug 2025 17:08:26 +0200 Subject: [PATCH 3/3] update --- app/build.gradle | 3 +-- .../fokus/OpenMobileNetworkToolkit/ScannerFragment.java | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 87e4050..bd27cae 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -174,8 +174,7 @@ dependencies { implementation "androidx.camera:camera-lifecycle:$camerax_version" implementation "androidx.camera:camera-view:$camerax_version" - // ZXing core (you'll copy this manually later for F-Droid compatibility) - implementation 'com.google.zxing:core:3.5.3' // only for local dev + implementation 'com.google.zxing:core:3.5.3' } configurations.implementation { diff --git a/app/src/main/java/de/fraunhofer/fokus/OpenMobileNetworkToolkit/ScannerFragment.java b/app/src/main/java/de/fraunhofer/fokus/OpenMobileNetworkToolkit/ScannerFragment.java index 2eb0d59..e113ddd 100644 --- a/app/src/main/java/de/fraunhofer/fokus/OpenMobileNetworkToolkit/ScannerFragment.java +++ b/app/src/main/java/de/fraunhofer/fokus/OpenMobileNetworkToolkit/ScannerFragment.java @@ -74,7 +74,7 @@ public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, isGranted -> { if (isGranted) { Log.d("ScannerFragment", "Camera permission granted. Starting camera..."); - startCamera(); // call your camera setup here + startCamera(); } else { Log.e("ScannerFragment", "Camera permission denied."); Toast.makeText(getContext(), "Camera permission is required to scan QR codes.", Toast.LENGTH_LONG).show();