1 /*
2  * Copyright (C) 2021 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 
17 package com.android.systemui.car.volume;
18 
19 import static android.car.media.CarAudioManager.AUDIO_FEATURE_VOLUME_GROUP_EVENTS;
20 import static android.car.media.CarAudioManager.AUDIO_FEATURE_VOLUME_GROUP_MUTING;
21 import static android.car.media.CarAudioManager.INVALID_AUDIO_ZONE;
22 import static android.car.media.CarAudioManager.PRIMARY_AUDIO_ZONE;
23 import static android.car.media.CarVolumeGroupEvent.EVENT_TYPE_MUTE_CHANGED;
24 import static android.car.media.CarVolumeGroupEvent.EVENT_TYPE_VOLUME_GAIN_INDEX_CHANGED;
25 import static android.car.media.CarVolumeGroupEvent.EVENT_TYPE_VOLUME_MAX_INDEX_CHANGED;
26 import static android.car.media.CarVolumeGroupEvent.EXTRA_INFO_SHOW_UI;
27 import static android.car.media.CarVolumeGroupEvent.EXTRA_INFO_VOLUME_INDEX_CHANGED_BY_AUDIO_SYSTEM;
28 
29 import android.animation.Animator;
30 import android.animation.AnimatorInflater;
31 import android.animation.AnimatorSet;
32 import android.animation.ObjectAnimator;
33 import android.animation.PropertyValuesHolder;
34 import android.annotation.DrawableRes;
35 import android.annotation.Nullable;
36 import android.app.Dialog;
37 import android.app.KeyguardManager;
38 import android.app.UiModeManager;
39 import android.car.Car;
40 import android.car.CarOccupantZoneManager;
41 import android.car.media.CarAudioManager;
42 import android.car.media.CarVolumeGroupEvent;
43 import android.car.media.CarVolumeGroupEventCallback;
44 import android.car.media.CarVolumeGroupInfo;
45 import android.content.BroadcastReceiver;
46 import android.content.Context;
47 import android.content.DialogInterface;
48 import android.content.Intent;
49 import android.content.IntentFilter;
50 import android.content.res.Configuration;
51 import android.content.res.TypedArray;
52 import android.content.res.XmlResourceParser;
53 import android.graphics.Color;
54 import android.graphics.PixelFormat;
55 import android.graphics.drawable.ColorDrawable;
56 import android.graphics.drawable.Drawable;
57 import android.os.Build;
58 import android.os.Debug;
59 import android.os.Handler;
60 import android.os.Looper;
61 import android.os.Message;
62 import android.util.AttributeSet;
63 import android.util.Log;
64 import android.util.SparseArray;
65 import android.util.Xml;
66 import android.view.Gravity;
67 import android.view.MotionEvent;
68 import android.view.View;
69 import android.view.ViewGroup;
70 import android.view.Window;
71 import android.view.WindowManager;
72 import android.widget.SeekBar;
73 import android.widget.SeekBar.OnSeekBarChangeListener;
74 
75 import androidx.recyclerview.widget.LinearLayoutManager;
76 import androidx.recyclerview.widget.RecyclerView;
77 
78 import com.android.systemui.R;
79 import com.android.systemui.car.CarServiceProvider;
80 import com.android.systemui.plugins.VolumeDialog;
81 import com.android.systemui.settings.UserTracker;
82 import com.android.systemui.statusbar.policy.ConfigurationController;
83 import com.android.systemui.volume.Events;
84 import com.android.systemui.volume.SystemUIInterpolators;
85 import com.android.systemui.volume.VolumeDialogImpl;
86 
87 import org.xmlpull.v1.XmlPullParserException;
88 
89 import java.io.IOException;
90 import java.util.ArrayList;
91 import java.util.List;
92 import java.util.concurrent.Executor;
93 
94 /**
95  * Car version of the volume dialog.
96  *
97  * Methods ending in "H" must be called on the (ui) handler.
98  */
99 public class CarVolumeDialogImpl
100         implements VolumeDialog, ConfigurationController.ConfigurationListener {
101 
102     private static final String TAG = "CarVolumeDialog";
103     private static final boolean DEBUG = Build.IS_USERDEBUG || Build.IS_ENG;
104 
105     private static final String XML_TAG_VOLUME_ITEMS = "carVolumeItems";
106     private static final String XML_TAG_VOLUME_ITEM = "item";
107     private static final int LISTVIEW_ANIMATION_DURATION_IN_MILLIS = 250;
108     private static final int DISMISS_DELAY_IN_MILLIS = 50;
109     private static final int ARROW_FADE_IN_START_DELAY_IN_MILLIS = 100;
110     private static final int INVALID_INDEX = -1;
111 
112     private final Context mContext;
113     private final H mHandler = new H();
114     // All the volume items.
115     private final SparseArray<VolumeItem> mVolumeItems = new SparseArray<>();
116     // Available volume items in car audio manager.
117     private final List<VolumeItem> mAvailableVolumeItems = new ArrayList<>();
118     // Volume items in the RecyclerView.
119     private final List<CarVolumeItem> mCarVolumeLineItems = new ArrayList<>();
120     private final KeyguardManager mKeyguard;
121     private final int mNormalTimeout;
122     private final int mHoveringTimeout;
123     private final int mExpNormalTimeout;
124     private final int mExpHoveringTimeout;
125     private final CarServiceProvider mCarServiceProvider;
126     private final ConfigurationController mConfigurationController;
127     private final UserTracker mUserTracker;
128     private final UiModeManager mUiModeManager;
129     private final Executor mExecutor;
130 
131     private Window mWindow;
132     private CustomDialog mDialog;
133     private RecyclerView mListView;
134     private CarVolumeItemAdapter mVolumeItemsAdapter;
135     private CarAudioManager mCarAudioManager;
136     private int mAudioZoneId = INVALID_AUDIO_ZONE;
137     private boolean mHovering;
138     private int mCurrentlyDisplayingGroupId;
139     private int mPreviouslyDisplayingGroupId;
140     private boolean mDismissing;
141     private boolean mExpanded;
142     private View mExpandIcon;
143     private boolean mHomeButtonPressedBroadcastReceiverRegistered;
144     private boolean mIsUiModeNight;
145 
146     private final CarAudioManager.CarVolumeCallback mVolumeChangeCallback =
147             new CarAudioManager.CarVolumeCallback() {
148                 @Override
149                 public void onGroupVolumeChanged(int zoneId, int groupId, int flags) {
150                     updateVolumeAndMute(zoneId, groupId, flags,
151                             EVENT_TYPE_VOLUME_GAIN_INDEX_CHANGED);
152                 }
153 
154                 @Override
155                 public void onMasterMuteChanged(int zoneId, int flags) {
156                     // ignored
157                 }
158 
159                 @Override
160                 public void onGroupMuteChanged(int zoneId, int groupId, int flags) {
161                     updateVolumeAndMute(zoneId, groupId, flags, EVENT_TYPE_MUTE_CHANGED);
162                 }
163 
164                 private void updateVolumeAndMute(int zoneId, int groupId, int flags,
165                         int eventTypes) {
166                     if (zoneId != mAudioZoneId) {
167                         return;
168                     }
169                     List<Integer> extraInfos = CarVolumeGroupEvent.convertFlagsToExtraInfo(flags,
170                             eventTypes);
171                     if (mCarAudioManager != null) {
172                         CarVolumeGroupInfo carVolumeGroupInfo =
173                                 mCarAudioManager.getVolumeGroupInfo(zoneId, groupId);
174                         boolean isMuted;
175                         int currentIndex;
176                         int maxIndex = INVALID_INDEX;
177                         if (carVolumeGroupInfo != null) {
178                             isMuted = carVolumeGroupInfo.isMuted();
179                             maxIndex = carVolumeGroupInfo.getMaxVolumeGainIndex();
180                             currentIndex = carVolumeGroupInfo.getVolumeGainIndex();
181                         } else {
182                             isMuted = isGroupMuted(mCarAudioManager, zoneId, groupId);
183                             currentIndex = getSeekbarValue(mCarAudioManager, zoneId, groupId);
184                         }
185                         updateVolumePreference(groupId, maxIndex, currentIndex, isMuted, eventTypes,
186                                 extraInfos);
187                     }
188                 }
189             };
190 
191     private final CarVolumeGroupEventCallback mCarVolumeGroupEventCallback =
192             new CarVolumeGroupEventCallback() {
193                 @Override
194                 public void onVolumeGroupEvent(List<CarVolumeGroupEvent> volumeGroupEvents) {
195                     updateVolumeGroupForEvents(volumeGroupEvents);
196                 }
197             };
198 
199     private final CarServiceProvider.CarServiceOnConnectedListener mCarServiceOnConnectedListener =
200             new CarServiceProvider.CarServiceOnConnectedListener() {
201                 @Override
202                 public void onConnected(Car car) {
203                     mExpanded = false;
204                     CarOccupantZoneManager carOccupantZoneManager =
205                             (CarOccupantZoneManager) car.getCarManager(
206                                     Car.CAR_OCCUPANT_ZONE_SERVICE);
207                     if (carOccupantZoneManager != null) {
208                         CarOccupantZoneManager.OccupantZoneInfo info =
209                                 carOccupantZoneManager.getOccupantZoneForUser(
210                                         mUserTracker.getUserHandle());
211                         if (info != null) {
212                             mAudioZoneId = carOccupantZoneManager.getAudioZoneIdForOccupant(info);
213                         }
214                     }
215                     if (mAudioZoneId == INVALID_AUDIO_ZONE) {
216                         // No audio zone found in occupant zone mapping - default to primary zone
217                         mAudioZoneId = PRIMARY_AUDIO_ZONE;
218                     }
219                     mCarAudioManager = (CarAudioManager) car.getCarManager(Car.AUDIO_SERVICE);
220                     if (mCarAudioManager != null) {
221                         int volumeGroupCount = mCarAudioManager.getVolumeGroupCount(mAudioZoneId);
222                         // Populates volume slider items from volume groups to UI.
223                         for (int groupId = 0; groupId < volumeGroupCount; groupId++) {
224                             VolumeItem volumeItem = getVolumeItemForUsages(
225                                     mCarAudioManager.getUsagesForVolumeGroupId(mAudioZoneId,
226                                             groupId));
227                             mAvailableVolumeItems.add(volumeItem);
228                             // The first one is the default item.
229                             if (groupId == 0) {
230                                 clearAllAndSetupDefaultCarVolumeLineItem(0);
231                             }
232                         }
233 
234                         // If list is already initiated, update its content.
235                         if (mVolumeItemsAdapter != null) {
236                             mVolumeItemsAdapter.notifyDataSetChanged();
237                         }
238 
239                         // if volume group events are enabled, use it. Else fallback to the legacy
240                         // volume group callbacks.
241                         if (mCarAudioManager.isAudioFeatureEnabled(
242                                 AUDIO_FEATURE_VOLUME_GROUP_EVENTS)) {
243                             mCarAudioManager.registerCarVolumeGroupEventCallback(mExecutor,
244                                     mCarVolumeGroupEventCallback);
245                         } else {
246                             mCarAudioManager.registerCarVolumeCallback(mVolumeChangeCallback);
247                         }
248                     }
249                 }
250             };
251 
252     private final BroadcastReceiver mHomeButtonPressedBroadcastReceiver = new BroadcastReceiver() {
253         @Override
254         public void onReceive(Context context, Intent intent) {
255             if (!intent.getAction().equals(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)) {
256                 return;
257             }
258 
259             dismissH(Events.DISMISS_REASON_VOLUME_CONTROLLER);
260         }
261     };
262 
263     private final UserTracker.Callback mUserTrackerCallback = new UserTracker.Callback() {
264         @Override
265         public void onUserChanged(int newUser, Context userContext) {
266             if (mHomeButtonPressedBroadcastReceiverRegistered) {
267                 mContext.unregisterReceiver(mHomeButtonPressedBroadcastReceiver);
268                 mContext.registerReceiverAsUser(mHomeButtonPressedBroadcastReceiver,
269                         mUserTracker.getUserHandle(),
270                         new IntentFilter(Intent.ACTION_CLOSE_SYSTEM_DIALOGS),
271                         /* broadcastPermission= */ null, /* scheduler= */ null,
272                         Context.RECEIVER_EXPORTED);
273             }
274         }
275     };
276 
CarVolumeDialogImpl( Context context, CarServiceProvider carServiceProvider, ConfigurationController configurationController, UserTracker userTracker)277     public CarVolumeDialogImpl(
278             Context context,
279             CarServiceProvider carServiceProvider,
280             ConfigurationController configurationController,
281             UserTracker userTracker) {
282         mContext = context;
283         mCarServiceProvider = carServiceProvider;
284         mUserTracker = userTracker;
285         mKeyguard = (KeyguardManager) mContext.getSystemService(Context.KEYGUARD_SERVICE);
286         mNormalTimeout = mContext.getResources().getInteger(
287                 R.integer.car_volume_dialog_display_normal_timeout);
288         mHoveringTimeout = mContext.getResources().getInteger(
289                 R.integer.car_volume_dialog_display_hovering_timeout);
290         mExpNormalTimeout = mContext.getResources().getInteger(
291                 R.integer.car_volume_dialog_display_expanded_normal_timeout);
292         mExpHoveringTimeout = mContext.getResources().getInteger(
293                 R.integer.car_volume_dialog_display_expanded_hovering_timeout);
294         mConfigurationController = configurationController;
295         mUiModeManager = mContext.getSystemService(UiModeManager.class);
296         mIsUiModeNight = mContext.getResources().getConfiguration().isNightModeActive();
297         mExecutor = context.getMainExecutor();
298     }
299 
getSeekbarValue(CarAudioManager carAudioManager, int volumeZoneId, int volumeGroupId)300     private static int getSeekbarValue(CarAudioManager carAudioManager, int volumeZoneId,
301             int volumeGroupId) {
302         return carAudioManager.getGroupVolume(volumeZoneId, volumeGroupId);
303     }
304 
isGroupMuted(CarAudioManager carAudioManager, int volumeZoneId, int volumeGroupId)305     private static boolean isGroupMuted(CarAudioManager carAudioManager, int volumeZoneId,
306             int volumeGroupId) {
307         if (!carAudioManager.isAudioFeatureEnabled(AUDIO_FEATURE_VOLUME_GROUP_MUTING)) {
308             return false;
309         }
310         return carAudioManager.isVolumeGroupMuted(volumeZoneId, volumeGroupId);
311     }
312 
getMaxSeekbarValue(CarAudioManager carAudioManager, int volumeZoneId, int volumeGroupId)313     private static int getMaxSeekbarValue(CarAudioManager carAudioManager, int volumeZoneId,
314             int volumeGroupId) {
315         return carAudioManager.getGroupMaxVolume(volumeZoneId, volumeGroupId);
316     }
317 
318     /**
319      * Build the volume window and connect to the CarService which registers with car audio
320      * manager.
321      */
322     @Override
init(int windowType, Callback callback)323     public void init(int windowType, Callback callback) {
324         initDialog();
325 
326         // The VolumeDialog is not initialized until the first volume change for a particular zone
327         // (to improve boot time by deferring initialization). Therefore, the dialog should be shown
328         // on init to handle the first audio change.
329         mHandler.obtainMessage(H.SHOW, Events.SHOW_REASON_VOLUME_CHANGED).sendToTarget();
330 
331         mCarServiceProvider.addListener(mCarServiceOnConnectedListener);
332         mContext.registerReceiverAsUser(mHomeButtonPressedBroadcastReceiver,
333                 mUserTracker.getUserHandle(), new IntentFilter(Intent.ACTION_CLOSE_SYSTEM_DIALOGS),
334                 /* broadcastPermission= */ null, /* scheduler= */ null, Context.RECEIVER_EXPORTED);
335         mHomeButtonPressedBroadcastReceiverRegistered = true;
336         mUserTracker.addCallback(mUserTrackerCallback, mContext.getMainExecutor());
337         mConfigurationController.addCallback(this);
338     }
339 
340     @Override
destroy()341     public void destroy() {
342         mHandler.removeCallbacksAndMessages(/* token= */ null);
343 
344         mUserTracker.removeCallback(mUserTrackerCallback);
345         mContext.unregisterReceiver(mHomeButtonPressedBroadcastReceiver);
346         mHomeButtonPressedBroadcastReceiverRegistered = false;
347 
348         cleanupAudioManager();
349         mConfigurationController.removeCallback(this);
350     }
351 
352     @Override
onLayoutDirectionChanged(boolean isLayoutRtl)353     public void onLayoutDirectionChanged(boolean isLayoutRtl) {
354         if (mListView != null) {
355             mListView.setLayoutDirection(
356                     isLayoutRtl ? View.LAYOUT_DIRECTION_RTL : View.LAYOUT_DIRECTION_LTR);
357         }
358     }
359 
360     @Override
onConfigChanged(Configuration newConfig)361     public void onConfigChanged(Configuration newConfig) {
362         ConfigurationController.ConfigurationListener.super.onConfigChanged(newConfig);
363         boolean isConfigNightMode = newConfig.isNightModeActive();
364 
365         if (isConfigNightMode != mIsUiModeNight) {
366             mIsUiModeNight = isConfigNightMode;
367             mUiModeManager.setNightModeActivated(mIsUiModeNight);
368             // Call notifyDataSetChanged to force trigger the mVolumeItemsAdapter#onBindViewHolder
369             // and reset items background color. notify() or invalidate() don't work here.
370             mVolumeItemsAdapter.notifyDataSetChanged();
371         }
372     }
373 
374     /**
375      * Reveals volume dialog.
376      */
show(int reason)377     public void show(int reason) {
378         mHandler.obtainMessage(H.SHOW, reason).sendToTarget();
379     }
380 
381     /**
382      * Hides volume dialog.
383      */
dismiss(int reason)384     public void dismiss(int reason) {
385         mHandler.obtainMessage(H.DISMISS, reason).sendToTarget();
386     }
387 
initDialog()388     private void initDialog() {
389         loadAudioUsageItems();
390         mCarVolumeLineItems.clear();
391         mDialog = new CustomDialog(mContext);
392 
393         mHovering = false;
394         mDismissing = false;
395         mExpanded = false;
396         mWindow = mDialog.getWindow();
397         mWindow.requestFeature(Window.FEATURE_NO_TITLE);
398         mWindow.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
399         mWindow.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND
400                 | WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR);
401         mWindow.addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
402                 | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
403                 | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
404                 | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
405                 | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
406                 | WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED);
407         mWindow.setType(WindowManager.LayoutParams.TYPE_VOLUME_OVERLAY);
408         mWindow.setWindowAnimations(com.android.internal.R.style.Animation_Toast);
409         final WindowManager.LayoutParams lp = mWindow.getAttributes();
410         lp.format = PixelFormat.TRANSLUCENT;
411         lp.setTitle(VolumeDialogImpl.class.getSimpleName());
412         lp.gravity = Gravity.TOP | Gravity.CENTER_HORIZONTAL;
413         lp.windowAnimations = -1;
414         mWindow.setAttributes(lp);
415 
416         mDialog.setContentView(R.layout.car_volume_dialog);
417         mWindow.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
418 
419         mDialog.setCanceledOnTouchOutside(true);
420         mDialog.setOnShowListener(dialog -> {
421             mListView.setTranslationY(-mListView.getHeight());
422             mListView.setAlpha(0);
423             PropertyValuesHolder pvhAlpha = PropertyValuesHolder.ofFloat(View.ALPHA, 1f);
424             PropertyValuesHolder pvhY = PropertyValuesHolder.ofFloat(View.TRANSLATION_Y, 0f);
425             ObjectAnimator showAnimator = ObjectAnimator.ofPropertyValuesHolder(mListView, pvhAlpha,
426                     pvhY);
427             showAnimator.setDuration(LISTVIEW_ANIMATION_DURATION_IN_MILLIS);
428             showAnimator.setInterpolator(new SystemUIInterpolators.LogDecelerateInterpolator());
429             showAnimator.start();
430         });
431         mListView = mWindow.findViewById(R.id.volume_list);
432         mListView.setOnHoverListener((v, event) -> {
433             int action = event.getActionMasked();
434             mHovering = (action == MotionEvent.ACTION_HOVER_ENTER)
435                     || (action == MotionEvent.ACTION_HOVER_MOVE);
436             rescheduleTimeoutH();
437             return true;
438         });
439 
440         mVolumeItemsAdapter = new CarVolumeItemAdapter(mContext, mCarVolumeLineItems);
441         mListView.setAdapter(mVolumeItemsAdapter);
442         mListView.setLayoutManager(new LinearLayoutManager(mContext));
443     }
444 
445 
showH(int reason)446     private void showH(int reason) {
447         if (mCarAudioManager == null) {
448             Log.w(TAG, "cannot show dialog - car audio manager is null");
449             return;
450         }
451 
452         if (DEBUG) {
453             Log.d(TAG, "showH r=" + Events.DISMISS_REASONS[reason]);
454         }
455 
456         mHandler.removeMessages(H.SHOW);
457         mHandler.removeMessages(H.DISMISS);
458 
459         rescheduleTimeoutH();
460 
461         // Refresh the data set before showing.
462         mVolumeItemsAdapter.notifyDataSetChanged();
463 
464         if (mDialog.isShowing()) {
465             if (mPreviouslyDisplayingGroupId == mCurrentlyDisplayingGroupId || mExpanded) {
466                 return;
467             }
468 
469             clearAllAndSetupDefaultCarVolumeLineItem(mCurrentlyDisplayingGroupId);
470             return;
471         }
472 
473         clearAllAndSetupDefaultCarVolumeLineItem(mCurrentlyDisplayingGroupId);
474         mDismissing = false;
475         mDialog.show();
476         Events.writeEvent(Events.EVENT_SHOW_DIALOG, reason, mKeyguard.isKeyguardLocked());
477     }
478 
clearAllAndSetupDefaultCarVolumeLineItem(int groupId)479     private void clearAllAndSetupDefaultCarVolumeLineItem(int groupId) {
480         mCarVolumeLineItems.clear();
481         if (groupId >= mAvailableVolumeItems.size()) {
482             Log.w(TAG, "group id not in available volume items");
483             return;
484         }
485         VolumeItem volumeItem = mAvailableVolumeItems.get(groupId);
486         volumeItem.mDefaultItem = true;
487         addCarVolumeListItem(volumeItem, mAudioZoneId, /* volumeGroupId = */ groupId,
488                 R.drawable.car_ic_keyboard_arrow_down, new ExpandIconListener());
489     }
490 
rescheduleTimeoutH()491     protected void rescheduleTimeoutH() {
492         mHandler.removeMessages(H.DISMISS);
493         final int timeout = computeTimeoutH();
494         mHandler.sendMessageDelayed(mHandler
495                 .obtainMessage(H.DISMISS, Events.DISMISS_REASON_TIMEOUT), timeout);
496 
497         if (DEBUG) {
498             Log.d(TAG, "rescheduleTimeout " + timeout + " " + Debug.getCaller());
499         }
500     }
501 
computeTimeoutH()502     private int computeTimeoutH() {
503         if (mExpanded) {
504             return mHovering ? mExpHoveringTimeout : mExpNormalTimeout;
505         } else {
506             return mHovering ? mHoveringTimeout : mNormalTimeout;
507         }
508     }
509 
dismissH(int reason)510     private void dismissH(int reason) {
511         if (DEBUG) {
512             Log.d(TAG, "dismissH r=" + Events.DISMISS_REASONS[reason]);
513         }
514 
515         mHandler.removeMessages(H.DISMISS);
516         mHandler.removeMessages(H.SHOW);
517         if (!mDialog.isShowing() || mDismissing) {
518             return;
519         }
520 
521         PropertyValuesHolder pvhAlpha = PropertyValuesHolder.ofFloat(View.ALPHA, 0f);
522         PropertyValuesHolder pvhY = PropertyValuesHolder.ofFloat(View.TRANSLATION_Y,
523                 (float) -mListView.getHeight());
524         ObjectAnimator dismissAnimator = ObjectAnimator.ofPropertyValuesHolder(mListView, pvhAlpha,
525                 pvhY);
526         dismissAnimator.setDuration(LISTVIEW_ANIMATION_DURATION_IN_MILLIS);
527         dismissAnimator.setInterpolator(new SystemUIInterpolators.LogAccelerateInterpolator());
528         dismissAnimator.addListener(new DismissAnimationListener());
529         dismissAnimator.start();
530 
531         Events.writeEvent(Events.EVENT_DISMISS_DIALOG, reason);
532     }
533 
loadAudioUsageItems()534     private void loadAudioUsageItems() {
535         if (DEBUG) {
536             Log.i(TAG, "loadAudioUsageItems start");
537         }
538 
539         try (XmlResourceParser parser = mContext.getResources().getXml(R.xml.car_volume_items)) {
540             AttributeSet attrs = Xml.asAttributeSet(parser);
541             int type;
542             // Traverse to the first start tag
543             while ((type = parser.next()) != XmlResourceParser.END_DOCUMENT
544                     && type != XmlResourceParser.START_TAG) {
545                 // Do Nothing (moving parser to start element)
546             }
547 
548             if (!XML_TAG_VOLUME_ITEMS.equals(parser.getName())) {
549                 throw new RuntimeException("Meta-data does not start with carVolumeItems tag");
550             }
551             int outerDepth = parser.getDepth();
552             int rank = 0;
553             while ((type = parser.next()) != XmlResourceParser.END_DOCUMENT
554                     && (type != XmlResourceParser.END_TAG || parser.getDepth() > outerDepth)) {
555                 if (type == XmlResourceParser.END_TAG) {
556                     continue;
557                 }
558                 if (XML_TAG_VOLUME_ITEM.equals(parser.getName())) {
559                     TypedArray item = mContext.getResources().obtainAttributes(
560                             attrs, R.styleable.carVolumeItems_item);
561                     int usage = item.getInt(R.styleable.carVolumeItems_item_usage,
562                             /* defValue= */ -1);
563                     if (usage >= 0) {
564                         VolumeItem volumeItem = new VolumeItem();
565                         volumeItem.mRank = rank;
566                         volumeItem.mIcon = item.getResourceId(
567                                 R.styleable.carVolumeItems_item_icon, /* defValue= */ 0);
568                         volumeItem.mMuteIcon = item.getResourceId(
569                                 R.styleable.carVolumeItems_item_mute_icon, /* defValue= */ 0);
570                         mVolumeItems.put(usage, volumeItem);
571                         rank++;
572                     }
573                     item.recycle();
574                 }
575             }
576         } catch (XmlPullParserException | IOException e) {
577             Log.e(TAG, "Error parsing volume groups configuration", e);
578         }
579 
580         if (DEBUG) {
581             Log.i(TAG,
582                     "loadAudioUsageItems finished. Number of volume items: " + mVolumeItems.size());
583         }
584     }
585 
getVolumeItemForUsages(int[] usages)586     private VolumeItem getVolumeItemForUsages(int[] usages) {
587         int rank = Integer.MAX_VALUE;
588         VolumeItem result = null;
589         for (int usage : usages) {
590             VolumeItem volumeItem = mVolumeItems.get(usage);
591             if (DEBUG) {
592                 Log.i(TAG, "getVolumeItemForUsage: " + usage + ": " + volumeItem);
593             }
594             if (volumeItem.mRank < rank) {
595                 rank = volumeItem.mRank;
596                 result = volumeItem;
597             }
598         }
599         return result;
600     }
601 
createCarVolumeListItem(VolumeItem volumeItem, int volumeZoneId, int volumeGroupId, Drawable supplementalIcon, int seekbarProgressValue, boolean isMuted, @Nullable View.OnClickListener supplementalIconOnClickListener)602     private CarVolumeItem createCarVolumeListItem(VolumeItem volumeItem, int volumeZoneId,
603             int volumeGroupId, Drawable supplementalIcon, int seekbarProgressValue,
604             boolean isMuted, @Nullable View.OnClickListener supplementalIconOnClickListener) {
605         CarVolumeItem carVolumeItem = new CarVolumeItem();
606         carVolumeItem.setMax(getMaxSeekbarValue(mCarAudioManager, volumeZoneId, volumeGroupId));
607         carVolumeItem.setProgress(seekbarProgressValue);
608         carVolumeItem.setIsMuted(isMuted);
609         carVolumeItem.setOnSeekBarChangeListener(
610                 new CarVolumeDialogImpl.VolumeSeekBarChangeListener(volumeZoneId, volumeGroupId,
611                         mCarAudioManager));
612         carVolumeItem.setGroupId(volumeGroupId);
613 
614         int color = mContext.getColor(R.color.car_volume_dialog_tint);
615         Drawable primaryIcon = mContext.getDrawable(volumeItem.mIcon);
616         primaryIcon.mutate().setTint(color);
617         carVolumeItem.setPrimaryIcon(primaryIcon);
618 
619         Drawable primaryMuteIcon = mContext.getDrawable(volumeItem.mMuteIcon);
620         primaryMuteIcon.mutate().setTint(color);
621         carVolumeItem.setPrimaryMuteIcon(primaryMuteIcon);
622 
623         if (supplementalIcon != null) {
624             supplementalIcon.mutate().setTint(color);
625             carVolumeItem.setSupplementalIcon(supplementalIcon,
626                     /* showSupplementalIconDivider= */ true);
627             carVolumeItem.setSupplementalIconListener(supplementalIconOnClickListener);
628         } else {
629             carVolumeItem.setSupplementalIcon(/* drawable= */ null,
630                     /* showSupplementalIconDivider= */ false);
631         }
632 
633         volumeItem.mCarVolumeItem = carVolumeItem;
634         volumeItem.mProgress = seekbarProgressValue;
635 
636         return carVolumeItem;
637     }
638 
addCarVolumeListItem(VolumeItem volumeItem, int volumeZoneId, int volumeGroupId, int supplementalIconId, @Nullable View.OnClickListener supplementalIconOnClickListener)639     private CarVolumeItem addCarVolumeListItem(VolumeItem volumeItem, int volumeZoneId,
640             int volumeGroupId, int supplementalIconId,
641             @Nullable View.OnClickListener supplementalIconOnClickListener) {
642         int seekbarProgressValue = getSeekbarValue(mCarAudioManager, volumeZoneId, volumeGroupId);
643         boolean isMuted = isGroupMuted(mCarAudioManager, volumeZoneId, volumeGroupId);
644         Drawable supplementalIcon = supplementalIconId == 0 ? null : mContext.getDrawable(
645                 supplementalIconId);
646         CarVolumeItem carVolumeItem = createCarVolumeListItem(volumeItem, volumeZoneId,
647                 volumeGroupId, supplementalIcon, seekbarProgressValue, isMuted,
648                 supplementalIconOnClickListener);
649         mCarVolumeLineItems.add(carVolumeItem);
650         return carVolumeItem;
651     }
652 
cleanupAudioManager()653     private void cleanupAudioManager() {
654         if (mCarAudioManager != null) {
655             if (mCarAudioManager.isAudioFeatureEnabled(AUDIO_FEATURE_VOLUME_GROUP_EVENTS)) {
656                 mCarAudioManager.unregisterCarVolumeGroupEventCallback(
657                         mCarVolumeGroupEventCallback);
658             } else {
659                 mCarAudioManager.unregisterCarVolumeCallback(mVolumeChangeCallback);
660             }
661             mCarAudioManager = null;
662         }
663         mCarVolumeLineItems.clear();
664     }
665 
666     /**
667      * Wrapper class which contains information of each volume group.
668      */
669     private static class VolumeItem {
670         private int mRank;
671         private boolean mDefaultItem = false;
672         @DrawableRes
673         private int mIcon;
674         @DrawableRes
675         private int mMuteIcon;
676         private CarVolumeItem mCarVolumeItem;
677         private int mProgress;
678         private boolean mIsMuted;
679     }
680 
681     private final class H extends Handler {
682 
683         private static final int SHOW = 1;
684         private static final int DISMISS = 2;
685 
H()686         private H() {
687             super(Looper.getMainLooper());
688         }
689 
690         @Override
handleMessage(Message msg)691         public void handleMessage(Message msg) {
692             switch (msg.what) {
693                 case SHOW:
694                     showH(msg.arg1);
695                     break;
696                 case DISMISS:
697                     dismissH(msg.arg1);
698                     break;
699                 default:
700             }
701         }
702     }
703 
704     private final class CustomDialog extends Dialog implements DialogInterface {
705 
CustomDialog(Context context)706         private CustomDialog(Context context) {
707             super(context, com.android.systemui.R.style.Theme_SystemUI);
708         }
709 
710         @Override
dispatchTouchEvent(MotionEvent ev)711         public boolean dispatchTouchEvent(MotionEvent ev) {
712             rescheduleTimeoutH();
713             return super.dispatchTouchEvent(ev);
714         }
715 
716         @Override
onStart()717         protected void onStart() {
718             super.setCanceledOnTouchOutside(true);
719             super.onStart();
720         }
721 
722         @Override
onStop()723         protected void onStop() {
724             super.onStop();
725         }
726 
727         @Override
onTouchEvent(MotionEvent event)728         public boolean onTouchEvent(MotionEvent event) {
729             if (isShowing()) {
730                 if (event.getAction() == MotionEvent.ACTION_OUTSIDE) {
731                     mHandler.obtainMessage(
732                             H.DISMISS, Events.DISMISS_REASON_TOUCH_OUTSIDE).sendToTarget();
733                     return true;
734                 }
735             }
736             return false;
737         }
738     }
739 
740     private final class DismissAnimationListener implements Animator.AnimatorListener {
741         @Override
onAnimationStart(Animator animation)742         public void onAnimationStart(Animator animation) {
743             mDismissing = true;
744         }
745 
746         @Override
onAnimationEnd(Animator animation)747         public void onAnimationEnd(Animator animation) {
748             mHandler.postDelayed(() -> {
749                 if (DEBUG) {
750                     Log.d(TAG, "mDialog.dismiss()");
751                 }
752                 mDialog.dismiss();
753                 mDismissing = false;
754                 // if mExpandIcon is null that means user never clicked on the expanded arrow
755                 // which implies that the dialog is still not expanded. In that case we do
756                 // not want to reset the state
757                 if (mExpandIcon != null && mExpanded) {
758                     toggleDialogExpansion(/* isClicked = */ false);
759                 }
760             }, DISMISS_DELAY_IN_MILLIS);
761         }
762 
763         @Override
onAnimationCancel(Animator animation)764         public void onAnimationCancel(Animator animation) {
765             // A canceled animation will also call onAnimationEnd so any necessary cleanup will
766             // already happen there
767             if (DEBUG) {
768                 Log.d(TAG, "dismiss animation canceled");
769             }
770         }
771 
772         @Override
onAnimationRepeat(Animator animation)773         public void onAnimationRepeat(Animator animation) {
774             // no-op
775         }
776     }
777 
778     private final class ExpandIconListener implements View.OnClickListener {
779         @Override
onClick(final View v)780         public void onClick(final View v) {
781             mExpandIcon = v;
782             toggleDialogExpansion(true);
783             rescheduleTimeoutH();
784         }
785     }
786 
toggleDialogExpansion(boolean isClicked)787     private void toggleDialogExpansion(boolean isClicked) {
788         mExpanded = !mExpanded;
789         Animator inAnimator;
790         if (mExpanded) {
791             for (int groupId = 0; groupId < mAvailableVolumeItems.size(); ++groupId) {
792                 if (groupId != mCurrentlyDisplayingGroupId) {
793                     VolumeItem volumeItem = mAvailableVolumeItems.get(groupId);
794                     addCarVolumeListItem(volumeItem, mAudioZoneId, groupId,
795                             /* supplementalIconId= */ 0,
796                             /* supplementalIconOnClickListener= */ null);
797                 }
798             }
799             inAnimator = AnimatorInflater.loadAnimator(
800                     mContext, R.anim.car_arrow_fade_in_rotate_up);
801 
802         } else {
803             clearAllAndSetupDefaultCarVolumeLineItem(mCurrentlyDisplayingGroupId);
804             inAnimator = AnimatorInflater.loadAnimator(
805                     mContext, R.anim.car_arrow_fade_in_rotate_down);
806         }
807 
808         Animator outAnimator = AnimatorInflater.loadAnimator(
809                 mContext, R.anim.car_arrow_fade_out);
810         inAnimator.setStartDelay(ARROW_FADE_IN_START_DELAY_IN_MILLIS);
811         AnimatorSet animators = new AnimatorSet();
812         animators.playTogether(outAnimator, inAnimator);
813         if (!isClicked) {
814             // Do not animate when the state is called to reset the dialogs view and not clicked
815             // by user.
816             animators.setDuration(0);
817         }
818         animators.setTarget(mExpandIcon);
819         animators.start();
820         mVolumeItemsAdapter.notifyDataSetChanged();
821     }
822 
823     private final class VolumeSeekBarChangeListener implements OnSeekBarChangeListener {
824 
825         private final int mVolumeZoneId;
826         private final int mVolumeGroupId;
827         private final CarAudioManager mCarAudioManager;
828 
VolumeSeekBarChangeListener(int volumeZoneId, int volumeGroupId, CarAudioManager carAudioManager)829         private VolumeSeekBarChangeListener(int volumeZoneId, int volumeGroupId,
830                 CarAudioManager carAudioManager) {
831             mVolumeZoneId = volumeZoneId;
832             mVolumeGroupId = volumeGroupId;
833             mCarAudioManager = carAudioManager;
834         }
835 
836         @Override
onProgressChanged(SeekBar seekBar, int progress, boolean fromUser)837         public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
838             if (!fromUser) {
839                 // For instance, if this event is originated from AudioService,
840                 // we can ignore it as it has already been handled and doesn't need to be
841                 // sent back down again.
842                 return;
843             }
844             if (mCarAudioManager == null) {
845                 Log.w(TAG, "Ignoring volume change event because the car isn't connected");
846                 return;
847             }
848             mAvailableVolumeItems.get(mVolumeGroupId).mProgress = progress;
849             mAvailableVolumeItems.get(
850                     mVolumeGroupId).mCarVolumeItem.setProgress(progress);
851             mCarAudioManager.setGroupVolume(mVolumeZoneId, mVolumeGroupId, progress, 0);
852         }
853 
854         @Override
onStartTrackingTouch(SeekBar seekBar)855         public void onStartTrackingTouch(SeekBar seekBar) {
856         }
857 
858         @Override
onStopTrackingTouch(SeekBar seekBar)859         public void onStopTrackingTouch(SeekBar seekBar) {
860         }
861     }
862 
updateVolumeGroupForEvents(List<CarVolumeGroupEvent> volumeGroupEvents)863     private void updateVolumeGroupForEvents(List<CarVolumeGroupEvent> volumeGroupEvents) {
864         List<CarVolumeGroupEvent> filteredEvents =
865                 filterVolumeGroupEventForZoneId(mAudioZoneId, volumeGroupEvents);
866         for (int index = 0; index < filteredEvents.size(); index++) {
867             CarVolumeGroupEvent event = filteredEvents.get(index);
868             int eventTypes = event.getEventTypes();
869             List<Integer> extraInfos = event.getExtraInfos();
870             List<CarVolumeGroupInfo> infos = event.getCarVolumeGroupInfos();
871             for (int infoIndex = 0; infoIndex < infos.size(); infoIndex++) {
872                 CarVolumeGroupInfo carVolumeGroupInfo = infos.get(infoIndex);
873                 updateVolumePreference(carVolumeGroupInfo.getId(),
874                         carVolumeGroupInfo.getMaxVolumeGainIndex(),
875                         carVolumeGroupInfo.getVolumeGainIndex(), carVolumeGroupInfo.isMuted(),
876                         eventTypes, extraInfos);
877             }
878         }
879     }
880 
filterVolumeGroupEventForZoneId(int zoneId, List<CarVolumeGroupEvent> volumeGroupEvents)881     private List<CarVolumeGroupEvent> filterVolumeGroupEventForZoneId(int zoneId,
882             List<CarVolumeGroupEvent> volumeGroupEvents) {
883         List<CarVolumeGroupEvent> filteredEvents = new ArrayList<>();
884         for (int index = 0; index < volumeGroupEvents.size(); index++) {
885             CarVolumeGroupEvent event = volumeGroupEvents.get(index);
886             List<CarVolumeGroupInfo> infos = event.getCarVolumeGroupInfos();
887             for (int infoIndex = 0; infoIndex < infos.size(); infoIndex++) {
888                 if (infos.get(infoIndex).getZoneId() == zoneId) {
889                     filteredEvents.add(event);
890                     break;
891                 }
892             }
893         }
894         return filteredEvents;
895     }
896 
updateVolumePreference(int groupId, int maxIndex, int currentIndex, boolean isMuted, int eventTypes, List<Integer> extraInfos)897     private void updateVolumePreference(int groupId, int maxIndex, int currentIndex,
898             boolean isMuted, int eventTypes, List<Integer> extraInfos) {
899         VolumeItem volumeItem = mAvailableVolumeItems.get(groupId);
900         boolean isShowing = mCarVolumeLineItems.stream().anyMatch(
901                 item -> item.getGroupId() == groupId);
902 
903         if (isShowing) {
904             if ((eventTypes & EVENT_TYPE_VOLUME_GAIN_INDEX_CHANGED) != 0) {
905                 volumeItem.mCarVolumeItem.setProgress(currentIndex);
906                 volumeItem.mProgress = currentIndex;
907             }
908             if ((eventTypes & EVENT_TYPE_MUTE_CHANGED) != 0) {
909                 volumeItem.mCarVolumeItem.setIsMuted(isMuted);
910                 volumeItem.mIsMuted = isMuted;
911             }
912             if ((eventTypes & EVENT_TYPE_VOLUME_MAX_INDEX_CHANGED) != 0
913                     && maxIndex != INVALID_INDEX) {
914                 volumeItem.mCarVolumeItem.setMax(maxIndex);
915             }
916         }
917 
918         if (extraInfos.contains(EXTRA_INFO_SHOW_UI)
919                 || extraInfos.contains(EXTRA_INFO_VOLUME_INDEX_CHANGED_BY_AUDIO_SYSTEM)) {
920             mPreviouslyDisplayingGroupId = mCurrentlyDisplayingGroupId;
921             mCurrentlyDisplayingGroupId = groupId;
922             mHandler.obtainMessage(H.SHOW,
923                     Events.SHOW_REASON_VOLUME_CHANGED).sendToTarget();
924         }
925     }
926 }
927