1/* 2 * Copyright (C) 2017 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 */ 16package com.android.car.settings.sound; 17 18import android.annotation.DrawableRes; 19import android.annotation.StringRes; 20import android.car.Car; 21import android.car.CarNotConnectedException; 22import android.car.media.CarAudioManager; 23import android.car.media.ICarVolumeCallback; 24import android.content.ComponentName; 25import android.content.ServiceConnection; 26import android.content.res.TypedArray; 27import android.content.res.XmlResourceParser; 28import android.media.AudioAttributes; 29import android.os.Bundle; 30import android.os.IBinder; 31import android.util.AttributeSet; 32import android.util.SparseArray; 33import android.util.Xml; 34 35import androidx.car.widget.ListItem; 36import androidx.car.widget.ListItemAdapter; 37import androidx.car.widget.ListItemProvider.ListProvider; 38import androidx.car.widget.PagedListView; 39 40import com.android.car.settings.R; 41import com.android.car.settings.common.BaseFragment; 42import com.android.car.settings.common.Logger; 43 44import org.xmlpull.v1.XmlPullParserException; 45 46import java.io.IOException; 47import java.util.ArrayList; 48import java.util.List; 49 50/** 51 * Activity hosts sound related settings. 52 */ 53public class SoundSettingsFragment extends BaseFragment { 54 private static final Logger LOG = new Logger(SoundSettingsFragment.class); 55 56 private static final String XML_TAG_VOLUME_ITEMS = "carVolumeItems"; 57 private static final String XML_TAG_VOLUME_ITEM = "item"; 58 59 private final SparseArray<VolumeItem> mVolumeItems = new SparseArray<>(); 60 61 private final List<ListItem> mVolumeLineItems = new ArrayList<>(); 62 63 private final ServiceConnection mServiceConnection = new ServiceConnection() { 64 @Override 65 public void onServiceConnected(ComponentName name, IBinder service) { 66 try { 67 mCarAudioManager = (CarAudioManager) mCar.getCarManager(Car.AUDIO_SERVICE); 68 int volumeGroupCount = mCarAudioManager.getVolumeGroupCount(); 69 cleanUpVolumeLineItems(); 70 // Populates volume slider items from volume groups to UI. 71 for (int groupId = 0; groupId < volumeGroupCount; groupId++) { 72 final VolumeItem volumeItem = getVolumeItemForUsages( 73 mCarAudioManager.getUsagesForVolumeGroupId(groupId)); 74 mVolumeLineItems.add(new VolumeLineItem( 75 getContext(), 76 mCarAudioManager, 77 groupId, 78 volumeItem.usage, 79 volumeItem.icon, 80 volumeItem.title)); 81 } 82 updateList(); 83 mCarAudioManager.registerVolumeCallback(mVolumeChangeCallback.asBinder()); 84 } catch (CarNotConnectedException e) { 85 LOG.e("Car is not connected!", e); 86 } 87 } 88 89 /** 90 * This does not gets called when service is properly disconnected. 91 * So we need to also handle cleanups in onStop(). 92 */ 93 @Override 94 public void onServiceDisconnected(ComponentName name) { 95 cleanupAudioManager(); 96 } 97 }; 98 99 private final ICarVolumeCallback mVolumeChangeCallback = new ICarVolumeCallback.Stub() { 100 @Override 101 public void onGroupVolumeChanged(int groupId, int flags) { 102 for (ListItem lineItem : mVolumeLineItems) { 103 VolumeLineItem volumeLineItem = (VolumeLineItem) lineItem; 104 if (volumeLineItem.getVolumeGroupId() == groupId) { 105 volumeLineItem.updateProgress(); 106 } 107 } 108 updateList(); 109 } 110 111 @Override 112 public void onMasterMuteChanged(int flags) { 113 // ignored 114 } 115 }; 116 117 private Car mCar; 118 private CarAudioManager mCarAudioManager; 119 private PagedListView mListView; 120 private ListItemAdapter mPagedListAdapter; 121 122 /** 123 * Creates a new instance of this fragment. 124 */ 125 public static SoundSettingsFragment newInstance() { 126 SoundSettingsFragment soundSettingsFragment = new SoundSettingsFragment(); 127 Bundle bundle = BaseFragment.getBundle(); 128 bundle.putInt(EXTRA_TITLE_ID, R.string.sound_settings); 129 bundle.putInt(EXTRA_LAYOUT, R.layout.list); 130 bundle.putInt(EXTRA_ACTION_BAR_LAYOUT, R.layout.action_bar); 131 soundSettingsFragment.setArguments(bundle); 132 return soundSettingsFragment; 133 } 134 135 private void cleanupAudioManager() { 136 try { 137 mCarAudioManager.unregisterVolumeCallback(mVolumeChangeCallback.asBinder()); 138 } catch (CarNotConnectedException e) { 139 LOG.e("Car is not connected!", e); 140 } 141 cleanUpVolumeLineItems(); 142 mCarAudioManager = null; 143 } 144 145 private void updateList() { 146 if (getActivity() != null && mPagedListAdapter != null) { 147 getActivity().runOnUiThread(() -> mPagedListAdapter.notifyDataSetChanged()); 148 } 149 } 150 151 @Override 152 public void onActivityCreated(Bundle savedInstanceState) { 153 super.onActivityCreated(savedInstanceState); 154 155 loadAudioUsageItems(); 156 mCar = Car.createCar(getContext(), mServiceConnection); 157 mListView = getView().findViewById(R.id.list); 158 mPagedListAdapter = new ListItemAdapter(getContext(), new ListProvider(mVolumeLineItems)); 159 mListView.setAdapter(mPagedListAdapter); 160 mListView.setMaxPages(PagedListView.UNLIMITED_PAGES); 161 } 162 163 @Override 164 public void onStart() { 165 super.onStart(); 166 mCar.connect(); 167 } 168 169 @Override 170 public void onStop() { 171 super.onStop(); 172 cleanUpVolumeLineItems(); 173 cleanupAudioManager(); 174 mCar.disconnect(); 175 } 176 177 private void cleanUpVolumeLineItems() { 178 for (ListItem item : mVolumeLineItems) { 179 ((VolumeLineItem) item).stop(); 180 } 181 mVolumeLineItems.clear(); 182 } 183 184 private void loadAudioUsageItems() { 185 try (XmlResourceParser parser = getResources().getXml(R.xml.car_volume_items)) { 186 AttributeSet attrs = Xml.asAttributeSet(parser); 187 int type; 188 // Traverse to the first start tag 189 while ((type=parser.next()) != XmlResourceParser.END_DOCUMENT 190 && type != XmlResourceParser.START_TAG) { 191 } 192 193 if (!XML_TAG_VOLUME_ITEMS.equals(parser.getName())) { 194 throw new RuntimeException("Meta-data does not start with carVolumeItems tag"); 195 } 196 int outerDepth = parser.getDepth(); 197 int rank = 0; 198 while ((type=parser.next()) != XmlResourceParser.END_DOCUMENT 199 && (type != XmlResourceParser.END_TAG || parser.getDepth() > outerDepth)) { 200 if (type == XmlResourceParser.END_TAG) { 201 continue; 202 } 203 if (XML_TAG_VOLUME_ITEM.equals(parser.getName())) { 204 TypedArray item = getResources().obtainAttributes( 205 attrs, R.styleable.carVolumeItems_item); 206 int usage = item.getInt(R.styleable.carVolumeItems_item_usage, -1); 207 if (usage >= 0) { 208 mVolumeItems.put(usage, new VolumeItem( 209 usage, rank, 210 item.getResourceId(R.styleable.carVolumeItems_item_title, 0), 211 item.getResourceId(R.styleable.carVolumeItems_item_icon, 0))); 212 rank++; 213 } 214 item.recycle(); 215 } 216 } 217 } catch (XmlPullParserException | IOException e) { 218 LOG.e("Error parsing volume groups configuration", e); 219 } 220 } 221 222 private VolumeItem getVolumeItemForUsages(int[] usages) { 223 int rank = Integer.MAX_VALUE; 224 VolumeItem result = null; 225 for (int usage : usages) { 226 VolumeItem volumeItem = mVolumeItems.get(usage); 227 if (volumeItem.rank < rank) { 228 rank = volumeItem.rank; 229 result = volumeItem; 230 } 231 } 232 return result; 233 } 234 235 /** 236 * Wrapper class which contains information to render volume item on UI. 237 */ 238 private static class VolumeItem { 239 private final @AudioAttributes.AttributeUsage int usage; 240 private final int rank; 241 private final @StringRes int title; 242 private final @DrawableRes int icon; 243 244 private VolumeItem(@AudioAttributes.AttributeUsage int usage, int rank, 245 @StringRes int title, @DrawableRes int icon) { 246 this.usage = usage; 247 this.rank = rank; 248 this.title = title; 249 this.icon = icon; 250 } 251 } 252} 253