• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2018 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License
15  */
16 
17 package com.android.settings.wifi.qrcode;
18 
19 import android.content.Context;
20 import android.content.res.Configuration;
21 import android.graphics.Matrix;
22 import android.graphics.Rect;
23 import android.graphics.SurfaceTexture;
24 import android.hardware.Camera;
25 import android.hardware.Camera.CameraInfo;
26 import android.hardware.Camera.Parameters;
27 import android.os.AsyncTask;
28 import android.os.Handler;
29 import android.os.Message;
30 import android.util.ArrayMap;
31 import android.util.Log;
32 import android.util.Size;
33 import android.view.Surface;
34 import android.view.WindowManager;
35 
36 import androidx.annotation.VisibleForTesting;
37 
38 import com.google.zxing.BarcodeFormat;
39 import com.google.zxing.BinaryBitmap;
40 import com.google.zxing.DecodeHintType;
41 import com.google.zxing.MultiFormatReader;
42 import com.google.zxing.ReaderException;
43 import com.google.zxing.Result;
44 import com.google.zxing.common.HybridBinarizer;
45 
46 import java.io.IOException;
47 import java.lang.ref.WeakReference;
48 import java.util.ArrayList;
49 import java.util.List;
50 import java.util.Map;
51 import java.util.concurrent.Executors;
52 import java.util.concurrent.Semaphore;
53 
54 /**
55  * Manage the camera for the QR scanner and help the decoder to get the image inside the scanning
56  * frame. Caller prepares a {@link SurfaceTexture} then call {@link #start(SurfaceTexture)} to
57  * start QR Code scanning. The scanning result will return by ScannerCallback interface. Caller
58  * can also call {@link #stop()} to halt QR Code scanning before the result returned.
59  */
60 public class QrCamera extends Handler {
61     private static final String TAG = "QrCamera";
62 
63     private static final int MSG_AUTO_FOCUS = 1;
64 
65     /**
66      * The max allowed difference between picture size ratio and preview size ratio.
67      * Uses to filter the picture sizes of similar preview size ratio, for example, if a preview
68      * size is 1920x1440, MAX_RATIO_DIFF 0.1 could allow picture size of 720x480 or 352x288 or
69      * 176x44 but not 1920x1080.
70      */
71     private static double MAX_RATIO_DIFF = 0.1;
72 
73     private static long AUTOFOCUS_INTERVAL_MS = 1500L;
74 
75     private static Map<DecodeHintType, List<BarcodeFormat>> HINTS = new ArrayMap<>();
76     private static List<BarcodeFormat> FORMATS = new ArrayList<>();
77 
78     static {
79         FORMATS.add(BarcodeFormat.QR_CODE);
HINTS.put(DecodeHintType.POSSIBLE_FORMATS, FORMATS)80         HINTS.put(DecodeHintType.POSSIBLE_FORMATS, FORMATS);
81     }
82 
83     private Camera mCamera;
84     private Size mPreviewSize;
85     private WeakReference<Context> mContext;
86     private ScannerCallback mScannerCallback;
87     private MultiFormatReader mReader;
88     private DecodingTask mDecodeTask;
89     private int mCameraOrientation;
90     private Camera.Parameters mParameters;
91 
QrCamera(Context context, ScannerCallback callback)92     public QrCamera(Context context, ScannerCallback callback) {
93         mContext =  new WeakReference<Context>(context);
94         mScannerCallback = callback;
95         mReader = new MultiFormatReader();
96         mReader.setHints(HINTS);
97     }
98 
99     /**
100      * The function start camera preview and capture pictures to decode QR code continuously in a
101      * background task.
102      *
103      * @param surface The surface to be used for live preview.
104      */
start(SurfaceTexture surface)105     public void start(SurfaceTexture surface) {
106         if (mDecodeTask == null) {
107             mDecodeTask = new DecodingTask(surface);
108             // Execute in the separate thread pool to prevent block other AsyncTask.
109             mDecodeTask.executeOnExecutor(Executors.newSingleThreadExecutor());
110         }
111     }
112 
113     /**
114      * The function stop camera preview and background decode task. Caller call this function when
115      * the surface is being destroyed.
116      */
stop()117     public void stop() {
118         removeMessages(MSG_AUTO_FOCUS);
119         if (mDecodeTask != null) {
120             mDecodeTask.cancel(true);
121             mDecodeTask = null;
122         }
123         if (mCamera != null) {
124             mCamera.stopPreview();
125         }
126     }
127 
128     /** The scanner which includes this QrCamera class should implement this */
129     public interface ScannerCallback {
130 
131         /**
132          * The function used to handle the decoding result of the QR code.
133          *
134          * @param result the result QR code after decoding.
135          */
handleSuccessfulResult(String result)136         void handleSuccessfulResult(String result);
137 
138         /** Request the QR code scanner to handle the failure happened. */
handleCameraFailure()139         void handleCameraFailure();
140 
141         /**
142          * The function used to get the background View size.
143          *
144          * @return Includes the background view size.
145          */
getViewSize()146         Size getViewSize();
147 
148         /**
149          * The function used to get the frame position inside the view
150          *
151          * @param previewSize Is the preview size set by camera
152          * @param cameraOrientation Is the orientation of current Camera
153          * @return The rectangle would like to crop from the camera preview shot.
154          */
getFramePosition(Size previewSize, int cameraOrientation)155         Rect getFramePosition(Size previewSize, int cameraOrientation);
156 
157         /**
158          * Sets the transform to associate with preview area.
159          *
160          * @param transform The transform to apply to the content of preview
161          */
setTransform(Matrix transform)162         void setTransform(Matrix transform);
163 
164         /**
165          * Verify QR code is valid or not. The camera will stop scanning if this callback returns
166          * true.
167          *
168          * @param qrCode The result QR code after decoding.
169          * @return Returns true if qrCode hold valid information.
170          */
isValid(String qrCode)171         boolean isValid(String qrCode);
172     }
173 
setCameraParameter()174     private void setCameraParameter() {
175         mParameters = mCamera.getParameters();
176         mPreviewSize = getBestPreviewSize(mParameters);
177         mParameters.setPreviewSize(mPreviewSize.getWidth(), mPreviewSize.getHeight());
178         Size pictureSize = getBestPictureSize(mParameters);
179         mParameters.setPreviewSize(pictureSize.getWidth(), pictureSize.getHeight());
180 
181         if (mParameters.getSupportedFlashModes().contains(Parameters.FLASH_MODE_OFF)) {
182             mParameters.setFlashMode(Parameters.FLASH_MODE_OFF);
183         }
184 
185         final List<String> supportedFocusModes = mParameters.getSupportedFocusModes();
186         if (supportedFocusModes.contains(Parameters.FOCUS_MODE_CONTINUOUS_PICTURE)) {
187             mParameters.setFocusMode(Parameters.FOCUS_MODE_CONTINUOUS_PICTURE);
188         } else if (supportedFocusModes.contains(Parameters.FOCUS_MODE_AUTO)) {
189             mParameters.setFocusMode(Parameters.FOCUS_MODE_AUTO);
190         }
191         mCamera.setParameters(mParameters);
192     }
193 
startPreview()194     private boolean startPreview() {
195         if (mContext.get() == null) {
196             return false;
197         }
198 
199         final WindowManager winManager =
200                 (WindowManager) mContext.get().getSystemService(Context.WINDOW_SERVICE);
201         final int rotation = winManager.getDefaultDisplay().getRotation();
202         int degrees = 0;
203         switch (rotation) {
204             case Surface.ROTATION_0:
205                 degrees = 0;
206                 break;
207             case Surface.ROTATION_90:
208                 degrees = 90;
209                 break;
210             case Surface.ROTATION_180:
211                 degrees = 180;
212                 break;
213             case Surface.ROTATION_270:
214                 degrees = 270;
215                 break;
216         }
217         final int rotateDegrees = (mCameraOrientation - degrees + 360) % 360;
218         mCamera.setDisplayOrientation(rotateDegrees);
219         mCamera.startPreview();
220         if (mParameters.getFocusMode() == Parameters.FOCUS_MODE_AUTO) {
221             mCamera.autoFocus(/* Camera.AutoFocusCallback */ null);
222             sendMessageDelayed(obtainMessage(MSG_AUTO_FOCUS), AUTOFOCUS_INTERVAL_MS);
223         }
224         return true;
225     }
226 
227     private class DecodingTask extends AsyncTask<Void, Void, String> {
228         private QrYuvLuminanceSource mImage;
229         private SurfaceTexture mSurface;
230 
DecodingTask(SurfaceTexture surface)231         private DecodingTask(SurfaceTexture surface) {
232             mSurface = surface;
233         }
234 
235         @Override
doInBackground(Void... tmp)236         protected String doInBackground(Void... tmp) {
237             if (!initCamera(mSurface)) {
238                 return null;
239             }
240 
241             final Semaphore imageGot = new Semaphore(0);
242             while (true) {
243                 // This loop will try to capture preview image continuously until a valid QR Code
244                 // decoded. The caller can also call {@link #stop()} to inturrupts scanning loop.
245                 mCamera.setOneShotPreviewCallback(
246                         (imageData, camera) -> {
247                             mImage = getFrameImage(imageData);
248                             imageGot.release();
249                         });
250                 try {
251                     // Semaphore.acquire() blocking until permit is available, or the thread is
252                     // interrupted.
253                     imageGot.acquire();
254                     Result qrCode = null;
255                     try {
256                         qrCode =
257                                 mReader.decodeWithState(
258                                         new BinaryBitmap(new HybridBinarizer(mImage)));
259                     } catch (ReaderException e) {
260                         // No logging since every time the reader cannot decode the
261                         // image, this ReaderException will be thrown.
262                     } finally {
263                         mReader.reset();
264                     }
265                     if (qrCode != null) {
266                         if (mScannerCallback.isValid(qrCode.getText())) {
267                             return qrCode.getText();
268                         }
269                     }
270                 } catch (InterruptedException e) {
271                     Thread.currentThread().interrupt();
272                     return null;
273                 }
274             }
275         }
276 
277         @Override
onPostExecute(String qrCode)278         protected void onPostExecute(String qrCode) {
279             if (qrCode != null) {
280                 mScannerCallback.handleSuccessfulResult(qrCode);
281             }
282         }
283 
initCamera(SurfaceTexture surface)284         private boolean initCamera(SurfaceTexture surface) {
285             final int numberOfCameras = Camera.getNumberOfCameras();
286             Camera.CameraInfo cameraInfo = new Camera.CameraInfo();
287             try {
288                 for (int i = 0; i < numberOfCameras; ++i) {
289                     Camera.getCameraInfo(i, cameraInfo);
290                     if (cameraInfo.facing == CameraInfo.CAMERA_FACING_BACK) {
291                         mCamera = Camera.open(i);
292                         mCamera.setPreviewTexture(surface);
293                         mCameraOrientation = cameraInfo.orientation;
294                         break;
295                     }
296                 }
297                 if (mCamera == null) {
298                     Log.e(TAG, "Cannot find available back camera.");
299                     mScannerCallback.handleCameraFailure();
300                     return false;
301                 }
302                 setCameraParameter();
303                 setTransformationMatrix(mScannerCallback.getViewSize());
304                 if (!startPreview()) {
305                     Log.e(TAG, "Error to init Camera");
306                     mCamera = null;
307                     mScannerCallback.handleCameraFailure();
308                     return false;
309                 }
310                 return true;
311             } catch (IOException e) {
312                 Log.e(TAG, "Error to init Camera");
313                 mCamera = null;
314                 mScannerCallback.handleCameraFailure();
315                 return false;
316             }
317         }
318     }
319 
320     /** Set transfom matrix to crop and center the preview picture */
setTransformationMatrix(Size viewSize)321     private void setTransformationMatrix(Size viewSize) {
322         // Check aspect ratio, can only handle square view.
323         final int viewRatio = (int)getRatio(viewSize.getWidth(), viewSize.getHeight());
324 
325         final boolean isPortrait = mContext.get().getResources().getConfiguration().orientation
326                 == Configuration.ORIENTATION_PORTRAIT ? true : false;
327 
328         final int previewWidth = isPortrait ? mPreviewSize.getWidth() : mPreviewSize.getHeight();
329         final int previewHeight = isPortrait ? mPreviewSize.getHeight() : mPreviewSize.getWidth();
330         final float ratioPreview = (float) getRatio(previewWidth, previewHeight);
331 
332         // Calculate transformation matrix.
333         float scaleX = 1.0f;
334         float scaleY = 1.0f;
335         if (previewWidth > previewHeight) {
336             scaleY = scaleX / ratioPreview;
337         } else {
338             scaleX = scaleY / ratioPreview;
339         }
340 
341         // Set the transform matrix.
342         final Matrix matrix = new Matrix();
343         matrix.setScale(scaleX, scaleY);
344         mScannerCallback.setTransform(matrix);
345     }
346 
getFrameImage(byte[] imageData)347     private QrYuvLuminanceSource getFrameImage(byte[] imageData) {
348         final Rect frame = mScannerCallback.getFramePosition(mPreviewSize, mCameraOrientation);
349         final QrYuvLuminanceSource image = new QrYuvLuminanceSource(imageData,
350                 mPreviewSize.getWidth(), mPreviewSize.getHeight());
351         return (QrYuvLuminanceSource)
352                 image.crop(frame.left, frame.top, frame.width(), frame.height());
353     }
354 
355     @Override
handleMessage(Message msg)356     public void handleMessage(Message msg) {
357         switch (msg.what) {
358             case MSG_AUTO_FOCUS:
359                 // Calling autoFocus(null) will only trigger the camera to focus once. In order
360                 // to make the camera continuously auto focus during scanning, need to periodly
361                 // trigger it.
362                 mCamera.autoFocus(/* Camera.AutoFocusCallback */ null);
363                 sendMessageDelayed(obtainMessage(MSG_AUTO_FOCUS), AUTOFOCUS_INTERVAL_MS);
364                 break;
365             default:
366                 Log.d(TAG, "Unexpected Message: " + msg.what);
367         }
368     }
369 
370     /** Get best preview size from the list of camera supported preview sizes. Compares the
371      * preview size and aspect ratio to choose the best one. */
getBestPreviewSize(Camera.Parameters parameters)372     private Size getBestPreviewSize(Camera.Parameters parameters) {
373         final double minRatioDiffPercent = 0.1;
374         final Size windowSize = mScannerCallback.getViewSize();
375         final double winRatio = getRatio(windowSize.getWidth(), windowSize.getHeight());
376         double bestChoiceRatio = 0;
377         Size bestChoice = new Size(0, 0);
378         for (Camera.Size size : parameters.getSupportedPreviewSizes()) {
379             double ratio = getRatio(size.width, size.height);
380             if (size.height * size.width > bestChoice.getWidth() * bestChoice.getHeight()
381                     && (Math.abs(bestChoiceRatio - winRatio) / winRatio > minRatioDiffPercent
382                     || Math.abs(ratio - winRatio) / winRatio <= minRatioDiffPercent)) {
383                 bestChoice = new Size(size.width, size.height);
384                 bestChoiceRatio = getRatio(size.width, size.height);
385             }
386         }
387         return bestChoice;
388     }
389 
390     /** Get best picture size from the list of camera supported picture sizes. Compares the
391      *  picture size and aspect ratio to choose the best one. */
getBestPictureSize(Camera.Parameters parameters)392     private Size getBestPictureSize(Camera.Parameters parameters) {
393         final Camera.Size previewSize = parameters.getPreviewSize();
394         final double previewRatio = getRatio(previewSize.width, previewSize.height);
395         List<Size> bestChoices = new ArrayList<>();
396         final List<Size> similarChoices = new ArrayList<>();
397 
398         // Filter by ratio
399         for (Camera.Size size : parameters.getSupportedPictureSizes()) {
400             double ratio = getRatio(size.width, size.height);
401             if (ratio == previewRatio) {
402                 bestChoices.add(new Size(size.width, size.height));
403             } else if (Math.abs(ratio - previewRatio) < MAX_RATIO_DIFF) {
404                 similarChoices.add(new Size(size.width, size.height));
405             }
406         }
407 
408         if (bestChoices.size() == 0 && similarChoices.size() == 0) {
409             Log.d(TAG, "No proper picture size, return default picture size");
410             Camera.Size defaultPictureSize = parameters.getPictureSize();
411             return new Size(defaultPictureSize.width, defaultPictureSize.height);
412         }
413 
414         if (bestChoices.size() == 0) {
415             bestChoices = similarChoices;
416         }
417 
418         // Get the best by area
419         int bestAreaDifference = Integer.MAX_VALUE;
420         Size bestChoice = null;
421         final int previewArea = previewSize.width * previewSize.height;
422         for (Size size : bestChoices) {
423             int areaDifference = Math.abs(size.getWidth() * size.getHeight() - previewArea);
424             if (areaDifference < bestAreaDifference) {
425                 bestAreaDifference = areaDifference;
426                 bestChoice = size;
427             }
428         }
429         return bestChoice;
430     }
431 
getRatio(double x, double y)432     private double getRatio(double x, double y) {
433         return (x < y) ? x / y : y / x;
434     }
435 
436     @VisibleForTesting
decodeImage(BinaryBitmap image)437     protected void decodeImage(BinaryBitmap image) {
438         Result qrCode = null;
439 
440         try {
441             qrCode = mReader.decodeWithState(image);
442         } catch (ReaderException e) {
443         } finally {
444             mReader.reset();
445         }
446 
447         if (qrCode != null) {
448             mScannerCallback.handleSuccessfulResult(qrCode.getText());
449         }
450     }
451 
452     /**
453      * After {@link #start(SurfaceTexture)}, DecodingTask runs continuously to capture images and
454      * decode QR code. DecodingTask become null After {@link #stop()}.
455      *
456      * Uses this method in test case to prevent power consumption problem.
457      */
isDecodeTaskAlive()458     public boolean isDecodeTaskAlive() {
459         return mDecodeTask != null;
460     }
461 }
462