1 /*
2  * Copyright (C) 2015 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 package com.google.android.car.kitchensink.volume;
17 
18 import static android.car.media.CarAudioManager.AUDIO_FEATURE_DYNAMIC_ROUTING;
19 import static android.car.media.CarAudioManager.AUDIO_FEATURE_VOLUME_GROUP_EVENTS;
20 
21 import android.car.Car;
22 import android.car.Car.CarServiceLifecycleListener;
23 import android.car.media.CarAudioManager;
24 import android.car.media.CarAudioManager.CarVolumeCallback;
25 import android.car.media.CarVolumeGroupEvent;
26 import android.car.media.CarVolumeGroupEventCallback;
27 import android.car.media.CarVolumeGroupInfo;
28 import android.content.Context;
29 import android.media.AudioManager;
30 import android.os.Bundle;
31 import android.util.Log;
32 import android.util.SparseArray;
33 import android.view.LayoutInflater;
34 import android.view.View;
35 import android.view.ViewGroup;
36 import android.widget.SeekBar;
37 
38 import androidx.annotation.Nullable;
39 import androidx.core.content.ContextCompat;
40 import androidx.fragment.app.Fragment;
41 import androidx.viewpager.widget.ViewPager;
42 
43 import com.google.android.car.kitchensink.R;
44 import com.google.android.material.tabs.TabLayout;
45 
46 import java.util.List;
47 
48 import javax.annotation.concurrent.GuardedBy;
49 
50 public final class VolumeTestFragment extends Fragment {
51     private static final String TAG = "CarVolumeTest";
52     private static final boolean DEBUG = true;
53 
54     private AudioManager mAudioManager;
55     private AudioZoneVolumeTabAdapter mAudioZoneAdapter;
56     @GuardedBy("mLock")
57     private final SparseArray<CarAudioZoneVolumeFragment> mZoneVolumeFragments =
58             new SparseArray<>();
59 
60     private CarAudioManager mCarAudioManager;
61     private Car mCar;
62 
63     private SeekBar mFader;
64     private SeekBar mBalance;
65 
66     private TabLayout mZonesTabLayout;
67     private final Object mLock = new Object();
68 
69     public static final class CarAudioZoneVolumeInfo {
70         public int groupId;
71         public String id;
72         public int maxGain;
73         public int minGain;
74         public String currentGain;
75         public boolean hasAudioFocus;
76         public boolean isMuted;
77         public boolean isBlocked;
78         public boolean isAttenuated;
79         public boolean isSystemMuted;
80     }
81 
82     private final class CarVolumeChangeListener extends CarVolumeCallback {
83         @Override
onGroupVolumeChanged(int zoneId, int groupId, int flags)84         public void onGroupVolumeChanged(int zoneId, int groupId, int flags) {
85             if (DEBUG) {
86                 Log.d(TAG, "onGroupVolumeChanged volume changed for zone "
87                         + zoneId);
88             }
89             sendFragmentChangedMessage(zoneId, groupId, flags);
90         }
91 
92         @Override
onGroupMuteChanged(int zoneId, int groupId, int flags)93         public void onGroupMuteChanged(int zoneId, int groupId, int flags) {
94             if (DEBUG) {
95                 Log.d(TAG, "onGroupMuteChanged mute changed for zone " + zoneId);
96             }
97             sendFragmentChangedMessage(zoneId, groupId, flags);
98         }
99 
100         @Override
onMasterMuteChanged(int zoneId, int flags)101         public void onMasterMuteChanged(int zoneId, int flags) {
102             if (DEBUG) {
103                 Log.d(TAG, "onMasterMuteChanged master mute "
104                         + mAudioManager.isMasterMute());
105             }
106         }
107 
sendFragmentChangedMessage(int zoneId, int groupId, int flags)108         private void sendFragmentChangedMessage(int zoneId, int groupId, int flags) {
109             CarAudioZoneVolumeFragment fragment;
110 
111             synchronized (mLock) {
112                 fragment = mZoneVolumeFragments.get(zoneId);
113             }
114 
115             if (fragment != null) {
116                 fragment.sendVolumeChangedMessage(groupId, flags);
117             }
118         }
119     }
120 
121     private  CarVolumeGroupEventCallback mEventCallback = (volumeGroupEvents) -> {
122         if (DEBUG) {
123             Log.d(TAG, "onVolumeGroupEvent received events: " + volumeGroupEvents);
124         }
125         sendFragmentChangedMessageForEvents(volumeGroupEvents);
126     };
127 
128     private final CarVolumeCallback mCarVolumeCallback = new CarVolumeChangeListener();
129 
130     private CarServiceLifecycleListener mCarServiceLifecycleListener = (car, ready) -> {
131         if (!ready) {
132             if (DEBUG) {
133                 Log.d(TAG, "Disconnect from Car Service");
134             }
135             return;
136         }
137         if (DEBUG) {
138             Log.d(TAG, "Connected to Car Service");
139         }
140         mCarAudioManager = (CarAudioManager) car.getCarManager(Car.AUDIO_SERVICE);
141         initVolumeInfo();
142         if (mCarAudioManager.isAudioFeatureEnabled(AUDIO_FEATURE_VOLUME_GROUP_EVENTS)
143                 && mCarAudioManager.isAudioFeatureEnabled(AUDIO_FEATURE_DYNAMIC_ROUTING)) {
144             mCarAudioManager.registerCarVolumeGroupEventCallback(
145                     ContextCompat.getMainExecutor(getActivity().getApplicationContext()),
146                     mEventCallback);
147         }
148         mCarAudioManager.registerCarVolumeCallback(mCarVolumeCallback);
149     };
150 
151     @Override
onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState)152     public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
153                              @Nullable Bundle savedInstanceState) {
154         mAudioManager = (AudioManager) getContext().getSystemService(Context.AUDIO_SERVICE);
155 
156         View v = inflater.inflate(R.layout.volume_test, container, false);
157 
158         mZonesTabLayout = v.findViewById(R.id.zones_tab);
159         ViewPager viewPager = (ViewPager) v.findViewById(R.id.zone_view_pager);
160 
161         mAudioZoneAdapter = new AudioZoneVolumeTabAdapter(getChildFragmentManager());
162         viewPager.setAdapter(mAudioZoneAdapter);
163         mZonesTabLayout.setupWithViewPager(viewPager);
164 
165         SeekBar.OnSeekBarChangeListener seekListener =
166                 new SeekBar.OnSeekBarChangeListener() {
167             public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
168                 final float percent = (progress - 100) / 100.0f;
169                 if (seekBar.getId() == R.id.fade_bar) {
170                     mCarAudioManager.setFadeTowardFront(percent);
171                 } else {
172                     mCarAudioManager.setBalanceTowardRight(percent);
173                 }
174             }
175 
176             public void onStartTrackingTouch(SeekBar seekBar) {}
177 
178             public void onStopTrackingTouch(SeekBar seekBar) {}
179         };
180 
181         mFader = v.findViewById(R.id.fade_bar);
182         mFader.setOnSeekBarChangeListener(seekListener);
183 
184         mBalance = v.findViewById(R.id.balance_bar);
185         mBalance.setOnSeekBarChangeListener(seekListener);
186 
187         mCar = Car.createCar(getActivity(), /* handler= */ null,
188                 Car.CAR_WAIT_TIMEOUT_WAIT_FOREVER, mCarServiceLifecycleListener);
189         return v;
190     }
191 
192     @Override
onDestroyView()193     public void onDestroyView() {
194         if (mCar != null && mCar.isConnected()) {
195             mCar.disconnect();
196             mCar = null;
197         }
198         super.onDestroyView();
199     }
200 
initVolumeInfo()201     private void initVolumeInfo() {
202         synchronized (mLock) {
203             List<Integer> audioZoneIds = mCarAudioManager.getAudioZoneIds();
204             for (int index = 0; index < audioZoneIds.size(); index++) {
205                 int zoneId = audioZoneIds.get(index);
206                 CarAudioZoneVolumeFragment fragment =
207                         new CarAudioZoneVolumeFragment(zoneId, mCarAudioManager, mAudioManager);
208                 mZonesTabLayout.addTab(mZonesTabLayout.newTab().setText("Audio Zone " + zoneId));
209                 mAudioZoneAdapter.addFragment(fragment, "Audio Zone " + zoneId);
210                 if (DEBUG) {
211                     Log.d(TAG, "Adding audio volume for zone " + zoneId);
212                 }
213                 mZoneVolumeFragments.put(zoneId, fragment);
214             }
215         }
216     }
217 
sendFragmentChangedMessageForEvents(List<CarVolumeGroupEvent> volumeGroupEvents)218     private void sendFragmentChangedMessageForEvents(List<CarVolumeGroupEvent> volumeGroupEvents) {
219         CarAudioZoneVolumeFragment fragment;
220         for (int index = 0; index < volumeGroupEvents.size(); index++) {
221             CarVolumeGroupEvent event = volumeGroupEvents.get(index);
222             List<CarVolumeGroupInfo> groupInfos = event.getCarVolumeGroupInfos();
223 
224             synchronized (mLock) {
225                 fragment = mZoneVolumeFragments.get(groupInfos.get(0).getZoneId());
226             }
227 
228             if (fragment != null) {
229                 fragment.sendEventReceivedMessage(event);
230             }
231         }
232     }
233 }
234