diff --git a/packages/SystemUI/res/drawable/ic_broadcast_code_eye.xml b/packages/SystemUI/res/drawable/ic_broadcast_code_eye.xml new file mode 100644 index 0000000000000..dc50a5dcab074 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_broadcast_code_eye.xml @@ -0,0 +1,26 @@ + + + + + diff --git a/packages/SystemUI/res/layout/media_output_broadcast_area.xml b/packages/SystemUI/res/layout/media_output_broadcast_area.xml new file mode 100644 index 0000000000000..4e33e123f12ad --- /dev/null +++ b/packages/SystemUI/res/layout/media_output_broadcast_area.xml @@ -0,0 +1,140 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/SystemUI/res/layout/media_output_broadcast_update_dialog.xml b/packages/SystemUI/res/layout/media_output_broadcast_update_dialog.xml new file mode 100644 index 0000000000000..8b7a019b791b4 --- /dev/null +++ b/packages/SystemUI/res/layout/media_output_broadcast_update_dialog.xml @@ -0,0 +1,29 @@ + + + + + \ No newline at end of file diff --git a/packages/SystemUI/res/layout/media_output_dialog.xml b/packages/SystemUI/res/layout/media_output_dialog.xml index 05343e75b448f..836f59dc8fb21 100644 --- a/packages/SystemUI/res/layout/media_output_dialog.xml +++ b/packages/SystemUI/res/layout/media_output_dialog.xml @@ -71,6 +71,13 @@ + + + 0dp 0dp + + + 216dp + 21dp + 256dp + 56dp + 43dp + 31dp + 24dp + 20dp + 18dp diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index d3b76d90d09f9..21ba6c6e7b158 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -2202,6 +2202,21 @@ Stop casting + + + How broadcasting works + + Broadcast + + People near you with compatible Bluetooth devices can listen to the media you\'re broadcasting + + To listen to your broadcast, people nearby with compatible Bluetooth devices can scan your QR code or use your broadcast name and password + + Broadcast Name + + Password + + Save Build number diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml index a61eda8f0e091..1312654fae98d 100644 --- a/packages/SystemUI/res/values/styles.xml +++ b/packages/SystemUI/res/values/styles.xml @@ -1064,4 +1064,66 @@ ?android:attr/textAppearanceSmall locale + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialog.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialog.java new file mode 100644 index 0000000000000..c277c334d9c4e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialog.java @@ -0,0 +1,249 @@ +/* + * 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.systemui.media.dialog; + +import android.app.AlertDialog; +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.text.method.HideReturnsTransformationMethod; +import android.text.method.PasswordTransformationMethod; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewStub; +import android.view.WindowManager; +import android.widget.Button; +import android.widget.EditText; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.core.graphics.drawable.IconCompat; + +import com.android.settingslib.qrcode.QrCodeGenerator; +import com.android.systemui.R; +import com.android.systemui.broadcast.BroadcastSender; +import com.android.systemui.dagger.SysUISingleton; +import com.android.systemui.statusbar.phone.SystemUIDialog; + +import com.google.zxing.WriterException; + +/** + * Dialog for media output broadcast. + */ +@SysUISingleton +public class MediaOutputBroadcastDialog extends MediaOutputBaseDialog { + private static final String TAG = "BroadcastDialog"; + + private ViewStub mBroadcastInfoArea; + private ImageView mBroadcastQrCodeView; + private ImageView mBroadcastNotify; + private TextView mBroadcastName; + private ImageView mBroadcastNameEdit; + private TextView mBroadcastCode; + private ImageView mBroadcastCodeEye; + private Boolean mIsPasswordHide = true; + private ImageView mBroadcastCodeEdit; + private Button mStopButton; + + static final int METADATA_BROADCAST_NAME = 0; + static final int METADATA_BROADCAST_CODE = 1; + + MediaOutputBroadcastDialog(Context context, boolean aboveStatusbar, + BroadcastSender broadcastSender, MediaOutputController mediaOutputController) { + super(context, broadcastSender, mediaOutputController); + mAdapter = new MediaOutputGroupAdapter(mMediaOutputController); + // TODO(b/226710953): Move the part to MediaOutputBaseDialog for every class + // that extends MediaOutputBaseDialog + if (!aboveStatusbar) { + getWindow().setType(WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY); + } + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + initBtQrCodeUI(); + } + + @Override + int getHeaderIconRes() { + return 0; + } + + @Override + IconCompat getHeaderIcon() { + return mMediaOutputController.getHeaderIcon(); + } + + @Override + int getHeaderIconSize() { + return mContext.getResources().getDimensionPixelSize( + R.dimen.media_output_dialog_header_album_icon_size); + } + + @Override + CharSequence getHeaderText() { + return mMediaOutputController.getHeaderTitle(); + } + + @Override + CharSequence getHeaderSubtitle() { + return mMediaOutputController.getHeaderSubTitle(); + } + + @Override + Drawable getAppSourceIcon() { + return mMediaOutputController.getAppSourceIcon(); + } + + @Override + int getStopButtonVisibility() { + return View.VISIBLE; + } + + // TODO(b/222674827): To get the information from BluetoothLeBroadcastMetadata(Broadcast code) + // and BluetoothLeAudioContentMetadata(Program info) when start Broadcast is successful. + private String getBroadcastMetaDataInfo(int metaData) { + switch (metaData) { + case METADATA_BROADCAST_NAME: + return ""; + case METADATA_BROADCAST_CODE: + return ""; + default: + return ""; + } + } + + private void initBtQrCodeUI() { + //add the view to xml + inflateBroadcastInfoArea(); + + //init UI component + mBroadcastQrCodeView = getDialogView().requireViewById(R.id.qrcode_view); + //Set the QR code view + setQrCodeView(); + + mBroadcastNotify = getDialogView().requireViewById(R.id.broadcast_info); + mBroadcastNotify.setOnClickListener(v -> { + mMediaOutputController.launchLeBroadcastNotifyDialog(null, null, + MediaOutputController.BroadcastNotifyDialog.ACTION_BROADCAST_INFO_ICON); + }); + mBroadcastName = getDialogView().requireViewById(R.id.broadcast_name_summary); + mBroadcastNameEdit = getDialogView().requireViewById(R.id.broadcast_name_edit); + mBroadcastNameEdit.setOnClickListener(v -> { + launchBroadcastUpdatedDialog(false, mBroadcastName.getText().toString()); + }); + mBroadcastCode = getDialogView().requireViewById(R.id.broadcast_code_summary); + mBroadcastCode.setTransformationMethod(PasswordTransformationMethod.getInstance()); + mBroadcastCodeEye = getDialogView().requireViewById(R.id.broadcast_code_eye); + mBroadcastCodeEye.setOnClickListener(v -> { + updateBroadcastCodeVisibility(); + }); + mBroadcastCodeEdit = getDialogView().requireViewById(R.id.broadcast_code_edit); + mBroadcastCodeEdit.setOnClickListener(v -> { + launchBroadcastUpdatedDialog(true, mBroadcastCode.getText().toString()); + }); + + mBroadcastName.setText(getBroadcastMetaDataInfo(METADATA_BROADCAST_NAME)); + mBroadcastCode.setText(getBroadcastMetaDataInfo(METADATA_BROADCAST_CODE)); + + mStopButton = getDialogView().requireViewById(R.id.stop); + mStopButton.setOnClickListener(v -> { + stopBroadcast(); + }); + } + + private void inflateBroadcastInfoArea() { + mBroadcastInfoArea = getDialogView().requireViewById(R.id.broadcast_qrcode); + mBroadcastInfoArea.inflate(); + } + + private void setQrCodeView() { + //get the MetaData, and convert to BT QR code format. + String broadcastMetaData = getBroadcastMetaData(); + if (broadcastMetaData.isEmpty()) { + //TDOD(b/226708424) Error handling for unable to generate the QR code bitmap + return; + } + try { + final int qrcodeSize = getContext().getResources().getDimensionPixelSize( + R.dimen.media_output_qrcode_size); + final Bitmap bmp = QrCodeGenerator.encodeQrCode(broadcastMetaData, qrcodeSize); + mBroadcastQrCodeView.setImageBitmap(bmp); + } catch (WriterException e) { + //TDOD(b/226708424) Error handling for unable to generate the QR code bitmap + Log.e(TAG, "Error generatirng QR code bitmap " + e); + } + } + + private void updateBroadcastCodeVisibility() { + mBroadcastCode.setTransformationMethod( + mIsPasswordHide ? HideReturnsTransformationMethod.getInstance() + : PasswordTransformationMethod.getInstance()); + mIsPasswordHide = !mIsPasswordHide; + } + + private void launchBroadcastUpdatedDialog(boolean isPassword, String editString) { + final View layout = LayoutInflater.from(mContext).inflate( + R.layout.media_output_broadcast_update_dialog, null); + final EditText editText = layout.requireViewById(R.id.broadcast_edit_text); + editText.setText(editString); + final AlertDialog alertDialog = new Builder(mContext) + .setTitle(isPassword ? R.string.media_output_broadcast_code + : R.string.media_output_broadcast_name) + .setView(layout) + .setNegativeButton(android.R.string.cancel, null) + .setPositiveButton(R.string.media_output_broadcast_dialog_save, + (d, w) -> { + updateBroadcast(isPassword, editText.getText().toString()); + }) + .create(); + + alertDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG); + SystemUIDialog.setShowForAllUsers(alertDialog, true); + SystemUIDialog.registerDismissListener(alertDialog); + alertDialog.show(); + } + + /** + * TODO(b/222674827): The method should be get the BluetoothLeBroadcastMetadata after + * starting the Broadcast session successfully. Then we will follow the BT QR code format + * that convert BluetoothLeBroadcastMetadata object to String format. + */ + private String getBroadcastMetaData() { + return ""; + } + + /** + * TODO(b/222676140): These method are about the LE Audio Broadcast API. The framework APIS + * will be wrapped in SettingsLib. And the UI will be executed through it. + */ + private void updateBroadcast(boolean isPassword, String updatedString) { + + } + + /** + * TODO(b/222676140): These method are about the LE Audio Broadcast API. The framework APIS + * will be wrapped in SettingsLib. And the UI will be executed through it. + */ + private void stopBroadcast() { + dismiss(); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java index 1cc96c0fd0de9..97e79e9cab1b5 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java @@ -18,6 +18,7 @@ package com.android.systemui.media.dialog; import static android.provider.Settings.ACTION_BLUETOOTH_PAIRING_SETTINGS; +import android.app.AlertDialog; import android.app.Notification; import android.app.WallpaperColors; import android.content.Context; @@ -46,6 +47,7 @@ import android.provider.Settings; import android.text.TextUtils; import android.util.Log; import android.view.View; +import android.view.WindowManager; import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; @@ -64,11 +66,13 @@ import com.android.settingslib.utils.ThreadUtils; import com.android.systemui.R; import com.android.systemui.animation.ActivityLaunchAnimator; import com.android.systemui.animation.DialogLaunchAnimator; +import com.android.systemui.broadcast.BroadcastSender; import com.android.systemui.media.nearby.NearbyMediaDevicesManager; import com.android.systemui.monet.ColorScheme; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection; +import com.android.systemui.statusbar.phone.SystemUIDialog; import java.util.ArrayList; import java.util.Collection; @@ -93,6 +97,7 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback, private final String mPackageName; private final Context mContext; private final MediaSessionManager mMediaSessionManager; + private final LocalBluetoothManager mLocalBluetoothManager; private final ActivityStarter mActivityStarter; private final DialogLaunchAnimator mDialogLaunchAnimator; private final List mGroupMediaDevices = new CopyOnWriteArrayList<>(); @@ -117,6 +122,11 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback, private int mColorConnectedItemBackground; private int mColorPositiveButtonText; + public enum BroadcastNotifyDialog { + ACTION_FIRST_LAUNCH, + ACTION_BROADCAST_INFO_ICON + } + @Inject public MediaOutputController(@NonNull Context context, String packageName, MediaSessionManager mediaSessionManager, LocalBluetoothManager @@ -127,6 +137,7 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback, mContext = context; mPackageName = packageName; mMediaSessionManager = mediaSessionManager; + mLocalBluetoothManager = lbm; mActivityStarter = starter; mNotifCollection = notifCollection; InfoMediaManager imm = new InfoMediaManager(mContext, packageName, null, lbm); @@ -628,6 +639,42 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback, mActivityStarter.startActivity(launchIntent, true, controller); } + void launchLeBroadcastNotifyDialog(View mediaOutputDialog, BroadcastSender broadcastSender, + BroadcastNotifyDialog action) { + final AlertDialog.Builder builder = new AlertDialog.Builder(mContext); + switch (action) { + case ACTION_FIRST_LAUNCH: + builder.setTitle(R.string.media_output_first_broadcast_title); + builder.setMessage(R.string.media_output_first_notify_broadcast_message); + builder.setNegativeButton(android.R.string.cancel, null); + builder.setPositiveButton(R.string.media_output_broadcast, + (d, w) -> { + launchMediaOutputBroadcastDialog(mediaOutputDialog, broadcastSender); + }); + break; + case ACTION_BROADCAST_INFO_ICON: + builder.setTitle(R.string.media_output_broadcast); + builder.setMessage(R.string.media_output_broadcasting_message); + builder.setPositiveButton(android.R.string.ok, null); + break; + } + + final AlertDialog dialog = builder.create(); + dialog.getWindow().setType(WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG); + SystemUIDialog.setShowForAllUsers(dialog, true); + SystemUIDialog.registerDismissListener(dialog); + dialog.show(); + } + + void launchMediaOutputBroadcastDialog(View mediaOutputDialog, BroadcastSender broadcastSender) { + MediaOutputController controller = new MediaOutputController(mContext, mPackageName, + mMediaSessionManager, mLocalBluetoothManager, mActivityStarter, + mNotifCollection, mDialogLaunchAnimator, Optional.of(mNearbyMediaDevicesManager)); + MediaOutputBroadcastDialog dialog = new MediaOutputBroadcastDialog(mContext, true, + broadcastSender, controller); + mDialogLaunchAnimator.showFromView(dialog, mediaOutputDialog); + } + boolean isActiveRemoteDevice(@NonNull MediaDevice device) { final List features = device.getFeatures(); return (features.contains(MediaRoute2Info.FEATURE_REMOTE_PLAYBACK)