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