Home | History | Annotate | Download | only in sound
      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  */
     16 package com.android.car.settings.sound;
     17 
     18 import android.annotation.DrawableRes;
     19 import android.annotation.StringRes;
     20 import android.car.Car;
     21 import android.car.CarNotConnectedException;
     22 import android.car.media.CarAudioManager;
     23 import android.car.media.ICarVolumeCallback;
     24 import android.content.ComponentName;
     25 import android.content.ServiceConnection;
     26 import android.content.res.TypedArray;
     27 import android.content.res.XmlResourceParser;
     28 import android.media.AudioAttributes;
     29 import android.os.Bundle;
     30 import android.os.IBinder;
     31 import android.util.AttributeSet;
     32 import android.util.SparseArray;
     33 import android.util.Xml;
     34 
     35 import androidx.car.widget.ListItem;
     36 import androidx.car.widget.ListItemAdapter;
     37 import androidx.car.widget.ListItemProvider.ListProvider;
     38 import androidx.car.widget.PagedListView;
     39 
     40 import com.android.car.settings.R;
     41 import com.android.car.settings.common.BaseFragment;
     42 import com.android.car.settings.common.Logger;
     43 
     44 import org.xmlpull.v1.XmlPullParserException;
     45 
     46 import java.io.IOException;
     47 import java.util.ArrayList;
     48 import java.util.List;
     49 
     50 /**
     51  * Activity hosts sound related settings.
     52  */
     53 public 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