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