1 /* 2 * Copyright (C) 2020 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.google.android.car.kitchensink.volume; 18 19 import static android.car.media.CarAudioManager.AUDIO_FEATURE_VOLUME_GROUP_MUTING; 20 import static android.media.AudioManager.FLAG_PLAY_SOUND; 21 22 import android.car.feature.Flags; 23 import android.car.media.CarAudioManager; 24 import android.car.media.CarVolumeGroupEvent; 25 import android.car.media.CarVolumeGroupInfo; 26 import android.media.AudioAttributes; 27 import android.media.AudioAttributes.AttributeUsage; 28 import android.media.AudioManager; 29 import android.media.Ringtone; 30 import android.media.RingtoneManager; 31 import android.net.Uri; 32 import android.os.Bundle; 33 import android.os.Handler; 34 import android.os.Message; 35 import android.util.Log; 36 import android.util.SparseIntArray; 37 import android.view.LayoutInflater; 38 import android.view.View; 39 import android.view.ViewGroup; 40 import android.widget.ListView; 41 42 import androidx.fragment.app.Fragment; 43 44 import com.android.internal.annotations.GuardedBy; 45 46 import com.google.android.car.kitchensink.R; 47 import com.google.android.car.kitchensink.volume.VolumeTestFragment.CarAudioZoneVolumeInfo; 48 49 import java.util.List; 50 51 public final class CarAudioZoneVolumeFragment extends Fragment { 52 private static final String TAG = "CarVolumeTest." 53 + CarAudioZoneVolumeFragment.class.getSimpleName(); 54 private static final boolean DEBUG = true; 55 56 private static final int MSG_VOLUME_CHANGED = 0; 57 private static final int MSG_REQUEST_FOCUS = 1; 58 private static final int MSG_FOCUS_CHANGED = 2; 59 private static final int MSG_STOP_RINGTONE = 3; 60 private static final int MSG_ADJUST_VOLUME = 4; 61 private static final int MSG_EVENT_RECEIVED = 5; 62 private static final long RINGTONE_STOP_TIME_MS = 3_000; 63 private static final int ADJUST_VOLUME_UP = 0; 64 private static final int ADJUST_VOLUME_DOWN = 1; 65 66 private final int mZoneId; 67 private final Object mLock = new Object(); 68 private final CarAudioManager mCarAudioManager; 69 private final AudioManager mAudioManager; 70 private CarAudioZoneVolumeInfo[] mVolumeInfos = 71 new CarAudioZoneVolumeInfo[0]; 72 private final Handler mHandler = new VolumeHandler(); 73 74 private CarAudioZoneVolumeAdapter mCarAudioZoneVolumeAdapter; 75 private final SparseIntArray mGroupIdIndexMap = new SparseIntArray(); 76 77 @GuardedBy("mLock") 78 private Ringtone mRingtone; 79 sendVolumeChangedMessage(int groupId, int flags)80 void sendVolumeChangedMessage(int groupId, int flags) { 81 mHandler.sendMessage(mHandler.obtainMessage(MSG_VOLUME_CHANGED, groupId, flags)); 82 } 83 adjustVolumeUp(int groupId)84 void adjustVolumeUp(int groupId) { 85 mHandler.sendMessage(mHandler.obtainMessage(MSG_ADJUST_VOLUME, groupId, ADJUST_VOLUME_UP)); 86 } 87 adjustVolumeDown(int groupId)88 void adjustVolumeDown(int groupId) { 89 mHandler.sendMessage(mHandler 90 .obtainMessage(MSG_ADJUST_VOLUME, groupId, ADJUST_VOLUME_DOWN)); 91 } 92 sendEventReceivedMessage(CarVolumeGroupEvent event)93 void sendEventReceivedMessage(CarVolumeGroupEvent event) { 94 mHandler.sendMessage(mHandler.obtainMessage(MSG_EVENT_RECEIVED, event)); 95 } 96 97 private class VolumeHandler extends Handler { 98 private AudioFocusListener mFocusListener; 99 100 @Override handleMessage(Message msg)101 public void handleMessage(Message msg) { 102 if (DEBUG) { 103 Log.d(TAG, "zone " + mZoneId + " handleMessage : " + getMessageName(msg)); 104 } 105 switch (msg.what) { 106 case MSG_VOLUME_CHANGED: 107 initVolumeInfo(); 108 playRingtoneForGroup(msg.arg1, msg.arg2); 109 break; 110 case MSG_STOP_RINGTONE: 111 stopRingtone(); 112 break; 113 case MSG_REQUEST_FOCUS: 114 int groupId = msg.arg1; 115 if (mFocusListener != null) { 116 mAudioManager.abandonAudioFocus(mFocusListener); 117 mVolumeInfos[mGroupIdIndexMap.get(groupId)].hasAudioFocus = false; 118 mCarAudioZoneVolumeAdapter.notifyDataSetChanged(); 119 } 120 121 mFocusListener = new AudioFocusListener(groupId); 122 mAudioManager.requestAudioFocus(mFocusListener, groupId, 123 AudioManager.AUDIOFOCUS_GAIN); 124 break; 125 case MSG_FOCUS_CHANGED: 126 int focusGroupId = msg.arg1; 127 mVolumeInfos[mGroupIdIndexMap.get(focusGroupId)].hasAudioFocus = true; 128 mCarAudioZoneVolumeAdapter.refreshVolumes(mVolumeInfos); 129 break; 130 case MSG_ADJUST_VOLUME: 131 adjustVolumeByOne(msg.arg1, msg.arg2 == ADJUST_VOLUME_UP); 132 break; 133 case MSG_EVENT_RECEIVED: 134 CarVolumeGroupEvent event = (CarVolumeGroupEvent) msg.obj; 135 handleVolumeGroupEventReceived(event); 136 break; 137 default: 138 Log.wtf(TAG, "VolumeHandler handleMessage called with unknown message" 139 + msg.what); 140 break; 141 } 142 } 143 } 144 CarAudioZoneVolumeFragment(int zoneId, CarAudioManager carAudioManager, AudioManager audioManager)145 public CarAudioZoneVolumeFragment(int zoneId, CarAudioManager carAudioManager, 146 AudioManager audioManager) { 147 mZoneId = zoneId; 148 mCarAudioManager = carAudioManager; 149 mAudioManager = audioManager; 150 } 151 152 @Override onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)153 public View onCreateView(LayoutInflater inflater, ViewGroup container, 154 Bundle savedInstanceState) { 155 if (DEBUG) { 156 Log.d(TAG, "onCreateView " + mZoneId); 157 } 158 View v = inflater.inflate(R.layout.zone_volume_tab, container, false); 159 ListView volumeListView = v.findViewById(R.id.volume_list); 160 mCarAudioZoneVolumeAdapter = 161 new CarAudioZoneVolumeAdapter(getContext(), R.layout.volume_item, mVolumeInfos, 162 this, mCarAudioManager.isAudioFeatureEnabled( 163 AUDIO_FEATURE_VOLUME_GROUP_MUTING)); 164 initVolumeInfo(); 165 volumeListView.setAdapter(mCarAudioZoneVolumeAdapter); 166 return v; 167 } 168 initVolumeInfo()169 void initVolumeInfo() { 170 int volumeGroupCount = mCarAudioManager.getVolumeGroupCount(mZoneId); 171 mVolumeInfos = new CarAudioZoneVolumeInfo[volumeGroupCount + 1]; 172 mGroupIdIndexMap.clear(); 173 CarAudioZoneVolumeInfo titlesInfo = new CarAudioZoneVolumeInfo(); 174 titlesInfo.id = "Group id"; 175 titlesInfo.currentGain = "Current"; 176 mVolumeInfos[0] = titlesInfo; 177 178 int i = 1; 179 for (int groupId = 0; groupId < volumeGroupCount; groupId++) { 180 CarVolumeGroupInfo groupInfo = mCarAudioManager.getVolumeGroupInfo(mZoneId, groupId); 181 mVolumeInfos[i] = createCarAudioZoneVolumeInfo(groupInfo); 182 mGroupIdIndexMap.put(groupId, i); 183 i++; 184 } 185 mCarAudioZoneVolumeAdapter.refreshVolumes(mVolumeInfos); 186 } 187 createCarAudioZoneVolumeInfo(CarVolumeGroupInfo info)188 private CarAudioZoneVolumeInfo createCarAudioZoneVolumeInfo(CarVolumeGroupInfo info) { 189 CarAudioZoneVolumeInfo volumeInfo = new CarAudioZoneVolumeInfo(); 190 191 volumeInfo.groupId = info.getId(); 192 volumeInfo.id = String.valueOf(info.getId()); 193 volumeInfo.currentGain = String.valueOf(info.getVolumeGainIndex()); 194 volumeInfo.maxGain = info.getMaxVolumeGainIndex(); 195 volumeInfo.minGain = info.getMinVolumeGainIndex(); 196 volumeInfo.isMuted = info.isMuted(); 197 volumeInfo.isAttenuated = info.isAttenuated(); 198 volumeInfo.isBlocked = info.isBlocked(); 199 if (Flags.carAudioMuteAmbiguity()) { 200 volumeInfo.isSystemMuted = info.isMutedBySystem(); 201 } else { 202 volumeInfo.isSystemMuted = info.isBlocked() && info.isMuted(); 203 } 204 if (DEBUG) { 205 Log.d(TAG, "createCarAudioZoneVolumeInfo: Group Id: " + info.getId() 206 + " max: " + volumeInfo.maxGain + " current: " + volumeInfo.currentGain 207 + " is muted " + volumeInfo.isMuted 208 + " is attenuated " + volumeInfo.isAttenuated 209 + " is blocked " + volumeInfo.isBlocked 210 + " is muted by system " + volumeInfo.isSystemMuted); 211 } 212 return volumeInfo; 213 } 214 adjustVolumeByOne(int groupId, boolean up)215 private void adjustVolumeByOne(int groupId, boolean up) { 216 if (mCarAudioManager == null) { 217 Log.e(TAG, "CarAudioManager is null"); 218 return; 219 } 220 int current = mCarAudioManager.getGroupVolume(mZoneId, groupId); 221 CarAudioZoneVolumeInfo info = getVolumeInfo(groupId); 222 int volume = up ? current + 1 : current - 1; 223 if (volume > info.maxGain) { 224 if (DEBUG) { 225 Log.d(TAG, "Reached " + groupId + " max volume " 226 + " limit " + volume); 227 } 228 return; 229 } 230 if (volume < info.minGain) { 231 if (DEBUG) { 232 Log.d(TAG, "Reached " + groupId + " min volume " 233 + " limit " + volume); 234 } 235 return; 236 } 237 mCarAudioManager.setGroupVolume(mZoneId, groupId, volume, /* flags= */ 0); 238 if (DEBUG) { 239 Log.d(TAG, "Set group " + groupId + " volume " 240 + mCarAudioManager.getGroupVolume(mZoneId, groupId) 241 + " in audio zone " + mZoneId); 242 } 243 } 244 getVolumeInfo(int groupId)245 private CarAudioZoneVolumeInfo getVolumeInfo(int groupId) { 246 return mVolumeInfos[mGroupIdIndexMap.get(groupId)]; 247 } 248 toggleMute(int groupId)249 public void toggleMute(int groupId) { 250 if (mCarAudioManager == null) { 251 Log.e(TAG, "CarAudioManager is null"); 252 return; 253 } 254 boolean isMuted = mCarAudioManager.isVolumeGroupMuted(mZoneId, groupId); 255 mCarAudioManager.setVolumeGroupMute(mZoneId, groupId, !isMuted, AudioManager.FLAG_SHOW_UI); 256 if (DEBUG) { 257 Log.d(TAG, "Set group mute " + groupId + " mute " + !isMuted + " in audio zone " 258 + mZoneId); 259 } 260 } 261 requestFocus(int groupId)262 void requestFocus(int groupId) { 263 // Automatic volume change only works for primary audio zone. 264 if (mZoneId == CarAudioManager.PRIMARY_AUDIO_ZONE) { 265 mHandler.sendMessage(mHandler 266 .obtainMessage(MSG_REQUEST_FOCUS, groupId, /* arg2= */ 0)); 267 } 268 } 269 playRingtoneForGroup(int groupId, int flags)270 private void playRingtoneForGroup(int groupId, int flags) { 271 if (DEBUG) { 272 Log.d(TAG, "playRingtoneForGroup(" + groupId + ") in zone " + mZoneId); 273 } 274 275 if ((flags & FLAG_PLAY_SOUND) == 0) { 276 return; 277 } 278 279 int usage = mCarAudioManager.getUsagesForVolumeGroupId(mZoneId, groupId)[0]; 280 if (isRingtoneActiveForUsage(usage)) { 281 return; 282 } 283 284 mHandler.removeMessages(MSG_STOP_RINGTONE); 285 286 stopRingtone(); 287 startRingtone(usage); 288 289 mHandler.sendEmptyMessageDelayed(MSG_STOP_RINGTONE, RINGTONE_STOP_TIME_MS); 290 } 291 startRingtone(@ttributeUsage int usage)292 private void startRingtone(@AttributeUsage int usage) { 293 if (DEBUG) { 294 Log.d(TAG, "Start ringtone for zone " + mZoneId + " and usage " 295 + AudioAttributes.usageToString(usage)); 296 } 297 298 AudioAttributes.Builder builder = new AudioAttributes.Builder() 299 .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION); 300 301 if (AudioAttributes.isSystemUsage(usage)) { 302 builder.setSystemUsage(usage); 303 } else { 304 builder.setUsage(usage); 305 } 306 307 AudioAttributes attributes = builder.build(); 308 309 Uri uri = RingtoneManager.getActualDefaultRingtoneUri(getContext(), 310 AudioAttributes.toLegacyStreamType(attributes)); 311 312 Ringtone ringtone = 313 RingtoneManager.getRingtone(mCarAudioZoneVolumeAdapter.getContext(), uri); 314 ringtone.setAudioAttributes(attributes); 315 ringtone.setLooping(true); 316 317 ringtone.play(); 318 319 synchronized (mLock) { 320 mRingtone = ringtone; 321 } 322 } 323 stopRingtone()324 private void stopRingtone() { 325 synchronized (mLock) { 326 if (mRingtone == null) { 327 return; 328 } 329 if (mRingtone.isPlaying()) { 330 mRingtone.stop(); 331 } 332 mRingtone = null; 333 } 334 } 335 isRingtoneActiveForUsage(@ttributeUsage int usage)336 boolean isRingtoneActiveForUsage(@AttributeUsage int usage) { 337 synchronized (mLock) { 338 return mRingtone != null && mRingtone.isPlaying() 339 && mRingtone.getAudioAttributes().getUsage() == usage; 340 } 341 } 342 343 private class AudioFocusListener implements AudioManager.OnAudioFocusChangeListener { 344 private final int mGroupId; AudioFocusListener(int groupId)345 AudioFocusListener(int groupId) { 346 mGroupId = groupId; 347 } 348 @Override onAudioFocusChange(int focusChange)349 public void onAudioFocusChange(int focusChange) { 350 if (focusChange == AudioManager.AUDIOFOCUS_GAIN) { 351 mHandler.sendMessage(mHandler 352 .obtainMessage(MSG_FOCUS_CHANGED, mGroupId, /* arg2= */ 0)); 353 } else { 354 Log.e(TAG, "Audio focus request failed"); 355 } 356 } 357 } 358 handleVolumeGroupEventReceived(CarVolumeGroupEvent event)359 private void handleVolumeGroupEventReceived(CarVolumeGroupEvent event) { 360 if (DEBUG) { 361 Log.d(TAG, "Handling volume group event: " + event); 362 } 363 364 if ((event.getEventTypes() & CarVolumeGroupEvent.EVENT_TYPE_ZONE_CONFIGURATION_CHANGED) 365 == CarVolumeGroupEvent.EVENT_TYPE_ZONE_CONFIGURATION_CHANGED) { 366 initVolumeInfo(); 367 return; 368 } 369 370 int flags = CarVolumeGroupEvent.convertExtraInfoToFlags(event.getExtraInfos()); 371 List<CarVolumeGroupInfo> infos = event.getCarVolumeGroupInfos(); 372 for (int index = 0; index < infos.size(); index++) { 373 CarVolumeGroupInfo info = infos.get(index); 374 int groupId = info.getId(); 375 if (info.getZoneId() == mZoneId) { 376 mVolumeInfos[groupId + 1] = createCarAudioZoneVolumeInfo(info); 377 playRingtoneForGroup(groupId, flags); 378 } 379 } 380 mCarAudioZoneVolumeAdapter.refreshVolumes(mVolumeInfos); 381 } 382 } 383