1 /* 2 * Copyright (C) 2023 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.settings.connecteddevice.audiosharing; 18 19 import static com.android.settings.connecteddevice.audiosharing.AudioSharingDashboardFragment.SHARE_THEN_PAIR_REQUEST_CODE; 20 import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsQrCodeFragment.getQrCodeDrawable; 21 import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast.EXTRA_PAIR_AND_JOIN_SHARING; 22 23 import android.app.Dialog; 24 import android.app.settings.SettingsEnums; 25 import android.bluetooth.BluetoothLeBroadcastMetadata; 26 import android.graphics.drawable.Drawable; 27 import android.os.Bundle; 28 import android.util.Log; 29 import android.util.Pair; 30 31 import androidx.annotation.NonNull; 32 import androidx.annotation.Nullable; 33 import androidx.annotation.VisibleForTesting; 34 import androidx.appcompat.app.AlertDialog; 35 import androidx.fragment.app.Fragment; 36 import androidx.fragment.app.FragmentManager; 37 import androidx.lifecycle.Lifecycle; 38 39 import com.android.settings.R; 40 import com.android.settings.bluetooth.BluetoothPairingDetail; 41 import com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsQrCodeFragment; 42 import com.android.settings.core.SubSettingLauncher; 43 import com.android.settings.overlay.FeatureFactory; 44 import com.android.settings.core.instrumentation.InstrumentedDialogFragment; 45 import com.android.settingslib.bluetooth.BluetoothLeBroadcastMetadataExt; 46 import com.android.settingslib.bluetooth.BluetoothUtils; 47 48 import com.google.common.collect.Iterables; 49 50 import java.nio.charset.StandardCharsets; 51 import java.util.List; 52 53 public class AudioSharingDialogFragment extends InstrumentedDialogFragment { 54 private static final String TAG = "AudioSharingDialog"; 55 56 private static final String BUNDLE_KEY_DEVICE_ITEMS = "bundle_key_device_items"; 57 private static final String BUNDLE_KEY_BROADCAST_METADATA = "bundle_key_broadcast_metadata"; 58 59 // The host creates an instance of this dialog fragment must implement this interface to receive 60 // event callbacks. 61 public interface DialogEventListener { 62 /** Called when users click the positive button in the dialog. */ onPositiveClick()63 default void onPositiveClick() {} 64 65 /** 66 * Called when users click the device item for sharing in the dialog. 67 * 68 * @param item The device item clicked. 69 */ onItemClick(@onNull AudioSharingDeviceItem item)70 default void onItemClick(@NonNull AudioSharingDeviceItem item) {} 71 72 /** Called when users click the cancel button in the dialog. */ onCancelClick()73 default void onCancelClick() {} 74 } 75 76 @Nullable private static DialogEventListener sListener; 77 private static Pair<Integer, Object>[] sEventData = new Pair[0]; 78 @Nullable private static Fragment sHost; 79 80 AudioSharingFeatureProvider audioSharingFeatureProvider = 81 FeatureFactory.getFeatureFactory().getAudioSharingFeatureProvider(); 82 83 @Override getMetricsCategory()84 public int getMetricsCategory() { 85 return SettingsEnums.DIALOG_AUDIO_SHARING_ADD_DEVICE; 86 } 87 88 /** 89 * Display the {@link AudioSharingDialogFragment} dialog. 90 * 91 * @param host The Fragment this dialog will be hosted. 92 * @param deviceItems The connected device items eligible for audio sharing. 93 * @param metadata The audio sharing metadata, nullable. 94 * @param listener The callback to handle the user action on this dialog. 95 * @param eventData The eventData to log with for dialog onClick events. 96 */ show( @ullable Fragment host, @NonNull List<AudioSharingDeviceItem> deviceItems, @Nullable BluetoothLeBroadcastMetadata metadata, @NonNull DialogEventListener listener, @NonNull Pair<Integer, Object>[] eventData)97 public static void show( 98 @Nullable Fragment host, 99 @NonNull List<AudioSharingDeviceItem> deviceItems, 100 @Nullable BluetoothLeBroadcastMetadata metadata, 101 @NonNull DialogEventListener listener, 102 @NonNull Pair<Integer, Object>[] eventData) { 103 if (host == null) { 104 Log.d(TAG, "Fail to show dialog, host is null"); 105 return; 106 } 107 if (BluetoothUtils.isAudioSharingUIAvailable(host.getContext())) { 108 final FragmentManager manager; 109 try { 110 manager = host.getChildFragmentManager(); 111 } catch (IllegalStateException e) { 112 Log.d(TAG, "Fail to show dialog: " + e.getMessage()); 113 return; 114 } 115 Lifecycle.State currentState = host.getLifecycle().getCurrentState(); 116 if (!currentState.isAtLeast(Lifecycle.State.STARTED)) { 117 Log.d(TAG, "Fail to show dialog with state: " + currentState); 118 return; 119 } 120 sHost = host; 121 sListener = listener; 122 sEventData = eventData; 123 AlertDialog dialog = AudioSharingDialogHelper.getDialogIfShowing(manager, TAG); 124 if (dialog != null) { 125 Log.d(TAG, "Dialog is showing, return."); 126 return; 127 } 128 Log.d(TAG, "Show up the dialog."); 129 final Bundle bundle = new Bundle(); 130 bundle.putParcelableList(BUNDLE_KEY_DEVICE_ITEMS, deviceItems); 131 if (metadata != null) { 132 bundle.putParcelable(BUNDLE_KEY_BROADCAST_METADATA, metadata); 133 } 134 AudioSharingDialogFragment dialogFrag = new AudioSharingDialogFragment(); 135 dialogFrag.setArguments(bundle); 136 dialogFrag.show(manager, TAG); 137 } 138 } 139 140 /** Return the tag of {@link AudioSharingDialogFragment} dialog. */ tag()141 public static @NonNull String tag() { 142 return TAG; 143 } 144 145 /** Test only: get the event data passed to the dialog. */ 146 @VisibleForTesting 147 @NonNull getEventData()148 Pair<Integer, Object>[] getEventData() { 149 return sEventData; 150 } 151 152 @Override 153 @NonNull onCreateDialog(Bundle savedInstanceState)154 public Dialog onCreateDialog(Bundle savedInstanceState) { 155 Bundle arguments = requireArguments(); 156 List<AudioSharingDeviceItem> deviceItems = 157 arguments.getParcelable(BUNDLE_KEY_DEVICE_ITEMS, List.class); 158 AudioSharingDialogFactory.DialogBuilder builder = 159 AudioSharingDialogFactory.newBuilder(getActivity()) 160 .setTitleIcon(com.android.settingslib.R.drawable.ic_bt_le_audio_sharing) 161 .setIsCustomBodyEnabled(true); 162 if (deviceItems == null) { 163 Log.d(TAG, "Create dialog error: null deviceItems"); 164 return builder.build(); 165 } 166 BluetoothLeBroadcastMetadata metadata = arguments.getParcelable( 167 BUNDLE_KEY_BROADCAST_METADATA, BluetoothLeBroadcastMetadata.class); 168 Drawable qrCodeDrawable = null; 169 if (deviceItems.isEmpty()) { 170 builder.setTitle(R.string.audio_sharing_share_dialog_title) 171 .setCustomPositiveButton( 172 R.string.audio_sharing_pair_button_label, 173 v -> { 174 if (sListener != null) { 175 sListener.onPositiveClick(); 176 } 177 logDialogPositiveBtnClick(); 178 dismiss(); 179 Bundle args = new Bundle(); 180 args.putBoolean(EXTRA_PAIR_AND_JOIN_SHARING, true); 181 SubSettingLauncher launcher = 182 new SubSettingLauncher(getContext()) 183 .setDestination( 184 BluetoothPairingDetail.class.getName()) 185 .setSourceMetricsCategory(getMetricsCategory()) 186 .setArguments(args); 187 if (sHost != null) { 188 launcher.setResultListener(sHost, SHARE_THEN_PAIR_REQUEST_CODE); 189 } 190 launcher.launch(); 191 }); 192 qrCodeDrawable = metadata == null ? null : getQrCodeDrawable(metadata, 193 getContext()).orElse(null); 194 if (qrCodeDrawable != null) { 195 String broadcastName = 196 metadata.getBroadcastName() == null ? "" : metadata.getBroadcastName(); 197 boolean hasPassword = metadata.getBroadcastCode() != null 198 && metadata.getBroadcastCode().length > 0; 199 String message = hasPassword ? getString( 200 R.string.audio_sharing_dialog_qr_code_content, broadcastName, 201 new String(metadata.getBroadcastCode(), StandardCharsets.UTF_8)) : 202 getString(R.string.audio_sharing_dialog_qr_code_content_no_password, 203 broadcastName); 204 builder.setCustomMessage(message) 205 .setCustomMessage2(R.string.audio_sharing_dialog_pair_new_device_content) 206 .setCustomNegativeButton(R.string.audio_streams_dialog_close, 207 v -> onCancelClick()); 208 } else { 209 builder.setCustomImage(R.drawable.audio_sharing_guidance) 210 .setCustomMessage(R.string.audio_sharing_dialog_connect_device_content) 211 .setCustomNegativeButton( 212 R.string.audio_sharing_qrcode_button_label, 213 v -> { 214 onCancelClick(); 215 new SubSettingLauncher(getContext()) 216 .setTitleRes(R.string.audio_streams_qr_code_page_title) 217 .setDestination( 218 AudioStreamsQrCodeFragment.class.getName()) 219 .setSourceMetricsCategory(getMetricsCategory()) 220 .launch(); 221 }); 222 } 223 } else if (deviceItems.size() == 1) { 224 AudioSharingDeviceItem deviceItem = Iterables.getOnlyElement(deviceItems); 225 builder.setTitle( 226 getString( 227 R.string.audio_sharing_share_with_dialog_title, 228 deviceItem.getName())) 229 .setCustomMessage(R.string.audio_sharing_dialog_share_content) 230 .setCustomPositiveButton( 231 R.string.audio_sharing_share_button_label, 232 v -> { 233 if (sListener != null) { 234 sListener.onItemClick(deviceItem); 235 } 236 logDialogPositiveBtnClick(); 237 dismiss(); 238 }) 239 .setCustomNegativeButton( 240 R.string.audio_sharing_no_thanks_button_label, v -> onCancelClick()); 241 } else { 242 builder.setTitle(R.string.audio_sharing_share_with_more_dialog_title) 243 .setCustomMessage(R.string.audio_sharing_dialog_share_more_content) 244 .setCustomDeviceActions( 245 new AudioSharingDeviceAdapter( 246 getContext(), 247 deviceItems, 248 (AudioSharingDeviceItem item) -> { 249 if (sListener != null) { 250 sListener.onItemClick(item); 251 } 252 logDialogPositiveBtnClick(); 253 dismiss(); 254 }, 255 AudioSharingDeviceAdapter.ActionType.SHARE)) 256 .setCustomNegativeButton( 257 com.android.settings.R.string.cancel, v -> onCancelClick()); 258 } 259 Dialog dialog = builder.build(); 260 dialog.show(); 261 if (deviceItems.isEmpty() && qrCodeDrawable != null) { 262 audioSharingFeatureProvider.setQrCode( 263 this, 264 dialog.getWindow().getDecorView(), 265 R.id.description_image, 266 qrCodeDrawable, 267 BluetoothLeBroadcastMetadataExt.INSTANCE.toQrCodeString(metadata)); 268 } 269 return dialog; 270 } 271 onCancelClick()272 private void onCancelClick() { 273 if (sListener != null) { 274 sListener.onCancelClick(); 275 } 276 logDialogNegativeBtnClick(); 277 dismiss(); 278 } 279 logDialogPositiveBtnClick()280 private void logDialogPositiveBtnClick() { 281 mMetricsFeatureProvider.action( 282 getContext(), 283 SettingsEnums.ACTION_AUDIO_SHARING_DIALOG_POSITIVE_BTN_CLICKED, 284 sEventData); 285 } 286 logDialogNegativeBtnClick()287 private void logDialogNegativeBtnClick() { 288 mMetricsFeatureProvider.action( 289 getContext(), 290 SettingsEnums.ACTION_AUDIO_SHARING_DIALOG_NEGATIVE_BTN_CLICKED, 291 sEventData); 292 } 293 } 294