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}