Fix device linking issues on newer devices.
|
@ -455,6 +455,7 @@ dependencies {
|
||||||
implementation project(':image-editor')
|
implementation project(':image-editor')
|
||||||
implementation project(':donations')
|
implementation project(':donations')
|
||||||
implementation project(':contacts')
|
implementation project(':contacts')
|
||||||
|
implementation project(':qr')
|
||||||
|
|
||||||
implementation libs.libsignal.android
|
implementation libs.libsignal.android
|
||||||
implementation libs.google.protobuf.javalite
|
implementation libs.google.protobuf.javalite
|
||||||
|
|
|
@ -25,11 +25,11 @@ import org.signal.libsignal.protocol.InvalidKeyException;
|
||||||
import org.signal.libsignal.protocol.ecc.Curve;
|
import org.signal.libsignal.protocol.ecc.Curve;
|
||||||
import org.signal.libsignal.protocol.ecc.ECPublicKey;
|
import org.signal.libsignal.protocol.ecc.ECPublicKey;
|
||||||
import org.signal.libsignal.zkgroup.profiles.ProfileKey;
|
import org.signal.libsignal.zkgroup.profiles.ProfileKey;
|
||||||
|
import org.signal.qr.kitkat.ScanListener;
|
||||||
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
|
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
|
||||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||||
import org.thoughtcrime.securesms.qr.ScanListener;
|
|
||||||
import org.thoughtcrime.securesms.util.Base64;
|
import org.thoughtcrime.securesms.util.Base64;
|
||||||
import org.thoughtcrime.securesms.util.DynamicLanguage;
|
import org.thoughtcrime.securesms.util.DynamicLanguage;
|
||||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
|
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
|
||||||
|
|
|
@ -15,26 +15,25 @@ import android.widget.LinearLayout;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.components.camera.CameraView;
|
import org.signal.qr.QrScannerView;
|
||||||
import org.thoughtcrime.securesms.qr.ScanListener;
|
import org.signal.qr.kitkat.ScanListener;
|
||||||
import org.thoughtcrime.securesms.qr.ScanningThread;
|
import org.thoughtcrime.securesms.util.LifecycleDisposable;
|
||||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||||
|
|
||||||
public class DeviceAddFragment extends LoggingFragment {
|
public class DeviceAddFragment extends LoggingFragment {
|
||||||
|
|
||||||
private ViewGroup container;
|
private final LifecycleDisposable lifecycleDisposable = new LifecycleDisposable();
|
||||||
private LinearLayout overlay;
|
|
||||||
private ImageView devicesImage;
|
private LinearLayout overlay;
|
||||||
private CameraView scannerView;
|
private ImageView devicesImage;
|
||||||
private ScanningThread scanningThread;
|
private ScanListener scanListener;
|
||||||
private ScanListener scanListener;
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup viewGroup, Bundle bundle) {
|
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup viewGroup, Bundle bundle) {
|
||||||
this.container = ViewUtil.inflate(inflater, viewGroup, R.layout.device_add_fragment);
|
ViewGroup container = ViewUtil.inflate(inflater, viewGroup, R.layout.device_add_fragment);
|
||||||
this.overlay = this.container.findViewById(R.id.overlay);
|
this.overlay = container.findViewById(R.id.overlay);
|
||||||
this.scannerView = this.container.findViewById(R.id.scanner);
|
QrScannerView scannerView = container.findViewById(R.id.scanner);
|
||||||
this.devicesImage = this.container.findViewById(R.id.devices);
|
this.devicesImage = container.findViewById(R.id.devices);
|
||||||
|
|
||||||
if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) {
|
if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) {
|
||||||
this.overlay.setOrientation(LinearLayout.HORIZONTAL);
|
this.overlay.setOrientation(LinearLayout.HORIZONTAL);
|
||||||
|
@ -43,7 +42,7 @@ public class DeviceAddFragment extends LoggingFragment {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= 21) {
|
if (Build.VERSION.SDK_INT >= 21) {
|
||||||
this.container.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
|
container.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
|
||||||
@TargetApi(21)
|
@TargetApi(21)
|
||||||
@Override
|
@Override
|
||||||
public void onLayoutChange(View v, int left, int top, int right, int bottom,
|
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
|
lifecycleDisposable.bindTo(getViewLifecycleOwner());
|
||||||
public void onResume() {
|
lifecycleDisposable.add(scannerView.getQrData().subscribe(qrData -> {
|
||||||
super.onResume();
|
if (scanListener != null) {
|
||||||
this.scanningThread = new ScanningThread();
|
scanListener.onQrDataFound(qrData);
|
||||||
this.scanningThread.setScanListener(scanListener);
|
}
|
||||||
this.scannerView.onResume();
|
}));
|
||||||
this.scannerView.setPreviewCallback(scanningThread);
|
|
||||||
this.scanningThread.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
return container;
|
||||||
public void onPause() {
|
|
||||||
super.onPause();
|
|
||||||
this.scannerView.onPause();
|
|
||||||
this.scanningThread.stopScanning();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onConfigurationChanged(@NonNull Configuration newConfiguration) {
|
public void onConfigurationChanged(@NonNull Configuration newConfiguration) {
|
||||||
super.onConfigurationChanged(newConfiguration);
|
super.onConfigurationChanged(newConfiguration);
|
||||||
|
|
||||||
this.scannerView.onPause();
|
|
||||||
|
|
||||||
if (newConfiguration.orientation == Configuration.ORIENTATION_LANDSCAPE) {
|
if (newConfiguration.orientation == Configuration.ORIENTATION_LANDSCAPE) {
|
||||||
overlay.setOrientation(LinearLayout.HORIZONTAL);
|
overlay.setOrientation(LinearLayout.HORIZONTAL);
|
||||||
} else {
|
} else {
|
||||||
overlay.setOrientation(LinearLayout.VERTICAL);
|
overlay.setOrientation(LinearLayout.VERTICAL);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.scannerView.onResume();
|
|
||||||
this.scannerView.setPreviewCallback(scanningThread);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public ImageView getDevicesImage() {
|
public ImageView getDevicesImage() {
|
||||||
return devicesImage;
|
return devicesImage;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setScanListener(ScanListener scanListener) {
|
public void setScanListener(ScanListener scanListener) {
|
||||||
this.scanListener = scanListener;
|
this.scanListener = scanListener;
|
||||||
|
|
||||||
if (this.scanningThread != null) {
|
|
||||||
this.scanningThread.setScanListener(scanListener);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,6 +38,7 @@ import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import org.signal.core.util.ThreadUtil;
|
import org.signal.core.util.ThreadUtil;
|
||||||
import org.signal.core.util.logging.Log;
|
import org.signal.core.util.logging.Log;
|
||||||
|
import org.signal.qr.kitkat.QrCameraView;
|
||||||
import org.thoughtcrime.securesms.R;
|
import org.thoughtcrime.securesms.R;
|
||||||
import org.thoughtcrime.securesms.util.BitmapUtil;
|
import org.thoughtcrime.securesms.util.BitmapUtil;
|
||||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||||
|
@ -228,7 +229,7 @@ public class CameraView extends ViewGroup {
|
||||||
listeners.add(listener);
|
listeners.add(listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setPreviewCallback(final @NonNull PreviewCallback previewCallback) {
|
public void setPreviewCallback(final @NonNull QrCameraView.PreviewCallback previewCallback) {
|
||||||
enqueueTask(new PostInitializationTask<Void>() {
|
enqueueTask(new PostInitializationTask<Void>() {
|
||||||
@Override
|
@Override
|
||||||
protected void onPostMain(Void avoid) {
|
protected void onPostMain(Void avoid) {
|
||||||
|
@ -243,7 +244,7 @@ public class CameraView extends ViewGroup {
|
||||||
final int rotation = getCameraPictureOrientation();
|
final int rotation = getCameraPictureOrientation();
|
||||||
final Size previewSize = camera.getParameters().getPreviewSize();
|
final Size previewSize = camera.getParameters().getPreviewSize();
|
||||||
if (data != null) {
|
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();
|
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 {
|
private enum State {
|
||||||
PAUSED, RESUMED, ACTIVE
|
PAUSED, RESUMED, ACTIVE
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,11 +13,11 @@ import androidx.navigation.Navigation;
|
||||||
|
|
||||||
import org.signal.core.util.ThreadUtil;
|
import org.signal.core.util.ThreadUtil;
|
||||||
import org.signal.core.util.logging.Log;
|
import org.signal.core.util.logging.Log;
|
||||||
|
import org.signal.qr.kitkat.ScanningThread;
|
||||||
import org.thoughtcrime.securesms.LoggingFragment;
|
import org.thoughtcrime.securesms.LoggingFragment;
|
||||||
import org.thoughtcrime.securesms.R;
|
import org.thoughtcrime.securesms.R;
|
||||||
import org.thoughtcrime.securesms.components.camera.CameraView;
|
import org.thoughtcrime.securesms.components.camera.CameraView;
|
||||||
import org.thoughtcrime.securesms.payments.MobileCoinPublicAddress;
|
import org.thoughtcrime.securesms.payments.MobileCoinPublicAddress;
|
||||||
import org.thoughtcrime.securesms.qr.ScanningThread;
|
|
||||||
import org.thoughtcrime.securesms.util.navigation.SafeNavigation;
|
import org.thoughtcrime.securesms.util.navigation.SafeNavigation;
|
||||||
|
|
||||||
public final class PaymentsTransferQrScanFragment extends LoggingFragment {
|
public final class PaymentsTransferQrScanFragment extends LoggingFragment {
|
||||||
|
|
|
@ -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> scanListener = new AtomicReference<>();
|
|
||||||
private final Map<DecodeHintType, String> 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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -6,11 +6,11 @@ import android.view.View
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import org.signal.core.util.ThreadUtil
|
import org.signal.core.util.ThreadUtil
|
||||||
|
import org.signal.qr.kitkat.ScanListener
|
||||||
import org.thoughtcrime.securesms.R
|
import org.thoughtcrime.securesms.R
|
||||||
import org.thoughtcrime.securesms.crypto.IdentityKeyParcelable
|
import org.thoughtcrime.securesms.crypto.IdentityKeyParcelable
|
||||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||||
import org.thoughtcrime.securesms.permissions.Permissions
|
import org.thoughtcrime.securesms.permissions.Permissions
|
||||||
import org.thoughtcrime.securesms.qr.ScanListener
|
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient
|
import org.thoughtcrime.securesms.recipients.Recipient
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||||
import org.thoughtcrime.securesms.util.ServiceUtil
|
import org.thoughtcrime.securesms.util.ServiceUtil
|
||||||
|
|
|
@ -9,11 +9,11 @@ import android.view.ViewGroup
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import androidx.core.view.OneShotPreDrawListener
|
import androidx.core.view.OneShotPreDrawListener
|
||||||
import androidx.fragment.app.Fragment
|
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.R
|
||||||
import org.thoughtcrime.securesms.components.ShapeScrim
|
import org.thoughtcrime.securesms.components.ShapeScrim
|
||||||
import org.thoughtcrime.securesms.components.camera.CameraView
|
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.ViewUtil
|
||||||
import org.thoughtcrime.securesms.util.fragments.requireListener
|
import org.thoughtcrime.securesms.util.fragments.requireListener
|
||||||
|
|
||||||
|
|
|
@ -1,51 +1,52 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
|
||||||
<FrameLayout android:layout_width="match_parent"
|
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:layout_height="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_weight="1"
|
android:layout_height="match_parent"
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
android:layout_weight="1">
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
|
||||||
|
|
||||||
<org.thoughtcrime.securesms.components.camera.CameraView
|
<org.signal.qr.QrScannerView
|
||||||
android:id="@+id/scanner"
|
android:id="@+id/scanner"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent" />
|
||||||
app:camera="0"/>
|
|
||||||
|
|
||||||
<LinearLayout android:id="@+id/overlay"
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:id="@+id/overlay"
|
||||||
android:layout_height="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:orientation="vertical"
|
android:layout_height="match_parent"
|
||||||
android:weightSum="2">
|
android:orientation="vertical"
|
||||||
|
android:weightSum="2">
|
||||||
|
|
||||||
<org.thoughtcrime.securesms.components.ShapeScrim
|
<org.thoughtcrime.securesms.components.ShapeScrim
|
||||||
android:layout_weight="1"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"/>
|
android:layout_height="match_parent"
|
||||||
|
android:layout_weight="1" />
|
||||||
|
|
||||||
|
|
||||||
<LinearLayout android:orientation="vertical"
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:paddingStart="16dp"
|
|
||||||
android:paddingEnd="16dp"
|
|
||||||
android:background="?android:windowBackground"
|
android:background="?android:windowBackground"
|
||||||
android:gravity="center">
|
android:gravity="center"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:paddingStart="16dp"
|
||||||
|
android:paddingEnd="16dp">
|
||||||
|
|
||||||
<androidx.appcompat.widget.AppCompatImageView
|
<androidx.appcompat.widget.AppCompatImageView
|
||||||
android:id="@+id/devices"
|
android:id="@+id/devices"
|
||||||
android:src="@drawable/ic_devices_white"
|
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="16dp"
|
||||||
|
android:src="@drawable/ic_devices_white"
|
||||||
android:tint="@color/core_grey_25"
|
android:tint="@color/core_grey_25"
|
||||||
android:transitionName="devices"
|
android:transitionName="devices" />
|
||||||
android:layout_marginBottom="16dp"/>
|
|
||||||
|
|
||||||
<TextView android:text="@string/device_add_fragment__scan_the_qr_code_displayed_on_the_device_to_link"
|
<TextView
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:textColor="?android:textColorSecondary"/>
|
android:text="@string/device_add_fragment__scan_the_qr_code_displayed_on_the_device_to_link"
|
||||||
|
android:textColor="?android:textColorSecondary" />
|
||||||
|
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
package="org.signal.qrtest">
|
||||||
|
|
||||||
|
<application
|
||||||
|
android:allowBackup="true"
|
||||||
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
|
android:supportsRtl="true"
|
||||||
|
android:theme="@style/Theme.App">
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".MainActivity"
|
||||||
|
android:exported="true">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
</application>
|
||||||
|
|
||||||
|
</manifest>
|
Po Szerokość: | Wysokość: | Rozmiar: 22 KiB |
|
@ -0,0 +1,28 @@
|
||||||
|
package org.signal.qrtest
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||||
|
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||||
|
import org.signal.qr.QrScannerView
|
||||||
|
|
||||||
|
class MainActivity : AppCompatActivity() {
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
setContentView(R.layout.activity_main)
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= 23) {
|
||||||
|
requestPermissions(arrayOf(android.Manifest.permission.CAMERA), 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
val scanner = findViewById<QrScannerView>(R.id.scanner)
|
||||||
|
scanner.start(this)
|
||||||
|
|
||||||
|
scanner.qrData
|
||||||
|
.distinctUntilChanged()
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribeBy { Toast.makeText(this, it, Toast.LENGTH_SHORT).show() }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,74 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector
|
||||||
|
android:height="108dp"
|
||||||
|
android:width="108dp"
|
||||||
|
android:viewportHeight="108"
|
||||||
|
android:viewportWidth="108"
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<path android:fillColor="#3DDC84"
|
||||||
|
android:pathData="M0,0h108v108h-108z"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M9,0L9,108"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M19,0L19,108"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M29,0L29,108"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M39,0L39,108"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M49,0L49,108"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M59,0L59,108"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M69,0L69,108"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M79,0L79,108"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M89,0L89,108"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M99,0L99,108"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M0,9L108,9"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M0,19L108,19"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M0,29L108,29"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M0,39L108,39"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M0,49L108,49"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M0,59L108,59"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M0,69L108,69"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M0,79L108,79"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M0,89L108,89"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M0,99L108,99"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M19,29L89,29"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M19,39L89,39"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M19,49L89,49"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M19,59L89,59"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M19,69L89,69"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M19,79L89,79"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M29,19L29,89"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M39,19L39,89"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M49,19L49,89"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M59,19L59,89"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M69,19L69,89"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M79,19L79,89"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
</vector>
|
|
@ -0,0 +1,15 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="108dp"
|
||||||
|
android:height="108dp"
|
||||||
|
android:viewportWidth="108"
|
||||||
|
android:viewportHeight="108"
|
||||||
|
android:tint="#000000">
|
||||||
|
<group android:scaleX="2.61"
|
||||||
|
android:scaleY="2.61"
|
||||||
|
android:translateX="22.68"
|
||||||
|
android:translateY="22.68">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M5,16c0,3.87 3.13,7 7,7s7,-3.13 7,-7v-4L5,12v4zM16.12,4.37l2.1,-2.1 -0.82,-0.83 -2.3,2.31C14.16,3.28 13.12,3 12,3s-2.16,0.28 -3.09,0.75L6.6,1.44l-0.82,0.83 2.1,2.1C6.14,5.64 5,7.68 5,10v1h14v-1c0,-2.32 -1.14,-4.36 -2.88,-5.63zM9,9c-0.55,0 -1,-0.45 -1,-1s0.45,-1 1,-1 1,0.45 1,1 -0.45,1 -1,1zM15,9c-0.55,0 -1,-0.45 -1,-1s0.45,-1 1,-1 1,0.45 1,1 -0.45,1 -1,1z"/>
|
||||||
|
</group>
|
||||||
|
</vector>
|
|
@ -0,0 +1,20 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:gravity="center_horizontal"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<org.signal.qr.QrScannerView
|
||||||
|
android:id="@+id/scanner"
|
||||||
|
android:layout_width="240dp"
|
||||||
|
android:layout_height="240dp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</FrameLayout>
|
|
@ -0,0 +1,5 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@drawable/ic_launcher_background"/>
|
||||||
|
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||||
|
</adaptive-icon>
|
|
@ -0,0 +1,5 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@drawable/ic_launcher_background"/>
|
||||||
|
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||||
|
</adaptive-icon>
|
Po Szerokość: | Wysokość: | Rozmiar: 2.3 KiB |
Po Szerokość: | Wysokość: | Rozmiar: 4.3 KiB |
Po Szerokość: | Wysokość: | Rozmiar: 1.8 KiB |
Po Szerokość: | Wysokość: | Rozmiar: 2.6 KiB |
Po Szerokość: | Wysokość: | Rozmiar: 3.1 KiB |
Po Szerokość: | Wysokość: | Rozmiar: 5.9 KiB |
Po Szerokość: | Wysokość: | Rozmiar: 5.5 KiB |
Po Szerokość: | Wysokość: | Rozmiar: 9.9 KiB |
Po Szerokość: | Wysokość: | Rozmiar: 7.4 KiB |
Po Szerokość: | Wysokość: | Rozmiar: 14 KiB |
|
@ -0,0 +1,16 @@
|
||||||
|
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
<!-- Base application theme. -->
|
||||||
|
<style name="Theme.ContactsTest" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
|
||||||
|
<!-- Primary brand color. -->
|
||||||
|
<item name="colorPrimary">@color/purple_200</item>
|
||||||
|
<item name="colorPrimaryVariant">@color/purple_700</item>
|
||||||
|
<item name="colorOnPrimary">@color/black</item>
|
||||||
|
<!-- Secondary brand color. -->
|
||||||
|
<item name="colorSecondary">@color/teal_200</item>
|
||||||
|
<item name="colorSecondaryVariant">@color/teal_200</item>
|
||||||
|
<item name="colorOnSecondary">@color/black</item>
|
||||||
|
<!-- Status bar color. -->
|
||||||
|
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
|
||||||
|
<!-- Customize your theme here. -->
|
||||||
|
</style>
|
||||||
|
</resources>
|
|
@ -0,0 +1,10 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="purple_200">#FFBB86FC</color>
|
||||||
|
<color name="purple_500">#FF6200EE</color>
|
||||||
|
<color name="purple_700">#FF3700B3</color>
|
||||||
|
<color name="teal_200">#FF03DAC5</color>
|
||||||
|
<color name="teal_700">#FF018786</color>
|
||||||
|
<color name="black">#FF000000</color>
|
||||||
|
<color name="white">#FFFFFFFF</color>
|
||||||
|
</resources>
|
|
@ -0,0 +1,3 @@
|
||||||
|
<resources>
|
||||||
|
<string name="app_name">QR</string>
|
||||||
|
</resources>
|
|
@ -0,0 +1,16 @@
|
||||||
|
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
<!-- Base application theme. -->
|
||||||
|
<style name="Theme.App" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
|
||||||
|
<!-- Primary brand color. -->
|
||||||
|
<item name="colorPrimary">#2c6bed</item>
|
||||||
|
<item name="colorPrimaryVariant">#1851b4</item>
|
||||||
|
<item name="colorOnPrimary">@color/white</item>
|
||||||
|
<!-- Secondary brand color. -->
|
||||||
|
<item name="colorSecondary">@color/teal_200</item>
|
||||||
|
<item name="colorSecondaryVariant">@color/teal_700</item>
|
||||||
|
<item name="colorOnSecondary">@color/black</item>
|
||||||
|
<!-- Status bar color. -->
|
||||||
|
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
|
||||||
|
<!-- Customize your theme here. -->
|
||||||
|
</style>
|
||||||
|
</resources>
|
|
@ -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'
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
package="org.signal.qr">
|
||||||
|
|
||||||
|
<uses-sdk tools:overrideLibrary="androidx.camera.core,androidx.camera.camera2,androidx.camera.lifecycle,androidx.camera.view" />
|
||||||
|
|
||||||
|
<uses-feature
|
||||||
|
android:name="android.hardware.camera"
|
||||||
|
android:required="false" />
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
|
|
||||||
|
</manifest>
|
|
@ -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<DecodeHintType, String>())
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<String> = PublishSubject.create()
|
||||||
|
|
||||||
|
val qrData: Observable<String> = 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()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<Size> sizes = parameters.getSupportedPreviewSizes();
|
||||||
|
List<Size> ideals = new LinkedList<>();
|
||||||
|
List<Size> 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<Size> {
|
||||||
|
@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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,472 @@
|
||||||
|
/*
|
||||||
|
Copyright (c) 2013-2014 CommonsWare, LLC
|
||||||
|
Portions Copyright (C) 2007 The Android Open Source Project
|
||||||
|
<p>
|
||||||
|
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> 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<CameraViewListener> 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<Void>() {
|
||||||
|
@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<Void>() {
|
||||||
|
private Optional<Camera> 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<Void>() {
|
||||||
|
@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<String> 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<Void>() {
|
||||||
|
@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<Result> 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<Result> extends SerialAsyncTask<Result> {
|
||||||
|
@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
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package org.thoughtcrime.securesms.qr;
|
package org.signal.qr.kitkat;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
|
|
@ -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> scanListener = new AtomicReference<>();
|
||||||
|
private final Map<DecodeHintType, String> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,6 +18,8 @@ include ':spinner'
|
||||||
include ':spinner-app'
|
include ':spinner-app'
|
||||||
include ':contacts'
|
include ':contacts'
|
||||||
include ':contacts-app'
|
include ':contacts-app'
|
||||||
|
include ':qr'
|
||||||
|
include ':qr-app'
|
||||||
|
|
||||||
project(':app').name = 'Signal-Android'
|
project(':app').name = 'Signal-Android'
|
||||||
project(':paging').projectDir = file('paging/lib')
|
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').projectDir = file('contacts/lib')
|
||||||
project(':contacts-app').projectDir = file('contacts/app')
|
project(':contacts-app').projectDir = file('contacts/app')
|
||||||
|
|
||||||
|
project(':qr').projectDir = file('qr/lib')
|
||||||
|
project(':qr-app').projectDir = file('qr/app')
|
||||||
|
|
||||||
rootProject.name='Signal'
|
rootProject.name='Signal'
|
||||||
|
|
||||||
apply from: 'dependencies.gradle'
|
apply from: 'dependencies.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
|
||||||
|
}
|
|
@ -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')
|
||||||
|
}
|