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