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.hardware.automotive.vehicle.V2_0.VehicleAudioContextFlag;
21import android.media.AudioManager;
22import android.media.IAudioService;
23import android.media.IVolumeController;
24import android.os.Handler;
25import android.os.HandlerThread;
26import android.os.Looper;
27import android.os.Message;
28import android.os.RemoteCallbackList;
29import android.os.RemoteException;
30import android.os.ServiceManager;
31import android.provider.Settings;
32import android.telecom.TelecomManager;
33import android.util.ArrayMap;
34import android.util.Log;
35import android.util.SparseArray;
36import android.view.KeyEvent;
37
38import com.android.car.CarVolumeService.CarVolumeController;
39import com.android.car.hal.AudioHalService;
40import com.android.internal.annotations.GuardedBy;
41
42import java.io.PrintWriter;
43import java.util.Map;
44
45/**
46 * A factory class to create {@link com.android.car.CarVolumeService.CarVolumeController} based
47 * on car properties.
48 */
49public class CarVolumeControllerFactory {
50    // STOPSHIP if true.
51    private static final boolean DBG = false;
52
53    public static CarVolumeController createCarVolumeController(Context context,
54            CarAudioService audioService, AudioHalService audioHal, CarInputService inputService) {
55        final boolean volumeSupported = audioHal.isAudioVolumeSupported();
56
57        // Case 1: Car Audio Module does not support volume controls
58        if (!volumeSupported) {
59            return new SimpleCarVolumeController(context);
60        }
61        return new CarExternalVolumeController(context, audioService, audioHal, inputService);
62    }
63
64    public static boolean interceptVolKeyBeforeDispatching(Context context) {
65        Log.d(CarLog.TAG_AUDIO, "interceptVolKeyBeforeDispatching");
66
67        TelecomManager telecomManager = (TelecomManager)
68                context.getSystemService(Context.TELECOM_SERVICE);
69        if (telecomManager != null && telecomManager.isRinging()) {
70            // If an incoming call is ringing, either VOLUME key means
71            // "silence ringer".  This is consistent with current android phone's behavior
72            Log.i(CarLog.TAG_AUDIO, "interceptKeyBeforeQueueing:"
73                    + " VOLUME key-down while ringing: Silence ringer!");
74
75            // Silence the ringer.  (It's safe to call this
76            // even if the ringer has already been silenced.)
77            telecomManager.silenceRinger();
78            return true;
79        }
80        return false;
81    }
82
83    public static boolean isVolumeKey(KeyEvent event) {
84        return event.getKeyCode() == KeyEvent.KEYCODE_VOLUME_DOWN
85                || event.getKeyCode() == KeyEvent.KEYCODE_VOLUME_UP;
86    }
87
88    /**
89     * To control volumes through {@link android.media.AudioManager} when car audio module does not
90     * support volume controls.
91     */
92    public static final class SimpleCarVolumeController extends CarVolumeController {
93        private final AudioManager mAudioManager;
94        private final Context mContext;
95
96        public SimpleCarVolumeController(Context context) {
97            mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
98            mContext = context;
99        }
100
101        @Override
102        void init() {
103        }
104
105        @Override
106        void release() {
107        }
108
109        @Override
110        public void setStreamVolume(int stream, int index, int flags) {
111            if (DBG) {
112                Log.d(CarLog.TAG_AUDIO, "setStreamVolume " + stream + " " + index + " " + flags);
113            }
114            mAudioManager.setStreamVolume(stream, index, flags);
115        }
116
117        @Override
118        public int getStreamVolume(int stream) {
119            return mAudioManager.getStreamVolume(stream);
120        }
121
122        @Override
123        public void setVolumeController(IVolumeController controller) {
124            mAudioManager.setVolumeController(controller);
125        }
126
127        @Override
128        public int getStreamMaxVolume(int stream) {
129            return mAudioManager.getStreamMaxVolume(stream);
130        }
131
132        @Override
133        public int getStreamMinVolume(int stream) {
134            return mAudioManager.getStreamMinVolume(stream);
135        }
136
137        @Override
138        public boolean onKeyEvent(KeyEvent event) {
139            if (!isVolumeKey(event)) {
140                return false;
141            }
142            handleVolumeKeyDefault(event);
143            return true;
144        }
145
146        @Override
147        public void dump(PrintWriter writer) {
148            writer.println("Volume controller:" + SimpleCarVolumeController.class.getSimpleName());
149            // nothing else to dump
150        }
151
152        private void handleVolumeKeyDefault(KeyEvent event) {
153            if (event.getAction() != KeyEvent.ACTION_DOWN
154                    || interceptVolKeyBeforeDispatching(mContext)) {
155                return;
156            }
157
158            boolean volUp = event.getKeyCode() == KeyEvent.KEYCODE_VOLUME_UP;
159            int flags = AudioManager.FLAG_SHOW_UI | AudioManager.FLAG_PLAY_SOUND
160                    | AudioManager.FLAG_FROM_KEY;
161            IAudioService audioService = getAudioService();
162            String pkgName = mContext.getOpPackageName();
163            try {
164                if (audioService != null) {
165                    audioService.adjustSuggestedStreamVolume(
166                            volUp ? AudioManager.ADJUST_RAISE : AudioManager.ADJUST_LOWER,
167                            AudioManager.USE_DEFAULT_STREAM_TYPE, flags, pkgName, CarLog.TAG_INPUT);
168                }
169            } catch (RemoteException e) {
170                Log.e(CarLog.TAG_INPUT, "Error calling android audio service.", e);
171            }
172        }
173
174        private static IAudioService getAudioService() {
175            IAudioService audioService = IAudioService.Stub.asInterface(
176                    ServiceManager.checkService(Context.AUDIO_SERVICE));
177            if (audioService == null) {
178                Log.w(CarLog.TAG_INPUT, "Unable to find IAudioService interface.");
179            }
180            return audioService;
181        }
182    }
183
184    /**
185     * The car volume controller to use when the car audio modules supports volume controls.
186     *
187     * Depending on whether the car support audio context and has persistent memory, we need to
188     * handle per context volume change properly.
189     *
190     * Regardless whether car supports audio context or not, we need to keep per audio context
191     * volume internally. If we only support single channel, then we only send the volume change
192     * event when that stream is in focus; Otherwise, we need to adjust the stream volume either on
193     * software mixer level or send it the car audio module if the car support audio context
194     * and multi channel. TODO: Add support for multi channel. bug: 32095376
195     *
196     * Per context volume should be persisted, so the volumes can stay the same across boots.
197     * Depending on the hardware property, this can be persisted on car side (or/and android side).
198     * TODO: we need to define one single source of truth if the car has memory. bug: 32091839
199     */
200    public static class CarExternalVolumeController extends CarVolumeController
201            implements CarInputService.KeyEventListener, AudioHalService.AudioHalVolumeListener,
202            CarAudioService.AudioContextChangeListener {
203        private static final String TAG = CarLog.TAG_AUDIO + ".VolCtrl";
204        private static final int MSG_UPDATE_VOLUME = 0;
205        private static final int MSG_UPDATE_HAL = 1;
206        private static final int MSG_SUPPRESS_UI_FOR_VOLUME = 2;
207        private static final int MSG_VOLUME_UI_RESTORE = 3;
208
209        // within 5 seconds after a UI invisible volume change (e.g., due to audio context change,
210        // or explicitly flag), we will not show UI in respond to that particular volume changes
211        // events from HAL (context and volume index must match).
212        private static final int HIDE_VOLUME_UI_MILLISECONDS = 5 * 1000; // 5 seconds
213
214        private final Context mContext;
215        private final AudioRoutingPolicy mPolicy;
216        private final AudioHalService mHal;
217        private final CarInputService mInputService;
218        private final CarAudioService mAudioService;
219
220        private int mSupportedAudioContext;
221
222        private boolean mHasExternalMemory;
223        private boolean mMasterVolumeOnly;
224
225        @GuardedBy("this")
226        private int mCurrentContext = CarVolumeService.DEFAULT_CAR_AUDIO_CONTEXT;
227        // current logical volume, the key is car audio context
228        @GuardedBy("this")
229        private final SparseArray<Integer> mCurrentCarContextVolume =
230                new SparseArray<>(VolumeUtils.CAR_AUDIO_CONTEXT.length);
231        // stream volume limit, the key is car audio context type
232        @GuardedBy("this")
233        private final SparseArray<Integer> mCarContextVolumeMax =
234                new SparseArray<>(VolumeUtils.CAR_AUDIO_CONTEXT.length);
235        // stream volume limit, the key is car audio context type
236        @GuardedBy("this")
237        private final RemoteCallbackList<IVolumeController> mVolumeControllers =
238                new RemoteCallbackList<>();
239        @GuardedBy("this")
240        private int[] mSuppressUiForVolume = new int[2];
241        @GuardedBy("this")
242        private boolean mShouldSuppress = false;
243        private HandlerThread mVolumeThread;
244        private Handler mHandler;
245
246        /**
247         * Convert an car context to the car stream.
248         *
249         * @return If car supports audio context, then it returns the car audio context. Otherwise,
250         *      it returns the physical stream that maps to this logical stream.
251         */
252        private int carContextToCarStream(int carContext) {
253            if (mSupportedAudioContext == 0) {
254                int physicalStream = mPolicy.getPhysicalStreamForLogicalStream(
255                        AudioHalService.carContextToCarUsage(carContext));
256                return physicalStream;
257            } else {
258                return carContext == VehicleAudioContextFlag.UNKNOWN_FLAG ?
259                        mCurrentContext : carContext;
260            }
261        }
262
263        private void writeVolumeToSettings(int carContext, int volume) {
264            String key = VolumeUtils.CAR_AUDIO_CONTEXT_SETTINGS.get(carContext);
265            if (key != null) {
266                Settings.Global.putInt(mContext.getContentResolver(), key, volume);
267            }
268        }
269
270        /**
271         * All updates to external components should be posted to this handler to avoid holding
272         * the internal lock while sending updates.
273         */
274        private final class VolumeHandler extends Handler {
275            public VolumeHandler(Looper looper) {
276                super(looper);
277            }
278            @Override
279            public void handleMessage(Message msg) {
280                int stream;
281                int volume;
282                switch (msg.what) {
283                    case MSG_UPDATE_VOLUME:
284                        // arg1 is car context
285                        stream = msg.arg1;
286                        volume = (int) msg.obj;
287                        int flag = msg.arg2;
288                        synchronized (CarExternalVolumeController.this) {
289                            // the suppressed stream is sending us update....
290                            if (mShouldSuppress && stream == mSuppressUiForVolume[0]) {
291                                // the volume matches, we want to suppress it
292                                if (volume == mSuppressUiForVolume[1]) {
293                                    if (DBG) {
294                                        Log.d(TAG, "Suppress Volume UI for stream "
295                                                + stream + " volume: " + volume);
296                                    }
297                                    flag &= ~AudioManager.FLAG_SHOW_UI;
298                                }
299                                // No matter if the volume matches or not, we will stop suppressing
300                                // UI for this stream now. After an audio context switch, user may
301                                // quickly turn the nob, -1 and +1, it ends the same volume,
302                                // but we should show the UI for both.
303                                removeMessages(MSG_VOLUME_UI_RESTORE);
304                                mShouldSuppress = false;
305                            }
306                        }
307                        final int size = mVolumeControllers.beginBroadcast();
308                        try {
309                            for (int i = 0; i < size; i++) {
310                                try {
311                                    mVolumeControllers.getBroadcastItem(i).volumeChanged(
312                                            VolumeUtils.carContextToAndroidStream(stream), flag);
313                                } catch (RemoteException ignored) {
314                                }
315                            }
316                        } finally {
317                            mVolumeControllers.finishBroadcast();
318                        }
319                        break;
320                    case MSG_UPDATE_HAL:
321                        stream = msg.arg1;
322                        volume = msg.arg2;
323                        synchronized (CarExternalVolumeController.this) {
324                            if (mMasterVolumeOnly) {
325                                stream = 0;
326                            }
327                        }
328                        mHal.setStreamVolume(stream, volume);
329                        break;
330                    case MSG_SUPPRESS_UI_FOR_VOLUME:
331                        if (DBG) {
332                            Log.d(TAG, "Suppress stream volume " + msg.arg1 + " " + msg.arg2);
333                        }
334                        synchronized (CarExternalVolumeController.this) {
335                            mShouldSuppress = true;
336                            mSuppressUiForVolume[0] = msg.arg1;
337                            mSuppressUiForVolume[1] = msg.arg2;
338                        }
339                        removeMessages(MSG_VOLUME_UI_RESTORE);
340                        sendMessageDelayed(obtainMessage(MSG_VOLUME_UI_RESTORE),
341                                HIDE_VOLUME_UI_MILLISECONDS);
342                        break;
343                    case MSG_VOLUME_UI_RESTORE:
344                        if (DBG) {
345                            Log.d(TAG, "Volume Ui suppress expired");
346                        }
347                        synchronized (CarExternalVolumeController.this) {
348                            mShouldSuppress = false;
349                        }
350                        break;
351                    default:
352                        break;
353                }
354            }
355        }
356
357        public CarExternalVolumeController(Context context, CarAudioService audioService,
358                                           AudioHalService hal, CarInputService inputService) {
359            mContext = context;
360            mAudioService = audioService;
361            mPolicy = audioService.getAudioRoutingPolicy();
362            mHal = hal;
363            mInputService = inputService;
364        }
365
366        @Override
367        void init() {
368            mSupportedAudioContext = mHal.getSupportedAudioVolumeContexts();
369            mHasExternalMemory = mHal.isExternalAudioVolumePersistent();
370            mMasterVolumeOnly = mHal.isAudioVolumeMasterOnly();
371            synchronized (this) {
372                mVolumeThread = new HandlerThread(TAG);
373                mVolumeThread.start();
374                mHandler = new VolumeHandler(mVolumeThread.getLooper());
375                initVolumeLimitLocked();
376                initCurrentVolumeLocked();
377            }
378            mInputService.setVolumeKeyListener(this);
379            mHal.setVolumeListener(this);
380            mAudioService.setAudioContextChangeListener(Looper.getMainLooper(), this);
381        }
382
383        @Override
384        void release() {
385            synchronized (this) {
386                if (mVolumeThread != null) {
387                    mVolumeThread.quit();
388                }
389            }
390        }
391
392        private void initVolumeLimitLocked() {
393            for (int i : VolumeUtils.CAR_AUDIO_CONTEXT) {
394                int carStream = carContextToCarStream(i);
395                Integer volumeMax = mHal.getStreamMaxVolume(carStream);
396                int max = volumeMax == null ? 0 : volumeMax;
397                if (max < 0) {
398                    max = 0;
399                }
400                // get default stream volume limit first.
401                mCarContextVolumeMax.put(i, max);
402            }
403        }
404
405        private void initCurrentVolumeLocked() {
406            if (mHasExternalMemory) {
407                // TODO: read per context volume from audio hal. bug: 32091839
408            } else {
409                // when vhal does not work, get call can take long. For that case,
410                // for the same physical streams, cache initial get results
411                Map<Integer, Integer> volumesPerCarStream =
412                        new ArrayMap<>(VolumeUtils.CAR_AUDIO_CONTEXT.length);
413                for (int i : VolumeUtils.CAR_AUDIO_CONTEXT) {
414                    String key = VolumeUtils.CAR_AUDIO_CONTEXT_SETTINGS.get(i);
415                    if (key != null) {
416                        int vol = Settings.Global.getInt(mContext.getContentResolver(), key, -1);
417                        if (vol >= 0) {
418                            // Read valid volume for this car context from settings and continue;
419                            mCurrentCarContextVolume.put(i, vol);
420                            if (DBG) {
421                                Log.d(TAG, "init volume from settings, car audio context: "
422                                        + i + " volume: " + vol);
423                            }
424                            continue;
425                        }
426                    }
427
428                    // There is no settings for this car context. Use the current physical car
429                    // stream volume as initial value instead, and put the volume into settings.
430                    int carStream = carContextToCarStream(i);
431                    Integer volume = volumesPerCarStream.get(carStream);
432                    if (volume == null) {
433                        volume = Integer.valueOf(mHal.getStreamVolume(mMasterVolumeOnly ? 0 :
434                            carStream));
435                        volumesPerCarStream.put(carStream, volume);
436                    }
437                    mCurrentCarContextVolume.put(i, volume);
438                    writeVolumeToSettings(i, volume);
439                    if (DBG) {
440                        Log.d(TAG, "init volume from physical stream," +
441                                " car audio context: " + i + " volume: " + volume);
442                    }
443                }
444            }
445        }
446
447        @Override
448        public void setStreamVolume(int stream, int index, int flags) {
449            synchronized (this) {
450                int carContext;
451                // Currently car context and android logical stream are not
452                // one-to-one mapping. In this API, Android side asks us to change a logical stream
453                // volume. If the current car audio context maps to this logical stream, then we
454                // change the volume for the current car audio context. Otherwise, we change the
455                // volume for the primary mapped car audio context.
456                if (VolumeUtils.carContextToAndroidStream(mCurrentContext) == stream) {
457                    carContext = mCurrentContext;
458                } else {
459                    carContext = VolumeUtils.androidStreamToCarContext(stream);
460                }
461                if (DBG) {
462                    Log.d(TAG, "Receive setStreamVolume logical stream: " + stream + " index: "
463                            + index + " flags: " + flags + " maps to car context: " + carContext);
464                }
465                setStreamVolumeInternalLocked(carContext, index, flags);
466            }
467        }
468
469        private void setStreamVolumeInternalLocked(int carContext, int index, int flags) {
470            if (mCarContextVolumeMax.get(carContext) == null) {
471                Log.e(TAG, "Stream type not supported " + carContext);
472                return;
473            }
474            int limit = mCarContextVolumeMax.get(carContext);
475            if (index > limit) {
476                Log.w(TAG, "Volume exceeds volume limit. context: " + carContext
477                        + " index: " + index + " limit: " + limit);
478                index = limit;
479            }
480
481            if (index < 0) {
482                index = 0;
483            }
484
485            if (mCurrentCarContextVolume.get(carContext) == index) {
486                return;
487            }
488
489            int carStream = carContextToCarStream(carContext);
490            if (DBG) {
491                Log.d(TAG, "Change car stream volume, stream: " + carStream + " volume:" + index);
492            }
493            // For single channel, only adjust the volume when the audio context is the current one.
494            if (mCurrentContext == carContext) {
495                if (DBG) {
496                    Log.d(TAG, "Sending volume change to HAL");
497                }
498                mHandler.sendMessage(mHandler.obtainMessage(MSG_UPDATE_HAL, carStream, index));
499                if ((flags & AudioManager.FLAG_SHOW_UI) != 0) {
500                    if (mShouldSuppress && mSuppressUiForVolume[0] == carContext) {
501                        // In this case, the caller explicitly says "Show_UI" for the same context.
502                        // We will respect the flag, and let the UI show.
503                        mShouldSuppress = false;
504                        mHandler.removeMessages(MSG_VOLUME_UI_RESTORE);
505                    }
506                } else {
507                    mHandler.sendMessage(mHandler.obtainMessage(MSG_SUPPRESS_UI_FOR_VOLUME,
508                            carContext, index));
509                }
510            }
511            // Record the current volume internally.
512            mCurrentCarContextVolume.put(carContext, index);
513            writeVolumeToSettings(mCurrentContext, index);
514        }
515
516        @Override
517        public int getStreamVolume(int stream) {
518            synchronized (this) {
519                if (VolumeUtils.carContextToAndroidStream(mCurrentContext) == stream) {
520                    return mCurrentCarContextVolume.get(mCurrentContext);
521                }
522                return mCurrentCarContextVolume.get(VolumeUtils.androidStreamToCarContext(stream));
523            }
524        }
525
526        @Override
527        public void setVolumeController(IVolumeController controller) {
528            synchronized (this) {
529                mVolumeControllers.register(controller);
530            }
531        }
532
533        @Override
534        public void onVolumeChange(int carStream, int volume, int volumeState) {
535            synchronized (this) {
536                int flag = getVolumeUpdateFlag(true);
537                if (DBG) {
538                    Log.d(TAG, "onVolumeChange carStream: " + carStream + " volume: " + volume
539                            + " volumeState: " + volumeState
540                            + " suppressUI? " + mShouldSuppress
541                            + " stream: " + mSuppressUiForVolume[0]
542                            + " volume: " + mSuppressUiForVolume[1]);
543                }
544                int currentCarStream = carContextToCarStream(mCurrentContext);
545                if (mMasterVolumeOnly) { //for master volume only H/W, always assume current stream
546                    carStream = currentCarStream;
547                }
548
549                // Map the UNKNOWN context to the current context.
550                if (mSupportedAudioContext != 0
551                        && carStream == VehicleAudioContextFlag.UNKNOWN_FLAG) {
552                    carStream = mCurrentContext;
553                }
554
555                if (currentCarStream == carStream) {
556                    mCurrentCarContextVolume.put(mCurrentContext, volume);
557                    writeVolumeToSettings(mCurrentContext, volume);
558                    mHandler.sendMessage(
559                            mHandler.obtainMessage(MSG_UPDATE_VOLUME, mCurrentContext, flag,
560                                    new Integer(volume)));
561                } else {
562                    // Hal is telling us a car stream volume has changed, but it is not the current
563                    // stream.
564                    // TODO:  b/63778359
565                    Log.w(TAG, "Car stream" + carStream
566                            + " volume changed, but it is not current stream, ignored.");
567                }
568            }
569        }
570
571        private int getVolumeUpdateFlag(boolean showUi) {
572            return showUi? AudioManager.FLAG_SHOW_UI : 0;
573        }
574
575        @Override
576        public void onVolumeLimitChange(int streamNumber, int volume) {
577            // TODO: How should this update be sent to SystemUI? bug: 32095237
578            // maybe send a volume update without showing UI.
579            synchronized (this) {
580                initVolumeLimitLocked();
581            }
582        }
583
584        @Override
585        public int getStreamMaxVolume(int stream) {
586            synchronized (this) {
587                if (VolumeUtils.carContextToAndroidStream(mCurrentContext) == stream) {
588                    return mCarContextVolumeMax.get(mCurrentContext);
589                } else {
590                    return mCarContextVolumeMax.get(VolumeUtils.androidStreamToCarContext(stream));
591                }
592            }
593        }
594
595        @Override
596        public int getStreamMinVolume(int stream) {
597            return 0;  // Min value is always zero.
598        }
599
600        @Override
601        public boolean onKeyEvent(KeyEvent event) {
602            if (!isVolumeKey(event)) {
603                return false;
604            }
605            final boolean down = event.getAction() == KeyEvent.ACTION_DOWN;
606            if (DBG) {
607                Log.d(TAG, "Receive volume keyevent " + event.toString());
608            }
609            // TODO: properly handle long press on volume key, bug: 32095989
610            if (!down || interceptVolKeyBeforeDispatching(mContext)) {
611                return true;
612            }
613
614            synchronized (this) {
615                int currentVolume = mCurrentCarContextVolume.get(mCurrentContext);
616                switch (event.getKeyCode()) {
617                    case KeyEvent.KEYCODE_VOLUME_UP:
618                        setStreamVolumeInternalLocked(mCurrentContext, currentVolume + 1,
619                                getVolumeUpdateFlag(true));
620                        break;
621                    case KeyEvent.KEYCODE_VOLUME_DOWN:
622                        setStreamVolumeInternalLocked(mCurrentContext, currentVolume - 1,
623                                getVolumeUpdateFlag(true));
624                        break;
625                }
626            }
627            return true;
628        }
629
630        @Override
631        public void onContextChange(int primaryFocusContext, int primaryFocusPhysicalStream) {
632            synchronized (this) {
633                if(DBG) {
634                    Log.d(TAG, "Audio context changed from " + mCurrentContext + " to: "
635                            + primaryFocusContext + " physical: " + primaryFocusPhysicalStream);
636                }
637                // if primaryFocusContext is 0, it means nothing is playing or holding focus,
638                // we will keep the last focus context and if the user changes the volume
639                // it will go to the last audio context.
640                if (primaryFocusContext == mCurrentContext || primaryFocusContext == 0) {
641                    return;
642                }
643                int oldContext = mCurrentContext;
644                mCurrentContext = primaryFocusContext;
645                // if car supports audio context and has external memory, then we don't need to do
646                // anything.
647                if(mSupportedAudioContext != 0 && mHasExternalMemory) {
648                    if (DBG) {
649                        Log.d(TAG, "Car support audio context and has external memory," +
650                                " no volume change needed from car service");
651                    }
652                    return;
653                }
654
655                // Otherwise, we need to tell Hal what the correct volume is for the new context.
656                int currentVolume = mCurrentCarContextVolume.get(primaryFocusContext);
657
658                int carStreamNumber = (mSupportedAudioContext == 0) ? primaryFocusPhysicalStream :
659                        primaryFocusContext;
660                if (DBG) {
661                    Log.d(TAG, "Change volume from: "
662                            + mCurrentCarContextVolume.get(oldContext)
663                            + " to: "+ currentVolume);
664                }
665                mHandler.sendMessage(mHandler.obtainMessage(MSG_UPDATE_HAL, carStreamNumber,
666                        currentVolume));
667                mHandler.sendMessage(mHandler.obtainMessage(MSG_SUPPRESS_UI_FOR_VOLUME,
668                        mCurrentContext, currentVolume));
669            }
670        }
671
672        @Override
673        public void dump(PrintWriter writer) {
674            writer.println("Volume controller:" +
675                    CarExternalVolumeController.class.getSimpleName());
676            synchronized (this) {
677                writer.println("mSupportedAudioContext:0x" +
678                        Integer.toHexString(mSupportedAudioContext) +
679                        ",mHasExternalMemory:" + mHasExternalMemory +
680                        ",mMasterVolumeOnly:" + mMasterVolumeOnly);
681                writer.println("mCurrentContext:0x" + Integer.toHexString(mCurrentContext));
682                writer.println("mCurrentCarContextVolume:");
683                dumpVolumes(writer, mCurrentCarContextVolume);
684                writer.println("mCarContextVolumeMax:");
685                dumpVolumes(writer, mCarContextVolumeMax);
686                writer.println("Number of volume controllers:" +
687                        mVolumeControllers.getRegisteredCallbackCount());
688            }
689        }
690
691        private void dumpVolumes(PrintWriter writer, SparseArray<Integer> array) {
692            for (int i = 0; i < array.size(); i++) {
693                writer.println("0x" + Integer.toHexString(array.keyAt(i)) + ":" + array.valueAt(i));
694            }
695        }
696    }
697}
698