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