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}