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