[Audiosharing] Branch existing LE QrCode scanner.
Bug: 308368124 Test: Manual Change-Id: I8d0d8150baedfee7d74f60a3c18ecbc93271cada
This commit is contained in:
@@ -4942,6 +4942,16 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name="com.android.settings.connecteddevice.audiosharing.audiostreams.qrcode.QrCodeScanModeActivity"
|
||||
android:permission="android.permission.BLUETOOTH_CONNECT"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="android.settings.BLUETOOTH_LE_AUDIO_QR_CODE_SCANNER"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".spa.SpaActivity"
|
||||
android:configChanges="orientation|screenLayout|screenSize|smallestScreenSize"
|
||||
|
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
* 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 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.settings.R;
|
||||
import com.android.settingslib.bluetooth.BluetoothBroadcastUtils;
|
||||
import com.android.settingslib.bluetooth.BluetoothUtils;
|
||||
|
||||
/**
|
||||
* Finding a broadcast through QR code.
|
||||
*
|
||||
* <p>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();
|
||||
}
|
||||
}
|
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user