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