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