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