From db7058fabecb9aae1929a957ec306b6eda889864 Mon Sep 17 00:00:00 2001 From: Zoey Chen Date: Tue, 29 Mar 2022 17:51:31 +0800 Subject: [PATCH] [LeAudio] Create QR code scanner activity and fragment hsv: https://hsv.googleplex.com/6255792371007488 Bug: 214340362 Test: manual Change-Id: I254b7d1b7191686da4c1ca7cd2eb980ab8606c4e --- packages/SettingsLib/AndroidManifest.xml | 10 + .../CachedBluetoothDeviceManager.java | 2 +- .../bluetooth/LocalBluetoothLeBroadcast.java | 4 - .../LocalBluetoothLeBroadcastAssistant.java | 4 - .../qrcode/QrCodeScanModeActivity.java | 109 ++++++++ .../qrcode/QrCodeScanModeBaseActivity.java | 46 ++++ .../qrcode/QrCodeScanModeController.java | 82 ++++++ .../qrcode/QrCodeScanModeFragment.java | 235 ++++++++++++++++++ 8 files changed, 483 insertions(+), 9 deletions(-) create mode 100644 packages/SettingsLib/src/com/android/settingslib/qrcode/QrCodeScanModeActivity.java create mode 100644 packages/SettingsLib/src/com/android/settingslib/qrcode/QrCodeScanModeBaseActivity.java create mode 100644 packages/SettingsLib/src/com/android/settingslib/qrcode/QrCodeScanModeController.java create mode 100644 packages/SettingsLib/src/com/android/settingslib/qrcode/QrCodeScanModeFragment.java diff --git a/packages/SettingsLib/AndroidManifest.xml b/packages/SettingsLib/AndroidManifest.xml index 13f8a372c9b57..3e67abfe19603 100644 --- a/packages/SettingsLib/AndroidManifest.xml +++ b/packages/SettingsLib/AndroidManifest.xml @@ -22,6 +22,16 @@ + + + + + + + + diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java index 3debd9a179da6..7fbd10025d607 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java @@ -47,7 +47,7 @@ public class CachedBluetoothDeviceManager { CsipDeviceManager mCsipDeviceManager; BluetoothDevice mOngoingSetMemberPair; - CachedBluetoothDeviceManager(Context context, LocalBluetoothManager localBtManager) { + public CachedBluetoothDeviceManager(Context context, LocalBluetoothManager localBtManager) { mContext = context; mBtManager = localBtManager; mHearingAidDeviceManager = new HearingAidDeviceManager(localBtManager, mCachedDevices); diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java index edcb576bacc4f..0c652f62f8e4a 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java @@ -66,10 +66,6 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile { private final ServiceListener mServiceListener = new ServiceListener() { @Override public void onServiceConnected(int profile, BluetoothProfile proxy) { - if (profile != BluetoothProfile.LE_AUDIO_BROADCAST) { - Log.d(TAG, "The profile is not LE_AUDIO_BROADCAST"); - return; - } if (DEBUG) { Log.d(TAG, "Bluetooth service connected"); } diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcastAssistant.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcastAssistant.java index 04c6ee2f92fd9..c248fff09d6f7 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcastAssistant.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcastAssistant.java @@ -65,10 +65,6 @@ public class LocalBluetoothLeBroadcastAssistant implements LocalBluetoothProfile private final ServiceListener mServiceListener = new ServiceListener() { @Override public void onServiceConnected(int profile, BluetoothProfile proxy) { - if (profile != BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT) { - Log.d(TAG, "The profile is not LE_AUDIO_BROADCAST_ASSISTANT"); - return; - } if (DEBUG) { Log.d(TAG, "Bluetooth service connected"); } diff --git a/packages/SettingsLib/src/com/android/settingslib/qrcode/QrCodeScanModeActivity.java b/packages/SettingsLib/src/com/android/settingslib/qrcode/QrCodeScanModeActivity.java new file mode 100644 index 0000000000000..9021fcbc3f992 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/qrcode/QrCodeScanModeActivity.java @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.qrcode; + +import static com.android.settingslib.bluetooth.BluetoothBroadcastUtils.EXTRA_BLUETOOTH_DEVICE_SINK; +import static com.android.settingslib.bluetooth.BluetoothBroadcastUtils.EXTRA_BLUETOOTH_SINK_IS_GROUP; + +import android.bluetooth.BluetoothDevice; +import android.content.Intent; +import android.os.Bundle; +import android.util.Log; + +import androidx.fragment.app.FragmentTransaction; + +import com.android.settingslib.bluetooth.BluetoothBroadcastUtils; +import com.android.settingslib.R; +import com.android.settingslib.bluetooth.BluetoothUtils; + +public class QrCodeScanModeActivity extends QrCodeScanModeBaseActivity { + private static final boolean DEBUG = BluetoothUtils.D; + private static final String TAG = "QrCodeScanModeActivity"; + + private boolean mIsGroupOp; + private BluetoothDevice mSink; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + } + + @Override + protected void handleIntent(Intent intent) { + String action = intent != null ? intent.getAction() : null; + if (DEBUG) { + Log.d(TAG, "handleIntent(), action = " + action); + } + + if (action == null) { + finish(); + return; + } + + switch (action) { + case BluetoothBroadcastUtils.ACTION_BLUETOOTH_LE_AUDIO_QR_CODE_SCANNER: + showQrCodeScannerFragment(intent); + break; + default: + if (DEBUG) { + Log.e(TAG, "Launch with an invalid action"); + } + finish(); + } + } + + protected void showQrCodeScannerFragment(Intent intent) { + if (DEBUG) { + Log.d(TAG, "showQrCodeScannerFragment"); + } + + if (intent != null) { + mSink = intent.getParcelableExtra(EXTRA_BLUETOOTH_DEVICE_SINK); + mIsGroupOp = intent.getBooleanExtra(EXTRA_BLUETOOTH_SINK_IS_GROUP, false); + if (DEBUG) { + Log.d(TAG, "get extra from intent"); + } + } else { + if (DEBUG) { + Log.d(TAG, "intent is null, can not get bluetooth information from intent."); + } + } + + QrCodeScanModeFragment fragment = + (QrCodeScanModeFragment) mFragmentManager.findFragmentByTag( + BluetoothBroadcastUtils.TAG_FRAGMENT_QR_CODE_SCANNER); + + if (fragment == null) { + fragment = new QrCodeScanModeFragment(mIsGroupOp, mSink); + } else { + if (fragment.isVisible()) { + return; + } + + // When the fragment in back stack but not on top of the stack, we can simply pop + // stack because current fragment transactions are arranged in an order + mFragmentManager.popBackStackImmediate(); + return; + } + final FragmentTransaction fragmentTransaction = mFragmentManager.beginTransaction(); + + fragmentTransaction.replace(R.id.fragment_container, fragment, + BluetoothBroadcastUtils.TAG_FRAGMENT_QR_CODE_SCANNER); + fragmentTransaction.commit(); + } +} + diff --git a/packages/SettingsLib/src/com/android/settingslib/qrcode/QrCodeScanModeBaseActivity.java b/packages/SettingsLib/src/com/android/settingslib/qrcode/QrCodeScanModeBaseActivity.java new file mode 100644 index 0000000000000..9aaec41e721a1 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/qrcode/QrCodeScanModeBaseActivity.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.qrcode; + +import android.content.Intent; +import android.os.Bundle; + +import androidx.fragment.app.FragmentManager; + +import com.android.settingslib.core.lifecycle.ObservableActivity; +import com.android.settingslib.R; + +public abstract class QrCodeScanModeBaseActivity extends ObservableActivity { + + protected FragmentManager mFragmentManager; + + protected abstract void handleIntent(Intent intent); + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setTheme(R.style.SudThemeGlifV3_DayNight); + + setContentView(R.layout.qrcode_scan_mode_activity); + mFragmentManager = getSupportFragmentManager(); + + if (savedInstanceState == null) { + handleIntent(getIntent()); + } + } +} diff --git a/packages/SettingsLib/src/com/android/settingslib/qrcode/QrCodeScanModeController.java b/packages/SettingsLib/src/com/android/settingslib/qrcode/QrCodeScanModeController.java new file mode 100644 index 0000000000000..d7640bb1b5649 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/qrcode/QrCodeScanModeController.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.qrcode; + +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothLeBroadcastMetadata; +import android.content.Context; +import android.util.Log; + +import com.android.settingslib.bluetooth.BluetoothUtils; +import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager; +import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant; +import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastMetadata; +import com.android.settingslib.bluetooth.LocalBluetoothManager; +import com.android.settingslib.bluetooth.LocalBluetoothProfileManager; + +public class QrCodeScanModeController { + + private static final boolean DEBUG = BluetoothUtils.D; + private static final String TAG = "QrCodeScanModeController"; + + private LocalBluetoothLeBroadcastMetadata mLocalBroadcastMetadata; + private LocalBluetoothLeBroadcastAssistant mLocalBroadcastAssistant; + private LocalBluetoothManager mLocalBluetoothManager; + private LocalBluetoothProfileManager mProfileManager; + + private LocalBluetoothManager.BluetoothManagerCallback + mOnInitCallback = new LocalBluetoothManager.BluetoothManagerCallback() { + @Override + public void onBluetoothManagerInitialized(Context appContext, + LocalBluetoothManager bluetoothManager) { + BluetoothUtils.setErrorListener(mErrorListener); + } + }; + + private BluetoothUtils.ErrorListener + mErrorListener = new BluetoothUtils.ErrorListener() { + @Override + public void onShowError(Context context, String name, int messageResId) { + if (DEBUG) { + Log.d(TAG, "Get error when initializing BluetoothManager. "); + } + } + }; + + public QrCodeScanModeController(Context context) { + if (DEBUG) { + Log.d(TAG, "QrCodeScanModeController constructor."); + } + mLocalBluetoothManager = LocalBluetoothManager.getInstance(context, mOnInitCallback); + mProfileManager = mLocalBluetoothManager.getProfileManager(); + mLocalBroadcastMetadata = new LocalBluetoothLeBroadcastMetadata(); + CachedBluetoothDeviceManager cachedDeviceManager = new CachedBluetoothDeviceManager(context, + mLocalBluetoothManager); + mLocalBroadcastAssistant = new LocalBluetoothLeBroadcastAssistant(context, + cachedDeviceManager, mProfileManager); + } + + private BluetoothLeBroadcastMetadata convertToBroadcastMetadata(String qrCodeString) { + return mLocalBroadcastMetadata.convertToBroadcastMetadata(qrCodeString); + } + + public void addSource(BluetoothDevice sink, String sourceMetadata, + boolean isGroupOp) { + mLocalBroadcastAssistant.addSource(sink, + convertToBroadcastMetadata(sourceMetadata), isGroupOp); + } +} diff --git a/packages/SettingsLib/src/com/android/settingslib/qrcode/QrCodeScanModeFragment.java b/packages/SettingsLib/src/com/android/settingslib/qrcode/QrCodeScanModeFragment.java new file mode 100644 index 0000000000000..81165aa3330c2 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/qrcode/QrCodeScanModeFragment.java @@ -0,0 +1,235 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.qrcode; + +import android.bluetooth.BluetoothDevice; +import android.content.Context; +import android.graphics.Matrix; +import android.graphics.Outline; +import android.graphics.Rect; +import android.graphics.SurfaceTexture; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.util.Log; +import android.util.Size; +import android.view.LayoutInflater; +import android.view.TextureView; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewOutlineProvider; +import android.view.accessibility.AccessibilityEvent; +import android.widget.TextView; + +import com.android.settingslib.bluetooth.BluetoothBroadcastUtils; +import com.android.settingslib.bluetooth.BluetoothUtils; +import com.android.settingslib.R; +import com.android.settingslib.core.lifecycle.ObservableFragment; + +import androidx.annotation.NonNull; +import androidx.annotation.StringRes; + +public class QrCodeScanModeFragment extends ObservableFragment implements + TextureView.SurfaceTextureListener, + QrCamera.ScannerCallback { + private static final boolean DEBUG = BluetoothUtils.D; + private static final String TAG = "QrCodeScanModeFragment"; + + /** Message sent to hide error message */ + private static final int MESSAGE_HIDE_ERROR_MESSAGE = 1; + /** Message sent to show error message */ + private static final int MESSAGE_SHOW_ERROR_MESSAGE = 2; + /** Message sent to broadcast QR code */ + private static final int MESSAGE_SCAN_BROADCAST_SUCCESS = 3; + + private static final long SHOW_ERROR_MESSAGE_INTERVAL = 10000; + private static final long SHOW_SUCCESS_SQUARE_INTERVAL = 1000; + + private boolean mIsGroupOp; + private int mCornerRadius; + private BluetoothDevice mSink; + private String mBroadcastMetadata; + private Context mContext; + private QrCamera mCamera; + private QrCodeScanModeController mController; + private TextureView mTextureView; + private TextView mSummary; + private TextView mErrorMessage; + + public QrCodeScanModeFragment(boolean isGroupOp, BluetoothDevice sink) { + mIsGroupOp = isGroupOp; + mSink = sink; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + mContext = getContext(); + mController = new QrCodeScanModeController(mContext); + } + + @Override + public final View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + return inflater.inflate(R.layout.qrcode_scanner_fragment, container, + /* attachToRoot */ false); + } + + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + mTextureView = view.findViewById(R.id.preview_view); + mCornerRadius = mContext.getResources().getDimensionPixelSize( + R.dimen.qrcode_preview_radius); + mTextureView.setSurfaceTextureListener(this); + mTextureView.setOutlineProvider(new ViewOutlineProvider() { + @Override + public void getOutline(View view, Outline outline) { + outline.setRoundRect(0,0, view.getWidth(), view.getHeight(), mCornerRadius); + } + }); + mTextureView.setClipToOutline(true); + mErrorMessage = view.findViewById(R.id.error_message); + } + + private void initCamera(SurfaceTexture surface) { + // Check if the camera has already created. + if (mCamera == null) { + mCamera = new QrCamera(mContext, this); + mCamera.start(surface); + } + } + + private void destroyCamera() { + if (mCamera != null) { + mCamera.stop(); + mCamera = null; + } + } + + @Override + public void onSurfaceTextureAvailable(@NonNull SurfaceTexture surface, int width, int height) { + initCamera(surface); + } + + @Override + public void onSurfaceTextureSizeChanged(@NonNull SurfaceTexture surface, int width, + int height) { + } + + @Override + public boolean onSurfaceTextureDestroyed(@NonNull SurfaceTexture surface) { + destroyCamera(); + return true; + } + + @Override + public void onSurfaceTextureUpdated(@NonNull SurfaceTexture surface) { + } + + @Override + public void handleSuccessfulResult(String qrCode) { + if (DEBUG) { + Log.d(TAG, "handleSuccessfulResult(), get the qr code string."); + } + mBroadcastMetadata = qrCode; + handleBtLeAudioScanner(); + } + + @Override + public void handleCameraFailure() { + destroyCamera(); + } + + @Override + public Size getViewSize() { + return new Size(mTextureView.getWidth(), mTextureView.getHeight()); + } + + @Override + public Rect getFramePosition(Size previewSize, int cameraOrientation) { + return new Rect(0, 0, previewSize.getHeight(), previewSize.getHeight()); + } + + @Override + public void setTransform(Matrix transform) { + mTextureView.setTransform(transform); + } + + @Override + public boolean isValid(String qrCode) { + if (qrCode.startsWith(BluetoothBroadcastUtils.SCHEME_BT_BROADCAST_METADATA)) { + return true; + } else { + showErrorMessage(R.string.bt_le_audio_qr_code_is_not_valid_format); + return false; + } + } + + protected boolean isDecodeTaskAlive() { + return mCamera != null && mCamera.isDecodeTaskAlive(); + } + + private final Handler mHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MESSAGE_HIDE_ERROR_MESSAGE: + mErrorMessage.setVisibility(View.INVISIBLE); + break; + + case MESSAGE_SHOW_ERROR_MESSAGE: + final String errorMessage = (String) msg.obj; + + mErrorMessage.setVisibility(View.VISIBLE); + mErrorMessage.setText(errorMessage); + mErrorMessage.sendAccessibilityEvent( + AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); + + // Cancel any pending messages to hide error view and requeue the message so + // user has time to see error + removeMessages(MESSAGE_HIDE_ERROR_MESSAGE); + sendEmptyMessageDelayed(MESSAGE_HIDE_ERROR_MESSAGE, + SHOW_ERROR_MESSAGE_INTERVAL); + break; + + case MESSAGE_SCAN_BROADCAST_SUCCESS: + mController.addSource(mSink, mBroadcastMetadata, mIsGroupOp); + updateSummary(); + mSummary.sendAccessibilityEvent( + AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); + break; + default: + } + } + }; + + private void showErrorMessage(@StringRes int messageResId) { + final Message message = mHandler.obtainMessage(MESSAGE_SHOW_ERROR_MESSAGE, + getString(messageResId)); + message.sendToTarget(); + } + + private void handleBtLeAudioScanner() { + Message message = mHandler.obtainMessage(MESSAGE_SCAN_BROADCAST_SUCCESS); + mHandler.sendMessageDelayed(message, SHOW_SUCCESS_SQUARE_INTERVAL); + } + + private void updateSummary() { + mSummary.setText(getString(R.string.bt_le_audio_scan_qr_code_scanner, + null /* broadcast_name*/));; + } +}