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_MUTING;
20 import static android.car.media.CarAudioManager.PRIMARY_AUDIO_ZONE;
21 
22 import android.animation.Animator;
23 import android.animation.AnimatorInflater;
24 import android.animation.AnimatorSet;
25 import android.annotation.DrawableRes;
26 import android.annotation.Nullable;
27 import android.app.Dialog;
28 import android.app.KeyguardManager;
29 import android.car.Car;
30 import android.car.media.CarAudioManager;
31 import android.content.BroadcastReceiver;
32 import android.content.Context;
33 import android.content.DialogInterface;
34 import android.content.Intent;
35 import android.content.IntentFilter;
36 import android.content.res.TypedArray;
37 import android.content.res.XmlResourceParser;
38 import android.graphics.Color;
39 import android.graphics.PixelFormat;
40 import android.graphics.drawable.ColorDrawable;
41 import android.graphics.drawable.Drawable;
42 import android.media.AudioManager;
43 import android.os.Build;
44 import android.os.Debug;
45 import android.os.Handler;
46 import android.os.Looper;
47 import android.os.Message;
48 import android.os.UserHandle;
49 import android.util.AttributeSet;
50 import android.util.Log;
51 import android.util.SparseArray;
52 import android.util.Xml;
53 import android.view.Gravity;
54 import android.view.MotionEvent;
55 import android.view.View;
56 import android.view.ViewGroup;
57 import android.view.Window;
58 import android.view.WindowManager;
59 import android.widget.SeekBar;
60 import android.widget.SeekBar.OnSeekBarChangeListener;
61 
62 import androidx.recyclerview.widget.LinearLayoutManager;
63 import androidx.recyclerview.widget.RecyclerView;
64 
65 import com.android.systemui.R;
66 import com.android.systemui.car.CarServiceProvider;
67 import com.android.systemui.plugins.VolumeDialog;
68 import com.android.systemui.volume.Events;
69 import com.android.systemui.volume.SystemUIInterpolators;
70 import com.android.systemui.volume.VolumeDialogImpl;
71 
72 import org.xmlpull.v1.XmlPullParserException;
73 
74 import java.io.IOException;
75 import java.util.ArrayList;
76 import java.util.List;
77 
78 /**
79  * Car version of the volume dialog.
80  *
81  * Methods ending in "H" must be called on the (ui) handler.
82  */
83 public class CarVolumeDialogImpl implements VolumeDialog {
84 
85     private static final String TAG = "CarVolumeDialog";
86     private static final boolean DEBUG = Build.IS_USERDEBUG || Build.IS_ENG;
87 
88     private static final String XML_TAG_VOLUME_ITEMS = "carVolumeItems";
89     private static final String XML_TAG_VOLUME_ITEM = "item";
90     private static final int LISTVIEW_ANIMATION_DURATION_IN_MILLIS = 250;
91     private static final int DISMISS_DELAY_IN_MILLIS = 50;
92     private static final int ARROW_FADE_IN_START_DELAY_IN_MILLIS = 100;
93 
94     private final Context mContext;
95     private final H mHandler = new H();
96     // All the volume items.
97     private final SparseArray<VolumeItem> mVolumeItems = new SparseArray<>();
98     // Available volume items in car audio manager.
99     private final List<VolumeItem> mAvailableVolumeItems = new ArrayList<>();
100     // Volume items in the RecyclerView.
101     private final List<CarVolumeItem> mCarVolumeLineItems = new ArrayList<>();
102     private final KeyguardManager mKeyguard;
103     private final int mNormalTimeout;
104     private final int mHoveringTimeout;
105     private final int mExpNormalTimeout;
106     private final int mExpHoveringTimeout;
107     private final CarServiceProvider mCarServiceProvider;
108 
109     private Window mWindow;
110     private CustomDialog mDialog;
111     private RecyclerView mListView;
112     private CarVolumeItemAdapter mVolumeItemsAdapter;
113     private CarAudioManager mCarAudioManager;
114     private boolean mHovering;
115     private int mCurrentlyDisplayingGroupId;
116     private int mPreviouslyDisplayingGroupId;
117     private boolean mShowing;
118     private boolean mDismissing;
119     private boolean mExpanded;
120     private View mExpandIcon;
121 
122     private final CarAudioManager.CarVolumeCallback mVolumeChangeCallback =
123             new CarAudioManager.CarVolumeCallback() {
124                 @Override
125                 public void onGroupVolumeChanged(int zoneId, int groupId, int flags) {
126                     updateVolumeAndMute(zoneId, groupId, flags);
127                 }
128 
129                 @Override
130                 public void onMasterMuteChanged(int zoneId, int flags) {
131                     // ignored
132                 }
133 
134                 @Override
135                 public void onGroupMuteChanged(int zoneId, int groupId, int flags) {
136                     updateVolumeAndMute(zoneId, groupId, flags);
137                 }
138 
139                 private void updateVolumeAndMute(int zoneId, int groupId, int flags) {
140                     // TODO: Include zoneId into consideration.
141                     VolumeItem volumeItem = mAvailableVolumeItems.get(groupId);
142                     boolean muted = isGroupMuted(mCarAudioManager, groupId);
143                     int value = getSeekbarValue(mCarAudioManager, groupId);
144 
145                     boolean isShowing = mCarVolumeLineItems.stream().anyMatch(
146                             item -> item.getGroupId() == groupId);
147 
148                     if ((value != volumeItem.mProgress || muted != volumeItem.mIsMuted)
149                             && isShowing) {
150                         volumeItem.mCarVolumeItem.setProgress(value);
151                         volumeItem.mProgress = value;
152                         volumeItem.mCarVolumeItem.setIsMuted(muted);
153                         volumeItem.mIsMuted = muted;
154                     }
155                     if ((flags & AudioManager.FLAG_SHOW_UI) != 0) {
156                         mPreviouslyDisplayingGroupId = mCurrentlyDisplayingGroupId;
157                         mCurrentlyDisplayingGroupId = groupId;
158                         mHandler.obtainMessage(H.SHOW,
159                                 Events.SHOW_REASON_VOLUME_CHANGED).sendToTarget();
160                     }
161                 }
162             };
163 
164     private final CarServiceProvider.CarServiceOnConnectedListener mCarServiceOnConnectedListener =
165             car -> {
166                 mExpanded = false;
167                 mCarAudioManager = (CarAudioManager) car.getCarManager(Car.AUDIO_SERVICE);
168                 int volumeGroupCount = mCarAudioManager.getVolumeGroupCount();
169                 // Populates volume slider items from volume groups to UI.
170                 for (int groupId = 0; groupId < volumeGroupCount; groupId++) {
171                     VolumeItem volumeItem = getVolumeItemForUsages(
172                             mCarAudioManager.getUsagesForVolumeGroupId(groupId));
173                     mAvailableVolumeItems.add(volumeItem);
174                     // The first one is the default item.
175                     if (groupId == 0) {
176                         clearAllAndSetupDefaultCarVolumeLineItem(0);
177                     }
178                 }
179 
180                 // If list is already initiated, update its content.
181                 if (mVolumeItemsAdapter != null) {
182                     mVolumeItemsAdapter.notifyDataSetChanged();
183                 }
184                 mCarAudioManager.registerCarVolumeCallback(mVolumeChangeCallback);
185             };
186 
187     private final BroadcastReceiver mHomeButtonPressedBroadcastReceiver = new BroadcastReceiver() {
188         @Override
189         public void onReceive(Context context, Intent intent) {
190             if (!intent.getAction().equals(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)) {
191                 return;
192             }
193 
194             dismissH(Events.DISMISS_REASON_VOLUME_CONTROLLER);
195         }
196     };
197 
CarVolumeDialogImpl(Context context, CarServiceProvider carServiceProvider)198     public CarVolumeDialogImpl(Context context, CarServiceProvider carServiceProvider) {
199         mContext = context;
200         mCarServiceProvider = carServiceProvider;
201         mKeyguard = (KeyguardManager) mContext.getSystemService(Context.KEYGUARD_SERVICE);
202         mNormalTimeout = mContext.getResources().getInteger(
203                 R.integer.car_volume_dialog_display_normal_timeout);
204         mHoveringTimeout = mContext.getResources().getInteger(
205                 R.integer.car_volume_dialog_display_hovering_timeout);
206         mExpNormalTimeout = mContext.getResources().getInteger(
207                 R.integer.car_volume_dialog_display_expanded_normal_timeout);
208         mExpHoveringTimeout = mContext.getResources().getInteger(
209                 R.integer.car_volume_dialog_display_expanded_hovering_timeout);
210     }
211 
getSeekbarValue(CarAudioManager carAudioManager, int volumeGroupId)212     private static int getSeekbarValue(CarAudioManager carAudioManager, int volumeGroupId) {
213         return carAudioManager.getGroupVolume(volumeGroupId);
214     }
215 
isGroupMuted(CarAudioManager carAudioManager, int volumeGroupId)216     private static boolean isGroupMuted(CarAudioManager carAudioManager, int volumeGroupId) {
217         if (!carAudioManager.isAudioFeatureEnabled(AUDIO_FEATURE_VOLUME_GROUP_MUTING)) {
218             return false;
219         }
220         return carAudioManager.isVolumeGroupMuted(PRIMARY_AUDIO_ZONE, volumeGroupId);
221     }
222 
getMaxSeekbarValue(CarAudioManager carAudioManager, int volumeGroupId)223     private static int getMaxSeekbarValue(CarAudioManager carAudioManager, int volumeGroupId) {
224         return carAudioManager.getGroupMaxVolume(volumeGroupId);
225     }
226 
227     /**
228      * Build the volume window and connect to the CarService which registers with car audio
229      * manager.
230      */
231     @Override
init(int windowType, Callback callback)232     public void init(int windowType, Callback callback) {
233         initDialog();
234         mCarServiceProvider.addListener(mCarServiceOnConnectedListener);
235 
236         mContext.registerReceiverAsUser(mHomeButtonPressedBroadcastReceiver, UserHandle.CURRENT,
237                 new IntentFilter(Intent.ACTION_CLOSE_SYSTEM_DIALOGS), /* broadcastPermission= */
238                 null, /* scheduler= */ null);
239     }
240 
241     @Override
destroy()242     public void destroy() {
243         mHandler.removeCallbacksAndMessages(/* token= */ null);
244 
245         mContext.unregisterReceiver(mHomeButtonPressedBroadcastReceiver);
246 
247         cleanupAudioManager();
248     }
249 
250     /**
251      * Reveals volume dialog.
252      */
show(int reason)253     public void show(int reason) {
254         mHandler.obtainMessage(H.SHOW, reason).sendToTarget();
255     }
256 
257     /**
258      * Hides volume dialog.
259      */
dismiss(int reason)260     public void dismiss(int reason) {
261         mHandler.obtainMessage(H.DISMISS, reason).sendToTarget();
262     }
263 
initDialog()264     private void initDialog() {
265         loadAudioUsageItems();
266         mCarVolumeLineItems.clear();
267         mDialog = new CustomDialog(mContext);
268 
269         mHovering = false;
270         mShowing = false;
271         mDismissing = false;
272         mExpanded = false;
273         mWindow = mDialog.getWindow();
274         mWindow.requestFeature(Window.FEATURE_NO_TITLE);
275         mWindow.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
276         mWindow.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND
277                 | WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR);
278         mWindow.addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
279                 | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
280                 | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
281                 | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
282                 | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
283                 | WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED);
284         mWindow.setType(WindowManager.LayoutParams.TYPE_VOLUME_OVERLAY);
285         mWindow.setWindowAnimations(com.android.internal.R.style.Animation_Toast);
286         final WindowManager.LayoutParams lp = mWindow.getAttributes();
287         lp.format = PixelFormat.TRANSLUCENT;
288         lp.setTitle(VolumeDialogImpl.class.getSimpleName());
289         lp.gravity = Gravity.TOP | Gravity.CENTER_HORIZONTAL;
290         lp.windowAnimations = -1;
291         mWindow.setAttributes(lp);
292 
293         mDialog.setContentView(R.layout.car_volume_dialog);
294         mWindow.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
295 
296         mDialog.setCanceledOnTouchOutside(true);
297         mDialog.setOnShowListener(dialog -> {
298             mListView.setTranslationY(-mListView.getHeight());
299             mListView.setAlpha(0);
300             mListView.animate()
301                     .alpha(1)
302                     .translationY(0)
303                     .setDuration(LISTVIEW_ANIMATION_DURATION_IN_MILLIS)
304                     .setInterpolator(new SystemUIInterpolators.LogDecelerateInterpolator())
305                     .start();
306         });
307         mListView = mWindow.findViewById(R.id.volume_list);
308         mListView.setOnHoverListener((v, event) -> {
309             int action = event.getActionMasked();
310             mHovering = (action == MotionEvent.ACTION_HOVER_ENTER)
311                     || (action == MotionEvent.ACTION_HOVER_MOVE);
312             rescheduleTimeoutH();
313             return true;
314         });
315 
316         mVolumeItemsAdapter = new CarVolumeItemAdapter(mContext, mCarVolumeLineItems);
317         mListView.setAdapter(mVolumeItemsAdapter);
318         mListView.setLayoutManager(new LinearLayoutManager(mContext));
319     }
320 
321 
showH(int reason)322     private void showH(int reason) {
323         if (DEBUG) {
324             Log.d(TAG, "showH r=" + Events.DISMISS_REASONS[reason]);
325         }
326 
327         mHandler.removeMessages(H.SHOW);
328         mHandler.removeMessages(H.DISMISS);
329 
330         rescheduleTimeoutH();
331 
332         // Refresh the data set before showing.
333         mVolumeItemsAdapter.notifyDataSetChanged();
334 
335         if (mShowing) {
336             if (mPreviouslyDisplayingGroupId == mCurrentlyDisplayingGroupId || mExpanded) {
337                 return;
338             }
339 
340             clearAllAndSetupDefaultCarVolumeLineItem(mCurrentlyDisplayingGroupId);
341             return;
342         }
343 
344         mShowing = true;
345         clearAllAndSetupDefaultCarVolumeLineItem(mCurrentlyDisplayingGroupId);
346         mDialog.show();
347         Events.writeEvent(Events.EVENT_SHOW_DIALOG, reason, mKeyguard.isKeyguardLocked());
348     }
349 
clearAllAndSetupDefaultCarVolumeLineItem(int groupId)350     private void clearAllAndSetupDefaultCarVolumeLineItem(int groupId) {
351         mCarVolumeLineItems.clear();
352         VolumeItem volumeItem = mAvailableVolumeItems.get(groupId);
353         volumeItem.mDefaultItem = true;
354         addCarVolumeListItem(volumeItem, /* volumeGroupId = */ groupId,
355                 R.drawable.car_ic_keyboard_arrow_down, new ExpandIconListener());
356     }
357 
rescheduleTimeoutH()358     protected void rescheduleTimeoutH() {
359         mHandler.removeMessages(H.DISMISS);
360         final int timeout = computeTimeoutH();
361         mHandler.sendMessageDelayed(mHandler
362                 .obtainMessage(H.DISMISS, Events.DISMISS_REASON_TIMEOUT), timeout);
363 
364         if (DEBUG) {
365             Log.d(TAG, "rescheduleTimeout " + timeout + " " + Debug.getCaller());
366         }
367     }
368 
computeTimeoutH()369     private int computeTimeoutH() {
370         if (mExpanded) {
371             return mHovering ? mExpHoveringTimeout : mExpNormalTimeout;
372         } else {
373             return mHovering ? mHoveringTimeout : mNormalTimeout;
374         }
375     }
376 
dismissH(int reason)377     private void dismissH(int reason) {
378         if (DEBUG) {
379             Log.d(TAG, "dismissH r=" + Events.DISMISS_REASONS[reason]);
380         }
381 
382         mHandler.removeMessages(H.DISMISS);
383         mHandler.removeMessages(H.SHOW);
384         if (!mShowing || mDismissing) {
385             return;
386         }
387 
388         mDismissing = true;
389         mListView.animate()
390                 .alpha(0)
391                 .translationY(-mListView.getHeight())
392                 .setDuration(LISTVIEW_ANIMATION_DURATION_IN_MILLIS)
393                 .setInterpolator(new SystemUIInterpolators.LogAccelerateInterpolator())
394                 .withEndAction(() -> mHandler.postDelayed(() -> {
395                     if (DEBUG) {
396                         Log.d(TAG, "mDialog.dismiss()");
397                     }
398                     mDialog.dismiss();
399                     mShowing = false;
400                     mDismissing = false;
401                     // if mExpandIcon is null that means user never clicked on the expanded arrow
402                     // which implies that the dialog is still not expanded. In that case we do
403                     // not want to reset the state
404                     if (mExpandIcon != null && mExpanded) {
405                         toggleDialogExpansion(/* isClicked = */ false);
406                     }
407                 }, DISMISS_DELAY_IN_MILLIS))
408                 .start();
409 
410         Events.writeEvent(Events.EVENT_DISMISS_DIALOG, reason);
411     }
412 
loadAudioUsageItems()413     private void loadAudioUsageItems() {
414         if (DEBUG) {
415             Log.i(TAG, "loadAudioUsageItems start");
416         }
417 
418         try (XmlResourceParser parser = mContext.getResources().getXml(R.xml.car_volume_items)) {
419             AttributeSet attrs = Xml.asAttributeSet(parser);
420             int type;
421             // Traverse to the first start tag
422             while ((type = parser.next()) != XmlResourceParser.END_DOCUMENT
423                     && type != XmlResourceParser.START_TAG) {
424                 // Do Nothing (moving parser to start element)
425             }
426 
427             if (!XML_TAG_VOLUME_ITEMS.equals(parser.getName())) {
428                 throw new RuntimeException("Meta-data does not start with carVolumeItems tag");
429             }
430             int outerDepth = parser.getDepth();
431             int rank = 0;
432             while ((type = parser.next()) != XmlResourceParser.END_DOCUMENT
433                     && (type != XmlResourceParser.END_TAG || parser.getDepth() > outerDepth)) {
434                 if (type == XmlResourceParser.END_TAG) {
435                     continue;
436                 }
437                 if (XML_TAG_VOLUME_ITEM.equals(parser.getName())) {
438                     TypedArray item = mContext.getResources().obtainAttributes(
439                             attrs, R.styleable.carVolumeItems_item);
440                     int usage = item.getInt(R.styleable.carVolumeItems_item_usage,
441                             /* defValue= */ -1);
442                     if (usage >= 0) {
443                         VolumeItem volumeItem = new VolumeItem();
444                         volumeItem.mRank = rank;
445                         volumeItem.mIcon = item.getResourceId(
446                                 R.styleable.carVolumeItems_item_icon, /* defValue= */ 0);
447                         volumeItem.mMuteIcon = item.getResourceId(
448                                 R.styleable.carVolumeItems_item_mute_icon, /* defValue= */ 0);
449                         mVolumeItems.put(usage, volumeItem);
450                         rank++;
451                     }
452                     item.recycle();
453                 }
454             }
455         } catch (XmlPullParserException | IOException e) {
456             Log.e(TAG, "Error parsing volume groups configuration", e);
457         }
458 
459         if (DEBUG) {
460             Log.i(TAG,
461                     "loadAudioUsageItems finished. Number of volume items: " + mVolumeItems.size());
462         }
463     }
464 
getVolumeItemForUsages(int[] usages)465     private VolumeItem getVolumeItemForUsages(int[] usages) {
466         int rank = Integer.MAX_VALUE;
467         VolumeItem result = null;
468         for (int usage : usages) {
469             VolumeItem volumeItem = mVolumeItems.get(usage);
470             if (DEBUG) {
471                 Log.i(TAG, "getVolumeItemForUsage: " + usage + ": " + volumeItem);
472             }
473             if (volumeItem.mRank < rank) {
474                 rank = volumeItem.mRank;
475                 result = volumeItem;
476             }
477         }
478         return result;
479     }
480 
createCarVolumeListItem(VolumeItem volumeItem, int volumeGroupId, Drawable supplementalIcon, int seekbarProgressValue, boolean isMuted, @Nullable View.OnClickListener supplementalIconOnClickListener)481     private CarVolumeItem createCarVolumeListItem(VolumeItem volumeItem, int volumeGroupId,
482             Drawable supplementalIcon, int seekbarProgressValue,
483             boolean isMuted, @Nullable View.OnClickListener supplementalIconOnClickListener) {
484         CarVolumeItem carVolumeItem = new CarVolumeItem();
485         carVolumeItem.setMax(getMaxSeekbarValue(mCarAudioManager, volumeGroupId));
486         carVolumeItem.setProgress(seekbarProgressValue);
487         carVolumeItem.setIsMuted(isMuted);
488         carVolumeItem.setOnSeekBarChangeListener(
489                 new CarVolumeDialogImpl.VolumeSeekBarChangeListener(volumeGroupId,
490                         mCarAudioManager));
491         carVolumeItem.setGroupId(volumeGroupId);
492 
493         int color = mContext.getColor(R.color.car_volume_dialog_tint);
494         Drawable primaryIcon = mContext.getDrawable(volumeItem.mIcon);
495         primaryIcon.mutate().setTint(color);
496         carVolumeItem.setPrimaryIcon(primaryIcon);
497 
498         Drawable primaryMuteIcon = mContext.getDrawable(volumeItem.mMuteIcon);
499         primaryMuteIcon.mutate().setTint(color);
500         carVolumeItem.setPrimaryMuteIcon(primaryMuteIcon);
501 
502         if (supplementalIcon != null) {
503             supplementalIcon.mutate().setTint(color);
504             carVolumeItem.setSupplementalIcon(supplementalIcon,
505                     /* showSupplementalIconDivider= */ true);
506             carVolumeItem.setSupplementalIconListener(supplementalIconOnClickListener);
507         } else {
508             carVolumeItem.setSupplementalIcon(/* drawable= */ null,
509                     /* showSupplementalIconDivider= */ false);
510         }
511 
512         volumeItem.mCarVolumeItem = carVolumeItem;
513         volumeItem.mProgress = seekbarProgressValue;
514 
515         return carVolumeItem;
516     }
517 
addCarVolumeListItem(VolumeItem volumeItem, int volumeGroupId, int supplementalIconId, @Nullable View.OnClickListener supplementalIconOnClickListener)518     private CarVolumeItem addCarVolumeListItem(VolumeItem volumeItem, int volumeGroupId,
519             int supplementalIconId,
520             @Nullable View.OnClickListener supplementalIconOnClickListener) {
521         int seekbarProgressValue = getSeekbarValue(mCarAudioManager, volumeGroupId);
522         boolean isMuted = isGroupMuted(mCarAudioManager, volumeGroupId);
523         Drawable supplementalIcon = supplementalIconId == 0 ? null : mContext.getDrawable(
524                 supplementalIconId);
525         CarVolumeItem carVolumeItem = createCarVolumeListItem(volumeItem, volumeGroupId,
526                 supplementalIcon, seekbarProgressValue, isMuted, supplementalIconOnClickListener);
527         mCarVolumeLineItems.add(carVolumeItem);
528         return carVolumeItem;
529     }
530 
cleanupAudioManager()531     private void cleanupAudioManager() {
532         mCarAudioManager.unregisterCarVolumeCallback(mVolumeChangeCallback);
533         mCarVolumeLineItems.clear();
534         mCarAudioManager = null;
535     }
536 
537     /**
538      * Wrapper class which contains information of each volume group.
539      */
540     private static class VolumeItem {
541         private int mRank;
542         private boolean mDefaultItem = false;
543         @DrawableRes
544         private int mIcon;
545         @DrawableRes
546         private int mMuteIcon;
547         private CarVolumeItem mCarVolumeItem;
548         private int mProgress;
549         private boolean mIsMuted;
550     }
551 
552     private final class H extends Handler {
553 
554         private static final int SHOW = 1;
555         private static final int DISMISS = 2;
556 
H()557         private H() {
558             super(Looper.getMainLooper());
559         }
560 
561         @Override
handleMessage(Message msg)562         public void handleMessage(Message msg) {
563             switch (msg.what) {
564                 case SHOW:
565                     showH(msg.arg1);
566                     break;
567                 case DISMISS:
568                     dismissH(msg.arg1);
569                     break;
570                 default:
571             }
572         }
573     }
574 
575     private final class CustomDialog extends Dialog implements DialogInterface {
576 
CustomDialog(Context context)577         private CustomDialog(Context context) {
578             super(context, com.android.systemui.R.style.Theme_SystemUI);
579         }
580 
581         @Override
dispatchTouchEvent(MotionEvent ev)582         public boolean dispatchTouchEvent(MotionEvent ev) {
583             rescheduleTimeoutH();
584             return super.dispatchTouchEvent(ev);
585         }
586 
587         @Override
onStart()588         protected void onStart() {
589             super.setCanceledOnTouchOutside(true);
590             super.onStart();
591         }
592 
593         @Override
onStop()594         protected void onStop() {
595             super.onStop();
596         }
597 
598         @Override
onTouchEvent(MotionEvent event)599         public boolean onTouchEvent(MotionEvent event) {
600             if (isShowing()) {
601                 if (event.getAction() == MotionEvent.ACTION_OUTSIDE) {
602                     mHandler.obtainMessage(
603                             H.DISMISS, Events.DISMISS_REASON_TOUCH_OUTSIDE).sendToTarget();
604                     return true;
605                 }
606             }
607             return false;
608         }
609     }
610 
611     private final class ExpandIconListener implements View.OnClickListener {
612         @Override
onClick(final View v)613         public void onClick(final View v) {
614             mExpandIcon = v;
615             toggleDialogExpansion(true);
616             rescheduleTimeoutH();
617         }
618     }
619 
toggleDialogExpansion(boolean isClicked)620     private void toggleDialogExpansion(boolean isClicked) {
621         mExpanded = !mExpanded;
622         Animator inAnimator;
623         if (mExpanded) {
624             for (int groupId = 0; groupId < mAvailableVolumeItems.size(); ++groupId) {
625                 if (groupId != mCurrentlyDisplayingGroupId) {
626                     VolumeItem volumeItem = mAvailableVolumeItems.get(groupId);
627                     addCarVolumeListItem(volumeItem, groupId, /* supplementalIconId= */ 0,
628                             /* supplementalIconOnClickListener= */ null);
629                 }
630             }
631             inAnimator = AnimatorInflater.loadAnimator(
632                     mContext, R.anim.car_arrow_fade_in_rotate_up);
633 
634         } else {
635             clearAllAndSetupDefaultCarVolumeLineItem(mCurrentlyDisplayingGroupId);
636             inAnimator = AnimatorInflater.loadAnimator(
637                     mContext, R.anim.car_arrow_fade_in_rotate_down);
638         }
639 
640         Animator outAnimator = AnimatorInflater.loadAnimator(
641                 mContext, R.anim.car_arrow_fade_out);
642         inAnimator.setStartDelay(ARROW_FADE_IN_START_DELAY_IN_MILLIS);
643         AnimatorSet animators = new AnimatorSet();
644         animators.playTogether(outAnimator, inAnimator);
645         if (!isClicked) {
646             // Do not animate when the state is called to reset the dialogs view and not clicked
647             // by user.
648             animators.setDuration(0);
649         }
650         animators.setTarget(mExpandIcon);
651         animators.start();
652         mVolumeItemsAdapter.notifyDataSetChanged();
653     }
654 
655     private final class VolumeSeekBarChangeListener implements OnSeekBarChangeListener {
656 
657         private final int mVolumeGroupId;
658         private final CarAudioManager mCarAudioManager;
659 
VolumeSeekBarChangeListener(int volumeGroupId, CarAudioManager carAudioManager)660         private VolumeSeekBarChangeListener(int volumeGroupId, CarAudioManager carAudioManager) {
661             mVolumeGroupId = volumeGroupId;
662             mCarAudioManager = carAudioManager;
663         }
664 
665         @Override
onProgressChanged(SeekBar seekBar, int progress, boolean fromUser)666         public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
667             if (!fromUser) {
668                 // For instance, if this event is originated from AudioService,
669                 // we can ignore it as it has already been handled and doesn't need to be
670                 // sent back down again.
671                 return;
672             }
673             if (mCarAudioManager == null) {
674                 Log.w(TAG, "Ignoring volume change event because the car isn't connected");
675                 return;
676             }
677             mAvailableVolumeItems.get(mVolumeGroupId).mProgress = progress;
678             mAvailableVolumeItems.get(
679                     mVolumeGroupId).mCarVolumeItem.setProgress(progress);
680             mCarAudioManager.setGroupVolume(mVolumeGroupId, progress, 0);
681         }
682 
683         @Override
onStartTrackingTouch(SeekBar seekBar)684         public void onStartTrackingTouch(SeekBar seekBar) {
685         }
686 
687         @Override
onStopTrackingTouch(SeekBar seekBar)688         public void onStopTrackingTouch(SeekBar seekBar) {
689         }
690     }
691 }
692