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 maybeOpenCamera();
186 }
187
188 @Override
189 protected void onPause() {
190 cameraHandler.post(closeRunnable);
191
192 super.onPause();
193 }
194
195 @Override
196 protected void onDestroy() {
197 // cancel background thread
198 cameraHandler.removeCallbacksAndMessages(null);
199 cameraThread.quit();
200
201 previewView.setSurfaceTextureListener(null);
202
203 super.onDestroy();
204 }
205
206 private void maybeOpenCamera() {
207 if (surfaceCreated && ContextCompat.checkSelfPermission(this,
208 Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED)
209 cameraHandler.post(openRunnable);
210 }
211
212 @Override
213 public void onSurfaceTextureAvailable(final SurfaceTexture surface, final int width, final int height) {
214 surfaceCreated = true;
215 maybeOpenCamera();
216 }
217
218 @Override
219 public boolean onSurfaceTextureDestroyed(final SurfaceTexture surface) {
220 surfaceCreated = false;
221 return true;
222 }
223
224 @Override
225 public void onSurfaceTextureSizeChanged(final SurfaceTexture surface, final int width, final int height) {
226 }
227
228 @Override
229 public void onSurfaceTextureUpdated(final SurfaceTexture surface) {
230 }
231
232 @Override
233 public void onAttachedToWindow() {
234 getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
235 }
236
237 @Override
238 public void onBackPressed() {
239 scannerView.setVisibility(View.GONE);
240 setResult(RESULT_CANCELED);
241 postFinish();
242 }
243
244 @Override
245 public boolean onKeyDown(final int keyCode, final KeyEvent event) {
246 switch (keyCode) {
247 case KeyEvent.KEYCODE_FOCUS:
248 case KeyEvent.KEYCODE_CAMERA:
249 // don't launch camera app
250 return true;
251 case KeyEvent.KEYCODE_VOLUME_DOWN:
252 case KeyEvent.KEYCODE_VOLUME_UP:
253 cameraHandler.post(() -> cameraManager.setTorch(keyCode == KeyEvent.KEYCODE_VOLUME_UP));
254 return true;
255 }
256
257 return super.onKeyDown(keyCode, event);
258 }
259
260 public void handleResult(final Result scanResult) {
261 vibrator.vibrate(VIBRATE_DURATION);
262
263 scannerView.setIsResult(true);
264
265 final Intent result = new Intent();
266 result.putExtra(INTENT_EXTRA_RESULT, scanResult.getText());
267 setResult(RESULT_OK, result);
268 postFinish();
269 }
270
271 private void postFinish() {
272 new Handler().postDelayed(this::finish, 50);
273 }
274
275 public static void scan(Activity activity) {
276 if (ContextCompat.checkSelfPermission(activity, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) {
277 Intent intent = new Intent(activity, ScanActivity.class);
278 activity.startActivityForResult(intent, REQUEST_SCAN_QR_CODE);
279 } else {
280 ActivityCompat.requestPermissions(activity, new String[]{Manifest.permission.CAMERA}, REQUEST_CAMERA_PERMISSIONS_TO_SCAN);
281 }
282
283 }
284
285 public static void onRequestPermissionResult(Activity activity, int requestCode, int[] grantResults) {
286 if (requestCode != REQUEST_CAMERA_PERMISSIONS_TO_SCAN) {
287 return;
288 }
289 if (grantResults.length > 0) {
290 if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
291 scan(activity);
292 } else {
293 Toast.makeText(activity, R.string.qr_code_scanner_needs_access_to_camera, Toast.LENGTH_SHORT).show();
294 }
295 }
296 }
297
298 private final class AutoFocusRunnable implements Runnable {
299 private final Camera camera;
300 private final Camera.AutoFocusCallback autoFocusCallback = new Camera.AutoFocusCallback() {
301 @Override
302 public void onAutoFocus(final boolean success, final Camera camera) {
303 // schedule again
304 cameraHandler.postDelayed(AutoFocusRunnable.this, AUTO_FOCUS_INTERVAL_MS);
305 }
306 };
307
308 public AutoFocusRunnable(final Camera camera) {
309 this.camera = camera;
310 }
311
312 @Override
313 public void run() {
314 try {
315 camera.autoFocus(autoFocusCallback);
316 } catch (final Exception x) {
317 Log.d(Config.LOGTAG, "problem with auto-focus, will not schedule again", x);
318 }
319 }
320 }
321}