From 74b9b9dbb42b020507df95b7f5965341b0ae9159 Mon Sep 17 00:00:00 2001 From: chelseahao Date: Tue, 14 Nov 2023 16:51:50 +0800 Subject: [PATCH] [Audiosharing] Branch existing LE QrCode scanner. Bug: 308368124 Test: Manual Change-Id: I8d0d8150baedfee7d74f60a3c18ecbc93271cada --- AndroidManifest.xml | 10 + .../qrcode/QrCodeScanModeActivity.java | 122 ++++++++ .../qrcode/QrCodeScanModeBaseActivity.java | 64 +++++ .../qrcode/QrCodeScanModeFragment.java | 268 ++++++++++++++++++ 4 files changed, 464 insertions(+) create mode 100644 src/com/android/settings/connecteddevice/audiosharing/audiostreams/qrcode/QrCodeScanModeActivity.java create mode 100644 src/com/android/settings/connecteddevice/audiosharing/audiostreams/qrcode/QrCodeScanModeBaseActivity.java create mode 100644 src/com/android/settings/connecteddevice/audiosharing/audiostreams/qrcode/QrCodeScanModeFragment.java diff --git a/AndroidManifest.xml b/AndroidManifest.xml index a08bda3c76c..75c6fbb3813 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -4942,6 +4942,16 @@ + + + + + + + To use intent action {@link + * BluetoothBroadcastUtils#ACTION_BLUETOOTH_LE_AUDIO_QR_CODE_SCANNER}, specify the bluetooth device + * sink of the broadcast to be provisioned in {@link + * BluetoothBroadcastUtils#EXTRA_BLUETOOTH_DEVICE_SINK} and check the operation for all coordinated + * set members throughout one session or not by {@link + * BluetoothBroadcastUtils#EXTRA_BLUETOOTH_SINK_IS_GROUP}. + */ +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(); + } 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/connecteddevice/audiosharing/audiostreams/qrcode/QrCodeScanModeBaseActivity.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/qrcode/QrCodeScanModeBaseActivity.java new file mode 100644 index 00000000000..637014a9021 --- /dev/null +++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/qrcode/QrCodeScanModeBaseActivity.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2023 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.connecteddevice.audiosharing.audiostreams.qrcode; + +import android.content.Intent; +import android.os.Bundle; +import android.os.SystemProperties; + +import androidx.fragment.app.FragmentManager; + +import com.android.settings.R; +import com.android.settingslib.core.lifecycle.ObservableActivity; + +import com.google.android.setupdesign.util.ThemeHelper; +import com.google.android.setupdesign.util.ThemeResolver; + +public abstract class QrCodeScanModeBaseActivity extends ObservableActivity { + + private static final String THEME_KEY = "setupwizard.theme"; + private static final String THEME_DEFAULT_VALUE = "SudThemeGlifV3_DayNight"; + protected FragmentManager mFragmentManager; + + protected abstract void handleIntent(Intent intent); + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + int defaultTheme = + ThemeHelper.isSetupWizardDayNightEnabled(this) + ? com.google.android.setupdesign.R.style.SudThemeGlifV3_DayNight + : com.google.android.setupdesign.R.style.SudThemeGlifV3_Light; + ThemeResolver themeResolver = + new ThemeResolver.Builder(ThemeResolver.getDefault()) + .setDefaultTheme(defaultTheme) + .setUseDayNight(true) + .build(); + setTheme( + themeResolver.resolve( + SystemProperties.get(THEME_KEY, THEME_DEFAULT_VALUE), + /* suppressDayNight= */ !ThemeHelper.isSetupWizardDayNightEnabled(this))); + + setContentView(R.layout.qrcode_scan_mode_activity); + mFragmentManager = getSupportFragmentManager(); + + if (savedInstanceState == null) { + handleIntent(getIntent()); + } + } +} diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/qrcode/QrCodeScanModeFragment.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/qrcode/QrCodeScanModeFragment.java new file mode 100644 index 00000000000..2b52039768e --- /dev/null +++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/qrcode/QrCodeScanModeFragment.java @@ -0,0 +1,268 @@ +/* + * Copyright (C) 2023 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.connecteddevice.audiosharing.audiostreams.qrcode; + +import android.app.Activity; +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.content.Intent; +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.os.VibrationEffect; +import android.os.Vibrator; +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 androidx.annotation.NonNull; +import androidx.annotation.StringRes; + +import com.android.settings.R; +import com.android.settings.core.InstrumentedFragment; +import com.android.settingslib.bluetooth.BluetoothBroadcastUtils; +import com.android.settingslib.bluetooth.BluetoothUtils; +import com.android.settingslib.qrcode.QrCamera; + +import java.time.Duration; + +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 static final Duration VIBRATE_DURATION_QR_CODE_RECOGNITION = Duration.ofMillis(3); + + public static final String KEY_BROADCAST_METADATA = "key_broadcast_metadata"; + + private int mCornerRadius; + private String mBroadcastMetadata; + private Context mContext; + private QrCamera mCamera; + private TextureView mTextureView; + private TextView mSummary; + private TextView mErrorMessage; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + mContext = getContext(); + } + + @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: + Log.d(TAG, "scan success"); + final Intent resultIntent = new Intent(); + resultIntent.putExtra(KEY_BROADCAST_METADATA, mBroadcastMetadata); + getActivity().setResult(Activity.RESULT_OK, resultIntent); + notifyUserForQrCodeRecognition(); + break; + default: + } + } + }; + + private void notifyUserForQrCodeRecognition() { + if (mCamera != null) { + mCamera.stop(); + } + + mErrorMessage.setVisibility(View.INVISIBLE); + + triggerVibrationForQrCodeRecognition(getContext()); + + getActivity().finish(); + } + + private static void triggerVibrationForQrCodeRecognition(Context context) { + Vibrator vibrator = context.getSystemService(Vibrator.class); + if (vibrator == null) { + return; + } + vibrator.vibrate( + VibrationEffect.createOneShot( + VIBRATE_DURATION_QR_CODE_RECOGNITION.toMillis(), + VibrationEffect.DEFAULT_AMPLITUDE)); + } + + 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)); + } + + @Override + public int getMetricsCategory() { + return SettingsEnums.LE_AUDIO_BROADCAST_SCAN_QR_CODE; + } +}