From 4b345980c3b2f5f47f3a95c115ad796a7be85fb2 Mon Sep 17 00:00:00 2001 From: Zoey Chen Date: Thu, 12 May 2022 11:27:40 +0800 Subject: [PATCH] [Le Audio] Move BT QR code scanner from Settingslibs to Settings Systemui memory regression, so we have to move the qr code activity back to Settings Bug: 228031398 Test: manual Change-Id: Ic30291e6d752c6c770c40e1329d9f95ceec8cca6 --- AndroidManifest.xml | 10 + res/drawable/ic_qr_code_scanner.xml | 23 ++ res/layout/qrcode_scan_mode_activity.xml | 30 +++ res/layout/qrcode_scanner_fragment.xml | 102 ++++++++ res/values/dimens.xml | 5 + res/values/strings.xml | 7 + res/values/styles.xml | 7 + ...uetoothFindBroadcastsHeaderController.java | 1 - .../bluetooth/QrCodeScanModeActivity.java | 111 ++++++++ .../bluetooth/QrCodeScanModeBaseActivity.java | 46 ++++ .../bluetooth/QrCodeScanModeController.java | 63 +++++ .../bluetooth/QrCodeScanModeFragment.java | 243 ++++++++++++++++++ 12 files changed, 647 insertions(+), 1 deletion(-) create mode 100644 res/drawable/ic_qr_code_scanner.xml create mode 100644 res/layout/qrcode_scan_mode_activity.xml create mode 100644 res/layout/qrcode_scanner_fragment.xml create mode 100644 src/com/android/settings/bluetooth/QrCodeScanModeActivity.java create mode 100644 src/com/android/settings/bluetooth/QrCodeScanModeBaseActivity.java create mode 100644 src/com/android/settings/bluetooth/QrCodeScanModeController.java create mode 100644 src/com/android/settings/bluetooth/QrCodeScanModeFragment.java 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 9ab0cc2d301..bd0be47de5f 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -14163,4 +14163,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; + } +}