• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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