ScanActivity.java

  1/*
  2 * Copyright 2012-2015 the original author or authors.
  3 *
  4 * This program is free software: you can redistribute it and/or modify
  5 * it under the terms of the GNU General Public License as published by
  6 * the Free Software Foundation, either version 3 of the License, or
  7 * (at your option) any later version.
  8 *
  9 * This program is distributed in the hope that it will be useful,
 10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 12 * GNU General Public License for more details.
 13 *
 14 * You should have received a copy of the GNU General Public License
 15 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 16 */
 17
 18package eu.siacs.conversations.ui;
 19
 20import java.util.EnumMap;
 21import java.util.Map;
 22
 23import com.google.zxing.BinaryBitmap;
 24import com.google.zxing.DecodeHintType;
 25import com.google.zxing.PlanarYUVLuminanceSource;
 26import com.google.zxing.ReaderException;
 27import com.google.zxing.Result;
 28import com.google.zxing.ResultPointCallback;
 29import com.google.zxing.common.HybridBinarizer;
 30import com.google.zxing.qrcode.QRCodeReader;
 31
 32import android.Manifest;
 33import android.app.Activity;
 34import android.content.Context;
 35import android.content.Intent;
 36import android.content.pm.PackageManager;
 37import android.graphics.Rect;
 38import android.graphics.RectF;
 39import android.graphics.SurfaceTexture;
 40import android.hardware.Camera;
 41import android.hardware.Camera.CameraInfo;
 42import android.os.Build;
 43import android.os.Bundle;
 44import android.os.Handler;
 45import android.os.HandlerThread;
 46import android.os.Process;
 47import android.os.Vibrator;
 48import android.support.v4.app.ActivityCompat;
 49import android.support.v4.content.ContextCompat;
 50import android.util.Log;
 51import android.view.KeyEvent;
 52import android.view.Surface;
 53import android.view.TextureView;
 54import android.view.TextureView.SurfaceTextureListener;
 55import android.view.View;
 56import android.view.WindowManager;
 57
 58import eu.siacs.conversations.Config;
 59import eu.siacs.conversations.R;
 60import eu.siacs.conversations.ui.service.CameraManager;
 61import eu.siacs.conversations.ui.widget.ScannerView;
 62
 63/**
 64 * @author Andreas Schildbach
 65 */
 66@SuppressWarnings("deprecation")
 67public final class ScanActivity extends Activity implements SurfaceTextureListener, ActivityCompat.OnRequestPermissionsResultCallback {
 68	public static final String INTENT_EXTRA_RESULT = "result";
 69
 70	private static final long VIBRATE_DURATION = 50L;
 71	private static final long AUTO_FOCUS_INTERVAL_MS = 2500L;
 72	private static boolean DISABLE_CONTINUOUS_AUTOFOCUS = Build.MODEL.equals("GT-I9100") // Galaxy S2
 73			|| Build.MODEL.equals("SGH-T989") // Galaxy S2
 74			|| Build.MODEL.equals("SGH-T989D") // Galaxy S2 X
 75			|| Build.MODEL.equals("SAMSUNG-SGH-I727") // Galaxy S2 Skyrocket
 76			|| Build.MODEL.equals("GT-I9300") // Galaxy S3
 77			|| Build.MODEL.equals("GT-N7000"); // Galaxy Note
 78	private final CameraManager cameraManager = new CameraManager();
 79	private ScannerView scannerView;
 80	private TextureView previewView;
 81	private volatile boolean surfaceCreated = false;
 82	private Vibrator vibrator;
 83	private HandlerThread cameraThread;
 84	private volatile Handler cameraHandler;
 85	private final Runnable closeRunnable = new Runnable() {
 86		@Override
 87		public void run() {
 88			cameraHandler.removeCallbacksAndMessages(null);
 89			cameraManager.close();
 90		}
 91	};
 92	private final Runnable fetchAndDecodeRunnable = new Runnable() {
 93		private final QRCodeReader reader = new QRCodeReader();
 94		private final Map<DecodeHintType, Object> hints = new EnumMap<DecodeHintType, Object>(DecodeHintType.class);
 95
 96		@Override
 97		public void run() {
 98			cameraManager.requestPreviewFrame((data, camera) -> decode(data));
 99		}
100
101		private void decode(final byte[] data) {
102			final PlanarYUVLuminanceSource source = cameraManager.buildLuminanceSource(data);
103			final BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
104
105			try {
106				hints.put(DecodeHintType.NEED_RESULT_POINT_CALLBACK, (ResultPointCallback) dot -> runOnUiThread(() -> scannerView.addDot(dot)));
107				final Result scanResult = reader.decode(bitmap, hints);
108
109				runOnUiThread(() -> handleResult(scanResult));
110			} catch (final ReaderException x) {
111				// retry
112				cameraHandler.post(fetchAndDecodeRunnable);
113			} finally {
114				reader.reset();
115			}
116		}
117	};
118	private final Runnable openRunnable = new Runnable() {
119		@Override
120		public void run() {
121			try {
122				final Camera camera = cameraManager.open(previewView, displayRotation(), !DISABLE_CONTINUOUS_AUTOFOCUS);
123
124				final Rect framingRect = cameraManager.getFrame();
125				final RectF framingRectInPreview = new RectF(cameraManager.getFramePreview());
126				framingRectInPreview.offsetTo(0, 0);
127				final boolean cameraFlip = cameraManager.getFacing() == CameraInfo.CAMERA_FACING_FRONT;
128				final int cameraRotation = cameraManager.getOrientation();
129
130				runOnUiThread(() -> scannerView.setFraming(framingRect, framingRectInPreview, displayRotation(), cameraRotation, cameraFlip));
131
132				final String focusMode = camera.getParameters().getFocusMode();
133				final boolean nonContinuousAutoFocus = Camera.Parameters.FOCUS_MODE_AUTO.equals(focusMode)
134						|| Camera.Parameters.FOCUS_MODE_MACRO.equals(focusMode);
135
136				if (nonContinuousAutoFocus)
137					cameraHandler.post(new AutoFocusRunnable(camera));
138
139				cameraHandler.post(fetchAndDecodeRunnable);
140			} catch (final Exception x) {
141				Log.d(Config.LOGTAG, "problem opening camera", x);
142			}
143		}
144
145		private int displayRotation() {
146			final int rotation = getWindowManager().getDefaultDisplay().getRotation();
147			if (rotation == Surface.ROTATION_0)
148				return 0;
149			else if (rotation == Surface.ROTATION_90)
150				return 90;
151			else if (rotation == Surface.ROTATION_180)
152				return 180;
153			else if (rotation == Surface.ROTATION_270)
154				return 270;
155			else
156				throw new IllegalStateException("rotation: " + rotation);
157		}
158	};
159
160	@Override
161	public void onCreate(final Bundle savedInstanceState) {
162		super.onCreate(savedInstanceState);
163
164		vibrator = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE);
165
166		setContentView(R.layout.activity_scan);
167		scannerView = findViewById(R.id.scan_activity_mask);
168		previewView = findViewById(R.id.scan_activity_preview);
169		previewView.setSurfaceTextureListener(this);
170
171		cameraThread = new HandlerThread("cameraThread", Process.THREAD_PRIORITY_BACKGROUND);
172		cameraThread.start();
173		cameraHandler = new Handler(cameraThread.getLooper());
174	}
175
176	@Override
177	protected void onResume() {
178		super.onResume();
179		maybeOpenCamera();
180	}
181
182	@Override
183	protected void onPause() {
184		cameraHandler.post(closeRunnable);
185
186		super.onPause();
187	}
188
189	@Override
190	protected void onDestroy() {
191		// cancel background thread
192		cameraHandler.removeCallbacksAndMessages(null);
193		cameraThread.quit();
194
195		previewView.setSurfaceTextureListener(null);
196
197		super.onDestroy();
198	}
199
200	private void maybeOpenCamera() {
201		if (surfaceCreated && ContextCompat.checkSelfPermission(this,
202				Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED)
203			cameraHandler.post(openRunnable);
204	}
205
206	@Override
207	public void onSurfaceTextureAvailable(final SurfaceTexture surface, final int width, final int height) {
208		surfaceCreated = true;
209		maybeOpenCamera();
210	}
211
212	@Override
213	public boolean onSurfaceTextureDestroyed(final SurfaceTexture surface) {
214		surfaceCreated = false;
215		return true;
216	}
217
218	@Override
219	public void onSurfaceTextureSizeChanged(final SurfaceTexture surface, final int width, final int height) {
220	}
221
222	@Override
223	public void onSurfaceTextureUpdated(final SurfaceTexture surface) {
224	}
225
226	@Override
227	public void onAttachedToWindow() {
228		getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
229	}
230
231	@Override
232	public void onBackPressed() {
233		scannerView.setVisibility(View.GONE);
234		setResult(RESULT_CANCELED);
235		postFinish();
236	}
237
238	@Override
239	public boolean onKeyDown(final int keyCode, final KeyEvent event) {
240		switch (keyCode) {
241			case KeyEvent.KEYCODE_FOCUS:
242			case KeyEvent.KEYCODE_CAMERA:
243				// don't launch camera app
244				return true;
245			case KeyEvent.KEYCODE_VOLUME_DOWN:
246			case KeyEvent.KEYCODE_VOLUME_UP:
247				cameraHandler.post(() -> cameraManager.setTorch(keyCode == KeyEvent.KEYCODE_VOLUME_UP));
248				return true;
249		}
250
251		return super.onKeyDown(keyCode, event);
252	}
253
254	public void handleResult(final Result scanResult) {
255		vibrator.vibrate(VIBRATE_DURATION);
256
257		scannerView.setIsResult(true);
258
259		final Intent result = new Intent();
260		result.putExtra(INTENT_EXTRA_RESULT, scanResult.getText());
261		setResult(RESULT_OK, result);
262		postFinish();
263	}
264
265	private void postFinish() {
266		new Handler().postDelayed(() -> finish(), 50);
267	}
268
269	private final class AutoFocusRunnable implements Runnable {
270		private final Camera camera;
271		private final Camera.AutoFocusCallback autoFocusCallback = new Camera.AutoFocusCallback() {
272			@Override
273			public void onAutoFocus(final boolean success, final Camera camera) {
274				// schedule again
275				cameraHandler.postDelayed(AutoFocusRunnable.this, AUTO_FOCUS_INTERVAL_MS);
276			}
277		};
278
279		public AutoFocusRunnable(final Camera camera) {
280			this.camera = camera;
281		}
282
283		@Override
284		public void run() {
285			try {
286				camera.autoFocus(autoFocusCallback);
287			} catch (final Exception x) {
288				Log.d(Config.LOGTAG, "problem with auto-focus, will not schedule again", x);
289			}
290		}
291	}
292}