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 android.Manifest;
 21import android.app.Activity;
 22import android.content.Context;
 23import android.content.Intent;
 24import android.content.pm.PackageManager;
 25import android.graphics.Rect;
 26import android.graphics.RectF;
 27import android.graphics.SurfaceTexture;
 28import android.hardware.Camera;
 29import android.hardware.Camera.CameraInfo;
 30import android.os.Build;
 31import android.os.Bundle;
 32import android.os.Handler;
 33import android.os.HandlerThread;
 34import android.os.Process;
 35import android.os.Vibrator;
 36import android.util.Log;
 37import android.view.KeyEvent;
 38import android.view.Surface;
 39import android.view.TextureView;
 40import android.view.TextureView.SurfaceTextureListener;
 41import android.view.View;
 42import android.view.WindowManager;
 43import android.widget.Toast;
 44
 45import androidx.core.app.ActivityCompat;
 46import androidx.core.content.ContextCompat;
 47
 48import com.google.zxing.BinaryBitmap;
 49import com.google.zxing.DecodeHintType;
 50import com.google.zxing.PlanarYUVLuminanceSource;
 51import com.google.zxing.ReaderException;
 52import com.google.zxing.Result;
 53import com.google.zxing.ResultPointCallback;
 54import com.google.zxing.common.HybridBinarizer;
 55import com.google.zxing.qrcode.QRCodeReader;
 56
 57import java.util.EnumMap;
 58import java.util.Map;
 59
 60import eu.siacs.conversations.Config;
 61import eu.siacs.conversations.R;
 62import eu.siacs.conversations.ui.service.CameraManager;
 63import eu.siacs.conversations.ui.widget.ScannerView;
 64
 65/**
 66 * @author Andreas Schildbach
 67 */
 68@SuppressWarnings("deprecation")
 69public final class ScanActivity extends Activity implements SurfaceTextureListener, ActivityCompat.OnRequestPermissionsResultCallback {
 70	public static final String INTENT_EXTRA_RESULT = "result";
 71
 72	public static final int REQUEST_SCAN_QR_CODE = 0x0987;
 73	private static final int REQUEST_CAMERA_PERMISSIONS_TO_SCAN = 0x6789;
 74
 75	private static final long VIBRATE_DURATION = 50L;
 76	private static final long AUTO_FOCUS_INTERVAL_MS = 2500L;
 77	private static final boolean DISABLE_CONTINUOUS_AUTOFOCUS = Build.MODEL.equals("GT-I9100") // Galaxy S2
 78			|| Build.MODEL.equals("SGH-T989") // Galaxy S2
 79			|| Build.MODEL.equals("SGH-T989D") // Galaxy S2 X
 80			|| Build.MODEL.equals("SAMSUNG-SGH-I727") // Galaxy S2 Skyrocket
 81			|| Build.MODEL.equals("GT-I9300") // Galaxy S3
 82			|| Build.MODEL.equals("GT-N7000"); // Galaxy Note
 83	private final CameraManager cameraManager = new CameraManager();
 84	private ScannerView scannerView;
 85	private TextureView previewView;
 86	private volatile boolean surfaceCreated = false;
 87	private Vibrator vibrator;
 88	private HandlerThread cameraThread;
 89	private volatile Handler cameraHandler;
 90	private final Runnable closeRunnable = new Runnable() {
 91		@Override
 92		public void run() {
 93			cameraHandler.removeCallbacksAndMessages(null);
 94			cameraManager.close();
 95		}
 96	};
 97	private final Runnable fetchAndDecodeRunnable = new Runnable() {
 98		private final QRCodeReader reader = new QRCodeReader();
 99		private final Map<DecodeHintType, Object> hints = new EnumMap<DecodeHintType, Object>(DecodeHintType.class);
100
101		@Override
102		public void run() {
103			cameraManager.requestPreviewFrame((data, camera) -> decode(data));
104		}
105
106		private void decode(final byte[] data) {
107			final PlanarYUVLuminanceSource source = cameraManager.buildLuminanceSource(data);
108			final BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
109
110			try {
111				hints.put(DecodeHintType.NEED_RESULT_POINT_CALLBACK, (ResultPointCallback) dot -> runOnUiThread(() -> scannerView.addDot(dot)));
112				final Result scanResult = reader.decode(bitmap, hints);
113
114				runOnUiThread(() -> handleResult(scanResult));
115			} catch (final ReaderException x) {
116				// retry
117				cameraHandler.post(fetchAndDecodeRunnable);
118			} finally {
119				reader.reset();
120			}
121		}
122	};
123	private final Runnable openRunnable = new Runnable() {
124		@Override
125		public void run() {
126			try {
127				final Camera camera = cameraManager.open(previewView, displayRotation(), !DISABLE_CONTINUOUS_AUTOFOCUS);
128
129				final Rect framingRect = cameraManager.getFrame();
130				final RectF framingRectInPreview = new RectF(cameraManager.getFramePreview());
131				framingRectInPreview.offsetTo(0, 0);
132				final boolean cameraFlip = cameraManager.getFacing() == CameraInfo.CAMERA_FACING_FRONT;
133				final int cameraRotation = cameraManager.getOrientation();
134
135				runOnUiThread(() -> scannerView.setFraming(framingRect, framingRectInPreview, displayRotation(), cameraRotation, cameraFlip));
136
137				final String focusMode = camera.getParameters().getFocusMode();
138				final boolean nonContinuousAutoFocus = Camera.Parameters.FOCUS_MODE_AUTO.equals(focusMode)
139						|| Camera.Parameters.FOCUS_MODE_MACRO.equals(focusMode);
140
141				if (nonContinuousAutoFocus)
142					cameraHandler.post(new AutoFocusRunnable(camera));
143
144				cameraHandler.post(fetchAndDecodeRunnable);
145			} catch (final Exception x) {
146				Log.d(Config.LOGTAG, "problem opening camera", x);
147			}
148		}
149
150		private int displayRotation() {
151			final int rotation = getWindowManager().getDefaultDisplay().getRotation();
152			if (rotation == Surface.ROTATION_0)
153				return 0;
154			else if (rotation == Surface.ROTATION_90)
155				return 90;
156			else if (rotation == Surface.ROTATION_180)
157				return 180;
158			else if (rotation == Surface.ROTATION_270)
159				return 270;
160			else
161				throw new IllegalStateException("rotation: " + rotation);
162		}
163	};
164
165	@Override
166	public void onCreate(final Bundle savedInstanceState) {
167		super.onCreate(savedInstanceState);
168
169		vibrator = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE);
170
171		setContentView(R.layout.activity_scan);
172		scannerView = findViewById(R.id.scan_activity_mask);
173		previewView = findViewById(R.id.scan_activity_preview);
174		previewView.setSurfaceTextureListener(this);
175
176		cameraThread = new HandlerThread("cameraThread", Process.THREAD_PRIORITY_BACKGROUND);
177		cameraThread.start();
178		cameraHandler = new Handler(cameraThread.getLooper());
179	}
180
181	@Override
182	protected void onResume() {
183		super.onResume();
184		maybeOpenCamera();
185	}
186
187	@Override
188	protected void onPause() {
189		cameraHandler.post(closeRunnable);
190
191		super.onPause();
192	}
193
194	@Override
195	protected void onDestroy() {
196		// cancel background thread
197		cameraHandler.removeCallbacksAndMessages(null);
198		cameraThread.quit();
199
200		previewView.setSurfaceTextureListener(null);
201
202		super.onDestroy();
203	}
204
205	private void maybeOpenCamera() {
206		if (surfaceCreated && ContextCompat.checkSelfPermission(this,
207				Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED)
208			cameraHandler.post(openRunnable);
209	}
210
211	@Override
212	public void onSurfaceTextureAvailable(final SurfaceTexture surface, final int width, final int height) {
213		surfaceCreated = true;
214		maybeOpenCamera();
215	}
216
217	@Override
218	public boolean onSurfaceTextureDestroyed(final SurfaceTexture surface) {
219		surfaceCreated = false;
220		return true;
221	}
222
223	@Override
224	public void onSurfaceTextureSizeChanged(final SurfaceTexture surface, final int width, final int height) {
225	}
226
227	@Override
228	public void onSurfaceTextureUpdated(final SurfaceTexture surface) {
229	}
230
231	@Override
232	public void onAttachedToWindow() {
233		getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
234	}
235
236	@Override
237	public void onBackPressed() {
238		scannerView.setVisibility(View.GONE);
239		setResult(RESULT_CANCELED);
240		postFinish();
241	}
242
243	@Override
244	public boolean onKeyDown(final int keyCode, final KeyEvent event) {
245		switch (keyCode) {
246			case KeyEvent.KEYCODE_FOCUS:
247			case KeyEvent.KEYCODE_CAMERA:
248				// don't launch camera app
249				return true;
250			case KeyEvent.KEYCODE_VOLUME_DOWN:
251			case KeyEvent.KEYCODE_VOLUME_UP:
252				cameraHandler.post(() -> cameraManager.setTorch(keyCode == KeyEvent.KEYCODE_VOLUME_UP));
253				return true;
254		}
255
256		return super.onKeyDown(keyCode, event);
257	}
258
259	public void handleResult(final Result scanResult) {
260		vibrator.vibrate(VIBRATE_DURATION);
261
262		scannerView.setIsResult(true);
263
264		final Intent result = new Intent();
265		result.putExtra(INTENT_EXTRA_RESULT, scanResult.getText());
266		setResult(RESULT_OK, result);
267		postFinish();
268	}
269
270	private void postFinish() {
271		new Handler().postDelayed(this::finish, 50);
272	}
273
274	public static void scan(Activity activity) {
275		if (ContextCompat.checkSelfPermission(activity, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) {
276			Intent intent = new Intent(activity, ScanActivity.class);
277			activity.startActivityForResult(intent, REQUEST_SCAN_QR_CODE);
278		} else {
279			ActivityCompat.requestPermissions(activity, new String[]{Manifest.permission.CAMERA}, REQUEST_CAMERA_PERMISSIONS_TO_SCAN);
280		}
281
282	}
283
284	public static void onRequestPermissionResult(Activity activity, int requestCode, int[] grantResults) {
285		if (requestCode != REQUEST_CAMERA_PERMISSIONS_TO_SCAN) {
286			return;
287		}
288		if (grantResults.length > 0) {
289			if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
290				scan(activity);
291			} else {
292				Toast.makeText(activity, R.string.qr_code_scanner_needs_access_to_camera, Toast.LENGTH_SHORT).show();
293			}
294		}
295	}
296
297	private final class AutoFocusRunnable implements Runnable {
298		private final Camera camera;
299		private final Camera.AutoFocusCallback autoFocusCallback = new Camera.AutoFocusCallback() {
300			@Override
301			public void onAutoFocus(final boolean success, final Camera camera) {
302				// schedule again
303				cameraHandler.postDelayed(AutoFocusRunnable.this, AUTO_FOCUS_INTERVAL_MS);
304			}
305		};
306
307		public AutoFocusRunnable(final Camera camera) {
308			this.camera = camera;
309		}
310
311		@Override
312		public void run() {
313			try {
314				camera.autoFocus(autoFocusCallback);
315			} catch (final Exception x) {
316				Log.d(Config.LOGTAG, "problem with auto-focus, will not schedule again", x);
317			}
318		}
319	}
320}