plumbing for volume support

- add volume key listening to CarInputService
- separate volume property monitoring from AudioHalService
  with additional APIs for volume capability check
- add audio context listening to CarAudioService

bug: 27744968

Change-Id: I5cebe141115ce86dc07fa8d590874ff5bb9bf66b
diff --git a/service/src/com/android/car/CarAudioService.java b/service/src/com/android/car/CarAudioService.java
index 5019fb4..82e5610 100644
--- a/service/src/com/android/car/CarAudioService.java
+++ b/service/src/com/android/car/CarAudioService.java
@@ -30,7 +30,7 @@
 import android.util.Log;
 
 import com.android.car.hal.AudioHalService;
-import com.android.car.hal.AudioHalService.AudioHalListener;
+import com.android.car.hal.AudioHalService.AudioHalFocusListener;
 import com.android.car.hal.VehicleHal;
 import com.android.internal.annotations.GuardedBy;
 
@@ -38,7 +38,17 @@
 import java.util.LinkedList;
 
 
-public class CarAudioService extends ICarAudio.Stub implements CarServiceBase, AudioHalListener {
+public class CarAudioService extends ICarAudio.Stub implements CarServiceBase,
+        AudioHalFocusListener {
+
+    public interface AudioContextChangeListener {
+        /**
+         * Notifies the current primary audio context (app holding focus).
+         * If there is no active context, context will be 0.
+         * Will use context like CarAudioManager.CAR_AUDIO_USAGE_*
+         */
+        void onContextChange(int primaryFocusContext, int primaryFocusPhysicalStream);
+    }
 
     private static final long FOCUS_RESPONSE_WAIT_TIMEOUT_MS = 1000;
 
@@ -50,7 +60,6 @@
     private final Context mContext;
     private final HandlerThread mFocusHandlerThread;
     private final CarAudioFocusChangeHandler mFocusHandler;
-    private final CarAudioVolumeHandler mVolumeHandler;
     private final SystemFocusListener mSystemFocusListener;
     private AudioPolicy mAudioPolicy;
     private final Object mLock = new Object();
@@ -83,6 +92,14 @@
     private boolean mCallActive = false;
     @GuardedBy("mLock")
     private int mCurrentAudioContexts = 0;
+    @GuardedBy("mLock")
+    private int mCurrentPrimaryAudioContext = 0;
+    @GuardedBy("mLock")
+    private int mCurrentPrimaryPhysicalStream = 0;
+    @GuardedBy("mLock")
+    private AudioContextChangeListener mAudioContextChangeListener;
+    @GuardedBy("mLock")
+    private CarAudioContextChangeHandler mCarAudioContextChangeHandler;
 
     private final AudioAttributes mAttributeBottom =
             CarAudioAttributesUtil.getAudioAttributesForCarUsage(
@@ -98,7 +115,6 @@
         mSystemFocusListener = new SystemFocusListener();
         mFocusHandlerThread.start();
         mFocusHandler = new CarAudioFocusChangeHandler(mFocusHandlerThread.getLooper());
-        mVolumeHandler = new CarAudioVolumeHandler(Looper.getMainLooper());
         mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
     }
 
@@ -134,7 +150,7 @@
         if (r != 0) {
             throw new RuntimeException("registerAudioPolicy failed " + r);
         }
-        mAudioHal.setListener(this);
+        mAudioHal.setFocusListener(this);
         int audioHwVariant = mAudioHal.getHwVariant();
         mAudioRoutingPolicy = AudioRoutingPolicy.create(mContext, audioHwVariant);
         mAudioHal.setAudioRoutingPolicy(mAudioRoutingPolicy);
@@ -154,9 +170,27 @@
             mTopFocusInfo = null;
             mPendingFocusChanges.clear();
             mRadioActive = false;
+            if (mCarAudioContextChangeHandler != null) {
+                mCarAudioContextChangeHandler.cancelAll();
+                mCarAudioContextChangeHandler = null;
+            }
+            mAudioContextChangeListener = null;
+            mCurrentPrimaryAudioContext = 0;
         }
     }
 
+    public synchronized void setAudioContextChangeListener(Looper looper,
+            AudioContextChangeListener listener) {
+        if (looper == null || listener == null) {
+            throw new IllegalArgumentException("looper or listener null");
+        }
+        if (mCarAudioContextChangeHandler != null) {
+            mCarAudioContextChangeHandler.cancelAll();
+        }
+        mCarAudioContextChangeHandler = new CarAudioContextChangeHandler(looper);
+        mAudioContextChangeListener = listener;
+    }
+
     @Override
     public void dump(PrintWriter writer) {
         writer.println("*CarAudioService*");
@@ -164,6 +198,8 @@
                 " mLastFocusRequestToCar:" + mLastFocusRequestToCar);
         writer.println(" mCurrentAudioContexts:0x" + Integer.toHexString(mCurrentAudioContexts));
         writer.println(" mCallActive:" + mCallActive + " mRadioActive:" + mRadioActive);
+        writer.println(" mCurrentPrimaryAudioContext:" + mCurrentPrimaryAudioContext +
+                " mCurrentPrimaryPhysicalStream:" + mCurrentPrimaryPhysicalStream);
         mAudioRoutingPolicy.dump(writer);
     }
 
@@ -178,17 +214,6 @@
     }
 
     @Override
-    public void onVolumeChange(int streamNumber, int volume, int volumeState) {
-        mVolumeHandler.handleVolumeChange(new VolumeStateChangeEvent(streamNumber, volume,
-                volumeState));
-    }
-
-    @Override
-    public void onVolumeLimitChange(int streamNumber, int volume) {
-        //TODO
-    }
-
-    @Override
     public void onStreamStatusChange(int state, int streamNumber) {
         mFocusHandler.handleStreamStateChange(state, streamNumber);
     }
@@ -333,10 +358,6 @@
                 androidFocus, flags, mAudioPolicy);
     }
 
-    private void doHandleVolumeChange(VolumeStateChangeEvent event) {
-        //TODO
-    }
-
     private void doHandleStreamStatusChange(int streamNumber, int state) {
         //TODO
     }
@@ -430,6 +451,31 @@
         int logicalStreamTypeForTop = CarAudioAttributesUtil.getCarUsageFromAudioAttributes(attrib);
         int physicalStreamTypeForTop = mAudioRoutingPolicy.getPhysicalStreamForLogicalStream(
                 logicalStreamTypeForTop);
+
+        // update primary context and notify if necessary
+        int primaryContext = logicalStreamTypeForTop;
+        switch (logicalStreamTypeForTop) {
+            case CarAudioAttributesUtil.CAR_AUDIO_TYPE_CARSERVICE_BOTTOM:
+            case CarAudioAttributesUtil.CAR_AUDIO_TYPE_CARSERVICE_CAR_PROXY:
+                primaryContext = 0;
+                break;
+        }
+        if (mCurrentPrimaryAudioContext != primaryContext) {
+            mCurrentPrimaryAudioContext = primaryContext;
+            if (primaryContext == CarAudioManager.CAR_AUDIO_USAGE_RADIO) {
+                // policy does not know radio. treat it same as music.
+                mCurrentPrimaryPhysicalStream =
+                        mAudioRoutingPolicy.getPhysicalStreamForLogicalStream(
+                                CarAudioManager.CAR_AUDIO_USAGE_MUSIC);
+            } else {
+                mCurrentPrimaryPhysicalStream = physicalStreamTypeForTop;
+            }
+            if (mCarAudioContextChangeHandler != null) {
+                mCarAudioContextChangeHandler.requestContextChangeNotification(
+                        mAudioContextChangeListener, primaryContext, physicalStreamTypeForTop);
+            }
+        }
+
         int audioContexts = 0;
         if (logicalStreamTypeForTop == CarAudioManager.CAR_AUDIO_USAGE_VOICE_CALL) {
             if (!mCallActive) {
@@ -440,7 +486,8 @@
             if (mCallActive) {
                 mCallActive = false;
             }
-            audioContexts = AudioHalService.logicalStreamToHalContextType(logicalStreamTypeForTop);
+            audioContexts =
+                    AudioHalService.logicalStreamToHalContextType(logicalStreamTypeForTop);
         }
         // other apps having focus
         int focusToRequest = AudioHalService.VEHICLE_AUDIO_FOCUS_REQUEST_RELEASE;
@@ -748,6 +795,37 @@
         }
     }
 
+    private class CarAudioContextChangeHandler extends Handler {
+        private static final int MSG_CONTEXT_CHANGE = 0;
+
+        private CarAudioContextChangeHandler(Looper looper) {
+            super(looper);
+        }
+
+        private void requestContextChangeNotification(AudioContextChangeListener listener,
+                int primaryContext, int physicalStream) {
+            Message msg = obtainMessage(MSG_CONTEXT_CHANGE, primaryContext, physicalStream,
+                    listener);
+            sendMessage(msg);
+        }
+
+        private void cancelAll() {
+            removeMessages(MSG_CONTEXT_CHANGE);
+        }
+
+        @Override
+        public void handleMessage(Message msg) {
+            switch (msg.what) {
+                case MSG_CONTEXT_CHANGE: {
+                    AudioContextChangeListener listener = (AudioContextChangeListener) msg.obj;
+                    int context = msg.arg1;
+                    int physicalStream = msg.arg2;
+                    listener.onContextChange(context, physicalStream);
+                } break;
+            }
+        }
+    }
+
     private class CarAudioFocusChangeHandler extends Handler {
         private static final int MSG_FOCUS_CHANGE = 0;
         private static final int MSG_STREAM_STATE_CHANGE = 1;
@@ -815,40 +893,6 @@
         }
     }
 
-    private class CarAudioVolumeHandler extends Handler {
-        private static final int MSG_VOLUME_CHANGE = 0;
-
-        private CarAudioVolumeHandler(Looper looper) {
-            super(looper);
-        }
-
-        private void handleVolumeChange(VolumeStateChangeEvent event) {
-            Message msg = obtainMessage(MSG_VOLUME_CHANGE, event);
-            sendMessage(msg);
-        }
-
-        @Override
-        public void handleMessage(Message msg) {
-            switch (msg.what) {
-                case MSG_VOLUME_CHANGE:
-                    doHandleVolumeChange((VolumeStateChangeEvent) msg.obj);
-                    break;
-            }
-        }
-    }
-
-    private static class VolumeStateChangeEvent {
-        public final int stream;
-        public final int volume;
-        public final int state;
-
-        public VolumeStateChangeEvent(int stream, int volume, int state) {
-            this.stream = stream;
-            this.volume = volume;
-            this.state = state;
-        }
-    }
-
     /** Wrapper class for holding the current focus state from car. */
     private static class FocusState {
         public final int focusState;
diff --git a/service/src/com/android/car/CarInputService.java b/service/src/com/android/car/CarInputService.java
index 8b0bd3d..af7f38c 100644
--- a/service/src/com/android/car/CarInputService.java
+++ b/service/src/com/android/car/CarInputService.java
@@ -48,6 +48,8 @@
 
     private KeyEventListener mInstumentClusterKeyListener;
 
+    private KeyEventListener mVolumeKeyListener;
+
     private ParcelFileDescriptor mInjectionDeviceFd;
 
     private int mKeyEventCount = 0;
@@ -86,6 +88,12 @@
         }
     }
 
+    public void setVolumeKeyListener(KeyEventListener listener) {
+        synchronized (this) {
+            mVolumeKeyListener = listener;
+        }
+    }
+
     @Override
     public void init() {
         InputHalService hal = VehicleHal.getInstance().getInputHal();
@@ -136,6 +144,10 @@
             case KeyEvent.KEYCODE_VOICE_ASSIST:
                 handleVoiceAssistKey(event);
                 return;
+            case KeyEvent.KEYCODE_VOLUME_UP:
+            case KeyEvent.KEYCODE_VOLUME_DOWN:
+                handleVolumeKey(event);
+                return;
             default:
                 break;
         }
@@ -197,6 +209,17 @@
         listener.onKeyEvent(event);
     }
 
+    private void handleVolumeKey(KeyEvent event) {
+        KeyEventListener listener = null;
+        synchronized (this) {
+            listener = mVolumeKeyListener;
+        }
+        if (listener == null) {
+            return;
+        }
+        listener.onKeyEvent(event);
+    }
+
     private void handleMainDisplayKey(KeyEvent event) {
         int fd;
         synchronized (this) {
diff --git a/service/src/com/android/car/hal/AudioHalService.java b/service/src/com/android/car/hal/AudioHalService.java
index 893fbff..16d3d38 100644
--- a/service/src/com/android/car/hal/AudioHalService.java
+++ b/service/src/com/android/car/hal/AudioHalService.java
@@ -33,7 +33,9 @@
 import com.android.car.vehiclenetwork.VehicleNetworkConsts.VehicleAudioRoutingPolicyIndex;
 import com.android.car.vehiclenetwork.VehicleNetworkConsts.VehicleAudioStreamState;
 import com.android.car.vehiclenetwork.VehicleNetworkConsts.VehicleAudioStreamStateIndex;
+import com.android.car.vehiclenetwork.VehicleNetworkConsts.VehicleAudioVolumeCapabilityFlag;
 import com.android.car.vehiclenetwork.VehicleNetworkConsts.VehicleAudioVolumeIndex;
+import com.android.car.vehiclenetwork.VehicleNetworkConsts.VehicleAudioVolumeLimitIndex;
 import com.android.car.vehiclenetwork.VehicleNetworkProto.VehiclePropConfig;
 import com.android.car.vehiclenetwork.VehicleNetworkProto.VehiclePropConfigs;
 import com.android.car.vehiclenetwork.VehicleNetworkProto.VehiclePropValue;
@@ -126,7 +128,7 @@
     public static final int AUDIO_CONTEXT_SYSTEM_SOUND_FLAG =
             VehicleAudioContextFlag.VEHICLE_AUDIO_CONTEXT_SYSTEM_SOUND_FLAG;
 
-    public interface AudioHalListener {
+    public interface AudioHalFocusListener {
         /**
          * Audio focus change from car.
          * @param focusState
@@ -136,6 +138,15 @@
          */
         void onFocusChange(int focusState, int streams, int externalFocus);
         /**
+         * Stream state change (start / stop) from android
+         * @param streamNumber
+         * @param state
+         */
+        void onStreamStatusChange(int streamNumber, int state);
+    }
+
+    public interface AudioHalVolumeListener {
+        /**
          * Audio volume change from car.
          * @param streamNumber
          * @param volume
@@ -148,16 +159,11 @@
          * @param volume
          */
         void onVolumeLimitChange(int streamNumber, int volume);
-        /**
-         * Stream state change (start / stop) from android
-         * @param streamNumber
-         * @param state
-         */
-        void onStreamStatusChange(int streamNumber, int state);
     }
 
     private final VehicleHal mVehicleHal;
-    private AudioHalListener mListener;
+    private AudioHalFocusListener mFocusListener;
+    private AudioHalVolumeListener mVolumeListener;
     private int mVariant;
 
     private List<VehiclePropValue> mQueuedEvents;
@@ -168,17 +174,41 @@
         mVehicleHal = vehicleHal;
     }
 
-    public void setListener(AudioHalListener listener) {
+    public void setFocusListener(AudioHalFocusListener focusListener) {
         List<VehiclePropValue> eventsToDispatch = null;
+        AudioHalVolumeListener volumeListener;
+        boolean shouldDispatch = false;
         synchronized (this) {
-            mListener = listener;
-            if (mQueuedEvents != null) {
+            mFocusListener = focusListener;
+            volumeListener = mVolumeListener;
+            shouldDispatch = canDispatchEventLocked();
+            if (shouldDispatch) {
+                if (mQueuedEvents != null) {
+                    eventsToDispatch = mQueuedEvents;
+                    mQueuedEvents = null;
+                }
+            }
+        }
+        if (shouldDispatch && eventsToDispatch != null) {
+            dispatchEventToListener(focusListener, volumeListener, eventsToDispatch);
+        }
+    }
+
+    public void setVolumeListener(AudioHalVolumeListener volumeListener) {
+        List<VehiclePropValue> eventsToDispatch = null;
+        AudioHalFocusListener focusListener;
+        boolean shouldDispatch = false;
+        synchronized (this) {
+            mVolumeListener = volumeListener;
+            focusListener = mFocusListener;
+            shouldDispatch = canDispatchEventLocked();
+            if (shouldDispatch) {
                 eventsToDispatch = mQueuedEvents;
                 mQueuedEvents = null;
             }
         }
-        if (eventsToDispatch != null) {
-            dispatchEventToListener(listener, eventsToDispatch);
+        if (shouldDispatch && eventsToDispatch != null) {
+            dispatchEventToListener(focusListener, volumeListener, eventsToDispatch);
         }
     }
 
@@ -270,6 +300,44 @@
         return isPropertySupportedLocked(VehicleNetworkConsts.VEHICLE_PROPERTY_AUDIO_FOCUS);
     }
 
+    public synchronized boolean isAudioVolumeSupported() {
+        return isPropertySupportedLocked(VehicleNetworkConsts.VEHICLE_PROPERTY_AUDIO_VOLUME);
+    }
+
+    public synchronized int getSupportedAudioVolumeContexts() {
+        if (!isPropertySupportedLocked(VehicleNetworkConsts.VEHICLE_PROPERTY_AUDIO_VOLUME)) {
+            throw new IllegalStateException("VEHICLE_PROPERTY_AUDIO_VOLUME not supported");
+        }
+        VehiclePropConfig config = mProperties.get(
+                VehicleNetworkConsts.VEHICLE_PROPERTY_AUDIO_VOLUME);
+        return config.getConfigArray(0);
+    }
+
+    /**
+     * Whether external audio module can memorize logical audio volumes or not.
+     * @return
+     */
+    public synchronized boolean isExternalAudioVolumePersistent() {
+        if (!isPropertySupportedLocked(VehicleNetworkConsts.VEHICLE_PROPERTY_AUDIO_VOLUME)) {
+            throw new IllegalStateException("VEHICLE_PROPERTY_AUDIO_VOLUME not supported");
+        }
+        VehiclePropConfig config = mProperties.get(
+                VehicleNetworkConsts.VEHICLE_PROPERTY_AUDIO_VOLUME);
+        if (config.getConfigArray(0) == 0) { // physical streams only
+            return false;
+        }
+        if ((config.getConfigArray(1) &
+                VehicleAudioVolumeCapabilityFlag.VEHICLE_AUDIO_VOLUME_CAPABILITY_PERSISTENT_STORAGE)
+                != 0) {
+            return true;
+        }
+        return false;
+    }
+
+    public synchronized boolean isAudioVolumeLimitSupported() {
+        return isPropertySupportedLocked(VehicleNetworkConsts.VEHICLE_PROPERTY_AUDIO_VOLUME_LIMIT);
+    }
+
     /**
      * Get the current audio focus state.
      * @return 0: focusState, 1: streams, 2: externalFocus
@@ -340,22 +408,38 @@
 
     @Override
     public void handleHalEvents(List<VehiclePropValue> values) {
-        AudioHalListener listener = null;
+        AudioHalFocusListener focusListener = null;
+        AudioHalVolumeListener volumeListener = null;
+        boolean shouldDispatch = false;
         synchronized (this) {
-            listener = mListener;
-            if (listener == null) {
+            focusListener = mFocusListener;
+            volumeListener = mVolumeListener;
+            shouldDispatch = canDispatchEventLocked();
+            if (!shouldDispatch) {
                 if (mQueuedEvents == null) {
                     mQueuedEvents = new LinkedList<VehiclePropValue>();
                 }
                 mQueuedEvents.addAll(values);
             }
         }
-        if (listener != null) {
-            dispatchEventToListener(listener, values);
+        if (shouldDispatch) {
+            dispatchEventToListener(focusListener, volumeListener, values);
         }
     }
 
-    private void dispatchEventToListener(AudioHalListener listener, List<VehiclePropValue> values) {
+    private boolean canDispatchEventLocked() {
+        if (mFocusListener == null && isFocusSupported()) {
+            return false;
+        }
+        if (mVolumeListener == null && isAudioVolumeSupported()) {
+            return false;
+        }
+        return true;
+    }
+
+    private void dispatchEventToListener(AudioHalFocusListener focusListener,
+            AudioHalVolumeListener volumeListener,
+            List<VehiclePropValue> values) {
         for (VehiclePropValue v : values) {
             switch (v.getProp()) {
                 case VehicleNetworkConsts.VEHICLE_PROPERTY_AUDIO_FOCUS: {
@@ -365,7 +449,7 @@
                             VehicleAudioFocusIndex.VEHICLE_AUDIO_FOCUS_INDEX_STREAMS);
                     int externalFocus = v.getInt32Values(
                             VehicleAudioFocusIndex.VEHICLE_AUDIO_FOCUS_INDEX_EXTERNAL_FOCUS_STATE);
-                    listener.onFocusChange(focusState, streams, externalFocus);
+                    focusListener.onFocusChange(focusState, streams, externalFocus);
                 } break;
                 case VehicleNetworkConsts.VEHICLE_PROPERTY_AUDIO_VOLUME: {
                     int volume = v.getInt32Values(
@@ -374,20 +458,25 @@
                             VehicleAudioVolumeIndex.VEHICLE_AUDIO_VOLUME_INDEX_STREAM);
                     int volumeState = v.getInt32Values(
                             VehicleAudioVolumeIndex.VEHICLE_AUDIO_VOLUME_INDEX_STATE);
-                    listener.onVolumeChange(streamNum, volume, volumeState);
+                    volumeListener.onVolumeChange(streamNum, volume, volumeState);
                 } break;
                 case VehicleNetworkConsts.VEHICLE_PROPERTY_AUDIO_VOLUME_LIMIT: {
-                    //TODO
+                    int stream = v.getInt32Values(
+                            VehicleAudioVolumeLimitIndex.VEHICLE_AUDIO_VOLUME_LIMIT_INDEX_STREAM);
+                    int maxVolume = v.getInt32Values(
+                            VehicleAudioVolumeLimitIndex.VEHICLE_AUDIO_VOLUME_LIMIT_INDEX_MAX_VOLUME);
+                    volumeListener.onVolumeLimitChange(stream, maxVolume);
                 } break;
                 case VehicleNetworkConsts.VEHICLE_PROPERTY_INTERNAL_AUDIO_STREAM_STATE: {
                     int state = v.getInt32Values(
                             VehicleAudioStreamStateIndex.VEHICLE_AUDIO_STREAM_STATE_INDEX_STATE);
                     int streamNum = v.getInt32Values(
                             VehicleAudioStreamStateIndex.VEHICLE_AUDIO_STREAM_STATE_INDEX_STREAM);
-                    listener.onStreamStatusChange(streamNum, state);
+                    focusListener.onStreamStatusChange(streamNum, state);
                 } break;
             }
         }
+        values.clear();
     }
 
     @Override