Skip to content

Commit

Permalink
Modernize Android qrcode module with CameraX and zxing-cpp
Browse files Browse the repository at this point in the history
The qrcode module used deprecated APIs like android.hardware.Camera and Play Services Vision API. Replace these with AndroidX CameraX and open source zxing-cpp. zxing-cpp works on all devices, no matter if they have Play Services or not.
  • Loading branch information
nift4 committed Aug 8, 2024
1 parent 596352d commit 25540f3
Show file tree
Hide file tree
Showing 9 changed files with 165 additions and 789 deletions.
14 changes: 8 additions & 6 deletions hellocardboard-android/build.gradle
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
apply plugin: 'com.android.application'

android {
compileSdkVersion 33
compileSdk = 34
lintOptions {
abortOnError false
}
Expand All @@ -16,7 +16,7 @@ android {
//
// See the release notes for details.
minSdkVersion 26
targetSdkVersion 33
targetSdkVersion 34
versionCode 1
versionName "1.25.0"
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
Expand All @@ -41,12 +41,14 @@ android {

dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'androidx.appcompat:appcompat:1.6.1'
// Android Mobile Vision
// TODO(b/213613345) Migrate to ML Kit.
implementation 'com.google.android.gms:play-services-vision:20.1.3'
implementation 'androidx.appcompat:appcompat:1.7.0'
implementation 'io.github.zxing-cpp:android:2.2.0'
implementation 'com.google.android.material:material:1.6.1'
implementation 'com.google.protobuf:protobuf-javalite:3.19.4'
implementation 'androidx.camera:camera-core:1.3.4'
implementation 'androidx.camera:camera-view:1.3.4'
implementation 'androidx.camera:camera-lifecycle:1.3.4'
implementation 'androidx.camera:camera-camera2:1.3.4'
implementation project(":sdk")
}

Expand Down
14 changes: 8 additions & 6 deletions sdk/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ apply plugin: 'com.android.library'
apply plugin: 'com.google.protobuf'

android {
compileSdkVersion 33
compileSdk = 34
lintOptions {
abortOnError false
}
Expand All @@ -16,7 +16,7 @@ android {
//
// See the release notes for details.
minSdkVersion 26
targetSdkVersion 33
targetSdkVersion 34
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
ndk {
abiFilters 'armeabi-v7a', 'arm64-v8a'
Expand Down Expand Up @@ -69,10 +69,12 @@ protobuf {

dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'androidx.appcompat:appcompat:1.6.1'
// Android Mobile Vision
// TODO(b/217176538) Migrate to ML Kit.
implementation 'com.google.android.gms:play-services-vision:20.1.3'
implementation 'androidx.appcompat:appcompat:1.7.0'
implementation 'io.github.zxing-cpp:android:2.2.0'
implementation 'com.google.android.material:material:1.6.1'
implementation 'com.google.protobuf:protobuf-javalite:3.19.4'
implementation 'androidx.camera:camera-core:1.3.4'
implementation 'androidx.camera:camera-view:1.3.4'
implementation 'androidx.camera:camera-lifecycle:1.3.4'
implementation 'androidx.camera:camera-camera2:1.3.4'
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
package com.google.cardboard.sdk;

import android.Manifest;
import android.app.Dialog;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
Expand All @@ -30,19 +29,16 @@
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.camera.view.PreviewView;
import androidx.core.app.ActivityCompat;
import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.GoogleApiAvailability;
import com.google.android.gms.vision.MultiProcessor;
import com.google.android.gms.vision.barcode.Barcode;
import com.google.android.gms.vision.barcode.BarcodeDetector;
import zxingcpp.BarcodeReader;
import com.google.cardboard.sdk.qrcode.CardboardParamsUtils;
import com.google.cardboard.sdk.qrcode.QrCodeContentProcessor;
import com.google.cardboard.sdk.qrcode.QrCodeTracker;
import com.google.cardboard.sdk.qrcode.QrCodeTrackerFactory;
import com.google.cardboard.sdk.qrcode.camera.CameraSource;
import com.google.cardboard.sdk.qrcode.camera.CameraSourcePreview;

import java.io.IOException;
import java.util.HashSet;

/**
* Manages the QR code capture activity. It scans permanently with the camera until it finds a valid
Expand All @@ -52,17 +48,11 @@ public class QrCodeCaptureActivity extends AppCompatActivity
implements QrCodeTracker.Listener, QrCodeContentProcessor.Listener {
private static final String TAG = QrCodeCaptureActivity.class.getSimpleName();

// Intent request code to handle updating play services if needed.
private static final int RC_HANDLE_GMS = 9001;

// Permission request codes
private static final int PERMISSIONS_REQUEST_CODE = 2;

// Min sdk version required for google play services.
private static final int MIN_SDK_VERSION = 23;

private CameraSource cameraSource;
private CameraSourcePreview cameraSourcePreview;
private PreviewView cameraSourcePreview;

// Flag used to avoid saving the device parameters more than once.
private static boolean qrCodeSaved = false;
Expand Down Expand Up @@ -154,26 +144,14 @@ private void launchPermissionsSettings() {

/** Creates and starts the camera. */
private void createCameraSource() {
Context context = getApplicationContext();

BarcodeDetector qrCodeDetector =
new BarcodeDetector.Builder(context).setBarcodeFormats(Barcode.QR_CODE).build();

QrCodeTrackerFactory qrCodeFactory = new QrCodeTrackerFactory(this);

qrCodeDetector.setProcessor(new MultiProcessor.Builder<>(qrCodeFactory).build());

// Check that native dependencies are downloaded.
if (!qrCodeDetector.isOperational()) {
Toast.makeText(this, R.string.missing_dependencies, Toast.LENGTH_LONG).show();
Log.w(
TAG,
"QR Code detector is not operational. Try connecting to WiFi and updating Google Play"
+ " Services or checking that the device storage isn't low.");
}

// Creates and starts the camera.
cameraSource = new CameraSource(getApplicationContext(), qrCodeDetector);
BarcodeReader.Options options = new BarcodeReader.Options();
HashSet<BarcodeReader.Format> formats = new HashSet<>();
formats.add(BarcodeReader.Format.QR_CODE);
options.setFormats(formats);
options.setTextMode(BarcodeReader.TextMode.PLAIN);
BarcodeReader qrCodeDetector = new BarcodeReader(options);
QrCodeTracker tracker = new QrCodeTracker(this);
cameraSource = new CameraSource(this, this, qrCodeDetector, tracker);
}

/** Restarts the camera. */
Expand All @@ -197,27 +175,17 @@ protected void onResume() {
@Override
protected void onPause() {
super.onPause();
if (cameraSourcePreview != null) {
cameraSourcePreview.stop();
cameraSourcePreview.release();
if (cameraSource != null) {
cameraSource.release();
cameraSource = null;
}
}

/** Starts or restarts the camera source, if it exists. */
private void startCameraSource() {
// Check that the device has play services available.
int code =
GoogleApiAvailability.getInstance()
.isGooglePlayServicesAvailable(getApplicationContext(), MIN_SDK_VERSION);
if (code != ConnectionResult.SUCCESS) {
Log.i(TAG, "isGooglePlayServicesAvailable() returned: " + new ConnectionResult(code));
Dialog dlg = GoogleApiAvailability.getInstance().getErrorDialog(this, code, RC_HANDLE_GMS);
dlg.show();
}

if (cameraSource != null) {
try {
cameraSourcePreview.start(cameraSource);
cameraSource.start(cameraSourcePreview);
} catch (IOException e) {
Log.e(TAG, "Unable to start camera source.", e);
cameraSource.release();
Expand Down Expand Up @@ -248,7 +216,7 @@ public void skipQrCodeCapture(View view) {
* @param qrCode Detected QR code.
*/
@Override
public void onQrCodeDetected(Barcode qrCode) {
public void onQrCodeDetected(BarcodeReader.Result qrCode) {
if (qrCode != null && !qrCodeSaved) {
qrCodeSaved = true;
QrCodeContentProcessor qrCodeContentProcessor = new QrCodeContentProcessor(this);
Expand All @@ -265,7 +233,10 @@ public void onQrCodeDetected(Barcode qrCode) {
public void onQrCodeSaved(boolean status) {
if (status) {
Log.d(TAG, "Device parameters saved in external storage.");
cameraSourcePreview.stop();
if (cameraSource != null) {
cameraSource.release();
cameraSource = null;
}
nativeIncrementDeviceParamsChangedCount();
finish();
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
import android.content.Context;
import android.util.Log;
import android.widget.Toast;
import com.google.android.gms.vision.barcode.Barcode;
import zxingcpp.BarcodeReader;
import com.google.cardboard.sdk.R;

/**
Expand Down Expand Up @@ -50,7 +50,7 @@ public interface Listener {
* Application. It is used to write device params to scoped storage via {@code
* Context.getFilesDir()}.
*/
public void processAndSaveQrCode(Barcode qrCode, Context context) {
public void processAndSaveQrCode(BarcodeReader.Result qrCode, Context context) {
new ProcessAndSaveQrCodeTask(context).execute(qrCode);
}

Expand All @@ -59,7 +59,7 @@ public void processAndSaveQrCode(Barcode qrCode, Context context) {
* external storage.
*/
public class ProcessAndSaveQrCodeTask
extends AsyncTask<Barcode, CardboardParamsUtils.UriToParamsStatus> {
extends AsyncTask<BarcodeReader.Result, CardboardParamsUtils.UriToParamsStatus> {
private final Context context;

/**
Expand All @@ -74,7 +74,7 @@ public ProcessAndSaveQrCodeTask(Context context) {
}

@Override
protected CardboardParamsUtils.UriToParamsStatus doInBackground(Barcode qrCode) {
protected CardboardParamsUtils.UriToParamsStatus doInBackground(BarcodeReader.Result qrCode) {
UrlFactory urlFactory = new UrlFactory();
return getParamsFromQrCode(qrCode, urlFactory);
}
Expand Down Expand Up @@ -109,13 +109,13 @@ protected void onPostExecute(CardboardParamsUtils.UriToParamsStatus result) {
* @return Cardboard device parameters, or null if there is an error.
*/
private static CardboardParamsUtils.UriToParamsStatus getParamsFromQrCode(
Barcode barcode, UrlFactory urlFactory) {
if (barcode.valueFormat != Barcode.TEXT && barcode.valueFormat != Barcode.URL) {
Log.e(TAG, "Invalid QR code format: " + barcode.valueFormat);
BarcodeReader.Result barcode, UrlFactory urlFactory) {
if (barcode.getText() == null) {
Log.e(TAG, "Invalid QR code format: text is null");
return CardboardParamsUtils.UriToParamsStatus.error(
CardboardParamsUtils.UriToParamsStatus.STATUS_UNEXPECTED_FORMAT);
}

return CardboardParamsUtils.getParamsFromUriString(barcode.rawValue, urlFactory);
return CardboardParamsUtils.getParamsFromUriString(barcode.getText(), urlFactory);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,34 +15,42 @@
*/
package com.google.cardboard.sdk.qrcode;

import com.google.android.gms.vision.Tracker;
import com.google.android.gms.vision.barcode.Barcode;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;

import zxingcpp.BarcodeReader;

/**
* QrCodeTracker is used for tracking or reading a QR code. This is used to receive newly detected
* items, add a graphical representation to an overlay, update the graphics as the item changes, and
* remove the graphics when the item goes away.
*/
public class QrCodeTracker extends Tracker<Barcode> {
public class QrCodeTracker {
private final Listener listener;
private final HashSet<BarcodeReader.Result> lastData = new HashSet<>();

/**
* Consume the item instance detected from an Activity or Fragment level by implementing the
* Listener interface method onQrCodeDetected.
*/
public interface Listener {
void onQrCodeDetected(Barcode qrCode);
void onQrCodeDetected(BarcodeReader.Result qrCode);
}

QrCodeTracker(Listener listener) {
public QrCodeTracker(Listener listener) {
this.listener = listener;
}

/** Start tracking the detected item instance. */
@Override
public void onNewItem(int id, Barcode item) {
if (item.displayValue != null) {
listener.onQrCodeDetected(item);
public void onItemsDetected(List<BarcodeReader.Result> data) {
for (BarcodeReader.Result result : data) {
if (lastData.stream().anyMatch(otherResult -> Arrays.equals(result.getBytes(), otherResult.getBytes()))) {
// This QR code already was detected in last frame, it's not new.
continue;
}
listener.onQrCodeDetected(result);
}
lastData.clear();
lastData.addAll(data);
}
}

This file was deleted.

Loading

0 comments on commit 25540f3

Please sign in to comment.