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.audiostreams; 18 19 import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant.LocalBluetoothLeBroadcastSourceState; 20 import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant.LocalBluetoothLeBroadcastSourceState.PAUSED; 21 import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant.LocalBluetoothLeBroadcastSourceState.STREAMING; 22 import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant.getLocalSourceState; 23 24 import android.bluetooth.BluetoothDevice; 25 import android.bluetooth.BluetoothLeBroadcastAssistant; 26 import android.bluetooth.BluetoothLeBroadcastReceiveState; 27 import android.content.Context; 28 import android.util.Log; 29 30 import androidx.annotation.NonNull; 31 import androidx.annotation.VisibleForTesting; 32 import androidx.lifecycle.DefaultLifecycleObserver; 33 import androidx.lifecycle.LifecycleOwner; 34 import androidx.preference.PreferenceScreen; 35 36 import com.android.settings.R; 37 import com.android.settings.bluetooth.Utils; 38 import com.android.settings.core.BasePreferenceController; 39 import com.android.settings.dashboard.DashboardFragment; 40 import com.android.settings.widget.EntityHeaderController; 41 import com.android.settingslib.bluetooth.BluetoothUtils; 42 import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant; 43 import com.android.settingslib.flags.Flags; 44 import com.android.settingslib.utils.ThreadUtils; 45 import com.android.settingslib.widget.LayoutPreference; 46 47 import java.util.concurrent.Executor; 48 import java.util.concurrent.Executors; 49 50 import javax.annotation.Nullable; 51 52 public class AudioStreamHeaderController extends BasePreferenceController 53 implements DefaultLifecycleObserver { 54 @VisibleForTesting 55 static final int AUDIO_STREAM_HEADER_LISTENING_NOW_SUMMARY = 56 R.string.audio_streams_listening_now; 57 58 static final int AUDIO_STREAM_HEADER_PRESENT_NOW_SUMMARY = R.string.audio_streams_present_now; 59 60 @VisibleForTesting static final String AUDIO_STREAM_HEADER_NOT_LISTENING_SUMMARY = ""; 61 private static final String TAG = "AudioStreamHeaderController"; 62 private static final String KEY = "audio_stream_header"; 63 private final Executor mExecutor; 64 private final AudioStreamsHelper mAudioStreamsHelper; 65 @Nullable private final LocalBluetoothLeBroadcastAssistant mLeBroadcastAssistant; 66 private final boolean mHysteresisModeFixAvailable; 67 68 @VisibleForTesting 69 final BluetoothLeBroadcastAssistant.Callback mBroadcastAssistantCallback = 70 new AudioStreamsBroadcastAssistantCallback() { 71 @Override 72 public void onSourceRemoved(BluetoothDevice sink, int sourceId, int reason) { 73 super.onSourceRemoved(sink, sourceId, reason); 74 updateSummary(); 75 } 76 77 @Override 78 public void onSourceLost(int broadcastId) { 79 super.onSourceLost(broadcastId); 80 updateSummary(); 81 } 82 83 @Override 84 public void onReceiveStateChanged( 85 BluetoothDevice sink, 86 int sourceId, 87 BluetoothLeBroadcastReceiveState state) { 88 super.onReceiveStateChanged(sink, sourceId, state); 89 var localSourceState = getLocalSourceState(state); 90 if (localSourceState == STREAMING) { 91 updateSummary(); 92 if (!Flags.audioStreamMediaServiceByReceiveState()) { 93 mAudioStreamsHelper.startMediaService( 94 mContext, mBroadcastId, mBroadcastName); 95 } 96 } else if (mHysteresisModeFixAvailable && localSourceState == PAUSED) { 97 // if source paused, only update the summary 98 updateSummary(); 99 } 100 } 101 }; 102 103 private @Nullable EntityHeaderController mHeaderController; 104 private @Nullable DashboardFragment mFragment; 105 private String mBroadcastName = ""; 106 private int mBroadcastId = -1; 107 AudioStreamHeaderController(Context context, String preferenceKey)108 public AudioStreamHeaderController(Context context, String preferenceKey) { 109 super(context, preferenceKey); 110 mExecutor = Executors.newSingleThreadExecutor(); 111 mAudioStreamsHelper = new AudioStreamsHelper(Utils.getLocalBtManager(context)); 112 mLeBroadcastAssistant = mAudioStreamsHelper.getLeBroadcastAssistant(); 113 mHysteresisModeFixAvailable = BluetoothUtils.isAudioSharingHysteresisModeFixAvailable( 114 context); 115 } 116 117 @Override onStart(@onNull LifecycleOwner owner)118 public void onStart(@NonNull LifecycleOwner owner) { 119 if (mLeBroadcastAssistant == null) { 120 Log.w(TAG, "onStart(): LeBroadcastAssistant is null!"); 121 return; 122 } 123 mLeBroadcastAssistant.registerServiceCallBack(mExecutor, mBroadcastAssistantCallback); 124 } 125 126 @Override onStop(@onNull LifecycleOwner owner)127 public void onStop(@NonNull LifecycleOwner owner) { 128 if (mLeBroadcastAssistant == null) { 129 Log.w(TAG, "onStop(): LeBroadcastAssistant is null!"); 130 return; 131 } 132 mLeBroadcastAssistant.unregisterServiceCallBack(mBroadcastAssistantCallback); 133 } 134 135 @Override displayPreference(PreferenceScreen screen)136 public final void displayPreference(PreferenceScreen screen) { 137 LayoutPreference headerPreference = screen.findPreference(KEY); 138 if (headerPreference != null && mFragment != null) { 139 mHeaderController = 140 EntityHeaderController.newInstance( 141 mFragment.getActivity(), 142 mFragment, 143 headerPreference.findViewById(com.android.settings.R.id.entity_header)); 144 if (mBroadcastName != null) { 145 mHeaderController.setLabel(mBroadcastName); 146 } 147 mHeaderController.setIcon( 148 screen.getContext() 149 .getDrawable( 150 com.android.settingslib.R.drawable.ic_bt_le_audio_sharing)); 151 screen.addPreference(headerPreference); 152 updateSummary(); 153 } 154 super.displayPreference(screen); 155 } 156 updateSummary()157 private void updateSummary() { 158 var unused = 159 ThreadUtils.postOnBackgroundThread( 160 () -> { 161 var sourceState = mAudioStreamsHelper.getConnectedBroadcastIdAndState( 162 mHysteresisModeFixAvailable).get(mBroadcastId); 163 var latestSummary = getLatestSummary(sourceState); 164 ThreadUtils.postOnMainThread( 165 () -> { 166 if (mHeaderController != null) { 167 mHeaderController.setSummary(latestSummary); 168 mHeaderController.done(true); 169 } 170 }); 171 }); 172 } 173 174 @Override getAvailabilityStatus()175 public int getAvailabilityStatus() { 176 return AVAILABLE; 177 } 178 179 @Override getPreferenceKey()180 public String getPreferenceKey() { 181 return KEY; 182 } 183 184 /** Initialize with {@link AudioStreamDetailsFragment} and broadcast name and id */ init( AudioStreamDetailsFragment audioStreamDetailsFragment, String broadcastName, int broadcastId)185 void init( 186 AudioStreamDetailsFragment audioStreamDetailsFragment, 187 String broadcastName, 188 int broadcastId) { 189 mFragment = audioStreamDetailsFragment; 190 mBroadcastName = broadcastName; 191 mBroadcastId = broadcastId; 192 } 193 getLatestSummary(@ullable LocalBluetoothLeBroadcastSourceState state)194 private String getLatestSummary(@Nullable LocalBluetoothLeBroadcastSourceState state) { 195 if (state == null) { 196 return AUDIO_STREAM_HEADER_NOT_LISTENING_SUMMARY; 197 } 198 if (mHysteresisModeFixAvailable) { 199 return state == STREAMING 200 ? mContext.getString(AUDIO_STREAM_HEADER_LISTENING_NOW_SUMMARY) 201 : mContext.getString(AUDIO_STREAM_HEADER_PRESENT_NOW_SUMMARY); 202 } 203 return mContext.getString(AUDIO_STREAM_HEADER_LISTENING_NOW_SUMMARY); 204 } 205 } 206