• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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         // Collapse or expand the app bar based on orientation for better display the qr camera.
120         AudioStreamsHelper.configureAppBarByOrientation(getActivity());
121         return inflater.inflate(
122                 R.layout.qrcode_scanner_fragment, container, /* attachToRoot */ false);
123     }
124 
125     @Override
onViewCreated(View view, Bundle savedInstanceState)126     public void onViewCreated(View view, Bundle savedInstanceState) {
127         mTextureView = view.findViewById(R.id.preview_view);
128         mCornerRadius =
129                 mContext.getResources()
130                         .getDimensionPixelSize(R.dimen.audio_streams_qrcode_preview_radius);
131         mTextureView.setSurfaceTextureListener(this);
132         mTextureView.setOutlineProvider(
133                 new ViewOutlineProvider() {
134                     @Override
135                     public void getOutline(View view, Outline outline) {
136                         outline.setRoundRect(
137                                 0, 0, view.getWidth(), view.getHeight(), mCornerRadius);
138                     }
139                 });
140         mTextureView.setClipToOutline(true);
141         mErrorMessage = view.findViewById(R.id.error_message);
142 
143         var device =
144                 AudioStreamsHelper.getCachedBluetoothDeviceInSharingOrLeConnected(
145                         mLocalBluetoothManager);
146         TextView summary = view.findViewById(android.R.id.summary);
147         if (summary != null && device.isPresent()) {
148             summary.setText(
149                     getString(
150                             R.string.audio_streams_main_page_qr_code_scanner_summary,
151                             device.get().getName()));
152         }
153     }
154 
155     @Override
onSurfaceTextureAvailable(@onNull SurfaceTexture surface, int width, int height)156     public void onSurfaceTextureAvailable(@NonNull SurfaceTexture surface, int width, int height) {
157         if (mCamera == null) {
158             mCamera = new QrCamera(mContext, this);
159             mCamera.start(surface);
160         }
161     }
162 
163     @Override
onSurfaceTextureSizeChanged( @onNull SurfaceTexture surface, int width, int height)164     public void onSurfaceTextureSizeChanged(
165             @NonNull SurfaceTexture surface, int width, int height) {}
166 
167     @Override
onSurfaceTextureDestroyed(@onNull SurfaceTexture surface)168     public boolean onSurfaceTextureDestroyed(@NonNull SurfaceTexture surface) {
169         destroyCamera();
170         return true;
171     }
172 
173     @Override
onSurfaceTextureUpdated(@onNull SurfaceTexture surface)174     public void onSurfaceTextureUpdated(@NonNull SurfaceTexture surface) {}
175 
176     @Override
handleSuccessfulResult(String qrCode)177     public void handleSuccessfulResult(String qrCode) {
178         if (DEBUG) {
179             Log.d(TAG, "handleSuccessfulResult(), get the qr code string.");
180         }
181         mBroadcastMetadata = qrCode;
182         Message message = mHandler.obtainMessage(MESSAGE_SCAN_BROADCAST_SUCCESS);
183         mHandler.sendMessageDelayed(message, SHOW_SUCCESS_SQUARE_INTERVAL);
184     }
185 
186     @Override
handleCameraFailure()187     public void handleCameraFailure() {
188         destroyCamera();
189     }
190 
191     @Override
getViewSize()192     public Size getViewSize() {
193         return new Size(mTextureView.getWidth(), mTextureView.getHeight());
194     }
195 
196     @Override
getFramePosition(Size previewSize, int cameraOrientation)197     public Rect getFramePosition(Size previewSize, int cameraOrientation) {
198         return new Rect(0, 0, previewSize.getHeight(), previewSize.getHeight());
199     }
200 
201     @Override
setTransform(Matrix transform)202     public void setTransform(Matrix transform) {
203         mTextureView.setTransform(transform);
204     }
205 
206     @Override
isValid(String qrCode)207     public boolean isValid(String qrCode) {
208         if (qrCode.startsWith(BluetoothBroadcastUtils.SCHEME_BT_BROADCAST_METADATA)) {
209             return true;
210         }
211         Message message =
212                 mHandler.obtainMessage(
213                         MESSAGE_SHOW_ERROR_MESSAGE,
214                         getString(R.string.audio_streams_qr_code_is_not_valid_format));
215         message.sendToTarget();
216         return false;
217     }
218 
destroyCamera()219     private void destroyCamera() {
220         if (mCamera != null) {
221             mCamera.stop();
222             mCamera = null;
223         }
224     }
225 
notifyUserForQrCodeRecognition()226     private void notifyUserForQrCodeRecognition() {
227         if (mCamera != null) {
228             mCamera.stop();
229         }
230 
231         mErrorMessage.setVisibility(View.INVISIBLE);
232         mTextureView.setVisibility(View.INVISIBLE);
233 
234         triggerVibrationForQrCodeRecognition(mContext);
235 
236         if (getActivity() != null) {
237             getActivity().finish();
238         }
239     }
240 
triggerVibrationForQrCodeRecognition(Context context)241     private static void triggerVibrationForQrCodeRecognition(Context context) {
242         Vibrator vibrator = context.getSystemService(Vibrator.class);
243         if (vibrator == null) {
244             return;
245         }
246         vibrator.vibrate(
247                 VibrationEffect.createOneShot(
248                         VIBRATE_DURATION_QR_CODE_RECOGNITION.toMillis(),
249                         VibrationEffect.DEFAULT_AMPLITUDE));
250     }
251 
252     @Override
getMetricsCategory()253     public int getMetricsCategory() {
254         return SettingsEnums.AUDIO_STREAM_QR_CODE_SCAN;
255     }
256 }
257