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