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 final double MAX_RATIO_DIFF = 0.1; 72 73 private static final 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 @VisibleForTesting 84 Camera mCamera; 85 private Size mPreviewSize; 86 private WeakReference<Context> mContext; 87 private ScannerCallback mScannerCallback; 88 private MultiFormatReader mReader; 89 private DecodingTask mDecodeTask; 90 private int mCameraOrientation; 91 @VisibleForTesting 92 Camera.Parameters mParameters; 93 QrCamera(Context context, ScannerCallback callback)94 public QrCamera(Context context, ScannerCallback callback) { 95 mContext = new WeakReference<Context>(context); 96 mScannerCallback = callback; 97 mReader = new MultiFormatReader(); 98 mReader.setHints(HINTS); 99 } 100 101 /** 102 * The function start camera preview and capture pictures to decode QR code continuously in a 103 * background task. 104 * 105 * @param surface The surface to be used for live preview. 106 */ start(SurfaceTexture surface)107 public void start(SurfaceTexture surface) { 108 if (mDecodeTask == null) { 109 mDecodeTask = new DecodingTask(surface); 110 // Execute in the separate thread pool to prevent block other AsyncTask. 111 mDecodeTask.executeOnExecutor(Executors.newSingleThreadExecutor()); 112 } 113 } 114 115 /** 116 * The function stop camera preview and background decode task. Caller call this function when 117 * the surface is being destroyed. 118 */ stop()119 public void stop() { 120 removeMessages(MSG_AUTO_FOCUS); 121 if (mDecodeTask != null) { 122 mDecodeTask.cancel(true); 123 mDecodeTask = null; 124 } 125 if (mCamera != null) { 126 mCamera.stopPreview(); 127 } 128 } 129 130 /** The scanner which includes this QrCamera class should implement this */ 131 public interface ScannerCallback { 132 133 /** 134 * The function used to handle the decoding result of the QR code. 135 * 136 * @param result the result QR code after decoding. 137 */ handleSuccessfulResult(String result)138 void handleSuccessfulResult(String result); 139 140 /** Request the QR code scanner to handle the failure happened. */ handleCameraFailure()141 void handleCameraFailure(); 142 143 /** 144 * The function used to get the background View size. 145 * 146 * @return Includes the background view size. 147 */ getViewSize()148 Size getViewSize(); 149 150 /** 151 * The function used to get the frame position inside the view 152 * 153 * @param previewSize Is the preview size set by camera 154 * @param cameraOrientation Is the orientation of current Camera 155 * @return The rectangle would like to crop from the camera preview shot. 156 */ getFramePosition(Size previewSize, int cameraOrientation)157 Rect getFramePosition(Size previewSize, int cameraOrientation); 158 159 /** 160 * Sets the transform to associate with preview area. 161 * 162 * @param transform The transform to apply to the content of preview 163 */ setTransform(Matrix transform)164 void setTransform(Matrix transform); 165 166 /** 167 * Verify QR code is valid or not. The camera will stop scanning if this callback returns 168 * true. 169 * 170 * @param qrCode The result QR code after decoding. 171 * @return Returns true if qrCode hold valid information. 172 */ isValid(String qrCode)173 boolean isValid(String qrCode); 174 } 175 176 @VisibleForTesting setCameraParameter()177 void setCameraParameter() { 178 mParameters = mCamera.getParameters(); 179 mPreviewSize = getBestPreviewSize(mParameters); 180 mParameters.setPreviewSize(mPreviewSize.getWidth(), mPreviewSize.getHeight()); 181 Size pictureSize = getBestPictureSize(mParameters); 182 mParameters.setPictureSize(pictureSize.getWidth(), pictureSize.getHeight()); 183 184 final List<String> supportedFlashModes = mParameters.getSupportedFlashModes(); 185 if (supportedFlashModes != null && 186 supportedFlashModes.contains(Parameters.FLASH_MODE_OFF)) { 187 mParameters.setFlashMode(Parameters.FLASH_MODE_OFF); 188 } 189 190 final List<String> supportedFocusModes = mParameters.getSupportedFocusModes(); 191 if (supportedFocusModes.contains(Parameters.FOCUS_MODE_CONTINUOUS_PICTURE)) { 192 mParameters.setFocusMode(Parameters.FOCUS_MODE_CONTINUOUS_PICTURE); 193 } else if (supportedFocusModes.contains(Parameters.FOCUS_MODE_AUTO)) { 194 mParameters.setFocusMode(Parameters.FOCUS_MODE_AUTO); 195 } 196 mCamera.setParameters(mParameters); 197 } 198 startPreview()199 private boolean startPreview() { 200 if (mContext.get() == null) { 201 return false; 202 } 203 204 final WindowManager winManager = 205 (WindowManager) mContext.get().getSystemService(Context.WINDOW_SERVICE); 206 final int rotation = winManager.getDefaultDisplay().getRotation(); 207 int degrees = 0; 208 switch (rotation) { 209 case Surface.ROTATION_0: 210 degrees = 0; 211 break; 212 case Surface.ROTATION_90: 213 degrees = 90; 214 break; 215 case Surface.ROTATION_180: 216 degrees = 180; 217 break; 218 case Surface.ROTATION_270: 219 degrees = 270; 220 break; 221 } 222 final int rotateDegrees = (mCameraOrientation - degrees + 360) % 360; 223 mCamera.setDisplayOrientation(rotateDegrees); 224 mCamera.startPreview(); 225 if (Parameters.FOCUS_MODE_AUTO.equals(mParameters.getFocusMode())) { 226 mCamera.autoFocus(/* Camera.AutoFocusCallback */ null); 227 sendMessageDelayed(obtainMessage(MSG_AUTO_FOCUS), AUTOFOCUS_INTERVAL_MS); 228 } 229 return true; 230 } 231 232 private class DecodingTask extends AsyncTask<Void, Void, String> { 233 private QrYuvLuminanceSource mImage; 234 private SurfaceTexture mSurface; 235 DecodingTask(SurfaceTexture surface)236 private DecodingTask(SurfaceTexture surface) { 237 mSurface = surface; 238 } 239 240 @Override doInBackground(Void... tmp)241 protected String doInBackground(Void... tmp) { 242 if (!initCamera(mSurface)) { 243 return null; 244 } 245 246 final Semaphore imageGot = new Semaphore(0); 247 while (true) { 248 // This loop will try to capture preview image continuously until a valid QR Code 249 // decoded. The caller can also call {@link #stop()} to interrupts scanning loop. 250 mCamera.setOneShotPreviewCallback( 251 (imageData, camera) -> { 252 mImage = getFrameImage(imageData); 253 imageGot.release(); 254 }); 255 try { 256 // Semaphore.acquire() blocking until permit is available, or the thread is 257 // interrupted. 258 imageGot.acquire(); 259 Result qrCode = null; 260 try { 261 qrCode = 262 mReader.decodeWithState( 263 new BinaryBitmap(new HybridBinarizer(mImage))); 264 } catch (ReaderException e) { 265 // No logging since every time the reader cannot decode the 266 // image, this ReaderException will be thrown. 267 } finally { 268 mReader.reset(); 269 } 270 if (qrCode != null) { 271 if (mScannerCallback.isValid(qrCode.getText())) { 272 return qrCode.getText(); 273 } 274 } 275 } catch (InterruptedException e) { 276 Thread.currentThread().interrupt(); 277 return null; 278 } 279 } 280 } 281 282 @Override onPostExecute(String qrCode)283 protected void onPostExecute(String qrCode) { 284 if (qrCode != null) { 285 mScannerCallback.handleSuccessfulResult(qrCode); 286 } 287 } 288 initCamera(SurfaceTexture surface)289 private boolean initCamera(SurfaceTexture surface) { 290 final int numberOfCameras = Camera.getNumberOfCameras(); 291 Camera.CameraInfo cameraInfo = new Camera.CameraInfo(); 292 try { 293 for (int i = 0; i < numberOfCameras; ++i) { 294 Camera.getCameraInfo(i, cameraInfo); 295 if (cameraInfo.facing == CameraInfo.CAMERA_FACING_BACK) { 296 releaseCamera(); 297 mCamera = Camera.open(i); 298 mCameraOrientation = cameraInfo.orientation; 299 break; 300 } 301 } 302 if (mCamera == null && numberOfCameras > 0) { 303 Log.i(TAG, "Can't find back camera. Opening a different camera"); 304 Camera.getCameraInfo(0, cameraInfo); 305 releaseCamera(); 306 mCamera = Camera.open(0); 307 mCameraOrientation = cameraInfo.orientation; 308 } 309 } catch (RuntimeException e) { 310 Log.e(TAG, "Fail to open camera: " + e); 311 mCamera = null; 312 mScannerCallback.handleCameraFailure(); 313 return false; 314 } 315 316 try { 317 if (mCamera == null) { 318 throw new IOException("Cannot find available camera"); 319 } 320 mCamera.setPreviewTexture(surface); 321 setCameraParameter(); 322 setTransformationMatrix(); 323 if (!startPreview()) { 324 throw new IOException("Lost contex"); 325 } 326 } catch (IOException ioe) { 327 Log.e(TAG, "Fail to startPreview camera: " + ioe); 328 mCamera = null; 329 mScannerCallback.handleCameraFailure(); 330 return false; 331 } 332 return true; 333 } 334 } 335 releaseCamera()336 private void releaseCamera() { 337 if (mCamera != null) { 338 mCamera.release(); 339 mCamera = null; 340 } 341 } 342 343 /** Set transform matrix to crop and center the preview picture */ setTransformationMatrix()344 private void setTransformationMatrix() { 345 final boolean isPortrait = mContext.get().getResources().getConfiguration().orientation 346 == Configuration.ORIENTATION_PORTRAIT; 347 348 final int previewWidth = isPortrait ? mPreviewSize.getWidth() : mPreviewSize.getHeight(); 349 final int previewHeight = isPortrait ? mPreviewSize.getHeight() : mPreviewSize.getWidth(); 350 final float ratioPreview = (float) getRatio(previewWidth, previewHeight); 351 352 // Calculate transformation matrix. 353 float scaleX = 1.0f; 354 float scaleY = 1.0f; 355 if (previewWidth > previewHeight) { 356 scaleY = scaleX / ratioPreview; 357 } else { 358 scaleX = scaleY / ratioPreview; 359 } 360 361 // Set the transform matrix. 362 final Matrix matrix = new Matrix(); 363 matrix.setScale(scaleX, scaleY); 364 mScannerCallback.setTransform(matrix); 365 } 366 getFrameImage(byte[] imageData)367 private QrYuvLuminanceSource getFrameImage(byte[] imageData) { 368 final Rect frame = mScannerCallback.getFramePosition(mPreviewSize, mCameraOrientation); 369 final QrYuvLuminanceSource image = new QrYuvLuminanceSource(imageData, 370 mPreviewSize.getWidth(), mPreviewSize.getHeight()); 371 return (QrYuvLuminanceSource) 372 image.crop(frame.left, frame.top, frame.width(), frame.height()); 373 } 374 375 @Override handleMessage(Message msg)376 public void handleMessage(Message msg) { 377 switch (msg.what) { 378 case MSG_AUTO_FOCUS: 379 // Calling autoFocus(null) will only trigger the camera to focus once. In order 380 // to make the camera continuously auto focus during scanning, need to periodically 381 // trigger it. 382 mCamera.autoFocus(/* Camera.AutoFocusCallback */ null); 383 sendMessageDelayed(obtainMessage(MSG_AUTO_FOCUS), AUTOFOCUS_INTERVAL_MS); 384 break; 385 default: 386 Log.d(TAG, "Unexpected Message: " + msg.what); 387 } 388 } 389 390 /** Get best preview size from the list of camera supported preview sizes. Compares the 391 * preview size and aspect ratio to choose the best one. */ getBestPreviewSize(Camera.Parameters parameters)392 private Size getBestPreviewSize(Camera.Parameters parameters) { 393 final double minRatioDiffPercent = 0.1; 394 final Size windowSize = mScannerCallback.getViewSize(); 395 final double winRatio = getRatio(windowSize.getWidth(), windowSize.getHeight()); 396 double bestChoiceRatio = 0; 397 Size bestChoice = new Size(0, 0); 398 for (Camera.Size size : parameters.getSupportedPreviewSizes()) { 399 double ratio = getRatio(size.width, size.height); 400 if (size.height * size.width > bestChoice.getWidth() * bestChoice.getHeight() 401 && (Math.abs(bestChoiceRatio - winRatio) / winRatio > minRatioDiffPercent 402 || Math.abs(ratio - winRatio) / winRatio <= minRatioDiffPercent)) { 403 bestChoice = new Size(size.width, size.height); 404 bestChoiceRatio = getRatio(size.width, size.height); 405 } 406 } 407 return bestChoice; 408 } 409 410 /** Get best picture size from the list of camera supported picture sizes. Compares the 411 * picture size and aspect ratio to choose the best one. */ getBestPictureSize(Camera.Parameters parameters)412 private Size getBestPictureSize(Camera.Parameters parameters) { 413 final Camera.Size previewSize = parameters.getPreviewSize(); 414 final double previewRatio = getRatio(previewSize.width, previewSize.height); 415 List<Size> bestChoices = new ArrayList<>(); 416 final List<Size> similarChoices = new ArrayList<>(); 417 418 // Filter by ratio 419 for (Camera.Size size : parameters.getSupportedPictureSizes()) { 420 double ratio = getRatio(size.width, size.height); 421 if (ratio == previewRatio) { 422 bestChoices.add(new Size(size.width, size.height)); 423 } else if (Math.abs(ratio - previewRatio) < MAX_RATIO_DIFF) { 424 similarChoices.add(new Size(size.width, size.height)); 425 } 426 } 427 428 if (bestChoices.size() == 0 && similarChoices.size() == 0) { 429 Log.d(TAG, "No proper picture size, return default picture size"); 430 Camera.Size defaultPictureSize = parameters.getPictureSize(); 431 return new Size(defaultPictureSize.width, defaultPictureSize.height); 432 } 433 434 if (bestChoices.size() == 0) { 435 bestChoices = similarChoices; 436 } 437 438 // Get the best by area 439 int bestAreaDifference = Integer.MAX_VALUE; 440 Size bestChoice = null; 441 final int previewArea = previewSize.width * previewSize.height; 442 for (Size size : bestChoices) { 443 int areaDifference = Math.abs(size.getWidth() * size.getHeight() - previewArea); 444 if (areaDifference < bestAreaDifference) { 445 bestAreaDifference = areaDifference; 446 bestChoice = size; 447 } 448 } 449 return bestChoice; 450 } 451 getRatio(double x, double y)452 private double getRatio(double x, double y) { 453 return (x < y) ? x / y : y / x; 454 } 455 456 @VisibleForTesting decodeImage(BinaryBitmap image)457 protected void decodeImage(BinaryBitmap image) { 458 Result qrCode = null; 459 460 try { 461 qrCode = mReader.decodeWithState(image); 462 } catch (ReaderException e) { 463 } finally { 464 mReader.reset(); 465 } 466 467 if (qrCode != null) { 468 mScannerCallback.handleSuccessfulResult(qrCode.getText()); 469 } 470 } 471 472 /** 473 * After {@link #start(SurfaceTexture)}, DecodingTask runs continuously to capture images and 474 * decode QR code. DecodingTask become null After {@link #stop()}. 475 * 476 * Uses this method in test case to prevent power consumption problem. 477 */ isDecodeTaskAlive()478 public boolean isDecodeTaskAlive() { 479 return mDecodeTask != null; 480 } 481 } 482