CarVolumeControllerFactory.java revision c4d442f4a0d3acf90b1c7a1dd7c222a8f32a193f
1/*
2 * Copyright (C) 2016 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
17package com.android.car;
18
19import android.content.Context;
20import android.media.AudioManager;
21import android.media.IAudioService;
22import android.media.IVolumeController;
23import android.os.Handler;
24import android.os.Looper;
25import android.os.Message;
26import android.os.RemoteCallbackList;
27import android.os.RemoteException;
28import android.os.ServiceManager;
29import android.util.Log;
30import android.util.Pair;
31import android.util.SparseArray;
32import android.view.KeyEvent;
33
34import com.android.car.CarVolumeService.CarVolumeController;
35import com.android.car.hal.AudioHalService;
36import com.android.internal.annotations.GuardedBy;
37
38/**
39 * A factory class to create {@link com.android.car.CarVolumeService.CarVolumeController} based
40 * on car properties.
41 */
42public class CarVolumeControllerFactory {
43
44    public static CarVolumeController createCarVolumeController(Context context,
45            CarAudioService audioService, AudioHalService audioHal, CarInputService inputService) {
46        final boolean volumeSupported = audioHal.isAudioVolumeSupported();
47
48        // Case 1: Car Audio Module does not support volume controls
49        if (!volumeSupported) {
50            return new SimpleCarVolumeController(context);
51        }
52        return new CarExternalVolumeController(context, audioService, audioHal, inputService);
53    }
54
55    /**
56     * To control volumes through {@link android.media.AudioManager} when car audio module does not
57     * support volume controls.
58     */
59    public static final class SimpleCarVolumeController extends CarVolumeController {
60        private final AudioManager mAudioManager;
61        private final Context mContext;
62
63        public SimpleCarVolumeController(Context context) {
64            mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
65            mContext = context;
66        }
67
68        @Override
69        void init() {
70        }
71
72        @Override
73        public void setStreamVolume(int stream, int index, int flags) {
74            mAudioManager.setStreamVolume(stream, index, flags);
75        }
76
77        @Override
78        public int getStreamVolume(int stream) {
79            return mAudioManager.getStreamVolume(stream);
80        }
81
82        @Override
83        public void setVolumeController(IVolumeController controller) {
84            mAudioManager.setVolumeController(controller);
85        }
86
87        @Override
88        public int getStreamMaxVolume(int stream) {
89            return mAudioManager.getStreamMaxVolume(stream);
90        }
91
92        @Override
93        public int getStreamMinVolume(int stream) {
94            return mAudioManager.getStreamMinVolume(stream);
95        }
96
97        @Override
98        public boolean onKeyEvent(KeyEvent event) {
99            handleVolumeKeyDefault(event);
100            return true;
101        }
102
103        private void handleVolumeKeyDefault(KeyEvent event) {
104            if (event.getAction() != KeyEvent.ACTION_DOWN) {
105                return;
106            }
107
108            boolean volUp = event.getKeyCode() == KeyEvent.KEYCODE_VOLUME_UP;
109            int flags = AudioManager.FLAG_SHOW_UI | AudioManager.FLAG_PLAY_SOUND
110                    | AudioManager.FLAG_FROM_KEY;
111            IAudioService audioService = getAudioService();
112            String pkgName = mContext.getOpPackageName();
113            try {
114                if (audioService != null) {
115                    audioService.adjustSuggestedStreamVolume(
116                            volUp ? AudioManager.ADJUST_RAISE : AudioManager.ADJUST_LOWER,
117                            AudioManager.USE_DEFAULT_STREAM_TYPE, flags, pkgName, CarLog.TAG_INPUT);
118                }
119            } catch (RemoteException e) {
120                Log.e(CarLog.TAG_INPUT, "Error calling android audio service.", e);
121            }
122        }
123
124        private static IAudioService getAudioService() {
125            IAudioService audioService = IAudioService.Stub.asInterface(
126                    ServiceManager.checkService(Context.AUDIO_SERVICE));
127            if (audioService == null) {
128                Log.w(CarLog.TAG_INPUT, "Unable to find IAudioService interface.");
129            }
130            return audioService;
131        }
132    }
133
134    /**
135     * The car volume controller to use when the car audio modules supports volume controls.
136     *
137     * Depending on whether the car support audio context and has persistent memory, we need to
138     * handle per context volume change properly.
139     *
140     * Regardless whether car supports audio context or not, we need to keep per audio context
141     * volume internally. If we only support single channel, then we only send the volume change
142     * event when that stream is in focus; Otherwise, we need to adjust the stream volume either on
143     * software mixer level or send it the car audio module if the car support audio context
144     * and multi channel. TODO: Add support for multi channel.
145     *
146     * Per context volume should be persisted, so the volumes can stay the same across boots.
147     * Depending on the hardware property, this can be persisted on car side (or/and android side).
148     * TODO: we need to define one single source of truth if the car has memory.
149     */
150    public static class CarExternalVolumeController extends CarVolumeController
151            implements CarInputService.KeyEventListener, AudioHalService.AudioHalVolumeListener,
152            CarAudioService.AudioContextChangeListener {
153        private static final String TAG = CarLog.TAG_AUDIO + "ExtVolCtrl";
154        private static final int MSG_UPDATE_VOLUME = 0;
155        private static final int MSG_UPDATE_HAL = 1;
156
157        private final Context mContext;
158        private final AudioRoutingPolicy mPolicy;
159        private final AudioHalService mHal;
160        private final CarInputService mInputService;
161        private final CarAudioService mAudioService;
162
163        private int mSupportedAudioContext;
164
165        private boolean mHasExternalMemory;
166
167        @GuardedBy("this")
168        private int mCurrentContext = CarVolumeService.DEFAULT_CAR_AUDIO_CONTEXT;
169        // current logical volume, the key is android stream type
170        @GuardedBy("this")
171        private final SparseArray<Integer> mCurrentLogicalVolume =
172                new SparseArray<>(VolumeUtils.LOGICAL_STREAMS.length);
173        // stream volume limit, the key is android stream type
174        @GuardedBy("this")
175        private final SparseArray<Integer> mLogicalStreamVolumeMax =
176                new SparseArray<>(VolumeUtils.LOGICAL_STREAMS.length);
177        // stream volume limit, the key is android stream type
178        @GuardedBy("this")
179        private final SparseArray<Integer> mLogicalStreamVolumeMin =
180                new SparseArray<>(VolumeUtils.LOGICAL_STREAMS.length);
181
182        @GuardedBy("this")
183        private final RemoteCallbackList<IVolumeController> mVolumeControllers =
184                new RemoteCallbackList<>();
185
186        private final Handler mHandler = new VolumeHandler();
187
188        /**
189         * Convert an android logical stream to the car stream.
190         *
191         * @return If car supports audio context, then it returns the car audio context. Otherwise,
192         *      it returns the physical stream that maps to this logical stream.
193         */
194        private int logicalStreamToCarStream(int logicalAndroidStream) {
195            if (mSupportedAudioContext == 0) {
196                int physicalStream = mPolicy.getPhysicalStreamForLogicalStream(
197                        CarVolumeService.androidStreamToCarUsage(logicalAndroidStream));
198                return physicalStream;
199            } else {
200                int carContext = VolumeUtils.androidStreamToCarContext(logicalAndroidStream);
201                if ((carContext & mSupportedAudioContext) == 0) {
202                    carContext = CarVolumeService.DEFAULT_CAR_AUDIO_CONTEXT;
203                }
204                return carContext;
205            }
206        }
207
208        /**
209         * All updates to external components should be posted to this handler to avoid holding
210         * the internal lock while sending updates.
211         */
212        private final class VolumeHandler extends Handler {
213            @Override
214            public void handleMessage(Message msg) {
215                int stream;
216                int volume;
217                switch (msg.what) {
218                    case MSG_UPDATE_VOLUME:
219                        stream = msg.arg1;
220                        int flag = msg.arg2;
221                        final int size = mVolumeControllers.beginBroadcast();
222                        try {
223                            for (int i = 0; i < size; i++) {
224                                try {
225                                    mVolumeControllers.getBroadcastItem(i)
226                                            .volumeChanged(stream, flag);
227                                } catch (RemoteException ignored) {
228                                }
229                            }
230                        } finally {
231                            mVolumeControllers.finishBroadcast();
232                        }
233                        break;
234                    case MSG_UPDATE_HAL:
235                        stream = msg.arg1;
236                        volume = msg.arg2;
237                        mHal.setStreamVolume(stream, volume);
238                        break;
239                    default:
240                        break;
241                }
242            }
243        }
244
245        public CarExternalVolumeController(Context context, CarAudioService audioService,
246                                           AudioHalService hal, CarInputService inputService) {
247            mContext = context;
248            mAudioService = audioService;
249            mPolicy = audioService.getAudioRoutingPolicy();
250            mHal = hal;
251            mInputService = inputService;
252        }
253
254        @Override
255        void init() {
256            mSupportedAudioContext = mHal.getSupportedAudioVolumeContexts();
257            mHasExternalMemory = mHal.isExternalAudioVolumePersistent();
258            synchronized (this) {
259                initVolumeLimitLocked();
260                initCurrentVolumeLocked();
261            }
262            mInputService.setVolumeKeyListener(this);
263            mHal.setVolumeListener(this);
264            mAudioService.setAudioContextChangeListener(Looper.getMainLooper(), this);
265        }
266
267        private void initVolumeLimitLocked() {
268            for (int i : VolumeUtils.LOGICAL_STREAMS) {
269                int carStream = logicalStreamToCarStream(i);
270                Pair<Integer, Integer> volumeMinMax = mHal.getStreamVolumeLimit(carStream);
271                int max;
272                int min;
273                if (volumeMinMax == null) {
274                    max = 0;
275                    min = 0;
276                } else {
277                    max = volumeMinMax.second >= 0 ? volumeMinMax.second : 0;
278                    min = volumeMinMax.first >=0 ? volumeMinMax.first : 0;
279                }
280                // get default stream volume limit first.
281                mLogicalStreamVolumeMax.put(i, max);
282                mLogicalStreamVolumeMin.put(i, min);
283            }
284        }
285
286        private void initCurrentVolumeLocked() {
287            if (mHasExternalMemory) {
288                // TODO: read per context volume from audio hal
289            } else {
290                // TODO: read the Android side volume from Settings and pass it to the audio module
291                // Here we just set it to the physical stream volume temporarily.
292                for (int i : VolumeUtils.LOGICAL_STREAMS) {
293                    mCurrentLogicalVolume.put(i, mHal.getStreamVolume(logicalStreamToCarStream(i)));
294                }
295            }
296        }
297
298        @Override
299        public void setStreamVolume(int stream, int index, int flags) {
300            synchronized (this) {
301                setStreamVolumeInternalLocked(stream, index, flags);
302            }
303        }
304
305        private void setStreamVolumeInternalLocked(int stream, int index, int flags) {
306            if (mLogicalStreamVolumeMax.get(stream) == null) {
307                Log.e(TAG, "Stream type not supported " + stream);
308                return;
309            }
310            int limit = mLogicalStreamVolumeMax.get(stream);
311            if (index > limit) {
312                Log.e(TAG, "Volume exceeds volume limit. stream: " + stream + " index: " + index
313                        + " limit: " + limit);
314                index = limit;
315            }
316
317            if (index < 0) {
318                index = 0;
319            }
320
321            if (mCurrentLogicalVolume.get(stream) == index) {
322                return;
323            }
324
325            int carStream = logicalStreamToCarStream(stream);
326            int carContext = VolumeUtils.androidStreamToCarContext(stream);
327
328            // For single channel, only adjust the volume when the audio context is the current one.
329            if (mCurrentContext == carContext) {
330                mHandler.sendMessage(mHandler.obtainMessage(MSG_UPDATE_HAL, carStream, index));
331            }
332            // Record the current volume internally.
333            mCurrentLogicalVolume.put(stream, index);
334            mHandler.sendMessage(mHandler.obtainMessage(MSG_UPDATE_VOLUME, stream,
335                    getVolumeUpdateFlag()));
336        }
337
338        @Override
339        public int getStreamVolume(int stream) {
340            synchronized (this) {
341                if (mCurrentLogicalVolume.get(stream) == null) {
342                    Log.d(TAG, "Invalid stream type " + stream);
343                    return 0;
344                }
345                return mCurrentLogicalVolume.get(stream);
346            }
347        }
348
349        @Override
350        public void setVolumeController(IVolumeController controller) {
351            synchronized (this) {
352                mVolumeControllers.register(controller);
353            }
354        }
355
356        @Override
357        public void onVolumeChange(int carStream, int volume, int volumeState) {
358            int flag = getVolumeUpdateFlag();
359            synchronized (this) {
360                // Assume single channel here.
361                int currentLogicalStream = VolumeUtils.carContextToAndroidStream(mCurrentContext);
362                int currentCarStream = logicalStreamToCarStream(currentLogicalStream);
363                if (currentCarStream == carStream) {
364                    mCurrentLogicalVolume.put(currentLogicalStream, volume);
365                    mHandler.sendMessage(
366                            mHandler.obtainMessage(MSG_UPDATE_VOLUME, currentLogicalStream, flag));
367                } else {
368                    // Hal is telling us a car stream volume has changed, but it is not the current
369                    // stream.
370                    Log.w(TAG, "Car stream" + carStream
371                            + " volume changed, but it is not current stream, ignored.");
372                }
373            }
374        }
375
376        private int getVolumeUpdateFlag() {
377            // TODO: Apply appropriate flags.
378            return AudioManager.FLAG_SHOW_UI | AudioManager.FLAG_PLAY_SOUND;
379        }
380
381        private void updateHalVolumeLocked(final int carStream, final int index) {
382            mHandler.sendMessage(mHandler.obtainMessage(MSG_UPDATE_HAL, carStream, index));
383        }
384
385        @Override
386        public void onVolumeLimitChange(int streamNumber, int volume) {
387            // TODO: How should this update be sent to SystemUI? maybe send a volume update without
388            // showing UI.
389            synchronized (this) {
390                initVolumeLimitLocked();
391            }
392        }
393
394        @Override
395        public int getStreamMaxVolume(int stream) {
396            synchronized (this) {
397                if (mLogicalStreamVolumeMax.get(stream) == null) {
398                    Log.e(TAG, "Stream type not supported " + stream);
399                    return 0;
400                }
401                return mLogicalStreamVolumeMax.get(stream);
402            }
403        }
404
405        @Override
406        public int getStreamMinVolume(int stream) {
407            synchronized (this) {
408                if (mLogicalStreamVolumeMin.get(stream) == null) {
409                    Log.e(TAG, "Stream type not supported " + stream);
410                    return 0;
411                }
412                return mLogicalStreamVolumeMin.get(stream);
413            }
414        }
415
416        @Override
417        public boolean onKeyEvent(KeyEvent event) {
418            int logicalStream = VolumeUtils.carContextToAndroidStream(mCurrentContext);
419            final boolean down = event.getAction() == KeyEvent.ACTION_DOWN;
420            // TODO: properly handle long press on volume key
421            if (!down) {
422                return true;
423            }
424
425            synchronized (this) {
426                int currentVolume = mCurrentLogicalVolume.get(logicalStream);
427                switch (event.getKeyCode()) {
428                    case KeyEvent.KEYCODE_VOLUME_UP:
429                        setStreamVolumeInternalLocked(logicalStream, currentVolume + 1,
430                                getVolumeUpdateFlag());
431                        break;
432                    case KeyEvent.KEYCODE_VOLUME_DOWN:
433                        setStreamVolumeInternalLocked(logicalStream, currentVolume - 1,
434                                getVolumeUpdateFlag());
435                        break;
436                }
437            }
438            return true;
439        }
440
441        @Override
442        public void onContextChange(int primaryFocusContext, int primaryFocusPhysicalStream) {
443            synchronized (this) {
444                if (primaryFocusContext == mCurrentContext) {
445                    return;
446                }
447                mCurrentContext = primaryFocusContext;
448
449                int currentVolume = mCurrentLogicalVolume.get(
450                        VolumeUtils.carContextToAndroidStream(primaryFocusContext));
451                if (mSupportedAudioContext == 0) {
452                    // Car does not support audio context, we need to reset the volume
453                    updateHalVolumeLocked(primaryFocusPhysicalStream, currentVolume);
454                } else {
455                    // car supports context, but does not have memory.
456                    if (!mHasExternalMemory) {
457                        updateHalVolumeLocked(primaryFocusContext, currentVolume);
458                    }
459                }
460            }
461        }
462    }
463}
464