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.settings.SettingsEnums; 20 import android.bluetooth.BluetoothDevice; 21 import android.content.Context; 22 import android.graphics.Matrix; 23 import android.graphics.Outline; 24 import android.graphics.Rect; 25 import android.graphics.SurfaceTexture; 26 import android.os.Bundle; 27 import android.os.Handler; 28 import android.os.Message; 29 import android.util.Log; 30 import android.util.Size; 31 import android.view.LayoutInflater; 32 import android.view.TextureView; 33 import android.view.View; 34 import android.view.ViewGroup; 35 import android.view.ViewOutlineProvider; 36 import android.view.accessibility.AccessibilityEvent; 37 import android.widget.TextView; 38 39 import androidx.annotation.NonNull; 40 import androidx.annotation.StringRes; 41 42 import com.android.settings.core.InstrumentedFragment; 43 import com.android.settingslib.R; 44 import com.android.settingslib.bluetooth.BluetoothBroadcastUtils; 45 import com.android.settingslib.bluetooth.BluetoothUtils; 46 import com.android.settingslib.qrcode.QrCamera; 47 48 public class QrCodeScanModeFragment extends InstrumentedFragment implements 49 TextureView.SurfaceTextureListener, 50 QrCamera.ScannerCallback { 51 private static final boolean DEBUG = BluetoothUtils.D; 52 private static final String TAG = "QrCodeScanModeFragment"; 53 54 /** Message sent to hide error message */ 55 private static final int MESSAGE_HIDE_ERROR_MESSAGE = 1; 56 /** Message sent to show error message */ 57 private static final int MESSAGE_SHOW_ERROR_MESSAGE = 2; 58 /** Message sent to broadcast QR code */ 59 private static final int MESSAGE_SCAN_BROADCAST_SUCCESS = 3; 60 61 private static final long SHOW_ERROR_MESSAGE_INTERVAL = 10000; 62 private static final long SHOW_SUCCESS_SQUARE_INTERVAL = 1000; 63 64 private boolean mIsGroupOp; 65 private int mCornerRadius; 66 private BluetoothDevice mSink; 67 private String mBroadcastMetadata; 68 private Context mContext; 69 private QrCamera mCamera; 70 private QrCodeScanModeController mController; 71 private TextureView mTextureView; 72 private TextView mSummary; 73 private TextView mErrorMessage; 74 QrCodeScanModeFragment(boolean isGroupOp, BluetoothDevice sink)75 public QrCodeScanModeFragment(boolean isGroupOp, BluetoothDevice sink) { 76 mIsGroupOp = isGroupOp; 77 mSink = sink; 78 } 79 80 @Override onCreate(Bundle savedInstanceState)81 public void onCreate(Bundle savedInstanceState) { 82 super.onCreate(savedInstanceState); 83 mContext = getContext(); 84 mController = new QrCodeScanModeController(mContext); 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 mController.addSource(mSink, mBroadcastMetadata, mIsGroupOp); 213 updateSummary(); 214 mSummary.sendAccessibilityEvent( 215 AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); 216 break; 217 default: 218 } 219 } 220 }; 221 showErrorMessage(@tringRes int messageResId)222 private void showErrorMessage(@StringRes int messageResId) { 223 final Message message = mHandler.obtainMessage(MESSAGE_SHOW_ERROR_MESSAGE, 224 getString(messageResId)); 225 message.sendToTarget(); 226 } 227 handleBtLeAudioScanner()228 private void handleBtLeAudioScanner() { 229 Message message = mHandler.obtainMessage(MESSAGE_SCAN_BROADCAST_SUCCESS); 230 mHandler.sendMessageDelayed(message, SHOW_SUCCESS_SQUARE_INTERVAL); 231 } 232 updateSummary()233 private void updateSummary() { 234 mSummary.setText(getString(R.string.bt_le_audio_scan_qr_code_scanner)); 235 } 236 237 @Override getMetricsCategory()238 public int getMetricsCategory() { 239 return SettingsEnums.LE_AUDIO_BROADCAST_SCAN_QR_CODE; 240 } 241 } 242