1 /** 2 * Copyright (C) 2022 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.bluetooth; 18 19 import android.app.Activity; 20 import android.app.settings.SettingsEnums; 21 import android.content.Context; 22 import android.content.Intent; 23 import android.graphics.Matrix; 24 import android.graphics.Outline; 25 import android.graphics.Rect; 26 import android.graphics.SurfaceTexture; 27 import android.os.Bundle; 28 import android.os.Handler; 29 import android.os.Message; 30 import android.os.VibrationEffect; 31 import android.os.Vibrator; 32 import android.util.Log; 33 import android.util.Size; 34 import android.view.LayoutInflater; 35 import android.view.TextureView; 36 import android.view.View; 37 import android.view.ViewGroup; 38 import android.view.ViewOutlineProvider; 39 import android.view.accessibility.AccessibilityEvent; 40 import android.widget.TextView; 41 42 import androidx.annotation.NonNull; 43 import androidx.annotation.StringRes; 44 45 import com.android.settings.R; 46 import com.android.settings.core.InstrumentedFragment; 47 import com.android.settingslib.bluetooth.BluetoothBroadcastUtils; 48 import com.android.settingslib.bluetooth.BluetoothUtils; 49 import com.android.settingslib.qrcode.QrCamera; 50 51 import java.time.Duration; 52 53 public class QrCodeScanModeFragment extends InstrumentedFragment implements 54 TextureView.SurfaceTextureListener, 55 QrCamera.ScannerCallback { 56 private static final boolean DEBUG = BluetoothUtils.D; 57 private static final String TAG = "QrCodeScanModeFragment"; 58 59 /** Message sent to hide error message */ 60 private static final int MESSAGE_HIDE_ERROR_MESSAGE = 1; 61 /** Message sent to show error message */ 62 private static final int MESSAGE_SHOW_ERROR_MESSAGE = 2; 63 /** Message sent to broadcast QR code */ 64 private static final int MESSAGE_SCAN_BROADCAST_SUCCESS = 3; 65 66 private static final long SHOW_ERROR_MESSAGE_INTERVAL = 10000; 67 private static final long SHOW_SUCCESS_SQUARE_INTERVAL = 1000; 68 69 private static final Duration VIBRATE_DURATION_QR_CODE_RECOGNITION = Duration.ofMillis(3); 70 71 public static final String KEY_BROADCAST_METADATA = "key_broadcast_metadata"; 72 73 private int mCornerRadius; 74 private String mBroadcastMetadata; 75 private Context mContext; 76 private QrCamera mCamera; 77 private TextureView mTextureView; 78 private TextView mSummary; 79 private TextView mErrorMessage; 80 81 @Override onCreate(Bundle savedInstanceState)82 public void onCreate(Bundle savedInstanceState) { 83 super.onCreate(savedInstanceState); 84 mContext = getContext(); 85 } 86 87 @Override onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)88 public final View onCreateView(LayoutInflater inflater, ViewGroup container, 89 Bundle savedInstanceState) { 90 return inflater.inflate(R.layout.qrcode_scanner_fragment, container, 91 /* attachToRoot */ false); 92 } 93 94 @Override onViewCreated(View view, Bundle savedInstanceState)95 public void onViewCreated(View view, Bundle savedInstanceState) { 96 mTextureView = view.findViewById(R.id.preview_view); 97 mCornerRadius = mContext.getResources().getDimensionPixelSize( 98 R.dimen.qrcode_preview_radius); 99 mTextureView.setSurfaceTextureListener(this); 100 mTextureView.setOutlineProvider(new ViewOutlineProvider() { 101 @Override 102 public void getOutline(View view, Outline outline) { 103 outline.setRoundRect(0,0, view.getWidth(), view.getHeight(), mCornerRadius); 104 } 105 }); 106 mTextureView.setClipToOutline(true); 107 mErrorMessage = view.findViewById(R.id.error_message); 108 } 109 initCamera(SurfaceTexture surface)110 private void initCamera(SurfaceTexture surface) { 111 // Check if the camera has already created. 112 if (mCamera == null) { 113 mCamera = new QrCamera(mContext, this); 114 mCamera.start(surface); 115 } 116 } 117 destroyCamera()118 private void destroyCamera() { 119 if (mCamera != null) { 120 mCamera.stop(); 121 mCamera = null; 122 } 123 } 124 125 @Override onSurfaceTextureAvailable(@onNull SurfaceTexture surface, int width, int height)126 public void onSurfaceTextureAvailable(@NonNull SurfaceTexture surface, int width, int height) { 127 initCamera(surface); 128 } 129 130 @Override onSurfaceTextureSizeChanged(@onNull SurfaceTexture surface, int width, int height)131 public void onSurfaceTextureSizeChanged(@NonNull SurfaceTexture surface, int width, 132 int height) { 133 } 134 135 @Override onSurfaceTextureDestroyed(@onNull SurfaceTexture surface)136 public boolean onSurfaceTextureDestroyed(@NonNull SurfaceTexture surface) { 137 destroyCamera(); 138 return true; 139 } 140 141 @Override onSurfaceTextureUpdated(@onNull SurfaceTexture surface)142 public void onSurfaceTextureUpdated(@NonNull SurfaceTexture surface) { 143 } 144 145 @Override handleSuccessfulResult(String qrCode)146 public void handleSuccessfulResult(String qrCode) { 147 if (DEBUG) { 148 Log.d(TAG, "handleSuccessfulResult(), get the qr code string."); 149 } 150 mBroadcastMetadata = qrCode; 151 handleBtLeAudioScanner(); 152 } 153 154 @Override handleCameraFailure()155 public void handleCameraFailure() { 156 destroyCamera(); 157 } 158 159 @Override getViewSize()160 public Size getViewSize() { 161 return new Size(mTextureView.getWidth(), mTextureView.getHeight()); 162 } 163 164 @Override getFramePosition(Size previewSize, int cameraOrientation)165 public Rect getFramePosition(Size previewSize, int cameraOrientation) { 166 return new Rect(0, 0, previewSize.getHeight(), previewSize.getHeight()); 167 } 168 169 @Override setTransform(Matrix transform)170 public void setTransform(Matrix transform) { 171 mTextureView.setTransform(transform); 172 } 173 174 @Override isValid(String qrCode)175 public boolean isValid(String qrCode) { 176 if (qrCode.startsWith(BluetoothBroadcastUtils.SCHEME_BT_BROADCAST_METADATA)) { 177 return true; 178 } else { 179 showErrorMessage(R.string.bt_le_audio_qr_code_is_not_valid_format); 180 return false; 181 } 182 } 183 isDecodeTaskAlive()184 protected boolean isDecodeTaskAlive() { 185 return mCamera != null && mCamera.isDecodeTaskAlive(); 186 } 187 188 private final Handler mHandler = new Handler() { 189 @Override 190 public void handleMessage(Message msg) { 191 switch (msg.what) { 192 case MESSAGE_HIDE_ERROR_MESSAGE: 193 mErrorMessage.setVisibility(View.INVISIBLE); 194 break; 195 196 case MESSAGE_SHOW_ERROR_MESSAGE: 197 final String errorMessage = (String) msg.obj; 198 199 mErrorMessage.setVisibility(View.VISIBLE); 200 mErrorMessage.setText(errorMessage); 201 mErrorMessage.sendAccessibilityEvent( 202 AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); 203 204 // Cancel any pending messages to hide error view and requeue the message so 205 // user has time to see error 206 removeMessages(MESSAGE_HIDE_ERROR_MESSAGE); 207 sendEmptyMessageDelayed(MESSAGE_HIDE_ERROR_MESSAGE, 208 SHOW_ERROR_MESSAGE_INTERVAL); 209 break; 210 211 case MESSAGE_SCAN_BROADCAST_SUCCESS: 212 Log.d(TAG, "scan success"); 213 final Intent resultIntent = new Intent(); 214 resultIntent.putExtra(KEY_BROADCAST_METADATA, mBroadcastMetadata); 215 getActivity().setResult(Activity.RESULT_OK, resultIntent); 216 notifyUserForQrCodeRecognition(); 217 break; 218 default: 219 } 220 } 221 }; 222 notifyUserForQrCodeRecognition()223 private void notifyUserForQrCodeRecognition() { 224 if (mCamera != null) { 225 mCamera.stop(); 226 } 227 228 mErrorMessage.setVisibility(View.INVISIBLE); 229 230 triggerVibrationForQrCodeRecognition(getContext()); 231 232 getActivity().finish(); 233 } 234 triggerVibrationForQrCodeRecognition(Context context)235 private static void triggerVibrationForQrCodeRecognition(Context context) { 236 Vibrator vibrator = context.getSystemService(Vibrator.class); 237 if (vibrator == null) { 238 return; 239 } 240 vibrator.vibrate(VibrationEffect.createOneShot( 241 VIBRATE_DURATION_QR_CODE_RECOGNITION.toMillis(), 242 VibrationEffect.DEFAULT_AMPLITUDE)); 243 } 244 showErrorMessage(@tringRes int messageResId)245 private void showErrorMessage(@StringRes int messageResId) { 246 final Message message = mHandler.obtainMessage(MESSAGE_SHOW_ERROR_MESSAGE, 247 getString(messageResId)); 248 message.sendToTarget(); 249 } 250 handleBtLeAudioScanner()251 private void handleBtLeAudioScanner() { 252 Message message = mHandler.obtainMessage(MESSAGE_SCAN_BROADCAST_SUCCESS); 253 mHandler.sendMessageDelayed(message, SHOW_SUCCESS_SQUARE_INTERVAL); 254 } 255 updateSummary()256 private void updateSummary() { 257 mSummary.setText(getString(R.string.bt_le_audio_scan_qr_code_scanner)); 258 } 259 260 @Override getMetricsCategory()261 public int getMetricsCategory() { 262 return SettingsEnums.LE_AUDIO_BROADCAST_SCAN_QR_CODE; 263 } 264 } 265