From d260c483931e484d15c23c7609f35ccebbac4b75 Mon Sep 17 00:00:00 2001 From: Cody Henthorne Date: Thu, 19 May 2022 16:26:03 -0400 Subject: [PATCH] Fix device linking issues on newer devices. --- app/build.gradle | 1 + .../securesms/DeviceActivity.java | 2 +- .../securesms/DeviceAddFragment.java | 61 +-- .../components/camera/CameraView.java | 39 +- .../PaymentsTransferQrScanFragment.java | 2 +- .../securesms/qr/ScanningThread.java | 126 ----- .../verify/VerifyIdentityFragment.kt | 2 +- .../securesms/verify/VerifyScanFragment.kt | 4 +- .../main/res/layout/device_add_fragment.xml | 57 +-- qr/app/build.gradle | 14 + qr/app/src/main/AndroidManifest.xml | 24 + qr/app/src/main/ic_launcher-playstore.png | Bin 0 -> 22059 bytes .../java/org/signal/qrtest/MainActivity.kt | 28 ++ .../res/drawable/ic_launcher_background.xml | 74 +++ .../res/drawable/ic_launcher_foreground.xml | 15 + qr/app/src/main/res/layout/activity_main.xml | 20 + .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 5 + .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 2399 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 0 -> 4406 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 1816 bytes .../res/mipmap-mdpi/ic_launcher_round.png | Bin 0 -> 2656 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 3194 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 0 -> 6043 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 5631 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 0 -> 10103 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 7593 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 0 -> 14519 bytes qr/app/src/main/res/values-night/themes.xml | 16 + qr/app/src/main/res/values/colors.xml | 10 + qr/app/src/main/res/values/strings.xml | 3 + qr/app/src/main/res/values/themes.xml | 16 + qr/lib/build.gradle | 18 + qr/lib/src/main/AndroidManifest.xml | 14 + .../main/java/org/signal/qr/QrProcessor.kt | 50 ++ .../main/java/org/signal/qr/QrScannerView.kt | 50 ++ .../main/java/org/signal/qr/ScannerView.kt | 10 + .../main/java/org/signal/qr/ScannerView19.kt | 49 ++ .../main/java/org/signal/qr/ScannerView21.kt | 103 ++++ .../signal/qr/kitkat/CameraSurfaceView.java | 33 ++ .../org/signal/qr/kitkat/CameraUtils.java | 109 ++++ .../org/signal/qr/kitkat/QrCameraView.java | 472 ++++++++++++++++++ .../org/signal/qr/kitkat}/ScanListener.java | 2 +- .../org/signal/qr/kitkat/ScanningThread.java | 88 ++++ settings.gradle | 5 + signalModule.gradle | 48 ++ signalModuleApp.gradle | 48 ++ 47 files changed, 1387 insertions(+), 236 deletions(-) delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/qr/ScanningThread.java create mode 100644 qr/app/build.gradle create mode 100644 qr/app/src/main/AndroidManifest.xml create mode 100644 qr/app/src/main/ic_launcher-playstore.png create mode 100644 qr/app/src/main/java/org/signal/qrtest/MainActivity.kt create mode 100644 qr/app/src/main/res/drawable/ic_launcher_background.xml create mode 100644 qr/app/src/main/res/drawable/ic_launcher_foreground.xml create mode 100644 qr/app/src/main/res/layout/activity_main.xml create mode 100644 qr/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 qr/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 qr/app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 qr/app/src/main/res/mipmap-hdpi/ic_launcher_round.png create mode 100644 qr/app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 qr/app/src/main/res/mipmap-mdpi/ic_launcher_round.png create mode 100644 qr/app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 qr/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png create mode 100644 qr/app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 qr/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png create mode 100644 qr/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 qr/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png create mode 100644 qr/app/src/main/res/values-night/themes.xml create mode 100644 qr/app/src/main/res/values/colors.xml create mode 100644 qr/app/src/main/res/values/strings.xml create mode 100644 qr/app/src/main/res/values/themes.xml create mode 100644 qr/lib/build.gradle create mode 100644 qr/lib/src/main/AndroidManifest.xml create mode 100644 qr/lib/src/main/java/org/signal/qr/QrProcessor.kt create mode 100644 qr/lib/src/main/java/org/signal/qr/QrScannerView.kt create mode 100644 qr/lib/src/main/java/org/signal/qr/ScannerView.kt create mode 100644 qr/lib/src/main/java/org/signal/qr/ScannerView19.kt create mode 100644 qr/lib/src/main/java/org/signal/qr/ScannerView21.kt create mode 100644 qr/lib/src/main/java/org/signal/qr/kitkat/CameraSurfaceView.java create mode 100644 qr/lib/src/main/java/org/signal/qr/kitkat/CameraUtils.java create mode 100644 qr/lib/src/main/java/org/signal/qr/kitkat/QrCameraView.java rename {app/src/main/java/org/thoughtcrime/securesms/qr => qr/lib/src/main/java/org/signal/qr/kitkat}/ScanListener.java (74%) create mode 100644 qr/lib/src/main/java/org/signal/qr/kitkat/ScanningThread.java create mode 100644 signalModule.gradle create mode 100644 signalModuleApp.gradle diff --git a/app/build.gradle b/app/build.gradle index d4ad9ef0a..b50ff455d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -455,6 +455,7 @@ dependencies { implementation project(':image-editor') implementation project(':donations') implementation project(':contacts') + implementation project(':qr') implementation libs.libsignal.android implementation libs.google.protobuf.javalite diff --git a/app/src/main/java/org/thoughtcrime/securesms/DeviceActivity.java b/app/src/main/java/org/thoughtcrime/securesms/DeviceActivity.java index ea9174873..67eb27b2d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/DeviceActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/DeviceActivity.java @@ -25,11 +25,11 @@ import org.signal.libsignal.protocol.InvalidKeyException; import org.signal.libsignal.protocol.ecc.Curve; import org.signal.libsignal.protocol.ecc.ECPublicKey; import org.signal.libsignal.zkgroup.profiles.ProfileKey; +import org.signal.qr.kitkat.ScanListener; import org.thoughtcrime.securesms.crypto.ProfileKeyUtil; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.permissions.Permissions; -import org.thoughtcrime.securesms.qr.ScanListener; import org.thoughtcrime.securesms.util.Base64; import org.thoughtcrime.securesms.util.DynamicLanguage; import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme; diff --git a/app/src/main/java/org/thoughtcrime/securesms/DeviceAddFragment.java b/app/src/main/java/org/thoughtcrime/securesms/DeviceAddFragment.java index 0ee804408..5d7e4ad1c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/DeviceAddFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/DeviceAddFragment.java @@ -15,26 +15,25 @@ import android.widget.LinearLayout; import androidx.annotation.NonNull; -import org.thoughtcrime.securesms.components.camera.CameraView; -import org.thoughtcrime.securesms.qr.ScanListener; -import org.thoughtcrime.securesms.qr.ScanningThread; +import org.signal.qr.QrScannerView; +import org.signal.qr.kitkat.ScanListener; +import org.thoughtcrime.securesms.util.LifecycleDisposable; import org.thoughtcrime.securesms.util.ViewUtil; public class DeviceAddFragment extends LoggingFragment { - private ViewGroup container; - private LinearLayout overlay; - private ImageView devicesImage; - private CameraView scannerView; - private ScanningThread scanningThread; - private ScanListener scanListener; + private final LifecycleDisposable lifecycleDisposable = new LifecycleDisposable(); + + private LinearLayout overlay; + private ImageView devicesImage; + private ScanListener scanListener; @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup viewGroup, Bundle bundle) { - this.container = ViewUtil.inflate(inflater, viewGroup, R.layout.device_add_fragment); - this.overlay = this.container.findViewById(R.id.overlay); - this.scannerView = this.container.findViewById(R.id.scanner); - this.devicesImage = this.container.findViewById(R.id.devices); + ViewGroup container = ViewUtil.inflate(inflater, viewGroup, R.layout.device_add_fragment); + this.overlay = container.findViewById(R.id.overlay); + QrScannerView scannerView = container.findViewById(R.id.scanner); + this.devicesImage = container.findViewById(R.id.devices); if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) { this.overlay.setOrientation(LinearLayout.HORIZONTAL); @@ -43,7 +42,7 @@ public class DeviceAddFragment extends LoggingFragment { } if (Build.VERSION.SDK_INT >= 21) { - this.container.addOnLayoutChangeListener(new View.OnLayoutChangeListener() { + container.addOnLayoutChangeListener(new View.OnLayoutChangeListener() { @TargetApi(21) @Override public void onLayoutChange(View v, int left, int top, int right, int bottom, @@ -59,52 +58,34 @@ public class DeviceAddFragment extends LoggingFragment { }); } - return this.container; - } + scannerView.start(getViewLifecycleOwner()); - @Override - public void onResume() { - super.onResume(); - this.scanningThread = new ScanningThread(); - this.scanningThread.setScanListener(scanListener); - this.scannerView.onResume(); - this.scannerView.setPreviewCallback(scanningThread); - this.scanningThread.start(); - } + lifecycleDisposable.bindTo(getViewLifecycleOwner()); + lifecycleDisposable.add(scannerView.getQrData().subscribe(qrData -> { + if (scanListener != null) { + scanListener.onQrDataFound(qrData); + } + })); - @Override - public void onPause() { - super.onPause(); - this.scannerView.onPause(); - this.scanningThread.stopScanning(); + return container; } @Override public void onConfigurationChanged(@NonNull Configuration newConfiguration) { super.onConfigurationChanged(newConfiguration); - this.scannerView.onPause(); - if (newConfiguration.orientation == Configuration.ORIENTATION_LANDSCAPE) { overlay.setOrientation(LinearLayout.HORIZONTAL); } else { overlay.setOrientation(LinearLayout.VERTICAL); } - - this.scannerView.onResume(); - this.scannerView.setPreviewCallback(scanningThread); } - public ImageView getDevicesImage() { return devicesImage; } public void setScanListener(ScanListener scanListener) { this.scanListener = scanListener; - - if (this.scanningThread != null) { - this.scanningThread.setScanListener(scanListener); - } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/camera/CameraView.java b/app/src/main/java/org/thoughtcrime/securesms/components/camera/CameraView.java index a7d84a2e6..c89e48d30 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/camera/CameraView.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/camera/CameraView.java @@ -38,6 +38,7 @@ import androidx.annotation.Nullable; import org.signal.core.util.ThreadUtil; import org.signal.core.util.logging.Log; +import org.signal.qr.kitkat.QrCameraView; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.util.BitmapUtil; import org.thoughtcrime.securesms.util.TextSecurePreferences; @@ -228,7 +229,7 @@ public class CameraView extends ViewGroup { listeners.add(listener); } - public void setPreviewCallback(final @NonNull PreviewCallback previewCallback) { + public void setPreviewCallback(final @NonNull QrCameraView.PreviewCallback previewCallback) { enqueueTask(new PostInitializationTask() { @Override protected void onPostMain(Void avoid) { @@ -243,7 +244,7 @@ public class CameraView extends ViewGroup { final int rotation = getCameraPictureOrientation(); final Size previewSize = camera.getParameters().getPreviewSize(); if (data != null) { - previewCallback.onPreviewFrame(new PreviewFrame(data, previewSize.width, previewSize.height, rotation)); + previewCallback.onPreviewFrame(new QrCameraView.PreviewFrame(data, previewSize.width, previewSize.height, rotation)); } } }); @@ -568,40 +569,6 @@ public class CameraView extends ViewGroup { void onCameraStop(); } - public interface PreviewCallback { - void onPreviewFrame(@NonNull PreviewFrame frame); - } - - public static class PreviewFrame { - private final @NonNull byte[] data; - private final int width; - private final int height; - private final int orientation; - - private PreviewFrame(@NonNull byte[] data, int width, int height, int orientation) { - this.data = data; - this.width = width; - this.height = height; - this.orientation = orientation; - } - - public @NonNull byte[] getData() { - return data; - } - - public int getWidth() { - return width; - } - - public int getHeight() { - return height; - } - - public int getOrientation() { - return orientation; - } - } - private enum State { PAUSED, RESUMED, ACTIVE } diff --git a/app/src/main/java/org/thoughtcrime/securesms/payments/preferences/transfer/PaymentsTransferQrScanFragment.java b/app/src/main/java/org/thoughtcrime/securesms/payments/preferences/transfer/PaymentsTransferQrScanFragment.java index ef47233b5..d959dc907 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/payments/preferences/transfer/PaymentsTransferQrScanFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/payments/preferences/transfer/PaymentsTransferQrScanFragment.java @@ -13,11 +13,11 @@ import androidx.navigation.Navigation; import org.signal.core.util.ThreadUtil; import org.signal.core.util.logging.Log; +import org.signal.qr.kitkat.ScanningThread; import org.thoughtcrime.securesms.LoggingFragment; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.components.camera.CameraView; import org.thoughtcrime.securesms.payments.MobileCoinPublicAddress; -import org.thoughtcrime.securesms.qr.ScanningThread; import org.thoughtcrime.securesms.util.navigation.SafeNavigation; public final class PaymentsTransferQrScanFragment extends LoggingFragment { diff --git a/app/src/main/java/org/thoughtcrime/securesms/qr/ScanningThread.java b/app/src/main/java/org/thoughtcrime/securesms/qr/ScanningThread.java deleted file mode 100644 index ecb1d2e2f..000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/qr/ScanningThread.java +++ /dev/null @@ -1,126 +0,0 @@ -package org.thoughtcrime.securesms.qr; - -import android.content.res.Configuration; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.google.zxing.BinaryBitmap; -import com.google.zxing.ChecksumException; -import com.google.zxing.DecodeHintType; -import com.google.zxing.FormatException; -import com.google.zxing.NotFoundException; -import com.google.zxing.PlanarYUVLuminanceSource; -import com.google.zxing.Result; -import com.google.zxing.common.HybridBinarizer; -import com.google.zxing.qrcode.QRCodeReader; - -import org.signal.core.util.logging.Log; -import org.thoughtcrime.securesms.components.camera.CameraView; -import org.thoughtcrime.securesms.components.camera.CameraView.PreviewFrame; -import org.thoughtcrime.securesms.util.Util; - -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.atomic.AtomicReference; - -public class ScanningThread extends Thread implements CameraView.PreviewCallback { - - private static final String TAG = Log.tag(ScanningThread.class); - - private final QRCodeReader reader = new QRCodeReader(); - private final AtomicReference scanListener = new AtomicReference<>(); - private final Map hints = new HashMap<>(); - - private boolean scanning = true; - private PreviewFrame previewFrame; - - public void setCharacterSet(String characterSet) { - hints.put(DecodeHintType.CHARACTER_SET, characterSet); - } - - public void setScanListener(ScanListener scanListener) { - this.scanListener.set(scanListener); - } - - @Override - public void onPreviewFrame(@NonNull PreviewFrame previewFrame) { - try { - synchronized (this) { - this.previewFrame = previewFrame; - this.notify(); - } - } catch (RuntimeException e) { - Log.w(TAG, e); - } - } - - - @Override - public void run() { - while (true) { - PreviewFrame ourFrame; - - synchronized (this) { - while (scanning && previewFrame == null) { - Util.wait(this, 0); - } - - if (!scanning) return; - else ourFrame = previewFrame; - - previewFrame = null; - } - - String data = getScannedData(ourFrame.getData(), ourFrame.getWidth(), ourFrame.getHeight(), ourFrame.getOrientation()); - ScanListener scanListener = this.scanListener.get(); - - if (data != null && scanListener != null) { - scanListener.onQrDataFound(data); - return; - } - } - } - - public void stopScanning() { - synchronized (this) { - scanning = false; - notify(); - } - } - - private @Nullable String getScannedData(byte[] data, int width, int height, int orientation) { - try { - if (orientation == Configuration.ORIENTATION_PORTRAIT) { - byte[] rotatedData = new byte[data.length]; - - for (int y = 0; y < height; y++) { - for (int x = 0; x < width; x++) { - rotatedData[x * height + height - y - 1] = data[x + y * width]; - } - } - - int tmp = width; - width = height; - height = tmp; - data = rotatedData; - } - - PlanarYUVLuminanceSource source = new PlanarYUVLuminanceSource(data, width, height, - 0, 0, width, height, - false); - - BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source)); - Result result = reader.decode(bitmap, hints); - - if (result != null) return result.getText(); - - } catch (NullPointerException | ChecksumException | FormatException e) { - Log.w(TAG, e); - } catch (NotFoundException e) { - // Thanks ZXing... - } - - return null; - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/verify/VerifyIdentityFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/verify/VerifyIdentityFragment.kt index 1b3f9b38c..e6d8fdcf7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/verify/VerifyIdentityFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/verify/VerifyIdentityFragment.kt @@ -6,11 +6,11 @@ import android.view.View import android.widget.Toast import androidx.fragment.app.Fragment import org.signal.core.util.ThreadUtil +import org.signal.qr.kitkat.ScanListener import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.crypto.IdentityKeyParcelable import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.permissions.Permissions -import org.thoughtcrime.securesms.qr.ScanListener import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.RecipientId import org.thoughtcrime.securesms.util.ServiceUtil diff --git a/app/src/main/java/org/thoughtcrime/securesms/verify/VerifyScanFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/verify/VerifyScanFragment.kt index e27f5a3c6..98ee2b3d3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/verify/VerifyScanFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/verify/VerifyScanFragment.kt @@ -9,11 +9,11 @@ import android.view.ViewGroup import android.widget.ImageView import androidx.core.view.OneShotPreDrawListener import androidx.fragment.app.Fragment +import org.signal.qr.kitkat.ScanListener +import org.signal.qr.kitkat.ScanningThread import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.components.ShapeScrim import org.thoughtcrime.securesms.components.camera.CameraView -import org.thoughtcrime.securesms.qr.ScanListener -import org.thoughtcrime.securesms.qr.ScanningThread import org.thoughtcrime.securesms.util.ViewUtil import org.thoughtcrime.securesms.util.fragments.requireListener diff --git a/app/src/main/res/layout/device_add_fragment.xml b/app/src/main/res/layout/device_add_fragment.xml index 15900e0e0..4b3482ec1 100644 --- a/app/src/main/res/layout/device_add_fragment.xml +++ b/app/src/main/res/layout/device_add_fragment.xml @@ -1,51 +1,52 @@ - + - + - + + android:layout_height="match_parent" + android:layout_weight="1" /> - + android:gravity="center" + android:orientation="vertical" + android:paddingStart="16dp" + android:paddingEnd="16dp"> + android:transitionName="devices" /> - + diff --git a/qr/app/build.gradle b/qr/app/build.gradle new file mode 100644 index 000000000..2652aaf47 --- /dev/null +++ b/qr/app/build.gradle @@ -0,0 +1,14 @@ +apply from: "$rootProject.projectDir/signalModuleApp.gradle" + +android { + defaultConfig { + applicationId "org.signal.qrtest" + } +} + +dependencies { + implementation project(':qr') + implementation libs.rxjava3.rxjava + implementation libs.rxjava3.rxandroid + implementation libs.rxjava3.rxkotlin +} \ No newline at end of file diff --git a/qr/app/src/main/AndroidManifest.xml b/qr/app/src/main/AndroidManifest.xml new file mode 100644 index 000000000..e6dda092b --- /dev/null +++ b/qr/app/src/main/AndroidManifest.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/qr/app/src/main/ic_launcher-playstore.png b/qr/app/src/main/ic_launcher-playstore.png new file mode 100644 index 0000000000000000000000000000000000000000..3002aeecee88d93075cd5a33824f9ae34395a027 GIT binary patch literal 22059 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�Km6dkegyEY(*w!!PB`{QcA{U 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 23) { + requestPermissions(arrayOf(android.Manifest.permission.CAMERA), 1) + } + + val scanner = findViewById(R.id.scanner) + scanner.start(this) + + scanner.qrData + .distinctUntilChanged() + .observeOn(AndroidSchedulers.mainThread()) + .subscribeBy { Toast.makeText(this, it, Toast.LENGTH_SHORT).show() } + } +} diff --git a/qr/app/src/main/res/drawable/ic_launcher_background.xml b/qr/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 000000000..ca3826a46 --- /dev/null +++ b/qr/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/qr/app/src/main/res/drawable/ic_launcher_foreground.xml b/qr/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 000000000..ef6972e03 --- /dev/null +++ b/qr/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/qr/app/src/main/res/layout/activity_main.xml b/qr/app/src/main/res/layout/activity_main.xml new file mode 100644 index 000000000..d37c2a1bc --- /dev/null +++ b/qr/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,20 @@ + + + + + + + + + + \ No newline at end of file diff --git a/qr/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/qr/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 000000000..bbd3e0212 --- /dev/null +++ b/qr/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/qr/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/qr/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 000000000..bbd3e0212 --- /dev/null +++ b/qr/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/qr/app/src/main/res/mipmap-hdpi/ic_launcher.png b/qr/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..7a12bc6f0e43f2b102f0bf3b449d8ca07b22c973 GIT binary patch literal 2399 zcmY*bc{mhm7ne1<*|LT@!QS$?kc9|L}bd6niHw!8@yZvwzei*`?M)7bwlg zqueHnIbvpxNua`G+_)!e=hDdu9Ty=5B>Z3uDLaHoC)ktVfjNIyx-xAK;x{ORzalb1 z-!-m1t&G*t=x)@>AERs-ts%CN4^}}25Az;w#T<_A?6<8G#nsGZq)xgI9tBLiL zN34snWLqqR2Uc@@$ZstC@%PCgR37A`k-0Q}54Zg$cAWU_5sbK>8XDS_#H?{I*vB`8 zw=Hzak4qxQqwJ~?lO*?|_sspjaxtK-xDa8eN-T_lyEFu($u$Ini{)420$Gp0Q|7lc z$RYdSt|5y*DP=FLs)%O9YL8@QjuRr&2`(2M6~2?nnO$ z)u^NQvn+}OQI%pwLJT#;4g+bTJ(ns$=q;OoAxK)dr`I9ShzPX_L6V=I@& z-qb`6l&W!WGoq$~iiWf;`DEJEaj8yC?CBF^R}o&J_NcJg5*PH@p1ETrLbM;2Z0O2#Exo-0YELF-a=j({~d`2mzkZ7A}x3+p0@XjsAqXRT(T+c5pYJzQJE-K2mwD7RK-tHY$*- z5DX6UxDU z?p*=N3?tg;ZL`laUM)~ZSj$V&JXH*ud7sdSCX5hgmI#SW*$vPSmd|%NR(d~lC!UOZ zo)woM+pfAGP<&kXMF_cZz-T@InCyoks6rgUb` zm*|{=%t5M6kDWBfBT$I2?~RpE9akvawd8yz&}Ti1!5t70{N+R6gTf#`kw&DVShudx zzHN1BRs?VX&&S_Da_R z)eRt)Xi`1TL*tCjP4;De=c$kc?tAn|d^}_0+mbA0nSH<>U3x#`0yeO1n|iq(U6wX3 zki&L+d9q-t*)`L#tzVER%(()e=Hff~w8-LVX6%if&>s7yXQ}r%=*`3+!WF7=%;E{p zqmGCdkHgB0{XU+Zm^#1PgJ1v3duy=nrHfodf|SOiJ5a~cni9WCDJ^pax~Y8? z2N*W|&9qifdT;d2oXe+go{h^g1FPIYFCH&ma8Rlfq50LBsxH{`nZ{4o$)MzvXwgBH z#=gc6lW)A`In=(ejYe)Kr0ZVUFOvWZ6r4<di?$D%G3P@OKwz7(lGcob450i$oyxPGBLhZb;hgOAS0T8?uEu5& zxF6N;5M!<@YDxi1mKk3_riYd(YRSip(0PJ7Qlw#Ipxb)obJMvDn}o&I=HV1y4$y>xED{-qUTnY?TKrJ{ zc*d?%B=|}T=_rXElDpBW4i*g4%A!sY55p}5oa?Ai_hs-C-z@QilCO1()ZbD}Z<1Fr zV~P`rOVDpGtUB4_n)CJEVGYJxHJWS7b#w&@MO}hqi^hj2SV$DU-O&T5z3I3&_8l%M zSS5o6xnHcUOnH$ zroO?xRfWRVvC@t!fwP3d)ldH`cFSMV9fUd26ZyW=JxSz9=ezB3cw@{Qc{2XV?_78H z?%BGLzTwLo@et>&0s34QDIIM!cG#Oc8F@bTb?4EH^@9>wj zVzmzaEdd5yi=S{lzNlN&WaKd9wKubcUO}~0_e@=k{)fjD=xWm~XEVIWX(nE?XVbR^ zDzKnR%9k=G>6cB2@WQkT4aAXjK`!R74qIw2s_!N#{81(EJq#gy$3tFq8l;Q1i`J=v z)OJhpX3xzC#jwKM{g=U)#{z1t$Tfa_qj$!XQ9VJ7oIP3qLUYif|Oh|S# z>UH(1Lbz;d(**_aLJi%Xy&fcY4&lmQl?1bJ+OsTDIO9GQ>P;!znNo{p_fF8Un><84 z0DpDkWy2>dI^c2Q^~N5nhVVeemHTr^h^N=}v3;NFFY8(GV`G#2Y)3YKmo#=Ll|}JQ zC}C}UtSgoSRRa;w2J1x?l)gNTw zI3Fi2 N>#O$W<))rb{|h=qvd;hj literal 0 HcmV?d00001 diff --git a/qr/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/qr/app/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..0330f9fef8fe9f03c4440cc73259e949da9ea8ba GIT binary patch literal 4406 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 literal 0 HcmV?d00001 diff --git a/qr/app/src/main/res/mipmap-mdpi/ic_launcher.png b/qr/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..3569ae9c7c89c59056a69adfc279e61146924ca6 GIT binary patch literal 1816 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 SN&24xV0^{Qpia*%@qYj!jtYhV literal 0 HcmV?d00001 diff --git a/qr/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/qr/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..f4c75f5c10a009d72547fcce7c9bc7d8c513cda9 GIT binary patch literal 6043 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 d8RpqrYKA!h} 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 literal 0 HcmV?d00001 diff --git a/qr/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/qr/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..97a63cdc9c654d9e39e763610813b65272c0fb18 GIT binary patch literal 7593 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 + + + \ No newline at end of file diff --git a/qr/app/src/main/res/values/colors.xml b/qr/app/src/main/res/values/colors.xml new file mode 100644 index 000000000..09837df62 --- /dev/null +++ b/qr/app/src/main/res/values/colors.xml @@ -0,0 +1,10 @@ + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + \ No newline at end of file diff --git a/qr/app/src/main/res/values/strings.xml b/qr/app/src/main/res/values/strings.xml new file mode 100644 index 000000000..94b7f4a70 --- /dev/null +++ b/qr/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + QR + \ No newline at end of file diff --git a/qr/app/src/main/res/values/themes.xml b/qr/app/src/main/res/values/themes.xml new file mode 100644 index 000000000..6cad716a6 --- /dev/null +++ b/qr/app/src/main/res/values/themes.xml @@ -0,0 +1,16 @@ + + + + \ No newline at end of file diff --git a/qr/lib/build.gradle b/qr/lib/build.gradle new file mode 100644 index 000000000..9b34c8ae6 --- /dev/null +++ b/qr/lib/build.gradle @@ -0,0 +1,18 @@ +apply from: "$rootProject.projectDir/signalModule.gradle" + +dependencies { + implementation libs.androidx.camera.core + implementation libs.androidx.camera.camera2 + implementation libs.androidx.camera.lifecycle + implementation libs.androidx.camera.view + implementation libs.androidx.lifecycle.common.java8 + + implementation libs.google.zxing.android.integration + implementation libs.google.zxing.core + + implementation libs.rxjava3.rxjava + + // Included to force proper versions for verification and match main app + implementation 'androidx.lifecycle:lifecycle-livedata:2.3.1' + implementation 'com.google.guava:guava:30.0-android' +} \ No newline at end of file diff --git a/qr/lib/src/main/AndroidManifest.xml b/qr/lib/src/main/AndroidManifest.xml new file mode 100644 index 000000000..698218b32 --- /dev/null +++ b/qr/lib/src/main/AndroidManifest.xml @@ -0,0 +1,14 @@ + + + + + + + + + + \ No newline at end of file diff --git a/qr/lib/src/main/java/org/signal/qr/QrProcessor.kt b/qr/lib/src/main/java/org/signal/qr/QrProcessor.kt new file mode 100644 index 000000000..a0b645dbb --- /dev/null +++ b/qr/lib/src/main/java/org/signal/qr/QrProcessor.kt @@ -0,0 +1,50 @@ +package org.signal.qr + +import com.google.zxing.BinaryBitmap +import com.google.zxing.ChecksumException +import com.google.zxing.DecodeHintType +import com.google.zxing.FormatException +import com.google.zxing.NotFoundException +import com.google.zxing.PlanarYUVLuminanceSource +import com.google.zxing.Result +import com.google.zxing.common.HybridBinarizer +import com.google.zxing.qrcode.QRCodeReader +import org.signal.core.util.logging.Log + +/** + * Wraps [QRCodeReader] for use from API19 or API21+. + */ +class QrProcessor { + + private val reader = QRCodeReader() + + fun getScannedData( + data: ByteArray, + width: Int, + height: Int + ): String? { + try { + val source = PlanarYUVLuminanceSource(data, width, height, 0, 0, width, height, false) + + val bitmap = BinaryBitmap(HybridBinarizer(source)) + val result: Result? = reader.decode(bitmap, emptyMap()) + + if (result != null) { + return result.text + } + } catch (e: NullPointerException) { + Log.w(TAG, e) + } catch (e: ChecksumException) { + Log.w(TAG, e) + } catch (e: FormatException) { + Log.w(TAG, e) + } catch (e: NotFoundException) { + // Thanks ZXing... + } + return null + } + + companion object { + private val TAG = Log.tag(QrProcessor::class.java) + } +} diff --git a/qr/lib/src/main/java/org/signal/qr/QrScannerView.kt b/qr/lib/src/main/java/org/signal/qr/QrScannerView.kt new file mode 100644 index 000000000..588e6371d --- /dev/null +++ b/qr/lib/src/main/java/org/signal/qr/QrScannerView.kt @@ -0,0 +1,50 @@ +package org.signal.qr + +import android.content.Context +import android.os.Build +import android.util.AttributeSet +import android.widget.FrameLayout +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.LifecycleOwner +import io.reactivex.rxjava3.core.Observable +import io.reactivex.rxjava3.subjects.PublishSubject + +/** + * View for starting up a camera and scanning a QR-Code. Safe to use on an API version and + * will delegate to legacy camera APIs or CameraX APIs when appropriate. + * + * QR-code data is emitted via [qrData] observable. + */ +class QrScannerView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : FrameLayout(context, attrs, defStyleAttr), ScannerView { + + private val scannerView: ScannerView + private val qrDataPublish: PublishSubject = PublishSubject.create() + + val qrData: Observable = qrDataPublish + + init { + val scannerView: FrameLayout = if (Build.VERSION.SDK_INT >= 21) { + ScannerView21(context) { qrDataPublish.onNext(it) } + } else { + ScannerView19(context) { qrDataPublish.onNext(it) } + } + + scannerView.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT) + addView(scannerView) + + this.scannerView = (scannerView as ScannerView) + } + + override fun start(lifecycleOwner: LifecycleOwner) { + scannerView.start(lifecycleOwner) + lifecycleOwner.lifecycle.addObserver(object : DefaultLifecycleObserver { + override fun onDestroy(owner: LifecycleOwner) { + qrDataPublish.onComplete() + } + }) + } +} diff --git a/qr/lib/src/main/java/org/signal/qr/ScannerView.kt b/qr/lib/src/main/java/org/signal/qr/ScannerView.kt new file mode 100644 index 000000000..2e064edef --- /dev/null +++ b/qr/lib/src/main/java/org/signal/qr/ScannerView.kt @@ -0,0 +1,10 @@ +package org.signal.qr + +import androidx.lifecycle.LifecycleOwner + +/** + * Common interface for interacting with QR scanning views. + */ +interface ScannerView { + fun start(lifecycleOwner: LifecycleOwner) +} diff --git a/qr/lib/src/main/java/org/signal/qr/ScannerView19.kt b/qr/lib/src/main/java/org/signal/qr/ScannerView19.kt new file mode 100644 index 000000000..d27adf480 --- /dev/null +++ b/qr/lib/src/main/java/org/signal/qr/ScannerView19.kt @@ -0,0 +1,49 @@ +package org.signal.qr + +import android.annotation.SuppressLint +import android.content.Context +import android.widget.FrameLayout +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.LifecycleOwner +import org.signal.qr.kitkat.QrCameraView +import org.signal.qr.kitkat.ScanListener +import org.signal.qr.kitkat.ScanningThread + +/** + * API19 version of QR scanning. Uses deprecated camera APIs. + */ +@SuppressLint("ViewConstructor") +internal class ScannerView19 constructor( + context: Context, + private val scanListener: ScanListener +) : FrameLayout(context), ScannerView { + + private var scanningThread: ScanningThread? = null + private val cameraView: QrCameraView + + init { + cameraView = QrCameraView(context) + cameraView.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT) + addView(cameraView) + } + + override fun start(lifecycleOwner: LifecycleOwner) { + lifecycleOwner.lifecycle.addObserver(object : DefaultLifecycleObserver { + override fun onResume(owner: LifecycleOwner) { + val scanningThread = ScanningThread() + scanningThread.setScanListener(scanListener) + cameraView.onResume() + cameraView.setPreviewCallback(scanningThread) + scanningThread.start() + + this@ScannerView19.scanningThread = scanningThread + } + + override fun onPause(owner: LifecycleOwner) { + cameraView.onPause() + scanningThread?.stopScanning() + scanningThread = null + } + }) + } +} diff --git a/qr/lib/src/main/java/org/signal/qr/ScannerView21.kt b/qr/lib/src/main/java/org/signal/qr/ScannerView21.kt new file mode 100644 index 000000000..3831fa960 --- /dev/null +++ b/qr/lib/src/main/java/org/signal/qr/ScannerView21.kt @@ -0,0 +1,103 @@ +package org.signal.qr + +import android.annotation.SuppressLint +import android.content.Context +import android.widget.FrameLayout +import androidx.annotation.RequiresApi +import androidx.camera.core.Camera +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.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.LifecycleOwner +import org.signal.core.util.logging.Log +import org.signal.qr.kitkat.ScanListener +import java.util.concurrent.Executors + +/** + * API21+ version of QR scanning view. Uses camerax APIs. + */ +@SuppressLint("ViewConstructor") +@RequiresApi(21) +internal class ScannerView21 constructor( + context: Context, + private val listener: ScanListener +) : FrameLayout(context), ScannerView { + + private val analyzerExecutor = Executors.newSingleThreadExecutor() + private var cameraProvider: ProcessCameraProvider? = null + private var camera: Camera? = null + private var previewView: PreviewView + private val qrProcessor = QrProcessor() + + init { + previewView = PreviewView(context) + previewView.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT) + addView(previewView) + } + + override fun start(lifecycleOwner: LifecycleOwner) { + previewView.post { + Log.i(TAG, "Starting") + ProcessCameraProvider.getInstance(context).apply { + addListener({ + try { + onCameraProvider(lifecycleOwner, get()) + } catch (e: Exception) { + Log.w(TAG, e) + } + }, ContextCompat.getMainExecutor(context)) + } + } + + lifecycleOwner.lifecycle.addObserver(object : DefaultLifecycleObserver { + override fun onDestroy(owner: LifecycleOwner) { + cameraProvider = null + camera = null + analyzerExecutor.shutdown() + } + }) + } + + private fun onCameraProvider(lifecycle: LifecycleOwner, cameraProvider: ProcessCameraProvider?) { + if (cameraProvider == null) { + Log.w(TAG, "Camera provider is null") + return + } + + Log.i(TAG, "Initializing use cases") + + val preview = Preview.Builder().build() + + val imageAnalysis = ImageAnalysis.Builder() + .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) + .build() + + imageAnalysis.setAnalyzer(analyzerExecutor) { proxy -> + val buffer = proxy.planes[0].buffer + val bytes = ByteArray(buffer.capacity()) + buffer.get(bytes) + + val data: String? = qrProcessor.getScannedData(bytes, proxy.width, proxy.height) + if (data != null) { + listener.onQrDataFound(data) + } + + proxy.close() + } + + cameraProvider.unbindAll() + camera = cameraProvider.bindToLifecycle(lifecycle, CameraSelector.DEFAULT_BACK_CAMERA, preview, imageAnalysis) + + preview.setSurfaceProvider(previewView.surfaceProvider) + + this.cameraProvider = cameraProvider + } + + companion object { + private val TAG = Log.tag(ScannerView21::class.java) + } +} diff --git a/qr/lib/src/main/java/org/signal/qr/kitkat/CameraSurfaceView.java b/qr/lib/src/main/java/org/signal/qr/kitkat/CameraSurfaceView.java new file mode 100644 index 000000000..fcdc4bbcd --- /dev/null +++ b/qr/lib/src/main/java/org/signal/qr/kitkat/CameraSurfaceView.java @@ -0,0 +1,33 @@ +package org.signal.qr.kitkat; + +import android.content.Context; +import android.view.SurfaceHolder; +import android.view.SurfaceView; + +public class CameraSurfaceView extends SurfaceView implements SurfaceHolder.Callback { + private boolean ready; + + @SuppressWarnings("deprecation") + public CameraSurfaceView(Context context) { + super(context); + getHolder().setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS); + getHolder().addCallback(this); + } + + public boolean isReady() { + return ready; + } + + @Override + public void surfaceCreated(SurfaceHolder holder) { + ready = true; + } + + @Override + public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {} + + @Override + public void surfaceDestroyed(SurfaceHolder holder) { + ready = false; + } +} diff --git a/qr/lib/src/main/java/org/signal/qr/kitkat/CameraUtils.java b/qr/lib/src/main/java/org/signal/qr/kitkat/CameraUtils.java new file mode 100644 index 000000000..13407c69b --- /dev/null +++ b/qr/lib/src/main/java/org/signal/qr/kitkat/CameraUtils.java @@ -0,0 +1,109 @@ +package org.signal.qr.kitkat; + +import android.app.Activity; +import android.hardware.Camera.CameraInfo; +import android.hardware.Camera.Parameters; +import android.hardware.Camera.Size; +import android.util.DisplayMetrics; +import android.view.Surface; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.signal.core.util.logging.Log; + +import java.util.Collections; +import java.util.Comparator; +import java.util.LinkedList; +import java.util.List; +import java.util.Locale; + +@SuppressWarnings("deprecation") +public class CameraUtils { + private static final String TAG = Log.tag(CameraUtils.class); + /* + * modified from: https://github.com/commonsguy/cwac-camera/blob/master/camera/src/com/commonsware/cwac/camera/CameraUtils.java + */ + public static @Nullable Size getPreferredPreviewSize(int displayOrientation, + int width, + int height, + @NonNull Parameters parameters) { + final int targetWidth = displayOrientation % 180 == 90 ? height : width; + final int targetHeight = displayOrientation % 180 == 90 ? width : height; + final double targetRatio = (double) targetWidth / targetHeight; + + Log.d(TAG, String.format(Locale.US, + "getPreferredPreviewSize(%d, %d, %d) -> target %dx%d, AR %.02f", + displayOrientation, width, height, + targetWidth, targetHeight, targetRatio)); + + List sizes = parameters.getSupportedPreviewSizes(); + List ideals = new LinkedList<>(); + List bigEnough = new LinkedList<>(); + + for (Size size : sizes) { + Log.d(TAG, String.format(Locale.US, " %dx%d (%.02f)", size.width, size.height, (float)size.width / size.height)); + + if (size.height == size.width * targetRatio && size.height >= targetHeight && size.width >= targetWidth) { + ideals.add(size); + Log.d(TAG, " (ideal ratio)"); + } else if (size.width >= targetWidth && size.height >= targetHeight) { + bigEnough.add(size); + Log.d(TAG, " (good size, suboptimal ratio)"); + } + } + + if (!ideals.isEmpty()) return Collections.min(ideals, new AreaComparator()); + else if (!bigEnough.isEmpty()) return Collections.min(bigEnough, new AspectRatioComparator(targetRatio)); + else return Collections.max(sizes, new AreaComparator()); + } + + // based on + // http://developer.android.com/reference/android/hardware/Camera.html#setDisplayOrientation(int) + // and http://stackoverflow.com/a/10383164/115145 + public static int getCameraDisplayOrientation(@NonNull Activity activity, + @NonNull CameraInfo info) + { + int rotation = activity.getWindowManager().getDefaultDisplay().getRotation(); + int degrees = 0; + DisplayMetrics dm = new DisplayMetrics(); + + activity.getWindowManager().getDefaultDisplay().getMetrics(dm); + + switch (rotation) { + case Surface.ROTATION_0: degrees = 0; break; + case Surface.ROTATION_90: degrees = 90; break; + case Surface.ROTATION_180: degrees = 180; break; + case Surface.ROTATION_270: degrees = 270; break; + } + + if (info.facing == CameraInfo.CAMERA_FACING_FRONT) { + return (360 - ((info.orientation + degrees) % 360)) % 360; + } else { + return (info.orientation - degrees + 360) % 360; + } + } + + private static class AreaComparator implements Comparator { + @Override + public int compare(Size lhs, Size rhs) { + return Long.signum(lhs.width * lhs.height - rhs.width * rhs.height); + } + } + + private static class AspectRatioComparator extends AreaComparator { + private final double target; + public AspectRatioComparator(double target) { + this.target = target; + } + + @Override + public int compare(Size lhs, Size rhs) { + final double lhsDiff = Math.abs(target - (double) lhs.width / lhs.height); + final double rhsDiff = Math.abs(target - (double) rhs.width / rhs.height); + if (lhsDiff < rhsDiff) return -1; + else if (lhsDiff > rhsDiff) return 1; + else return super.compare(lhs, rhs); + } + } +} diff --git a/qr/lib/src/main/java/org/signal/qr/kitkat/QrCameraView.java b/qr/lib/src/main/java/org/signal/qr/kitkat/QrCameraView.java new file mode 100644 index 000000000..3172d77a4 --- /dev/null +++ b/qr/lib/src/main/java/org/signal/qr/kitkat/QrCameraView.java @@ -0,0 +1,472 @@ +/* + Copyright (c) 2013-2014 CommonsWare, LLC + Portions Copyright (C) 2007 The Android Open Source Project +

+ Licensed under the Apache License, Version 2.0 (the "License"); you may + not use this file except in compliance with the License. You may obtain + a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +package org.signal.qr.kitkat; + +import android.app.Activity; +import android.content.Context; +import android.content.pm.ActivityInfo; +import android.graphics.Color; +import android.hardware.Camera; +import android.hardware.Camera.CameraInfo; +import android.hardware.Camera.Parameters; +import android.hardware.Camera.Size; +import android.os.AsyncTask; +import android.util.AttributeSet; +import android.view.OrientationEventListener; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.signal.core.util.ThreadUtil; +import org.signal.core.util.logging.Log; + +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.Optional; + +@SuppressWarnings("deprecation") +public class QrCameraView extends ViewGroup { + private static final String TAG = Log.tag(QrCameraView.class); + + private final CameraSurfaceView surface; + private final OnOrientationChange onOrientationChange; + + private volatile Optional camera = Optional.empty(); + private final int cameraId = CameraInfo.CAMERA_FACING_BACK; + private volatile int displayOrientation = -1; + + private @NonNull State state = State.PAUSED; + private @Nullable Size previewSize; + private final List listeners = Collections.synchronizedList(new LinkedList<>()); + private int outputOrientation = -1; + + public QrCameraView(Context context) { + this(context, null); + } + + public QrCameraView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public QrCameraView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + setBackgroundColor(Color.BLACK); + + surface = new CameraSurfaceView(getContext()); + onOrientationChange = new OnOrientationChange(context.getApplicationContext()); + addView(surface); + } + + public void onResume() { + if (state != State.PAUSED) return; + state = State.RESUMED; + Log.i(TAG, "onResume() queued"); + enqueueTask(new SerialAsyncTask() { + @Override + protected + @Nullable + Void onRunBackground() { + try { + long openStartMillis = System.currentTimeMillis(); + camera = Optional.ofNullable(Camera.open(cameraId)); + Log.i(TAG, "camera.open() -> " + (System.currentTimeMillis() - openStartMillis) + "ms"); + synchronized (QrCameraView.this) { + QrCameraView.this.notifyAll(); + } + camera.ifPresent(value -> onCameraReady(value)); + } catch (Exception e) { + Log.w(TAG, e); + } + return null; + } + + @Override + protected void onPostMain(Void avoid) { + if (!camera.isPresent()) { + Log.w(TAG, "tried to open camera but got null"); + for (CameraViewListener listener : listeners) { + listener.onCameraFail(); + } + return; + } + + if (getActivity().getRequestedOrientation() != ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED) { + onOrientationChange.enable(); + } + Log.i(TAG, "onResume() completed"); + } + }); + } + + public void onPause() { + if (state == State.PAUSED) return; + state = State.PAUSED; + Log.i(TAG, "onPause() queued"); + + enqueueTask(new SerialAsyncTask() { + private Optional cameraToDestroy; + + @Override + protected void onPreMain() { + cameraToDestroy = camera; + camera = Optional.empty(); + } + + @Override + protected Void onRunBackground() { + if (cameraToDestroy.isPresent()) { + try { + stopPreview(); + cameraToDestroy.get().setPreviewCallback(null); + cameraToDestroy.get().release(); + Log.w(TAG, "released old camera instance"); + } catch (Exception e) { + Log.w(TAG, e); + } + } + return null; + } + + @Override protected void onPostMain(Void avoid) { + onOrientationChange.disable(); + displayOrientation = -1; + outputOrientation = -1; + removeView(surface); + addView(surface); + Log.i(TAG, "onPause() completed"); + } + }); + + for (CameraViewListener listener : listeners) { + listener.onCameraStop(); + } + } + + public boolean isStarted() { + return state != State.PAUSED; + } + + @SuppressWarnings("SuspiciousNameCombination") + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + final int width = r - l; + final int height = b - t; + final int previewWidth; + final int previewHeight; + + if (camera.isPresent() && previewSize != null) { + if (displayOrientation == 90 || displayOrientation == 270) { + previewWidth = previewSize.height; + previewHeight = previewSize.width; + } else { + previewWidth = previewSize.width; + previewHeight = previewSize.height; + } + } else { + previewWidth = width; + previewHeight = height; + } + + if (previewHeight == 0 || previewWidth == 0) { + Log.w(TAG, "skipping layout due to zero-width/height preview size"); + return; + } + + if (width * previewHeight > height * previewWidth) { + final int scaledChildHeight = previewHeight * width / previewWidth; + surface.layout(0, (height - scaledChildHeight) / 2, width, (height + scaledChildHeight) / 2); + } else { + final int scaledChildWidth = previewWidth * height / previewHeight; + surface.layout((width - scaledChildWidth) / 2, 0, (width + scaledChildWidth) / 2, height); + } + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + Log.i(TAG, "onSizeChanged(" + oldw + "x" + oldh + " -> " + w + "x" + h + ")"); + super.onSizeChanged(w, h, oldw, oldh); + camera.ifPresent(value -> startPreview(value.getParameters())); + } + + public void addListener(@NonNull CameraViewListener listener) { + listeners.add(listener); + } + + public void setPreviewCallback(final @NonNull PreviewCallback previewCallback) { + enqueueTask(new PostInitializationTask() { + @Override + protected void onPostMain(Void avoid) { + camera.ifPresent(value -> value.setPreviewCallback((data, camera) -> { + if (!QrCameraView.this.camera.isPresent()) { + return; + } + + final int rotation = getCameraPictureOrientation(); + final Size previewSize = camera.getParameters().getPreviewSize(); + if (data != null) { + previewCallback.onPreviewFrame(new PreviewFrame(data, previewSize.width, previewSize.height, rotation)); + } + })); + } + }); + } + + private void onCameraReady(final @NonNull Camera camera) { + final Parameters parameters = camera.getParameters(); + + parameters.setRecordingHint(true); + final List focusModes = parameters.getSupportedFocusModes(); + if (focusModes.contains(Parameters.FOCUS_MODE_CONTINUOUS_PICTURE)) { + parameters.setFocusMode(Parameters.FOCUS_MODE_CONTINUOUS_PICTURE); + } else if (focusModes.contains(Parameters.FOCUS_MODE_CONTINUOUS_VIDEO)) { + parameters.setFocusMode(Parameters.FOCUS_MODE_CONTINUOUS_VIDEO); + } + + displayOrientation = CameraUtils.getCameraDisplayOrientation(getActivity(), getCameraInfo()); + camera.setDisplayOrientation(displayOrientation); + camera.setParameters(parameters); + enqueueTask(new PostInitializationTask() { + @Override + protected Void onRunBackground() { + try { + camera.setPreviewDisplay(surface.getHolder()); + startPreview(parameters); + } catch (Exception e) { + Log.w(TAG, "couldn't set preview display", e); + } + return null; + } + }); + } + + private void startPreview(final @NonNull Parameters parameters) { + camera.ifPresent(camera -> { + try { + final Size preferredPreviewSize = getPreferredPreviewSize(parameters); + + if (preferredPreviewSize != null && !parameters.getPreviewSize().equals(preferredPreviewSize)) { + Log.i(TAG, "starting preview with size " + preferredPreviewSize.width + "x" + preferredPreviewSize.height); + if (state == State.ACTIVE) stopPreview(); + previewSize = preferredPreviewSize; + parameters.setPreviewSize(preferredPreviewSize.width, preferredPreviewSize.height); + camera.setParameters(parameters); + } else { + previewSize = parameters.getPreviewSize(); + } + long previewStartMillis = System.currentTimeMillis(); + camera.startPreview(); + Log.i(TAG, "camera.startPreview() -> " + (System.currentTimeMillis() - previewStartMillis) + "ms"); + state = State.ACTIVE; + ThreadUtil.runOnMain(() -> { + requestLayout(); + for (CameraViewListener listener : listeners) { + listener.onCameraStart(); + } + }); + } catch (Exception e) { + Log.w(TAG, e); + } + }); + } + + private void stopPreview() { + camera.ifPresent(camera -> { + try { + camera.stopPreview(); + state = State.RESUMED; + } catch (Exception e) { + Log.w(TAG, e); + } + }); + } + + private Size getPreferredPreviewSize(@NonNull Parameters parameters) { + return CameraUtils.getPreferredPreviewSize(displayOrientation, + getMeasuredWidth(), + getMeasuredHeight(), + parameters); + } + + private int getCameraPictureOrientation() { + if (getActivity().getRequestedOrientation() != ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED) { + outputOrientation = getCameraPictureRotation(getActivity().getWindowManager() + .getDefaultDisplay() + .getOrientation()); + } else if (getCameraInfo().facing == CameraInfo.CAMERA_FACING_FRONT) { + outputOrientation = (360 - displayOrientation) % 360; + } else { + outputOrientation = displayOrientation; + } + + return outputOrientation; + } + + private @NonNull CameraInfo getCameraInfo() { + final CameraInfo info = new CameraInfo(); + Camera.getCameraInfo(cameraId, info); + return info; + } + + // XXX this sucks + private Activity getActivity() { + return (Activity) getContext(); + } + + public int getCameraPictureRotation(int orientation) { + final CameraInfo info = getCameraInfo(); + final int rotation; + + orientation = (orientation + 45) / 90 * 90; + + if (info.facing == CameraInfo.CAMERA_FACING_FRONT) { + rotation = (info.orientation - orientation + 360) % 360; + } else { + rotation = (info.orientation + orientation) % 360; + } + + return rotation; + } + + private class OnOrientationChange extends OrientationEventListener { + public OnOrientationChange(Context context) { + super(context); + disable(); + } + + @Override + public void onOrientationChanged(int orientation) { + camera.ifPresent(camera -> { + if (orientation != ORIENTATION_UNKNOWN) { + int newOutputOrientation = getCameraPictureRotation(orientation); + + if (newOutputOrientation != outputOrientation) { + outputOrientation = newOutputOrientation; + + Parameters params = camera.getParameters(); + + params.setRotation(outputOrientation); + + try { + camera.setParameters(params); + } catch (Exception e) { + Log.e(TAG, "Exception updating camera parameters in orientation change", e); + } + } + } + }); + } + } + + private void enqueueTask(SerialAsyncTask job) { + AsyncTask.SERIAL_EXECUTOR.execute(job); + } + + public static abstract class SerialAsyncTask implements Runnable { + + @Override + public final void run() { + if (!onWait()) { + Log.w(TAG, "skipping task, preconditions not met in onWait()"); + return; + } + + ThreadUtil.runOnMainSync(this::onPreMain); + final Result result = onRunBackground(); + ThreadUtil.runOnMainSync(() -> onPostMain(result)); + } + + protected boolean onWait() {return true;} + + protected void onPreMain() {} + + protected Result onRunBackground() {return null;} + + protected void onPostMain(Result result) {} + } + + private abstract class PostInitializationTask extends SerialAsyncTask { + @Override protected boolean onWait() { + synchronized (QrCameraView.this) { + if (!camera.isPresent()) { + return false; + } + while (getMeasuredHeight() <= 0 || getMeasuredWidth() <= 0 || !surface.isReady()) { + Log.i(TAG, String.format("waiting. surface ready? %s", surface.isReady())); + waitFor(); + } + return true; + } + } + } + + private void waitFor() { + try { + wait(0); + } catch (InterruptedException ie) { + throw new AssertionError(ie); + } + } + + public interface CameraViewListener { + void onImageCapture(@NonNull final byte[] imageBytes); + + void onCameraFail(); + + void onCameraStart(); + + void onCameraStop(); + } + + public interface PreviewCallback { + void onPreviewFrame(@NonNull PreviewFrame frame); + } + + public static class PreviewFrame { + private final @NonNull byte[] data; + private final int width; + private final int height; + private final int orientation; + + public PreviewFrame(@NonNull byte[] data, int width, int height, int orientation) { + this.data = data; + this.width = width; + this.height = height; + this.orientation = orientation; + } + + public @NonNull byte[] getData() { + return data; + } + + public int getWidth() { + return width; + } + + public int getHeight() { + return height; + } + + public int getOrientation() { + return orientation; + } + } + + private enum State { + PAUSED, RESUMED, ACTIVE + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/qr/ScanListener.java b/qr/lib/src/main/java/org/signal/qr/kitkat/ScanListener.java similarity index 74% rename from app/src/main/java/org/thoughtcrime/securesms/qr/ScanListener.java rename to qr/lib/src/main/java/org/signal/qr/kitkat/ScanListener.java index 93224ae18..e902827cf 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/qr/ScanListener.java +++ b/qr/lib/src/main/java/org/signal/qr/kitkat/ScanListener.java @@ -1,4 +1,4 @@ -package org.thoughtcrime.securesms.qr; +package org.signal.qr.kitkat; import androidx.annotation.NonNull; diff --git a/qr/lib/src/main/java/org/signal/qr/kitkat/ScanningThread.java b/qr/lib/src/main/java/org/signal/qr/kitkat/ScanningThread.java new file mode 100644 index 000000000..ae440a744 --- /dev/null +++ b/qr/lib/src/main/java/org/signal/qr/kitkat/ScanningThread.java @@ -0,0 +1,88 @@ +package org.signal.qr.kitkat; + +import androidx.annotation.NonNull; + +import com.google.zxing.DecodeHintType; + +import org.signal.core.util.logging.Log; +import org.signal.qr.QrProcessor; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; + +import static org.signal.qr.kitkat.QrCameraView.PreviewCallback; +import static org.signal.qr.kitkat.QrCameraView.PreviewFrame; + +public class ScanningThread extends Thread implements PreviewCallback { + + private static final String TAG = Log.tag(ScanningThread.class); + + private final QrProcessor processor = new QrProcessor(); + private final AtomicReference scanListener = new AtomicReference<>(); + private final Map hints = new HashMap<>(); + + private boolean scanning = true; + private PreviewFrame previewFrame; + + public void setCharacterSet(String characterSet) { + hints.put(DecodeHintType.CHARACTER_SET, characterSet); + } + + public void setScanListener(ScanListener scanListener) { + this.scanListener.set(scanListener); + } + + @Override + public void onPreviewFrame(@NonNull PreviewFrame previewFrame) { + try { + synchronized (this) { + this.previewFrame = previewFrame; + this.notify(); + } + } catch (RuntimeException e) { + Log.w(TAG, e); + } + } + + @Override + public void run() { + while (true) { + PreviewFrame ourFrame; + + synchronized (this) { + while (scanning && previewFrame == null) { + waitFor(); + } + + if (!scanning) return; + else ourFrame = previewFrame; + + previewFrame = null; + } + + String data = processor.getScannedData(ourFrame.getData(), ourFrame.getWidth(), ourFrame.getHeight()); + ScanListener scanListener = this.scanListener.get(); + + if (data != null && scanListener != null) { + scanListener.onQrDataFound(data); + return; + } + } + } + + public void stopScanning() { + synchronized (this) { + scanning = false; + notify(); + } + } + + private void waitFor() { + try { + wait(0); + } catch (InterruptedException ie) { + throw new AssertionError(ie); + } + } +} diff --git a/settings.gradle b/settings.gradle index e5cf35768..78f4e11b5 100644 --- a/settings.gradle +++ b/settings.gradle @@ -18,6 +18,8 @@ include ':spinner' include ':spinner-app' include ':contacts' include ':contacts-app' +include ':qr' +include ':qr-app' project(':app').name = 'Signal-Android' project(':paging').projectDir = file('paging/lib') @@ -40,6 +42,9 @@ project(':spinner-app').projectDir = file('spinner/app') project(':contacts').projectDir = file('contacts/lib') project(':contacts-app').projectDir = file('contacts/app') +project(':qr').projectDir = file('qr/lib') +project(':qr-app').projectDir = file('qr/app') + rootProject.name='Signal' apply from: 'dependencies.gradle' diff --git a/signalModule.gradle b/signalModule.gradle new file mode 100644 index 000000000..e49aca2c3 --- /dev/null +++ b/signalModule.gradle @@ -0,0 +1,48 @@ +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' +apply plugin: 'org.jlleitschuh.gradle.ktlint' + +android { + buildToolsVersion BUILD_TOOL_VERSION + compileSdkVersion COMPILE_SDK + + defaultConfig { + minSdkVersion MINIMUM_SDK + targetSdkVersion TARGET_SDK + multiDexEnabled true + } + + compileOptions { + coreLibraryDesugaringEnabled true + sourceCompatibility JAVA_VERSION + targetCompatibility JAVA_VERSION + } + + kotlinOptions { + jvmTarget = '1.8' + } + + lintOptions { + disable 'InvalidVectorPath' + } +} + +ktlint { + // Use a newer version to resolve https://github.com/JLLeitschuh/ktlint-gradle/issues/507 + version = "0.43.2" +} + +dependencies { + lintChecks project(':lintchecks') + + implementation project(':core-util') + + coreLibraryDesugaring libs.android.tools.desugar + + implementation libs.androidx.core.ktx + implementation libs.androidx.fragment.ktx + implementation libs.androidx.annotation + implementation libs.androidx.appcompat + + implementation libs.kotlin.stdlib.jdk8 +} diff --git a/signalModuleApp.gradle b/signalModuleApp.gradle new file mode 100644 index 000000000..9a11e709f --- /dev/null +++ b/signalModuleApp.gradle @@ -0,0 +1,48 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply plugin: 'org.jlleitschuh.gradle.ktlint' + +android { + buildToolsVersion BUILD_TOOL_VERSION + compileSdkVersion COMPILE_SDK + + defaultConfig { + versionCode 1 + versionName "1.0" + + minSdkVersion 19 + targetSdkVersion TARGET_SDK + multiDexEnabled true + } + + kotlinOptions { + jvmTarget = '1.8' + } + + compileOptions { + coreLibraryDesugaringEnabled true + sourceCompatibility JAVA_VERSION + targetCompatibility JAVA_VERSION + } +} + +ktlint { + // Use a newer version to resolve https://github.com/JLLeitschuh/ktlint-gradle/issues/507 + version = "0.43.2" +} + +dependencies { + coreLibraryDesugaring libs.android.tools.desugar + + implementation "androidx.activity:activity-ktx:1.2.2" + + implementation libs.androidx.appcompat + implementation libs.material.material + implementation libs.androidx.constraintlayout + + implementation libs.kotlin.stdlib.jdk8 + + testImplementation testLibs.junit.junit + + implementation project(':core-util') +} \ No newline at end of file