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