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