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