Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
2 changes: 2 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
<uses-permission
android:name="android.permission.READ_PRECISE_PHONE_STATE"
tools:ignore="ProtectedPermissions" />
<uses-permission android:name="android.permission.CAMERA"/>
<uses-feature android:name="android.hardware.camera" android:required="true"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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<Intent> exportPreferencesLauncher = registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(), result -> {
Expand Down Expand Up @@ -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);
Expand All @@ -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());
Expand Down Expand Up @@ -209,7 +237,16 @@ private List<String> getKeysFromJson(String jsonString) {
return keys;
}


private void importPreferenceFromText(String jsonString) {
try {
List<String> 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();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
/*
* SPDX-FileCopyrightText: 2025 Peter Hasse <peter.hasse@fokus.fraunhofer.de>
* SPDX-FileCopyrightText: 2025 Johann Hackler <johann.hackler@fokus.fraunhofer.de>
* 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<String> 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<DecodeHintType, Object> 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<ProcessCameraProvider> 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();
}
}
5 changes: 5 additions & 0 deletions app/src/main/res/drawable/qr_code_scanner_24.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#5B70E0" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">

<path android:fillColor="@android:color/white" android:pathData="M9.5,6.5v3h-3v-3H9.5M11,5H5v6h6V5L11,5zM9.5,14.5v3h-3v-3H9.5M11,13H5v6h6V13L11,13zM17.5,6.5v3h-3v-3H17.5M19,5h-6v6h6V5L19,5zM13,13h1.5v1.5H13V13zM14.5,14.5H16V16h-1.5V14.5zM16,13h1.5v1.5H16V13zM13,16h1.5v1.5H13V16zM14.5,17.5H16V19h-1.5V17.5zM16,16h1.5v1.5H16V16zM17.5,14.5H19V16h-1.5V14.5zM17.5,17.5H19V19h-1.5V17.5zM22,7h-2V4h-3V2h5V7zM22,22v-5h-2v3h-3v2H22zM2,22h5v-2H4v-3H2V22zM2,2v5h2V4h3V2H2z"/>

</vector>
18 changes: 18 additions & 0 deletions app/src/main/res/layout/fragment_scanner.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ SPDX-FileCopyrightText: 2025 Peter Hasse <peter.hasse@fokus.fraunhofer.de>
~ SPDX-FileCopyrightText: 2025 Johann Hackler <johann.hackler@fokus.fraunhofer.de>
~ SPDX-FileCopyrightText: 2025 Fraunhofer FOKUS
~
~ SPDX-License-Identifier: BSD-3-Clause-Clear
-->

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:id="@+id/fragment_scanner"
android:layout_height="match_parent">
<androidx.camera.view.PreviewView
android:id="@+id/previewView"
android:layout_width="match_parent"
android:layout_height="match_parent" />

</androidx.constraintlayout.widget.ConstraintLayout>
10 changes: 10 additions & 0 deletions app/src/main/res/layout/fragment_shared_preferences_io.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,16 @@
android:orientation="vertical"
android:id="@+id/fragment_shared_preferences_io"
android:padding="16dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/fragment_shared_preferences_io_button_scan_qr_code"
android:src="@drawable/qr_code_scanner_24">
</ImageButton>
</LinearLayout>


</LinearLayout>
6 changes: 6 additions & 0 deletions app/src/main/res/navigation/nav_graph.xml
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,13 @@
app:destination="@id/workProfileActivity" />

</fragment>
<fragment
android:id="@+id/fragment_scanner"
android:label="ScannerFragment"
android:name="de.fraunhofer.fokus.OpenMobileNetworkToolkit.ScannerFragment"
tools:layout="@layout/fragment_scanner">

</fragment>
<fragment
android:id="@+id/about_fragment"
android:name="de.fraunhofer.fokus.OpenMobileNetworkToolkit.AboutFragment"
Expand Down