diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index ae87edb5628..71c07a05148 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -4474,6 +4474,16 @@
+
+
+
+
+
+
+
diff --git a/res/drawable/ic_qr_code_scanner.xml b/res/drawable/ic_qr_code_scanner.xml
new file mode 100644
index 00000000000..f6f63c5ae7f
--- /dev/null
+++ b/res/drawable/ic_qr_code_scanner.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/res/layout/qrcode_scan_mode_activity.xml b/res/layout/qrcode_scan_mode_activity.xml
new file mode 100644
index 00000000000..f0a182b3d67
--- /dev/null
+++ b/res/layout/qrcode_scan_mode_activity.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
diff --git a/res/layout/qrcode_scanner_fragment.xml b/res/layout/qrcode_scanner_fragment.xml
new file mode 100644
index 00000000000..2c543f23abf
--- /dev/null
+++ b/res/layout/qrcode_scanner_fragment.xml
@@ -0,0 +1,102 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 1582d217d1c..3d616a83f74 100755
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -485,4 +485,9 @@
24dp
8dp
+
+
+ 40dp
+ 30dp
+ 27dp
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 5d61307713a..1d685ef4366 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -14171,4 +14171,11 @@
Can\u2019t connect. Try again.
Wrong password
+
+
+
+ To start listening, center the QR code below
+
+ QR code isn\u0027t a valid format
+
diff --git a/res/values/styles.xml b/res/values/styles.xml
index 7a879931a00..f147ce98903 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -962,4 +962,11 @@
- 0dp
- false
+
+
diff --git a/src/com/android/settings/bluetooth/BluetoothFindBroadcastsHeaderController.java b/src/com/android/settings/bluetooth/BluetoothFindBroadcastsHeaderController.java
index 1527f2145fe..6aaa1b8b65e 100644
--- a/src/com/android/settings/bluetooth/BluetoothFindBroadcastsHeaderController.java
+++ b/src/com/android/settings/bluetooth/BluetoothFindBroadcastsHeaderController.java
@@ -33,7 +33,6 @@ import com.android.settingslib.bluetooth.CachedBluetoothDevice;
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant;
import com.android.settingslib.bluetooth.LocalBluetoothManager;
import com.android.settingslib.core.lifecycle.Lifecycle;
-import com.android.settingslib.qrcode.QrCodeScanModeActivity;
import com.android.settingslib.widget.LayoutPreference;
/**
diff --git a/src/com/android/settings/bluetooth/QrCodeScanModeActivity.java b/src/com/android/settings/bluetooth/QrCodeScanModeActivity.java
new file mode 100644
index 00000000000..5c5b61f091c
--- /dev/null
+++ b/src/com/android/settings/bluetooth/QrCodeScanModeActivity.java
@@ -0,0 +1,111 @@
+/**
+ * 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.settings.bluetooth;
+
+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.R;
+import com.android.settingslib.bluetooth.BluetoothBroadcastUtils;
+import com.android.settingslib.bluetooth.BluetoothUtils;
+
+//TODO (b/232365943): Add test case for tthe QrCode UI.
+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 (intent == null) {
+ if (DEBUG) {
+ Log.d(TAG, "intent is null, can not get bluetooth information from intent.");
+ }
+ return;
+ }
+
+ if (DEBUG) {
+ Log.d(TAG, "showQrCodeScannerFragment");
+ }
+
+ 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");
+ }
+
+ 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/src/com/android/settings/bluetooth/QrCodeScanModeBaseActivity.java b/src/com/android/settings/bluetooth/QrCodeScanModeBaseActivity.java
new file mode 100644
index 00000000000..af8a6e9d97f
--- /dev/null
+++ b/src/com/android/settings/bluetooth/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.settings.bluetooth;
+
+import android.content.Intent;
+import android.os.Bundle;
+
+import androidx.fragment.app.FragmentManager;
+
+import com.android.settingslib.R;
+import com.android.settingslib.core.lifecycle.ObservableActivity;
+
+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/src/com/android/settings/bluetooth/QrCodeScanModeController.java b/src/com/android/settings/bluetooth/QrCodeScanModeController.java
new file mode 100644
index 00000000000..4504b4b71aa
--- /dev/null
+++ b/src/com/android/settings/bluetooth/QrCodeScanModeController.java
@@ -0,0 +1,63 @@
+/**
+ * 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.settings.bluetooth;
+
+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;
+
+ public QrCodeScanModeController(Context context) {
+ if (DEBUG) {
+ Log.d(TAG, "QrCodeScanModeController constructor.");
+ }
+ mLocalBluetoothManager = Utils.getLocalBtManager(context);
+ 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/src/com/android/settings/bluetooth/QrCodeScanModeFragment.java b/src/com/android/settings/bluetooth/QrCodeScanModeFragment.java
new file mode 100644
index 00000000000..dcf89ca9d0e
--- /dev/null
+++ b/src/com/android/settings/bluetooth/QrCodeScanModeFragment.java
@@ -0,0 +1,243 @@
+/**
+ * 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.settings.bluetooth;
+
+import android.app.settings.SettingsEnums;
+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.settings.core.InstrumentedFragment;
+import com.android.settingslib.R;
+import com.android.settingslib.bluetooth.BluetoothBroadcastUtils;
+import com.android.settingslib.bluetooth.BluetoothUtils;
+import com.android.settingslib.core.lifecycle.ObservableFragment;
+import com.android.settingslib.qrcode.QrCamera;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.StringRes;
+
+public class QrCodeScanModeFragment extends InstrumentedFragment 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*/));;
+ }
+
+ @Override
+ public int getMetricsCategory() {
+ return SettingsEnums.LE_AUDIO_BROADCAST_SCAN_QR_CODE;
+ }
+}