package org.thoughtcrime.securesms; import android.animation.Animator; import android.annotation.TargetApi; import android.content.Context; import android.content.res.Configuration; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.app.Fragment; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewAnimationUtils; import android.view.ViewGroup; import android.view.animation.DecelerateInterpolator; import android.widget.ImageView; import android.widget.LinearLayout; import com.google.zxing.BinaryBitmap; import com.google.zxing.ChecksumException; 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.thoughtcrime.securesms.components.camera.CameraView; import org.thoughtcrime.securesms.components.camera.CameraView.PreviewCallback; import org.thoughtcrime.securesms.components.camera.CameraView.PreviewFrame; import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.ViewUtil; public class DeviceAddFragment extends Fragment implements PreviewCallback { private static final String TAG = DeviceAddFragment.class.getSimpleName(); private final QRCodeReader reader = new QRCodeReader(); private ViewGroup container; private LinearLayout overlay; private ImageView devicesImage; private CameraView scannerView; private PreviewFrame previewFrame; private ScanningThread scanningThread; private ScanListener scanListener; @Override public View onCreateView(LayoutInflater inflater, ViewGroup viewGroup, Bundle bundle) { this.container = ViewUtil.inflate(inflater, viewGroup, R.layout.device_add_fragment); this.overlay = ViewUtil.findById(this.container, R.id.overlay); this.scannerView = ViewUtil.findById(this.container, R.id.scanner); this.devicesImage = ViewUtil.findById(this.container, R.id.devices); this.scannerView.onResume(); this.scannerView.setPreviewCallback(this); if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) { this.overlay.setOrientation(LinearLayout.HORIZONTAL); } else { this.overlay.setOrientation(LinearLayout.VERTICAL); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { this.container.addOnLayoutChangeListener(new View.OnLayoutChangeListener() { @TargetApi(Build.VERSION_CODES.LOLLIPOP) @Override public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) { v.removeOnLayoutChangeListener(this); Animator reveal = ViewAnimationUtils.createCircularReveal(v, right, bottom, 0, (int) Math.hypot(right, bottom)); reveal.setInterpolator(new DecelerateInterpolator(2f)); reveal.setDuration(800); reveal.start(); } }); } return this.container; } @Override public void onResume() { super.onResume(); this.scannerView.onResume(); this.scannerView.setPreviewCallback(this); this.previewFrame = null; this.scanningThread = new ScanningThread(); this.scanningThread.start(); } @Override public void onPause() { super.onPause(); this.scannerView.onPause(); this.scanningThread.stopScanning(); } @Override public void onConfigurationChanged(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(this); } @Override public void onPreviewFrame(@NonNull PreviewFrame previewFrame) { Context context = getActivity(); try { if (context != null) { synchronized (this) { this.previewFrame = previewFrame; this.notify(); } } } catch (RuntimeException e) { Log.w(TAG, e); } } public ImageView getDevicesImage() { return devicesImage; } public void setScanListener(ScanListener scanListener) { this.scanListener = scanListener; } private class ScanningThread extends Thread { private boolean scanning = true; @Override public void run() { while (true) { PreviewFrame ourFrame; synchronized (DeviceAddFragment.this) { while (scanning && previewFrame == null) { Util.wait(DeviceAddFragment.this, 0); } if (!scanning) return; else ourFrame = previewFrame; previewFrame = null; } String url = getUrl(ourFrame.getData(), ourFrame.getWidth(), ourFrame.getHeight(), ourFrame.getOrientation()); if (url != null && scanListener != null) { Uri uri = Uri.parse(url); scanListener.onUrlFound(uri); return; } } } public void stopScanning() { synchronized (DeviceAddFragment.this) { scanning = false; DeviceAddFragment.this.notify(); } } private @Nullable String getUrl(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); if (result != null) return result.getText(); } catch (NullPointerException | ChecksumException | FormatException e) { Log.w(TAG, e); } catch (NotFoundException e) { // Thanks ZXing... } return null; } } public interface ScanListener { public void onUrlFound(Uri uri); } }