diff options
author | Xin Li <delphij@google.com> | 2021-02-21 09:39:53 -0800 |
---|---|---|
committer | Xin Li <delphij@google.com> | 2021-02-21 09:39:53 -0800 |
commit | be473bf819b8570945b0d238beaaa2fa63e60c02 (patch) | |
tree | addad6a0ab92967c35ca90cf4056940be91be73a /packages/SystemUI/src | |
parent | 6cc86364fb326b3fead32008e076147e57755e98 (diff) | |
parent | 3078660c4eb37fb00ad1e69cc695bd20f1ee7440 (diff) |
Merge ab/7061308 into stage.
Bug: 180401296
Merged-In: I4bf82035631ccff6d5a6144d6d9b1d203b076851
Change-Id: I1b5f3a672a55eaabba0f5389bab110b395553559
Diffstat (limited to 'packages/SystemUI/src')
31 files changed, 2313 insertions, 56 deletions
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java index 60541eb56afc..60cd24019b97 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java @@ -2579,6 +2579,11 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab return true; } + // change in battery overheat + if (current.health != old.health) { + return true; + } + return false; } diff --git a/packages/SystemUI/src/com/android/systemui/Dependency.java b/packages/SystemUI/src/com/android/systemui/Dependency.java index 7c3671385868..097beba32bdf 100644 --- a/packages/SystemUI/src/com/android/systemui/Dependency.java +++ b/packages/SystemUI/src/com/android/systemui/Dependency.java @@ -46,6 +46,7 @@ import com.android.systemui.dump.DumpManager; import com.android.systemui.fragments.FragmentService; import com.android.systemui.keyguard.ScreenLifecycle; import com.android.systemui.keyguard.WakefulnessLifecycle; +import com.android.systemui.media.dialog.MediaOutputDialogFactory; import com.android.systemui.model.SysUiState; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.DarkIconDispatcher; @@ -347,6 +348,7 @@ public class Dependency { @Inject Lazy<RecordingController> mRecordingController; @Inject Lazy<ProtoTracer> mProtoTracer; @Inject Lazy<Divider> mDivider; + @Inject Lazy<MediaOutputDialogFactory> mMediaOutputDialogFactory; @Inject public Dependency() { @@ -547,6 +549,8 @@ public class Dependency { mProviders.put(RecordingController.class, mRecordingController::get); mProviders.put(Divider.class, mDivider::get); + mProviders.put(MediaOutputDialogFactory.class, mMediaOutputDialogFactory::get); + sDependency = this; } diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIFactory.java b/packages/SystemUI/src/com/android/systemui/SystemUIFactory.java index 5674fdd3bb36..eebf70b7564c 100644 --- a/packages/SystemUI/src/com/android/systemui/SystemUIFactory.java +++ b/packages/SystemUI/src/com/android/systemui/SystemUIFactory.java @@ -18,6 +18,7 @@ package com.android.systemui; import android.annotation.NonNull; import android.content.Context; +import android.content.res.AssetManager; import android.content.res.Resources; import android.os.Handler; import android.os.Looper; @@ -39,6 +40,7 @@ import com.android.systemui.screenshot.ScreenshotNotificationSmartActionsProvide import com.android.systemui.statusbar.NotificationListener; import com.android.systemui.statusbar.NotificationMediaManager; import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator; +import com.android.systemui.statusbar.phone.BackGestureTfClassifierProvider; import com.android.systemui.statusbar.phone.DozeParameters; import com.android.systemui.statusbar.phone.KeyguardBouncer; import com.android.systemui.statusbar.phone.KeyguardBypassController; @@ -182,4 +184,13 @@ public class SystemUIFactory { return mContext; } } + + /** + * Creates an instance of BackGestureTfClassifierProvider. + * This method is overridden in vendor specific implementation of Sys UI. + */ + public BackGestureTfClassifierProvider createBackGestureTfClassifierProvider( + AssetManager am, String modelName) { + return new BackGestureTfClassifierProvider(); + } } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java index dbe5a77965be..c1233fe6b9da 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java @@ -96,7 +96,7 @@ public class AuthContainerView extends LinearLayout @VisibleForTesting final WakefulnessLifecycle mWakefulnessLifecycle; - private @ContainerState int mContainerState = STATE_UNKNOWN; + @VisibleForTesting @ContainerState int mContainerState = STATE_UNKNOWN; // Non-null only if the dialog is in the act of dismissing and has not sent the reason yet. @Nullable @AuthDialogCallback.DismissedReason Integer mPendingCallbackReason; @@ -607,10 +607,11 @@ public class AuthContainerView extends LinearLayout mWindowManager.removeView(this); } - private void onDialogAnimatedIn() { + @VisibleForTesting + void onDialogAnimatedIn() { if (mContainerState == STATE_PENDING_DISMISS) { Log.d(TAG, "onDialogAnimatedIn(): mPendingDismissDialog=true, dismissing now"); - animateAway(false /* sendReason */, 0); + animateAway(AuthDialogCallback.DISMISSED_USER_CANCELED); return; } mContainerState = STATE_SHOWING; diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ToggleRangeBehavior.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ToggleRangeBehavior.kt index aa11df41a7b7..a3a937acec76 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ui/ToggleRangeBehavior.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ToggleRangeBehavior.kt @@ -301,7 +301,7 @@ class ToggleRangeBehavior : Behavior { } fun findNearestStep(value: Float): Float { - var minDiff = 1000f + var minDiff = Float.MAX_VALUE var f = rangeTemplate.getMinValue() while (f <= rangeTemplate.getMaxValue()) { diff --git a/packages/SystemUI/src/com/android/systemui/dagger/DefaultBroadcastReceiverBinder.java b/packages/SystemUI/src/com/android/systemui/dagger/DefaultBroadcastReceiverBinder.java index 6e8d63b2c516..25d02a66d56d 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/DefaultBroadcastReceiverBinder.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/DefaultBroadcastReceiverBinder.java @@ -18,6 +18,7 @@ package com.android.systemui.dagger; import android.content.BroadcastReceiver; +import com.android.systemui.media.dialog.MediaOutputDialogReceiver; import com.android.systemui.screenshot.ActionProxyReceiver; import com.android.systemui.screenshot.DeleteScreenshotReceiver; import com.android.systemui.screenshot.SmartActionsReceiver; @@ -59,4 +60,13 @@ public abstract class DefaultBroadcastReceiverBinder { public abstract BroadcastReceiver bindSmartActionsReceiver( SmartActionsReceiver broadcastReceiver); + /** + * + */ + @Binds + @IntoMap + @ClassKey(MediaOutputDialogReceiver.class) + public abstract BroadcastReceiver bindMediaOutputDialogReceiver( + MediaOutputDialogReceiver broadcastReceiver); + } diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java index 810cecca517f..bffe05085887 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java +++ b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java @@ -42,10 +42,10 @@ import androidx.annotation.UiThread; import androidx.constraintlayout.widget.ConstraintSet; import com.android.settingslib.Utils; -import com.android.settingslib.media.MediaOutputSliceConstants; import com.android.settingslib.widget.AdaptiveIcon; import com.android.systemui.R; import com.android.systemui.dagger.qualifiers.Background; +import com.android.systemui.media.dialog.MediaOutputDialogFactory; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.statusbar.phone.KeyguardDismissUtil; import com.android.systemui.util.animation.TransitionLayout; @@ -93,7 +93,7 @@ public class MediaControlPanel { private int mAlbumArtRadius; // This will provide the corners for the album art. private final ViewOutlineProvider mViewOutlineProvider; - + private final MediaOutputDialogFactory mMediaOutputDialogFactory; /** * Initialize a new control panel * @param context @@ -104,7 +104,8 @@ public class MediaControlPanel { public MediaControlPanel(Context context, @Background Executor backgroundExecutor, ActivityStarter activityStarter, MediaViewController mediaViewController, SeekBarViewModel seekBarViewModel, Lazy<MediaDataManager> lazyMediaDataManager, - KeyguardDismissUtil keyguardDismissUtil) { + KeyguardDismissUtil keyguardDismissUtil, MediaOutputDialogFactory + mediaOutputDialogFactory) { mContext = context; mBackgroundExecutor = backgroundExecutor; mActivityStarter = activityStarter; @@ -112,6 +113,7 @@ public class MediaControlPanel { mMediaViewController = mediaViewController; mMediaDataManagerLazy = lazyMediaDataManager; mKeyguardDismissUtil = keyguardDismissUtil; + mMediaOutputDialogFactory = mediaOutputDialogFactory; loadDimens(); mViewOutlineProvider = new ViewOutlineProvider() { @@ -274,13 +276,7 @@ public class MediaControlPanel { setVisibleAndAlpha(collapsedSet, R.id.media_seamless, true /*visible */); setVisibleAndAlpha(expandedSet, R.id.media_seamless, true /*visible */); mViewHolder.getSeamless().setOnClickListener(v -> { - final Intent intent = new Intent() - .setAction(MediaOutputSliceConstants.ACTION_MEDIA_OUTPUT) - .putExtra(MediaOutputSliceConstants.EXTRA_PACKAGE_NAME, - data.getPackageName()) - .putExtra(MediaOutputSliceConstants.KEY_MEDIA_SESSION_TOKEN, mToken); - mActivityStarter.startActivity(intent, false, true /* dismissShade */, - Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + mMediaOutputDialogFactory.create(data.getPackageName(), true); }); ImageView iconView = mViewHolder.getSeamlessIcon(); @@ -359,7 +355,15 @@ public class MediaControlPanel { final MediaController controller = getController(); mBackgroundExecutor.execute(() -> mSeekBarViewModel.updateController(controller)); + // Guts label + boolean isDismissible = data.isClearable(); + mViewHolder.getSettingsText().setText(isDismissible + ? R.string.controls_media_close_session + : R.string.controls_media_active_session); + // Dismiss + mViewHolder.getDismissLabel().setAlpha(isDismissible ? 1 : DISABLED_ALPHA); + mViewHolder.getDismiss().setEnabled(isDismissible); mViewHolder.getDismiss().setOnClickListener(v -> { if (mKey != null) { closeGuts(); diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt index 436d4510aa67..833d08808a0b 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt @@ -610,7 +610,8 @@ class MediaDataManager( // Move to resume key (aka package name) if that key doesn't already exist. val resumeAction = getResumeMediaAction(removed.resumeAction!!) val updated = removed.copy(token = null, actions = listOf(resumeAction), - actionsToShowInCompact = listOf(0), active = false, resumption = true) + actionsToShowInCompact = listOf(0), active = false, resumption = true, + isClearable = true) val pkg = removed?.packageName val migrate = mediaEntries.put(pkg, updated) == null // Notify listeners of "new" controls when migrating or removed and update when not diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaTimeoutListener.kt b/packages/SystemUI/src/com/android/systemui/media/MediaTimeoutListener.kt index dcb7767a680a..63a361de4d87 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaTimeoutListener.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaTimeoutListener.kt @@ -46,7 +46,7 @@ class MediaTimeoutListener @Inject constructor( /** * Callback representing that a media object is now expired: * @param token Media session unique identifier - * @param pauseTimeuot True when expired for {@code PAUSED_MEDIA_TIMEOUT} + * @param pauseTimeout True when expired for {@code PAUSED_MEDIA_TIMEOUT} */ lateinit var timeoutCallback: (String, Boolean) -> Unit @@ -57,11 +57,10 @@ class MediaTimeoutListener @Inject constructor( // Having an old key means that we're migrating from/to resumption. We should update // the old listener to make sure that events will be dispatched to the new location. val migrating = oldKey != null && key != oldKey - var wasPlaying = false if (migrating) { val reusedListener = mediaListeners.remove(oldKey) if (reusedListener != null) { - wasPlaying = reusedListener.playing ?: false + val wasPlaying = reusedListener.playing ?: false if (DEBUG) Log.d(TAG, "migrating key $oldKey to $key, for resumption") reusedListener.mediaData = data reusedListener.key = key @@ -159,9 +158,8 @@ class MediaTimeoutListener @Inject constructor( Log.v(TAG, "Execute timeout for $key") } timedOut = true - if (dispatchEvents) { - timeoutCallback(key, timedOut) - } + // this event is async, so it's safe even when `dispatchEvents` is false + timeoutCallback(key, timedOut) }, PAUSED_MEDIA_TIMEOUT) } else { expireMediaTimeout(key, "playback started - $state, $key") diff --git a/packages/SystemUI/src/com/android/systemui/media/PlayerViewHolder.kt b/packages/SystemUI/src/com/android/systemui/media/PlayerViewHolder.kt index 666a6038a8b6..16327bd9064a 100644 --- a/packages/SystemUI/src/com/android/systemui/media/PlayerViewHolder.kt +++ b/packages/SystemUI/src/com/android/systemui/media/PlayerViewHolder.kt @@ -60,8 +60,10 @@ class PlayerViewHolder private constructor(itemView: View) { val action4 = itemView.requireViewById<ImageButton>(R.id.action4) // Settings screen + val settingsText = itemView.requireViewById<TextView>(R.id.remove_text) val cancel = itemView.requireViewById<View>(R.id.cancel) - val dismiss = itemView.requireViewById<View>(R.id.dismiss) + val dismiss = itemView.requireViewById<ViewGroup>(R.id.dismiss) + val dismissLabel = dismiss.getChildAt(0) val settings = itemView.requireViewById<View>(R.id.settings) init { diff --git a/packages/SystemUI/src/com/android/systemui/media/SeekBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/media/SeekBarViewModel.kt index 9e326aaec3c1..c8244589ce44 100644 --- a/packages/SystemUI/src/com/android/systemui/media/SeekBarViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/media/SeekBarViewModel.kt @@ -91,9 +91,9 @@ class SeekBarViewModel @Inject constructor(@Background private val bgExecutor: R } private var playbackState: PlaybackState? = null private var callback = object : MediaController.Callback() { - override fun onPlaybackStateChanged(state: PlaybackState) { + override fun onPlaybackStateChanged(state: PlaybackState?) { playbackState = state - if (PlaybackState.STATE_NONE.equals(playbackState)) { + if (playbackState == null || PlaybackState.STATE_NONE.equals(playbackState)) { clearController() } else { checkIfPollingNeeded() diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java new file mode 100644 index 000000000000..0d5faff65aab --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java @@ -0,0 +1,227 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.media.dialog; + +import static android.text.Spanned.SPAN_EXCLUSIVE_EXCLUSIVE; + +import android.graphics.PorterDuff; +import android.graphics.PorterDuffColorFilter; +import android.graphics.drawable.Drawable; +import android.text.SpannableString; +import android.text.TextUtils; +import android.text.style.ForegroundColorSpan; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; + +import com.android.settingslib.Utils; +import com.android.settingslib.media.LocalMediaManager.MediaDeviceState; +import com.android.settingslib.media.MediaDevice; +import com.android.systemui.R; + +import java.util.List; + +/** + * Adapter for media output dialog. + */ +public class MediaOutputAdapter extends MediaOutputBaseAdapter { + + private static final String TAG = "MediaOutputAdapter"; + private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + + private ViewGroup mConnectedItem; + private boolean mInclueDynamicGroup; + + public MediaOutputAdapter(MediaOutputController controller) { + super(controller); + } + + @Override + public MediaDeviceBaseViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, + int viewType) { + super.onCreateViewHolder(viewGroup, viewType); + + return new MediaDeviceViewHolder(mHolderView); + } + + @Override + public void onBindViewHolder(@NonNull MediaDeviceBaseViewHolder viewHolder, int position) { + final int size = mController.getMediaDevices().size(); + if (position == size && mController.isZeroMode()) { + viewHolder.onBind(CUSTOMIZED_ITEM_PAIR_NEW, false /* topMargin */, + true /* bottomMargin */); + } else if (mInclueDynamicGroup) { + if (position == 0) { + viewHolder.onBind(CUSTOMIZED_ITEM_DYNAMIC_GROUP, true /* topMargin */, + false /* bottomMargin */); + } else { + // When group item is added at the first(position == 0), devices will be added from + // the second item(position == 1). It means that the index of device list starts + // from "position - 1". + viewHolder.onBind(((List<MediaDevice>) (mController.getMediaDevices())) + .get(position - 1), + false /* topMargin */, position == size /* bottomMargin */); + } + } else if (position < size) { + viewHolder.onBind(((List<MediaDevice>) (mController.getMediaDevices())).get(position), + position == 0 /* topMargin */, position == (size - 1) /* bottomMargin */); + } else if (DEBUG) { + Log.d(TAG, "Incorrect position: " + position); + } + } + + @Override + public int getItemCount() { + mInclueDynamicGroup = mController.getSelectedMediaDevice().size() > 1; + if (mController.isZeroMode() || mInclueDynamicGroup) { + // Add extra one for "pair new" or dynamic group + return mController.getMediaDevices().size() + 1; + } + return mController.getMediaDevices().size(); + } + + @Override + CharSequence getItemTitle(MediaDevice device) { + if (device.getDeviceType() == MediaDevice.MediaDeviceType.TYPE_BLUETOOTH_DEVICE + && !device.isConnected()) { + final CharSequence deviceName = device.getName(); + // Append status to title only for the disconnected Bluetooth device. + final SpannableString spannableTitle = new SpannableString( + mContext.getString(R.string.media_output_dialog_disconnected, deviceName)); + spannableTitle.setSpan(new ForegroundColorSpan( + Utils.getColorAttrDefaultColor(mContext, android.R.attr.textColorSecondary)), + deviceName.length(), + spannableTitle.length(), SPAN_EXCLUSIVE_EXCLUSIVE); + return spannableTitle; + } + return super.getItemTitle(device); + } + + class MediaDeviceViewHolder extends MediaDeviceBaseViewHolder { + + MediaDeviceViewHolder(View view) { + super(view); + } + + @Override + void onBind(MediaDevice device, boolean topMargin, boolean bottomMargin) { + super.onBind(device, topMargin, bottomMargin); + final boolean currentlyConnected = !mInclueDynamicGroup && isCurrentlyConnected(device); + if (currentlyConnected) { + mConnectedItem = mContainerLayout; + } + mBottomDivider.setVisibility(View.GONE); + mCheckBox.setVisibility(View.GONE); + if (currentlyConnected && mController.isActiveRemoteDevice(device)) { + // Init active device layout + mDivider.setVisibility(View.VISIBLE); + mDivider.setTransitionAlpha(1); + mAddIcon.setVisibility(View.VISIBLE); + mAddIcon.setTransitionAlpha(1); + mAddIcon.setOnClickListener(v -> onEndItemClick()); + } else { + // Init non-active device layout + mDivider.setVisibility(View.GONE); + mAddIcon.setVisibility(View.GONE); + } + if (mController.isTransferring()) { + if (device.getState() == MediaDeviceState.STATE_CONNECTING + && !mController.hasAdjustVolumeUserRestriction()) { + setTwoLineLayout(device, true /* bFocused */, false /* showSeekBar*/, + true /* showProgressBar */, false /* showSubtitle */); + } else { + setSingleLineLayout(getItemTitle(device), false /* bFocused */); + } + } else { + // Set different layout for each device + if (device.getState() == MediaDeviceState.STATE_CONNECTING_FAILED) { + setTwoLineLayout(device, false /* bFocused */, + false /* showSeekBar */, false /* showProgressBar */, + true /* showSubtitle */); + mSubTitleText.setText(R.string.media_output_dialog_connect_failed); + mContainerLayout.setOnClickListener(v -> onItemClick(v, device)); + } else if (!mController.hasAdjustVolumeUserRestriction() && currentlyConnected) { + setTwoLineLayout(device, true /* bFocused */, true /* showSeekBar */, + false /* showProgressBar */, false /* showSubtitle */); + initSeekbar(device); + } else { + setSingleLineLayout(getItemTitle(device), false /* bFocused */); + mContainerLayout.setOnClickListener(v -> onItemClick(v, device)); + } + } + } + + @Override + void onBind(int customizedItem, boolean topMargin, boolean bottomMargin) { + super.onBind(customizedItem, topMargin, bottomMargin); + if (customizedItem == CUSTOMIZED_ITEM_PAIR_NEW) { + mCheckBox.setVisibility(View.GONE); + mDivider.setVisibility(View.GONE); + mAddIcon.setVisibility(View.GONE); + mBottomDivider.setVisibility(View.GONE); + setSingleLineLayout(mContext.getText(R.string.media_output_dialog_pairing_new), + false /* bFocused */); + final Drawable d = mContext.getDrawable(R.drawable.ic_add); + d.setColorFilter(new PorterDuffColorFilter( + Utils.getColorAccentDefaultColor(mContext), PorterDuff.Mode.SRC_IN)); + mTitleIcon.setImageDrawable(d); + mContainerLayout.setOnClickListener(v -> onItemClick(CUSTOMIZED_ITEM_PAIR_NEW)); + } else if (customizedItem == CUSTOMIZED_ITEM_DYNAMIC_GROUP) { + mConnectedItem = mContainerLayout; + mBottomDivider.setVisibility(View.GONE); + mCheckBox.setVisibility(View.GONE); + mDivider.setVisibility(View.VISIBLE); + mDivider.setTransitionAlpha(1); + mAddIcon.setVisibility(View.VISIBLE); + mAddIcon.setTransitionAlpha(1); + mAddIcon.setOnClickListener(v -> onEndItemClick()); + mTitleIcon.setImageDrawable(getSpeakerDrawable()); + final CharSequence sessionName = mController.getSessionName(); + final CharSequence title = TextUtils.isEmpty(sessionName) + ? mContext.getString(R.string.media_output_dialog_group) : sessionName; + setTwoLineLayout(title, true /* bFocused */, true /* showSeekBar */, + false /* showProgressBar */, false /* showSubtitle */); + initSessionSeekbar(); + } + } + + private void onItemClick(View view, MediaDevice device) { + if (mController.isTransferring()) { + return; + } + + playSwitchingAnim(mConnectedItem, view); + mController.connectDevice(device); + device.setState(MediaDeviceState.STATE_CONNECTING); + if (!isAnimating()) { + notifyDataSetChanged(); + } + } + + private void onItemClick(int customizedItem) { + if (customizedItem == CUSTOMIZED_ITEM_PAIR_NEW) { + mController.launchBluetoothPairing(); + } + } + + private void onEndItemClick() { + mController.launchMediaOutputGroupDialog(); + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java new file mode 100644 index 000000000000..f1d4804aa622 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java @@ -0,0 +1,322 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.media.dialog; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.content.Context; +import android.content.res.ColorStateList; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffColorFilter; +import android.graphics.Typeface; +import android.graphics.drawable.Drawable; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CheckBox; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.ProgressBar; +import android.widget.RelativeLayout; +import android.widget.SeekBar; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.android.settingslib.bluetooth.BluetoothUtils; +import com.android.settingslib.media.MediaDevice; +import com.android.systemui.Interpolators; +import com.android.systemui.R; + +/** + * Base adapter for media output dialog. + */ +public abstract class MediaOutputBaseAdapter extends + RecyclerView.Adapter<MediaOutputBaseAdapter.MediaDeviceBaseViewHolder> { + + static final int CUSTOMIZED_ITEM_PAIR_NEW = 1; + static final int CUSTOMIZED_ITEM_GROUP = 2; + static final int CUSTOMIZED_ITEM_DYNAMIC_GROUP = 3; + + final MediaOutputController mController; + + private int mMargin; + private boolean mIsAnimating; + + Context mContext; + View mHolderView; + boolean mIsDragging; + + public MediaOutputBaseAdapter(MediaOutputController controller) { + mController = controller; + mIsDragging = false; + } + + @Override + public MediaDeviceBaseViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, + int viewType) { + mContext = viewGroup.getContext(); + mMargin = mContext.getResources().getDimensionPixelSize( + R.dimen.media_output_dialog_list_margin); + mHolderView = LayoutInflater.from(mContext).inflate(R.layout.media_output_list_item, + viewGroup, false); + + return null; + } + + CharSequence getItemTitle(MediaDevice device) { + return device.getName(); + } + + boolean isCurrentlyConnected(MediaDevice device) { + return TextUtils.equals(device.getId(), + mController.getCurrentConnectedMediaDevice().getId()); + } + + boolean isDragging() { + return mIsDragging; + } + + boolean isAnimating() { + return mIsAnimating; + } + + /** + * ViewHolder for binding device view. + */ + abstract class MediaDeviceBaseViewHolder extends RecyclerView.ViewHolder { + + private static final int ANIM_DURATION = 200; + + final LinearLayout mContainerLayout; + final TextView mTitleText; + final TextView mTwoLineTitleText; + final TextView mSubTitleText; + final ImageView mTitleIcon; + final ImageView mAddIcon; + final ProgressBar mProgressBar; + final SeekBar mSeekBar; + final RelativeLayout mTwoLineLayout; + final View mDivider; + final View mBottomDivider; + final CheckBox mCheckBox; + + MediaDeviceBaseViewHolder(View view) { + super(view); + mContainerLayout = view.requireViewById(R.id.device_container); + mTitleText = view.requireViewById(R.id.title); + mSubTitleText = view.requireViewById(R.id.subtitle); + mTwoLineLayout = view.requireViewById(R.id.two_line_layout); + mTwoLineTitleText = view.requireViewById(R.id.two_line_title); + mTitleIcon = view.requireViewById(R.id.title_icon); + mProgressBar = view.requireViewById(R.id.volume_indeterminate_progress); + mSeekBar = view.requireViewById(R.id.volume_seekbar); + mDivider = view.requireViewById(R.id.end_divider); + mBottomDivider = view.requireViewById(R.id.bottom_divider); + mAddIcon = view.requireViewById(R.id.add_icon); + mCheckBox = view.requireViewById(R.id.check_box); + } + + void onBind(MediaDevice device, boolean topMargin, boolean bottomMargin) { + mTitleIcon.setImageIcon(mController.getDeviceIconCompat(device).toIcon(mContext)); + setMargin(topMargin, bottomMargin); + } + + void onBind(int customizedItem, boolean topMargin, boolean bottomMargin) { + setMargin(topMargin, bottomMargin); + } + + private void setMargin(boolean topMargin, boolean bottomMargin) { + ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) mContainerLayout + .getLayoutParams(); + params.topMargin = topMargin ? mMargin : 0; + params.bottomMargin = bottomMargin ? mMargin : 0; + mContainerLayout.setLayoutParams(params); + } + + void setSingleLineLayout(CharSequence title, boolean bFocused) { + mTwoLineLayout.setVisibility(View.GONE); + mProgressBar.setVisibility(View.GONE); + mTitleText.setVisibility(View.VISIBLE); + mTitleText.setTranslationY(0); + mTitleText.setText(title); + if (bFocused) { + mTitleText.setTypeface(Typeface.create(mContext.getString( + com.android.internal.R.string.config_headlineFontFamilyMedium), + Typeface.NORMAL)); + } else { + mTitleText.setTypeface(Typeface.create(mContext.getString( + com.android.internal.R.string.config_headlineFontFamily), Typeface.NORMAL)); + } + } + + void setTwoLineLayout(MediaDevice device, boolean bFocused, boolean showSeekBar, + boolean showProgressBar, boolean showSubtitle) { + setTwoLineLayout(device, null, bFocused, showSeekBar, showProgressBar, showSubtitle); + } + + void setTwoLineLayout(CharSequence title, boolean bFocused, boolean showSeekBar, + boolean showProgressBar, boolean showSubtitle) { + setTwoLineLayout(null, title, bFocused, showSeekBar, showProgressBar, showSubtitle); + } + + private void setTwoLineLayout(MediaDevice device, CharSequence title, boolean bFocused, + boolean showSeekBar, boolean showProgressBar, boolean showSubtitle) { + mTitleText.setVisibility(View.GONE); + mTwoLineLayout.setVisibility(View.VISIBLE); + mSeekBar.setAlpha(1); + mSeekBar.setVisibility(showSeekBar ? View.VISIBLE : View.GONE); + mProgressBar.setVisibility(showProgressBar ? View.VISIBLE : View.GONE); + mSubTitleText.setVisibility(showSubtitle ? View.VISIBLE : View.GONE); + mTwoLineTitleText.setTranslationY(0); + if (device == null) { + mTwoLineTitleText.setText(title); + } else { + mTwoLineTitleText.setText(getItemTitle(device)); + } + + if (bFocused) { + mTwoLineTitleText.setTypeface(Typeface.create(mContext.getString( + com.android.internal.R.string.config_headlineFontFamilyMedium), + Typeface.NORMAL)); + } else { + mTwoLineTitleText.setTypeface(Typeface.create(mContext.getString( + com.android.internal.R.string.config_headlineFontFamily), Typeface.NORMAL)); + } + } + + void initSeekbar(MediaDevice device) { + mSeekBar.setMax(device.getMaxVolume()); + mSeekBar.setMin(0); + final int currentVolume = device.getCurrentVolume(); + if (mSeekBar.getProgress() != currentVolume) { + mSeekBar.setProgress(currentVolume); + } + mSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + if (device == null || !fromUser) { + return; + } + mController.adjustVolume(device, progress); + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + mIsDragging = true; + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + mIsDragging = false; + } + }); + } + + void initSessionSeekbar() { + mSeekBar.setMax(mController.getSessionVolumeMax()); + mSeekBar.setMin(0); + final int currentVolume = mController.getSessionVolume(); + if (mSeekBar.getProgress() != currentVolume) { + mSeekBar.setProgress(currentVolume); + } + mSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + if (!fromUser) { + return; + } + mController.adjustSessionVolume(progress); + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + mIsDragging = true; + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + mIsDragging = false; + } + }); + } + + void playSwitchingAnim(@NonNull View from, @NonNull View to) { + final float delta = (float) (mContext.getResources().getDimensionPixelSize( + R.dimen.media_output_dialog_title_anim_y_delta)); + final SeekBar fromSeekBar = from.requireViewById(R.id.volume_seekbar); + final TextView toTitleText = to.requireViewById(R.id.title); + if (fromSeekBar.getVisibility() != View.VISIBLE || toTitleText.getVisibility() + != View.VISIBLE) { + return; + } + mIsAnimating = true; + // Animation for title text + toTitleText.setTypeface(Typeface.create(mContext.getString( + com.android.internal.R.string.config_headlineFontFamilyMedium), + Typeface.NORMAL)); + toTitleText.animate() + .setDuration(ANIM_DURATION) + .translationY(-delta) + .setInterpolator(Interpolators.FAST_OUT_SLOW_IN) + .setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + to.requireViewById(R.id.volume_indeterminate_progress).setVisibility( + View.VISIBLE); + } + }); + // Animation for seek bar + fromSeekBar.animate() + .alpha(0) + .setDuration(ANIM_DURATION) + .setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + final TextView fromTitleText = from.requireViewById( + R.id.two_line_title); + fromTitleText.setTypeface(Typeface.create(mContext.getString( + com.android.internal.R.string.config_headlineFontFamily), + Typeface.NORMAL)); + fromTitleText.animate() + .setDuration(ANIM_DURATION) + .translationY(delta) + .setInterpolator(Interpolators.FAST_OUT_SLOW_IN) + .setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mIsAnimating = false; + notifyDataSetChanged(); + } + }); + } + }); + } + + Drawable getSpeakerDrawable() { + final Drawable drawable = mContext.getDrawable(R.drawable.ic_speaker_group_black_24dp) + .mutate(); + final ColorStateList list = mContext.getResources().getColorStateList( + R.color.advanced_icon_color, mContext.getTheme()); + drawable.setColorFilter(new PorterDuffColorFilter(list.getDefaultColor(), + PorterDuff.Mode.SRC_IN)); + return BluetoothUtils.buildAdvancedDrawable(mContext, drawable); + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java new file mode 100644 index 000000000000..745f36bfc7e5 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java @@ -0,0 +1,227 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.media.dialog; + +import static android.view.WindowInsets.Type.navigationBars; +import static android.view.WindowInsets.Type.statusBars; + +import android.content.Context; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.text.TextUtils; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewTreeObserver; +import android.view.Window; +import android.view.WindowInsets; +import android.view.WindowManager; +import android.widget.Button; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import androidx.annotation.VisibleForTesting; +import androidx.core.graphics.drawable.IconCompat; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.android.settingslib.R; +import com.android.systemui.statusbar.phone.SystemUIDialog; + +/** + * Base dialog for media output UI + */ +public abstract class MediaOutputBaseDialog extends SystemUIDialog implements + MediaOutputController.Callback, Window.Callback { + + private static final String TAG = "MediaOutputDialog"; + + private final Handler mMainThreadHandler = new Handler(Looper.getMainLooper()); + private final RecyclerView.LayoutManager mLayoutManager; + + final Context mContext; + final MediaOutputController mMediaOutputController; + + @VisibleForTesting + View mDialogView; + private TextView mHeaderTitle; + private TextView mHeaderSubtitle; + private ImageView mHeaderIcon; + private RecyclerView mDevicesRecyclerView; + private LinearLayout mDeviceListLayout; + private Button mDoneButton; + private Button mStopButton; + private int mListMaxHeight; + + MediaOutputBaseAdapter mAdapter; + + private final ViewTreeObserver.OnGlobalLayoutListener mDeviceListLayoutListener = () -> { + // Set max height for list + if (mDeviceListLayout.getHeight() > mListMaxHeight) { + ViewGroup.LayoutParams params = mDeviceListLayout.getLayoutParams(); + params.height = mListMaxHeight; + mDeviceListLayout.setLayoutParams(params); + } + }; + + public MediaOutputBaseDialog(Context context, MediaOutputController mediaOutputController) { + super(context, R.style.Theme_SystemUI_Dialog_MediaOutput); + mContext = context; + mMediaOutputController = mediaOutputController; + mLayoutManager = new LinearLayoutManager(mContext); + mListMaxHeight = context.getResources().getDimensionPixelSize( + R.dimen.media_output_dialog_list_max_height); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + mDialogView = LayoutInflater.from(mContext).inflate(R.layout.media_output_dialog, null); + final Window window = getWindow(); + final WindowManager.LayoutParams lp = window.getAttributes(); + lp.gravity = Gravity.BOTTOM; + // Config insets to make sure the layout is above the navigation bar + lp.setFitInsetsTypes(statusBars() | navigationBars()); + lp.setFitInsetsSides(WindowInsets.Side.all()); + lp.setFitInsetsIgnoringVisibility(true); + window.setAttributes(lp); + window.setContentView(mDialogView); + window.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); + window.setWindowAnimations(R.style.Animation_MediaOutputDialog); + + mHeaderTitle = mDialogView.requireViewById(R.id.header_title); + mHeaderSubtitle = mDialogView.requireViewById(R.id.header_subtitle); + mHeaderIcon = mDialogView.requireViewById(R.id.header_icon); + mDevicesRecyclerView = mDialogView.requireViewById(R.id.list_result); + mDeviceListLayout = mDialogView.requireViewById(R.id.device_list); + mDoneButton = mDialogView.requireViewById(R.id.done); + mStopButton = mDialogView.requireViewById(R.id.stop); + + mDeviceListLayout.getViewTreeObserver().addOnGlobalLayoutListener( + mDeviceListLayoutListener); + // Init device list + mDevicesRecyclerView.setLayoutManager(mLayoutManager); + mDevicesRecyclerView.setAdapter(mAdapter); + // Init header icon + mHeaderIcon.setOnClickListener(v -> onHeaderIconClick()); + // Init bottom buttons + mDoneButton.setOnClickListener(v -> dismiss()); + mStopButton.setOnClickListener(v -> { + mMediaOutputController.releaseSession(); + dismiss(); + }); + } + + @Override + public void onStart() { + super.onStart(); + mMediaOutputController.start(this); + } + + @Override + public void onStop() { + super.onStop(); + mMediaOutputController.stop(); + } + + @VisibleForTesting + void refresh() { + // Update header icon + final int iconRes = getHeaderIconRes(); + final IconCompat iconCompat = getHeaderIcon(); + if (iconRes != 0) { + mHeaderIcon.setVisibility(View.VISIBLE); + mHeaderIcon.setImageResource(iconRes); + } else if (iconCompat != null) { + mHeaderIcon.setVisibility(View.VISIBLE); + mHeaderIcon.setImageIcon(iconCompat.toIcon(mContext)); + } else { + mHeaderIcon.setVisibility(View.GONE); + } + if (mHeaderIcon.getVisibility() == View.VISIBLE) { + final int size = getHeaderIconSize(); + final int padding = mContext.getResources().getDimensionPixelSize( + R.dimen.media_output_dialog_header_icon_padding); + mHeaderIcon.setLayoutParams(new LinearLayout.LayoutParams(size + padding, size)); + } + // Update title and subtitle + mHeaderTitle.setText(getHeaderText()); + final CharSequence subTitle = getHeaderSubtitle(); + if (TextUtils.isEmpty(subTitle)) { + mHeaderSubtitle.setVisibility(View.GONE); + mHeaderTitle.setGravity(Gravity.START | Gravity.CENTER_VERTICAL); + } else { + mHeaderSubtitle.setVisibility(View.VISIBLE); + mHeaderSubtitle.setText(subTitle); + mHeaderTitle.setGravity(Gravity.NO_GRAVITY); + } + if (!mAdapter.isDragging() && !mAdapter.isAnimating()) { + mAdapter.notifyDataSetChanged(); + } + // Show when remote media session is available + mStopButton.setVisibility(getStopButtonVisibility()); + } + + abstract int getHeaderIconRes(); + + abstract IconCompat getHeaderIcon(); + + abstract int getHeaderIconSize(); + + abstract CharSequence getHeaderText(); + + abstract CharSequence getHeaderSubtitle(); + + abstract int getStopButtonVisibility(); + + @Override + public void onMediaChanged() { + mMainThreadHandler.post(() -> refresh()); + } + + @Override + public void onMediaStoppedOrPaused() { + if (isShowing()) { + dismiss(); + } + } + + @Override + public void onRouteChanged() { + mMainThreadHandler.post(() -> refresh()); + } + + @Override + public void dismissDialog() { + dismiss(); + } + + @Override + public void onWindowFocusChanged(boolean hasFocus) { + super.onWindowFocusChanged(hasFocus); + if (!hasFocus && isShowing()) { + dismiss(); + } + } + + void onHeaderIconClick() { + } +} diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java new file mode 100644 index 000000000000..4f8519289a07 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java @@ -0,0 +1,514 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.media.dialog; + +import android.app.Notification; +import android.content.Context; +import android.content.Intent; +import android.graphics.Bitmap; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.Icon; +import android.media.MediaMetadata; +import android.media.MediaRoute2Info; +import android.media.MediaRouter2Manager; +import android.media.RoutingSessionInfo; +import android.media.session.MediaController; +import android.media.session.MediaSessionManager; +import android.media.session.PlaybackState; +import android.os.UserHandle; +import android.os.UserManager; +import android.text.TextUtils; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; +import androidx.core.graphics.drawable.IconCompat; + +import com.android.internal.logging.UiEventLogger; +import com.android.settingslib.RestrictedLockUtilsInternal; +import com.android.settingslib.Utils; +import com.android.settingslib.bluetooth.BluetoothUtils; +import com.android.settingslib.bluetooth.LocalBluetoothManager; +import com.android.settingslib.media.InfoMediaManager; +import com.android.settingslib.media.LocalMediaManager; +import com.android.settingslib.media.MediaDevice; +import com.android.settingslib.media.MediaOutputSliceConstants; +import com.android.settingslib.utils.ThreadUtils; +import com.android.systemui.R; +import com.android.systemui.plugins.ActivityStarter; +import com.android.systemui.statusbar.notification.NotificationEntryManager; +import com.android.systemui.statusbar.notification.collection.NotificationEntry; +import com.android.systemui.statusbar.phone.ShadeController; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +import javax.inject.Inject; + +/** + * Controller for media output dialog + */ +public class MediaOutputController implements LocalMediaManager.DeviceCallback { + + private static final String TAG = "MediaOutputController"; + private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + + private final String mPackageName; + private final Context mContext; + private final MediaSessionManager mMediaSessionManager; + private final ShadeController mShadeController; + private final ActivityStarter mActivityStarter; + private final List<MediaDevice> mGroupMediaDevices = new CopyOnWriteArrayList<>(); + private final boolean mAboveStatusbar; + private final NotificationEntryManager mNotificationEntryManager; + private final MediaRouter2Manager mRouterManager; + @VisibleForTesting + final List<MediaDevice> mMediaDevices = new CopyOnWriteArrayList<>(); + + private MediaController mMediaController; + @VisibleForTesting + Callback mCallback; + @VisibleForTesting + LocalMediaManager mLocalMediaManager; + + private MediaOutputMetricLogger mMetricLogger; + private UiEventLogger mUiEventLogger; + + @Inject + public MediaOutputController(@NonNull Context context, String packageName, + boolean aboveStatusbar, MediaSessionManager mediaSessionManager, LocalBluetoothManager + lbm, ShadeController shadeController, ActivityStarter starter, + NotificationEntryManager notificationEntryManager, UiEventLogger uiEventLogger, + MediaRouter2Manager routerManager) { + mContext = context; + mPackageName = packageName; + mMediaSessionManager = mediaSessionManager; + mShadeController = shadeController; + mActivityStarter = starter; + mAboveStatusbar = aboveStatusbar; + mNotificationEntryManager = notificationEntryManager; + InfoMediaManager imm = new InfoMediaManager(mContext, packageName, null, lbm); + mLocalMediaManager = new LocalMediaManager(mContext, lbm, imm, packageName); + mMetricLogger = new MediaOutputMetricLogger(mContext, mPackageName); + mUiEventLogger = uiEventLogger; + mRouterManager = routerManager; + } + + void start(@NonNull Callback cb) { + mMediaDevices.clear(); + if (!TextUtils.isEmpty(mPackageName)) { + for (MediaController controller : mMediaSessionManager.getActiveSessions(null)) { + if (TextUtils.equals(controller.getPackageName(), mPackageName)) { + mMediaController = controller; + mMediaController.unregisterCallback(mCb); + mMediaController.registerCallback(mCb); + break; + } + } + } + if (mMediaController == null) { + if (DEBUG) { + Log.d(TAG, "No media controller for " + mPackageName); + } + } + if (mLocalMediaManager == null) { + if (DEBUG) { + Log.d(TAG, "No local media manager " + mPackageName); + } + return; + } + mCallback = cb; + mLocalMediaManager.unregisterCallback(this); + mLocalMediaManager.stopScan(); + mLocalMediaManager.registerCallback(this); + mLocalMediaManager.startScan(); + if (mRouterManager != null) { + mRouterManager.startScan(); + } + } + + void stop() { + if (mMediaController != null) { + mMediaController.unregisterCallback(mCb); + } + if (mLocalMediaManager != null) { + mLocalMediaManager.unregisterCallback(this); + mLocalMediaManager.stopScan(); + } + if (mRouterManager != null) { + mRouterManager.stopScan(); + } + mMediaDevices.clear(); + } + + @Override + public void onDeviceListUpdate(List<MediaDevice> devices) { + buildMediaDevices(devices); + mCallback.onRouteChanged(); + } + + @Override + public void onSelectedDeviceStateChanged(MediaDevice device, + @LocalMediaManager.MediaDeviceState int state) { + mCallback.onRouteChanged(); + mMetricLogger.logOutputSuccess(device.toString(), mMediaDevices); + } + + @Override + public void onDeviceAttributesChanged() { + mCallback.onRouteChanged(); + } + + @Override + public void onRequestFailed(int reason) { + mCallback.onRouteChanged(); + mMetricLogger.logOutputFailure(mMediaDevices, reason); + } + + CharSequence getHeaderTitle() { + if (mMediaController != null) { + final MediaMetadata metadata = mMediaController.getMetadata(); + if (metadata != null) { + return metadata.getDescription().getTitle(); + } + } + return mContext.getText(R.string.controls_media_title); + } + + CharSequence getHeaderSubTitle() { + if (mMediaController == null) { + return null; + } + final MediaMetadata metadata = mMediaController.getMetadata(); + if (metadata == null) { + return null; + } + return metadata.getDescription().getSubtitle(); + } + + IconCompat getHeaderIcon() { + if (mMediaController == null) { + return null; + } + final MediaMetadata metadata = mMediaController.getMetadata(); + if (metadata != null) { + final Bitmap bitmap = metadata.getDescription().getIconBitmap(); + if (bitmap != null) { + final Bitmap roundBitmap = Utils.convertCornerRadiusBitmap(mContext, bitmap, + (float) mContext.getResources().getDimensionPixelSize( + R.dimen.media_output_dialog_icon_corner_radius)); + return IconCompat.createWithBitmap(roundBitmap); + } + } + if (DEBUG) { + Log.d(TAG, "Media meta data does not contain icon information"); + } + return getNotificationIcon(); + } + + IconCompat getDeviceIconCompat(MediaDevice device) { + Drawable drawable = device.getIcon(); + if (drawable == null) { + if (DEBUG) { + Log.d(TAG, "getDeviceIconCompat() device : " + device.getName() + + ", drawable is null"); + } + // Use default Bluetooth device icon to handle getIcon() is null case. + drawable = mContext.getDrawable(com.android.internal.R.drawable.ic_bt_headphones_a2dp); + } + return BluetoothUtils.createIconWithDrawable(drawable); + } + + IconCompat getNotificationIcon() { + if (TextUtils.isEmpty(mPackageName)) { + return null; + } + for (NotificationEntry entry + : mNotificationEntryManager.getActiveNotificationsForCurrentUser()) { + final Notification notification = entry.getSbn().getNotification(); + if (notification.hasMediaSession() + && TextUtils.equals(entry.getSbn().getPackageName(), mPackageName)) { + final Icon icon = notification.getLargeIcon(); + if (icon == null) { + break; + } + return IconCompat.createFromIcon(icon); + } + } + return null; + } + + private void buildMediaDevices(List<MediaDevice> devices) { + // For the first time building list, to make sure the top device is the connected device. + if (mMediaDevices.isEmpty()) { + final MediaDevice connectedMediaDevice = getCurrentConnectedMediaDevice(); + if (connectedMediaDevice == null) { + if (DEBUG) { + Log.d(TAG, "No connected media device."); + } + mMediaDevices.addAll(devices); + return; + } + for (MediaDevice device : devices) { + if (TextUtils.equals(device.getId(), connectedMediaDevice.getId())) { + mMediaDevices.add(0, device); + } else { + mMediaDevices.add(device); + } + } + return; + } + // To keep the same list order + final Collection<MediaDevice> targetMediaDevices = new ArrayList<>(); + for (MediaDevice originalDevice : mMediaDevices) { + for (MediaDevice newDevice : devices) { + if (TextUtils.equals(originalDevice.getId(), newDevice.getId())) { + targetMediaDevices.add(newDevice); + break; + } + } + } + if (targetMediaDevices.size() != devices.size()) { + devices.removeAll(targetMediaDevices); + targetMediaDevices.addAll(devices); + } + mMediaDevices.clear(); + mMediaDevices.addAll(targetMediaDevices); + } + + List<MediaDevice> getGroupMediaDevices() { + final List<MediaDevice> selectedDevices = getSelectedMediaDevice(); + final List<MediaDevice> selectableDevices = getSelectableMediaDevice(); + if (mGroupMediaDevices.isEmpty()) { + mGroupMediaDevices.addAll(selectedDevices); + mGroupMediaDevices.addAll(selectableDevices); + return mGroupMediaDevices; + } + // To keep the same list order + final Collection<MediaDevice> sourceDevices = new ArrayList<>(); + final Collection<MediaDevice> targetMediaDevices = new ArrayList<>(); + sourceDevices.addAll(selectedDevices); + sourceDevices.addAll(selectableDevices); + for (MediaDevice originalDevice : mGroupMediaDevices) { + for (MediaDevice newDevice : sourceDevices) { + if (TextUtils.equals(originalDevice.getId(), newDevice.getId())) { + targetMediaDevices.add(newDevice); + sourceDevices.remove(newDevice); + break; + } + } + } + // Add new devices at the end of list if necessary + if (!sourceDevices.isEmpty()) { + targetMediaDevices.addAll(sourceDevices); + } + mGroupMediaDevices.clear(); + mGroupMediaDevices.addAll(targetMediaDevices); + + return mGroupMediaDevices; + } + + void resetGroupMediaDevices() { + mGroupMediaDevices.clear(); + } + + void connectDevice(MediaDevice device) { + mMetricLogger.updateOutputEndPoints(getCurrentConnectedMediaDevice(), device); + + ThreadUtils.postOnBackgroundThread(() -> { + mLocalMediaManager.connectDevice(device); + }); + } + + Collection<MediaDevice> getMediaDevices() { + return mMediaDevices; + } + + MediaDevice getCurrentConnectedMediaDevice() { + return mLocalMediaManager.getCurrentConnectedDevice(); + } + + private MediaDevice getMediaDeviceById(String id) { + return mLocalMediaManager.getMediaDeviceById(new ArrayList<>(mMediaDevices), id); + } + + boolean addDeviceToPlayMedia(MediaDevice device) { + return mLocalMediaManager.addDeviceToPlayMedia(device); + } + + boolean removeDeviceFromPlayMedia(MediaDevice device) { + return mLocalMediaManager.removeDeviceFromPlayMedia(device); + } + + List<MediaDevice> getSelectableMediaDevice() { + return mLocalMediaManager.getSelectableMediaDevice(); + } + + List<MediaDevice> getSelectedMediaDevice() { + return mLocalMediaManager.getSelectedMediaDevice(); + } + + List<MediaDevice> getDeselectableMediaDevice() { + return mLocalMediaManager.getDeselectableMediaDevice(); + } + + void adjustSessionVolume(String sessionId, int volume) { + mLocalMediaManager.adjustSessionVolume(sessionId, volume); + } + + void adjustSessionVolume(int volume) { + mLocalMediaManager.adjustSessionVolume(volume); + } + + int getSessionVolumeMax() { + return mLocalMediaManager.getSessionVolumeMax(); + } + + int getSessionVolume() { + return mLocalMediaManager.getSessionVolume(); + } + + CharSequence getSessionName() { + return mLocalMediaManager.getSessionName(); + } + + void releaseSession() { + mLocalMediaManager.releaseSession(); + } + + List<RoutingSessionInfo> getActiveRemoteMediaDevices() { + final List<RoutingSessionInfo> sessionInfos = new ArrayList<>(); + for (RoutingSessionInfo info : mLocalMediaManager.getActiveMediaSession()) { + if (!info.isSystemSession()) { + sessionInfos.add(info); + } + } + return sessionInfos; + } + + void adjustVolume(MediaDevice device, int volume) { + ThreadUtils.postOnBackgroundThread(() -> { + device.requestSetVolume(volume); + }); + } + + String getPackageName() { + return mPackageName; + } + + boolean hasAdjustVolumeUserRestriction() { + if (RestrictedLockUtilsInternal.checkIfRestrictionEnforced( + mContext, UserManager.DISALLOW_ADJUST_VOLUME, UserHandle.myUserId()) != null) { + return true; + } + final UserManager um = mContext.getSystemService(UserManager.class); + return um.hasBaseUserRestriction(UserManager.DISALLOW_ADJUST_VOLUME, + UserHandle.of(UserHandle.myUserId())); + } + + boolean isTransferring() { + for (MediaDevice device : mMediaDevices) { + if (device.getState() == LocalMediaManager.MediaDeviceState.STATE_CONNECTING) { + return true; + } + } + return false; + } + + boolean isZeroMode() { + if (mMediaDevices.size() == 1) { + final MediaDevice device = mMediaDevices.iterator().next(); + // Add "pair new" only when local output device exists + final int type = device.getDeviceType(); + if (type == MediaDevice.MediaDeviceType.TYPE_PHONE_DEVICE + || type == MediaDevice.MediaDeviceType.TYPE_3POINT5_MM_AUDIO_DEVICE + || type == MediaDevice.MediaDeviceType.TYPE_USB_C_AUDIO_DEVICE) { + return true; + } + } + return false; + } + + void launchBluetoothPairing() { + mCallback.dismissDialog(); + final ActivityStarter.OnDismissAction postKeyguardAction = () -> { + mContext.sendBroadcast(new Intent() + .setAction(MediaOutputSliceConstants.ACTION_LAUNCH_BLUETOOTH_PAIRING) + .setPackage(MediaOutputSliceConstants.SETTINGS_PACKAGE_NAME)); + mShadeController.animateCollapsePanels(); + return true; + }; + mActivityStarter.dismissKeyguardThenExecute(postKeyguardAction, null, true); + } + + void launchMediaOutputDialog() { + mCallback.dismissDialog(); + new MediaOutputDialog(mContext, mAboveStatusbar, this, mUiEventLogger); + } + + void launchMediaOutputGroupDialog() { + mCallback.dismissDialog(); + new MediaOutputGroupDialog(mContext, mAboveStatusbar, this); + } + + boolean isActiveRemoteDevice(@NonNull MediaDevice device) { + final List<String> features = device.getFeatures(); + return (features.contains(MediaRoute2Info.FEATURE_REMOTE_PLAYBACK) + || features.contains(MediaRoute2Info.FEATURE_REMOTE_AUDIO_PLAYBACK) + || features.contains(MediaRoute2Info.FEATURE_REMOTE_VIDEO_PLAYBACK) + || features.contains(MediaRoute2Info.FEATURE_REMOTE_GROUP_PLAYBACK)); + } + + private final MediaController.Callback mCb = new MediaController.Callback() { + @Override + public void onMetadataChanged(MediaMetadata metadata) { + mCallback.onMediaChanged(); + } + + @Override + public void onPlaybackStateChanged(PlaybackState playbackState) { + final int state = playbackState.getState(); + if (state == PlaybackState.STATE_STOPPED || state == PlaybackState.STATE_PAUSED) { + mCallback.onMediaStoppedOrPaused(); + } + } + }; + + interface Callback { + /** + * Override to handle the media content updating. + */ + void onMediaChanged(); + + /** + * Override to handle the media state updating. + */ + void onMediaStoppedOrPaused(); + + /** + * Override to handle the device updating. + */ + void onRouteChanged(); + + /** + * Override to dismiss dialog. + */ + void dismissDialog(); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java new file mode 100644 index 000000000000..fedeceabe73a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.media.dialog; + +import android.content.Context; +import android.os.Bundle; +import android.view.View; +import android.view.WindowManager; + +import androidx.core.graphics.drawable.IconCompat; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.logging.UiEvent; +import com.android.internal.logging.UiEventLogger; +import com.android.systemui.R; + +import javax.inject.Singleton; + +/** + * Dialog for media output transferring. + */ +@Singleton +public class MediaOutputDialog extends MediaOutputBaseDialog { + final UiEventLogger mUiEventLogger; + + MediaOutputDialog(Context context, boolean aboveStatusbar, MediaOutputController + mediaOutputController, UiEventLogger uiEventLogger) { + super(context, mediaOutputController); + mUiEventLogger = uiEventLogger; + mAdapter = new MediaOutputAdapter(mMediaOutputController); + if (!aboveStatusbar) { + getWindow().setType(WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY); + } + show(); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + mUiEventLogger.log(MediaOutputEvent.MEDIA_OUTPUT_DIALOG_SHOW); + } + + @Override + int getHeaderIconRes() { + return 0; + } + + @Override + IconCompat getHeaderIcon() { + return mMediaOutputController.getHeaderIcon(); + } + + @Override + int getHeaderIconSize() { + return mContext.getResources().getDimensionPixelSize( + R.dimen.media_output_dialog_header_album_icon_size); + } + + @Override + CharSequence getHeaderText() { + return mMediaOutputController.getHeaderTitle(); + } + + @Override + CharSequence getHeaderSubtitle() { + return mMediaOutputController.getHeaderSubTitle(); + } + + @Override + int getStopButtonVisibility() { + return mMediaOutputController.isActiveRemoteDevice( + mMediaOutputController.getCurrentConnectedMediaDevice()) ? View.VISIBLE : View.GONE; + } + + @VisibleForTesting + public enum MediaOutputEvent implements UiEventLogger.UiEventEnum { + @UiEvent(doc = "The MediaOutput dialog became visible on the screen.") + MEDIA_OUTPUT_DIALOG_SHOW(655); + + private final int mId; + + MediaOutputEvent(int id) { + mId = id; + } + + @Override + public int getId() { + return mId; + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogFactory.kt b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogFactory.kt new file mode 100644 index 000000000000..e1a504c3e084 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogFactory.kt @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.media.dialog + +import android.content.Context +import android.media.session.MediaSessionManager +import android.media.MediaRouter2Manager +import com.android.internal.logging.UiEventLogger +import com.android.settingslib.bluetooth.LocalBluetoothManager +import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.statusbar.notification.NotificationEntryManager +import com.android.systemui.statusbar.phone.ShadeController +import javax.inject.Inject + +/** + * Factory to create [MediaOutputDialog] objects. + */ +class MediaOutputDialogFactory @Inject constructor( + private val context: Context, + private val mediaSessionManager: MediaSessionManager, + private val lbm: LocalBluetoothManager?, + private val shadeController: ShadeController, + private val starter: ActivityStarter, + private val notificationEntryManager: NotificationEntryManager, + private val uiEventLogger: UiEventLogger, + private val routerManager: MediaRouter2Manager +) { + companion object { + var mediaOutputDialog: MediaOutputDialog? = null + } + + /** Creates a [MediaOutputDialog] for the given package. */ + fun create(packageName: String, aboveStatusBar: Boolean) { + mediaOutputDialog?.dismiss() + mediaOutputDialog = MediaOutputController(context, packageName, aboveStatusBar, + mediaSessionManager, lbm, shadeController, starter, notificationEntryManager, + uiEventLogger, routerManager).run { + MediaOutputDialog(context, aboveStatusBar, this, uiEventLogger) + } + } + + /** dismiss [MediaOutputDialog] if exist. */ + fun dismiss() { + mediaOutputDialog?.dismiss() + mediaOutputDialog = null + } +} diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogReceiver.kt b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogReceiver.kt new file mode 100644 index 000000000000..0ce0c020ccde --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogReceiver.kt @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.media.dialog + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.text.TextUtils +import com.android.settingslib.media.MediaOutputSliceConstants +import javax.inject.Inject + +/** + * BroadcastReceiver for handling media output intent + */ +class MediaOutputDialogReceiver @Inject constructor( + private val mediaOutputDialogFactory: MediaOutputDialogFactory +) : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + if (TextUtils.equals(MediaOutputSliceConstants.ACTION_LAUNCH_MEDIA_OUTPUT_DIALOG, + intent.action)) { + mediaOutputDialogFactory.create( + intent.getStringExtra(MediaOutputSliceConstants.EXTRA_PACKAGE_NAME), false) + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputGroupAdapter.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputGroupAdapter.java new file mode 100644 index 000000000000..24e076bb22f1 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputGroupAdapter.java @@ -0,0 +1,177 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.media.dialog; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.drawable.Drawable; +import android.text.TextUtils; +import android.util.Log; +import android.util.TypedValue; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; + +import com.android.settingslib.media.MediaDevice; +import com.android.systemui.R; + +import java.util.List; + +/** + * Adapter for media output dynamic group dialog. + */ +public class MediaOutputGroupAdapter extends MediaOutputBaseAdapter { + + private static final String TAG = "MediaOutputGroupAdapter"; + private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + + private final List<MediaDevice> mGroupMediaDevices; + + public MediaOutputGroupAdapter(MediaOutputController controller) { + super(controller); + mGroupMediaDevices = controller.getGroupMediaDevices(); + } + + @Override + public MediaDeviceBaseViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, + int viewType) { + super.onCreateViewHolder(viewGroup, viewType); + + return new GroupViewHolder(mHolderView); + } + + @Override + public void onBindViewHolder(@NonNull MediaDeviceBaseViewHolder viewHolder, int position) { + // Add "Group" + if (position == 0) { + viewHolder.onBind(CUSTOMIZED_ITEM_GROUP, true /* topMargin */, + false /* bottomMargin */); + return; + } + // Add available devices + final int newPosition = position - 1; + final int size = mGroupMediaDevices.size(); + if (newPosition < size) { + viewHolder.onBind(mGroupMediaDevices.get(newPosition), false /* topMargin */, + newPosition == (size - 1) /* bottomMargin */); + return; + } + if (DEBUG) { + Log.d(TAG, "Incorrect position: " + position); + } + } + + @Override + public int getItemCount() { + // Require extra item for group volume operation + return mGroupMediaDevices.size() + 1; + } + + @Override + CharSequence getItemTitle(MediaDevice device) { + return super.getItemTitle(device); + } + + class GroupViewHolder extends MediaDeviceBaseViewHolder { + + GroupViewHolder(View view) { + super(view); + } + + @Override + void onBind(MediaDevice device, boolean topMargin, boolean bottomMargin) { + super.onBind(device, topMargin, bottomMargin); + mDivider.setVisibility(View.GONE); + mAddIcon.setVisibility(View.GONE); + mBottomDivider.setVisibility(View.GONE); + mCheckBox.setVisibility(View.VISIBLE); + mCheckBox.setOnCheckedChangeListener((buttonView, isChecked) -> { + onCheckBoxClicked(isChecked, device); + }); + setTwoLineLayout(device, false /* bFocused */, true /* showSeekBar */, + false /* showProgressBar */, false /* showSubtitle*/); + initSeekbar(device); + final List<MediaDevice> selectedDevices = mController.getSelectedMediaDevice(); + if (isDeviceIncluded(mController.getSelectableMediaDevice(), device)) { + mCheckBox.setButtonDrawable(R.drawable.ic_check_box); + mCheckBox.setChecked(false); + mCheckBox.setEnabled(true); + } else if (isDeviceIncluded(selectedDevices, device)) { + if (selectedDevices.size() == 1 || !isDeviceIncluded( + mController.getDeselectableMediaDevice(), device)) { + mCheckBox.setButtonDrawable(getDisabledCheckboxDrawable()); + mCheckBox.setChecked(true); + mCheckBox.setEnabled(false); + } else { + mCheckBox.setButtonDrawable(R.drawable.ic_check_box); + mCheckBox.setChecked(true); + mCheckBox.setEnabled(true); + } + } + } + + @Override + void onBind(int customizedItem, boolean topMargin, boolean bottomMargin) { + super.onBind(customizedItem, topMargin, bottomMargin); + if (customizedItem == CUSTOMIZED_ITEM_GROUP) { + setTwoLineLayout(mContext.getText(R.string.media_output_dialog_group), + true /* bFocused */, true /* showSeekBar */, false /* showProgressBar */, + false /* showSubtitle*/); + mTitleIcon.setImageDrawable(getSpeakerDrawable()); + mBottomDivider.setVisibility(View.VISIBLE); + mCheckBox.setVisibility(View.GONE); + mDivider.setVisibility(View.GONE); + mAddIcon.setVisibility(View.GONE); + initSessionSeekbar(); + } + } + + private void onCheckBoxClicked(boolean isChecked, MediaDevice device) { + if (isChecked && isDeviceIncluded(mController.getSelectableMediaDevice(), device)) { + mController.addDeviceToPlayMedia(device); + } else if (!isChecked && isDeviceIncluded(mController.getDeselectableMediaDevice(), + device)) { + mController.removeDeviceFromPlayMedia(device); + } + } + + private Drawable getDisabledCheckboxDrawable() { + final Drawable drawable = mContext.getDrawable(R.drawable.ic_check_box_blue_24dp) + .mutate(); + final Bitmap checkbox = Bitmap.createBitmap(drawable.getIntrinsicWidth(), + drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888); + final Canvas canvas = new Canvas(checkbox); + TypedValue value = new TypedValue(); + mContext.getTheme().resolveAttribute(android.R.attr.disabledAlpha, value, true); + drawable.setAlpha((int) (value.getFloat() * 255)); + drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); + drawable.draw(canvas); + + return drawable; + } + + private boolean isDeviceIncluded(List<MediaDevice> deviceList, MediaDevice targetDevice) { + for (MediaDevice device : deviceList) { + if (TextUtils.equals(device.getId(), targetDevice.getId())) { + return true; + } + } + return false; + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputGroupDialog.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputGroupDialog.java new file mode 100644 index 000000000000..407930492fbe --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputGroupDialog.java @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.media.dialog; + +import android.content.Context; +import android.os.Bundle; +import android.view.View; +import android.view.WindowManager; + +import androidx.core.graphics.drawable.IconCompat; + +import com.android.systemui.R; + +/** + * Dialog for media output group. + */ +public class MediaOutputGroupDialog extends MediaOutputBaseDialog { + + MediaOutputGroupDialog(Context context, boolean aboveStatusbar, MediaOutputController + mediaOutputController) { + super(context, mediaOutputController); + mMediaOutputController.resetGroupMediaDevices(); + mAdapter = new MediaOutputGroupAdapter(mMediaOutputController); + if (!aboveStatusbar) { + getWindow().setType(WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY); + } + show(); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + } + + @Override + int getHeaderIconRes() { + return R.drawable.ic_arrow_back; + } + + @Override + IconCompat getHeaderIcon() { + return null; + } + + @Override + int getHeaderIconSize() { + return mContext.getResources().getDimensionPixelSize( + R.dimen.media_output_dialog_header_back_icon_size); + } + + @Override + CharSequence getHeaderText() { + return mContext.getString(R.string.media_output_dialog_add_output); + } + + @Override + CharSequence getHeaderSubtitle() { + final int size = mMediaOutputController.getSelectedMediaDevice().size(); + if (size == 1) { + return mContext.getText(R.string.media_output_dialog_single_device); + } + return mContext.getString(R.string.media_output_dialog_multiple_devices, size); + } + + @Override + int getStopButtonVisibility() { + return View.VISIBLE; + } + + @Override + void onHeaderIconClick() { + mMediaOutputController.launchMediaOutputDialog(); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputMetricLogger.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputMetricLogger.java new file mode 100644 index 000000000000..ac0295ed6d14 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputMetricLogger.java @@ -0,0 +1,221 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.media.dialog; + +import static android.media.MediaRoute2ProviderService.REASON_INVALID_COMMAND; +import static android.media.MediaRoute2ProviderService.REASON_NETWORK_ERROR; +import static android.media.MediaRoute2ProviderService.REASON_REJECTED; +import static android.media.MediaRoute2ProviderService.REASON_ROUTE_NOT_AVAILABLE; +import static android.media.MediaRoute2ProviderService.REASON_UNKNOWN_ERROR; + +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.util.Log; + +import com.android.settingslib.media.MediaDevice; +import com.android.systemui.shared.system.SysUiStatsLog; + +import java.util.List; + +/** + * Metric logger for media output features + */ +public class MediaOutputMetricLogger { + + private static final String TAG = "MediaOutputMetricLogger"; + private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + + private final Context mContext; + private final String mPackageName; + private MediaDevice mSourceDevice, mTargetDevice; + private int mWiredDeviceCount; + private int mConnectedBluetoothDeviceCount; + private int mRemoteDeviceCount; + private int mAppliedDeviceCountWithinRemoteGroup; + + public MediaOutputMetricLogger(Context context, String packageName) { + mContext = context; + mPackageName = packageName; + } + + /** + * Update the endpoints of a content switching operation. + * This method should be called before a switching operation, so the metric logger can track + * source and target devices. + * @param source the current connected media device + * @param target the target media device for content switching to + */ + public void updateOutputEndPoints(MediaDevice source, MediaDevice target) { + mSourceDevice = source; + mTargetDevice = target; + + if (DEBUG) { + Log.d(TAG, "updateOutputEndPoints -" + + " source:" + mSourceDevice.toString() + + " target:" + mTargetDevice.toString()); + } + } + + /** + * Do the metric logging of content switching success. + * @param selectedDeviceType string representation of the target media device + * @param deviceList media device list for device count updating + */ + public void logOutputSuccess(String selectedDeviceType, List<MediaDevice> deviceList) { + if (DEBUG) { + Log.d(TAG, "logOutputSuccess - selected device: " + selectedDeviceType); + } + + updateLoggingDeviceCount(deviceList); + + SysUiStatsLog.write( + SysUiStatsLog.MEDIAOUTPUT_OP_SWITCH_REPORTED, + getLoggingDeviceType(mSourceDevice, true), + getLoggingDeviceType(mTargetDevice, false), + SysUiStatsLog.MEDIA_OUTPUT_OP_SWITCH_REPORTED__RESULT__OK, + SysUiStatsLog.MEDIA_OUTPUT_OP_SWITCH_REPORTED__SUBRESULT__NO_ERROR, + getLoggingPackageName(), + mWiredDeviceCount, + mConnectedBluetoothDeviceCount, + mRemoteDeviceCount, + mAppliedDeviceCountWithinRemoteGroup); + } + + /** + * Do the metric logging of content switching failure. + * @param deviceList media device list for device count updating + * @param reason the reason of content switching failure + */ + public void logOutputFailure(List<MediaDevice> deviceList, int reason) { + if (DEBUG) { + Log.e(TAG, "logRequestFailed - " + reason); + } + + updateLoggingDeviceCount(deviceList); + + SysUiStatsLog.write( + SysUiStatsLog.MEDIAOUTPUT_OP_SWITCH_REPORTED, + getLoggingDeviceType(mSourceDevice, true), + getLoggingDeviceType(mTargetDevice, false), + SysUiStatsLog.MEDIA_OUTPUT_OP_SWITCH_REPORTED__RESULT__ERROR, + getLoggingSwitchOpSubResult(reason), + getLoggingPackageName(), + mWiredDeviceCount, + mConnectedBluetoothDeviceCount, + mRemoteDeviceCount, + mAppliedDeviceCountWithinRemoteGroup); + } + + private void updateLoggingDeviceCount(List<MediaDevice> deviceList) { + mWiredDeviceCount = mConnectedBluetoothDeviceCount = mRemoteDeviceCount = 0; + mAppliedDeviceCountWithinRemoteGroup = 0; + + for (MediaDevice mediaDevice : deviceList) { + if (mediaDevice.isConnected()) { + switch (mediaDevice.getDeviceType()) { + case MediaDevice.MediaDeviceType.TYPE_3POINT5_MM_AUDIO_DEVICE: + case MediaDevice.MediaDeviceType.TYPE_USB_C_AUDIO_DEVICE: + mWiredDeviceCount++; + break; + case MediaDevice.MediaDeviceType.TYPE_BLUETOOTH_DEVICE: + mConnectedBluetoothDeviceCount++; + break; + case MediaDevice.MediaDeviceType.TYPE_CAST_DEVICE: + case MediaDevice.MediaDeviceType.TYPE_CAST_GROUP_DEVICE: + mRemoteDeviceCount++; + break; + default: + } + } + } + + if (DEBUG) { + Log.d(TAG, "connected devices:" + " wired: " + mWiredDeviceCount + + " bluetooth: " + mConnectedBluetoothDeviceCount + + " remote: " + mRemoteDeviceCount); + } + } + + private int getLoggingDeviceType(MediaDevice device, boolean isSourceDevice) { + switch (device.getDeviceType()) { + case MediaDevice.MediaDeviceType.TYPE_PHONE_DEVICE: + return isSourceDevice + ? SysUiStatsLog.MEDIA_OUTPUT_OP_SWITCH_REPORTED__SOURCE__BUILTIN_SPEAKER + : SysUiStatsLog.MEDIA_OUTPUT_OP_SWITCH_REPORTED__TARGET__BUILTIN_SPEAKER; + case MediaDevice.MediaDeviceType.TYPE_3POINT5_MM_AUDIO_DEVICE: + return isSourceDevice + ? SysUiStatsLog + .MEDIA_OUTPUT_OP_SWITCH_REPORTED__SOURCE__WIRED_3POINT5_MM_AUDIO + : SysUiStatsLog + .MEDIA_OUTPUT_OP_SWITCH_REPORTED__TARGET__WIRED_3POINT5_MM_AUDIO; + case MediaDevice.MediaDeviceType.TYPE_USB_C_AUDIO_DEVICE: + return isSourceDevice + ? SysUiStatsLog.MEDIA_OUTPUT_OP_SWITCH_REPORTED__SOURCE__USB_C_AUDIO + : SysUiStatsLog.MEDIA_OUTPUT_OP_SWITCH_REPORTED__TARGET__USB_C_AUDIO; + case MediaDevice.MediaDeviceType.TYPE_BLUETOOTH_DEVICE: + return isSourceDevice + ? SysUiStatsLog.MEDIA_OUTPUT_OP_SWITCH_REPORTED__SOURCE__BLUETOOTH + : SysUiStatsLog.MEDIA_OUTPUT_OP_SWITCH_REPORTED__TARGET__BLUETOOTH; + case MediaDevice.MediaDeviceType.TYPE_CAST_DEVICE: + return isSourceDevice + ? SysUiStatsLog.MEDIA_OUTPUT_OP_SWITCH_REPORTED__SOURCE__REMOTE_SINGLE + : SysUiStatsLog.MEDIA_OUTPUT_OP_SWITCH_REPORTED__TARGET__REMOTE_SINGLE; + case MediaDevice.MediaDeviceType.TYPE_CAST_GROUP_DEVICE: + return isSourceDevice + ? SysUiStatsLog.MEDIA_OUTPUT_OP_SWITCH_REPORTED__SOURCE__REMOTE_GROUP + : SysUiStatsLog.MEDIA_OUTPUT_OP_SWITCH_REPORTED__TARGET__REMOTE_GROUP; + default: + return isSourceDevice + ? SysUiStatsLog.MEDIA_OUTPUT_OP_SWITCH_REPORTED__SOURCE__UNKNOWN_TYPE + : SysUiStatsLog.MEDIA_OUTPUT_OP_SWITCH_REPORTED__TARGET__UNKNOWN_TYPE; + } + } + + private int getLoggingSwitchOpSubResult(int reason) { + switch (reason) { + case REASON_REJECTED: + return SysUiStatsLog.MEDIA_OUTPUT_OP_SWITCH_REPORTED__SUBRESULT__REJECTED; + case REASON_NETWORK_ERROR: + return SysUiStatsLog.MEDIA_OUTPUT_OP_SWITCH_REPORTED__SUBRESULT__NETWORK_ERROR; + case REASON_ROUTE_NOT_AVAILABLE: + return SysUiStatsLog + .MEDIA_OUTPUT_OP_SWITCH_REPORTED__SUBRESULT__ROUTE_NOT_AVAILABLE; + case REASON_INVALID_COMMAND: + return SysUiStatsLog.MEDIA_OUTPUT_OP_SWITCH_REPORTED__SUBRESULT__INVALID_COMMAND; + case REASON_UNKNOWN_ERROR: + default: + return SysUiStatsLog.MEDIA_OUTPUT_OP_SWITCH_REPORTED__SUBRESULT__UNKNOWN_ERROR; + } + } + + private String getLoggingPackageName() { + if (mPackageName != null && !mPackageName.isEmpty()) { + try { + final ApplicationInfo applicationInfo = mContext.getPackageManager() + .getApplicationInfo(mPackageName, /* default flag */ 0); + if ((applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0 + || (applicationInfo.flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0) { + return mPackageName; + } + } catch (Exception ex) { + Log.e(TAG, mPackageName + " is invalid."); + } + } + + return ""; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java index 7e1dc6634cec..39d2f71e7e0b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java @@ -118,6 +118,8 @@ public class KeyguardIndicationController implements StateListener, private boolean mPowerPluggedIn; private boolean mPowerPluggedInWired; private boolean mPowerCharged; + private boolean mBatteryOverheated; + private boolean mEnableBatteryDefender; private int mChargingSpeed; private int mChargingWattage; private int mBatteryLevel; @@ -401,7 +403,7 @@ public class KeyguardIndicationController implements StateListener, } else if (!TextUtils.isEmpty(mAlignmentIndication)) { mTextView.switchIndication(mAlignmentIndication); mTextView.setTextColor(mContext.getColor(R.color.misalignment_text_color)); - } else if (mPowerPluggedIn) { + } else if (mPowerPluggedIn || mEnableBatteryDefender) { String indication = computePowerIndication(); if (animate) { animateText(mTextView, indication); @@ -421,7 +423,7 @@ public class KeyguardIndicationController implements StateListener, String trustManagedIndication = getTrustManagedIndication(); String powerIndication = null; - if (mPowerPluggedIn) { + if (mPowerPluggedIn || mEnableBatteryDefender) { powerIndication = computePowerIndication(); } @@ -451,7 +453,7 @@ public class KeyguardIndicationController implements StateListener, } else if (!TextUtils.isEmpty(mAlignmentIndication)) { mTextView.switchIndication(mAlignmentIndication); isError = true; - } else if (mPowerPluggedIn) { + } else if (mPowerPluggedIn || mEnableBatteryDefender) { if (DEBUG_CHARGING_SPEED) { powerIndication += ", " + (mChargingWattage / 1000) + " mW"; } @@ -528,8 +530,15 @@ public class KeyguardIndicationController implements StateListener, return mContext.getResources().getString(R.string.keyguard_charged); } - final boolean hasChargingTime = mChargingTimeRemaining > 0; int chargingId; + String percentage = NumberFormat.getPercentInstance().format(mBatteryLevel / 100f); + + if (mBatteryOverheated) { + chargingId = R.string.keyguard_plugged_in_charging_limited; + return mContext.getResources().getString(chargingId, percentage); + } + + final boolean hasChargingTime = mChargingTimeRemaining > 0; if (mPowerPluggedInWired) { switch (mChargingSpeed) { case BatteryStatus.CHARGING_FAST: @@ -554,8 +563,6 @@ public class KeyguardIndicationController implements StateListener, : R.string.keyguard_plugged_in_wireless; } - String percentage = NumberFormat.getPercentInstance() - .format(mBatteryLevel / 100f); if (hasChargingTime) { // We now have battery percentage in these strings and it's expected that all // locales will also have it in the future. For now, we still have to support the old @@ -685,6 +692,8 @@ public class KeyguardIndicationController implements StateListener, mChargingWattage = status.maxChargingWattage; mChargingSpeed = status.getChargingSpeed(mContext); mBatteryLevel = status.level; + mBatteryOverheated = status.isOverheated(); + mEnableBatteryDefender = mBatteryOverheated && status.isPluggedIn(); try { mChargingTimeRemaining = mPowerPluggedIn ? mBatteryInfo.computeChargeTimeRemaining() : -1; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/MediaTransferManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/MediaTransferManager.java index ac3523b2fffd..1b1a51b8a57b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/MediaTransferManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/MediaTransferManager.java @@ -17,7 +17,6 @@ package com.android.systemui.statusbar; import android.content.Context; -import android.content.Intent; import android.content.res.ColorStateList; import android.graphics.drawable.Drawable; import android.graphics.drawable.GradientDrawable; @@ -36,10 +35,9 @@ import com.android.settingslib.bluetooth.LocalBluetoothManager; import com.android.settingslib.media.InfoMediaManager; import com.android.settingslib.media.LocalMediaManager; import com.android.settingslib.media.MediaDevice; -import com.android.settingslib.media.MediaOutputSliceConstants; import com.android.settingslib.widget.AdaptiveIcon; import com.android.systemui.Dependency; -import com.android.systemui.plugins.ActivityStarter; +import com.android.systemui.media.dialog.MediaOutputDialogFactory; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; @@ -51,7 +49,7 @@ import java.util.List; */ public class MediaTransferManager { private final Context mContext; - private final ActivityStarter mActivityStarter; + private final MediaOutputDialogFactory mMediaOutputDialogFactory; private MediaDevice mDevice; private List<View> mViews = new ArrayList<>(); private LocalMediaManager mLocalMediaManager; @@ -74,12 +72,7 @@ public class MediaTransferManager { ViewParent parent = view.getParent(); StatusBarNotification statusBarNotification = getRowForParent(parent).getEntry().getSbn(); - final Intent intent = new Intent() - .setAction(MediaOutputSliceConstants.ACTION_MEDIA_OUTPUT) - .putExtra(MediaOutputSliceConstants.EXTRA_PACKAGE_NAME, - statusBarNotification.getPackageName()); - mActivityStarter.startActivity(intent, false, true /* dismissShade */, - Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + mMediaOutputDialogFactory.create(statusBarNotification.getPackageName(), true); return true; } }; @@ -107,7 +100,7 @@ public class MediaTransferManager { public MediaTransferManager(Context context) { mContext = context; - mActivityStarter = Dependency.get(ActivityStarter.class); + mMediaOutputDialogFactory = Dependency.get(MediaOutputDialogFactory.class); LocalBluetoothManager lbm = Dependency.get(LocalBluetoothManager.class); InfoMediaManager imm = new InfoMediaManager(mContext, null, null, lbm); mLocalMediaManager = new LocalMediaManager(mContext, lbm, imm, null); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java index 8cd82cc77530..38e1de3cada0 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java @@ -40,6 +40,7 @@ import android.os.Trace; import android.os.UserHandle; import android.provider.DeviceConfig; import android.provider.DeviceConfig.Properties; +import android.service.notification.NotificationListenerService; import android.util.ArraySet; import android.util.Log; import android.view.View; @@ -52,6 +53,7 @@ import com.android.systemui.Dumpable; import com.android.systemui.Interpolators; import com.android.systemui.colorextraction.SysuiColorExtractor; import com.android.systemui.dagger.qualifiers.Main; +import com.android.systemui.media.MediaData; import com.android.systemui.media.MediaDataManager; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.statusbar.dagger.StatusBarModule; @@ -250,6 +252,24 @@ public class NotificationMediaManager implements Dumpable { } }); + mMediaDataManager.addListener(new MediaDataManager.Listener() { + @Override + public void onMediaDataLoaded(@NonNull String key, + @Nullable String oldKey, @NonNull MediaData data) { + } + + @Override + public void onMediaDataRemoved(@NonNull String key) { + NotificationEntry entry = mEntryManager.getPendingOrActiveNotif(key); + if (entry != null) { + // TODO(b/160713608): "removing" this notification won't happen and + // won't send the 'deleteIntent' if the notification is ongoing. + mEntryManager.performRemoveNotification(entry.getSbn(), + NotificationListenerService.REASON_CANCEL); + } + } + }); + mShowCompactMediaSeekbar = "true".equals( DeviceConfig.getProperty(DeviceConfig.NAMESPACE_SYSTEMUI, SystemUiDeviceConfigFlags.COMPACT_MEDIA_SEEKBAR_ENABLED)); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManager.java index d04389d6cbe8..9fd729f425c1 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManager.java @@ -574,6 +574,7 @@ public class NotificationEntryManager implements NotificationEntry entry = mPendingNotifications.get(key); if (entry != null) { entry.setSbn(notification); + entry.setRanking(ranking); } else { entry = new NotificationEntry( notification, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinator.java index e9cbf32ee052..943ace968632 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinator.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinator.java @@ -32,8 +32,6 @@ import javax.inject.Singleton; */ @Singleton public class RankingCoordinator implements Coordinator { - private static final String TAG = "RankingNotificationCoordinator"; - private final StatusBarStateController mStatusBarStateController; @Inject @@ -46,7 +44,7 @@ public class RankingCoordinator implements Coordinator { mStatusBarStateController.addCallback(mStatusBarStateCallback); pipeline.addPreGroupFilter(mSuspendedFilter); - pipeline.addPreGroupFilter(mDozingFilter); + pipeline.addPreGroupFilter(mDndVisualEffectsFilter); } /** @@ -61,10 +59,10 @@ public class RankingCoordinator implements Coordinator { } }; - private final NotifFilter mDozingFilter = new NotifFilter("IsDozingFilter") { + private final NotifFilter mDndVisualEffectsFilter = new NotifFilter( + "DndSuppressingVisualEffects") { @Override public boolean shouldFilterOut(NotificationEntry entry, long now) { - // Dozing + DND Settings from Ranking object if (mStatusBarStateController.isDozing() && entry.shouldSuppressAmbient()) { return true; } @@ -77,7 +75,7 @@ public class RankingCoordinator implements Coordinator { new StatusBarStateController.StateListener() { @Override public void onDozingChanged(boolean isDozing) { - mDozingFilter.invalidateList(); + mDndVisualEffectsFilter.invalidateList(); } }; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java index 94e12e82f850..70dd80e71e7a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java @@ -504,7 +504,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView /** * Returns whether this row is considered non-blockable (i.e. it's a non-blockable system notif - * or is in a whitelist). + * or is in an allowList). */ public boolean getIsNonblockable() { boolean isNonblockable = Dependency.get(NotificationBlockingHelperManager.class) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BackGestureTfClassifierProvider.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BackGestureTfClassifierProvider.java new file mode 100644 index 000000000000..0af79c3886b9 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BackGestureTfClassifierProvider.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.systemui.statusbar.phone; + +import android.content.res.AssetManager; + +import java.util.HashMap; +import java.util.Map; + +/** + * This class can be overridden by a vendor-specific sys UI implementation, + * in order to provide classification models for the Back Gesture. + */ +public class BackGestureTfClassifierProvider { + private static final String TAG = "BackGestureTfClassifierProvider"; + + /** + * Default implementation that returns an empty map. + * This method is overridden in vendor-specific Sys UI implementation. + * + * @param am An AssetManager to get the vocab file. + */ + public Map<String, Integer> loadVocab(AssetManager am) { + return new HashMap<String, Integer>(); + } + + /** + * This method is overridden in vendor-specific Sys UI implementation. + * + * @param featuresVector List of input features. + * + */ + public float predict(Object[] featuresVector) { + return -1; + } + + /** + * Interpreter owns resources. This method releases the resources after + * use to avoid memory leak. + * This method is overridden in vendor-specific Sys UI implementation. + * + */ + public void release() {} + + /** + * Returns whether to use the ML model for Back Gesture. + * This method is overridden in vendor-specific Sys UI implementation. + * + */ + public boolean isActive() { + return false; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/EdgeBackGestureHandler.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/EdgeBackGestureHandler.java index 00a932cb1e8a..603679afc109 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/EdgeBackGestureHandler.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/EdgeBackGestureHandler.java @@ -56,6 +56,7 @@ import com.android.internal.config.sysui.SystemUiDeviceConfigFlags; import com.android.internal.policy.GestureNavigationSettingsObserver; import com.android.systemui.Dependency; import com.android.systemui.R; +import com.android.systemui.SystemUIFactory; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.bubbles.BubbleController; import com.android.systemui.model.SysUiState; @@ -74,8 +75,10 @@ import com.android.systemui.tracing.nano.EdgeBackGestureHandlerProto; import com.android.systemui.tracing.nano.SystemUiTraceProto; import java.io.PrintWriter; +import java.util.ArrayDeque; import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.concurrent.Executor; /** @@ -117,8 +120,33 @@ public class EdgeBackGestureHandler extends CurrentUserTracker implements Displa public void onTaskStackChanged() { mGestureBlockingActivityRunning = isGestureBlockingActivityRunning(); } + @Override + public void onTaskCreated(int taskId, ComponentName componentName) { + if (componentName != null) { + mPackageName = componentName.getPackageName(); + } else { + mPackageName = "_UNKNOWN"; + } + } }; + private DeviceConfig.OnPropertiesChangedListener mOnPropertiesChangedListener = + new DeviceConfig.OnPropertiesChangedListener() { + @Override + public void onPropertiesChanged(DeviceConfig.Properties properties) { + if (DeviceConfig.NAMESPACE_SYSTEMUI.equals(properties.getNamespace()) + && (properties.getKeyset().contains( + SystemUiDeviceConfigFlags.BACK_GESTURE_ML_MODEL_THRESHOLD) + || properties.getKeyset().contains( + SystemUiDeviceConfigFlags.USE_BACK_GESTURE_ML_MODEL) + || properties.getKeyset().contains( + SystemUiDeviceConfigFlags.BACK_GESTURE_ML_MODEL_NAME))) { + updateMLModelState(); + } + } + }; + + private final Context mContext; private final OverviewProxyService mOverviewProxyService; private final Runnable mStateChangeCallback; @@ -173,6 +201,19 @@ public class EdgeBackGestureHandler extends CurrentUserTracker implements Displa private int mRightInset; private int mSysUiFlags; + // For Tf-Lite model. + private BackGestureTfClassifierProvider mBackGestureTfClassifierProvider; + private Map<String, Integer> mVocab; + private boolean mUseMLModel; + // minimum width below which we do not run the model + private int mMLEnableWidth; + private float mMLModelThreshold; + private String mPackageName; + private float mMLResults; + + private static final int MAX_LOGGED_PREDICTIONS = 10; + private ArrayDeque<String> mPredictionLog = new ArrayDeque<>(); + private final GestureNavigationSettingsObserver mGestureNavigationSettingsObserver; private final NavigationEdgeBackPlugin.BackCallback mBackCallback = @@ -230,7 +271,6 @@ public class EdgeBackGestureHandler extends CurrentUserTracker implements Displa Log.e(TAG, "Failed to add gesture blocking activities", e); } } - mLongPressTimeout = Math.min(MAX_LONG_PRESS_TIMEOUT, ViewConfiguration.getLongPressTimeout()); @@ -258,6 +298,11 @@ public class EdgeBackGestureHandler extends CurrentUserTracker implements Displa mBottomGestureHeight = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, gestureHeight, dm); + // Set the minimum bounds to activate ML to 12dp or the minimum of configured values + mMLEnableWidth = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 12.0f, dm); + if (mMLEnableWidth > mEdgeWidthRight) mMLEnableWidth = mEdgeWidthRight; + if (mMLEnableWidth > mEdgeWidthLeft) mMLEnableWidth = mEdgeWidthLeft; + // Reduce the default touch slop to ensure that we can intercept the gesture // before the app starts to react to it. // TODO(b/130352502) Tune this value and extract into a constant @@ -344,6 +389,7 @@ public class EdgeBackGestureHandler extends CurrentUserTracker implements Displa mContext.getSystemService(DisplayManager.class).unregisterDisplayListener(this); mPluginManager.removePluginListener(this); ActivityManagerWrapper.getInstance().unregisterTaskStackListener(mTaskStackListener); + DeviceConfig.removeOnPropertiesChangedListener(mOnPropertiesChangedListener); try { WindowManagerGlobal.getWindowManagerService() @@ -359,6 +405,9 @@ public class EdgeBackGestureHandler extends CurrentUserTracker implements Displa mContext.getSystemService(DisplayManager.class).registerDisplayListener(this, mContext.getMainThreadHandler()); ActivityManagerWrapper.getInstance().registerTaskStackListener(mTaskStackListener); + DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_SYSTEMUI, + runnable -> (mContext.getMainThreadHandler()).post(runnable), + mOnPropertiesChangedListener); try { WindowManagerGlobal.getWindowManagerService() @@ -379,6 +428,8 @@ public class EdgeBackGestureHandler extends CurrentUserTracker implements Displa mPluginManager.addPluginListener( this, NavigationEdgeBackPlugin.class, /*allowMultiple=*/ false); } + // Update the ML model resources. + updateMLModelState(); } @Override @@ -431,12 +482,71 @@ public class EdgeBackGestureHandler extends CurrentUserTracker implements Displa } } + private void updateMLModelState() { + boolean newState = mIsEnabled && DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SYSTEMUI, + SystemUiDeviceConfigFlags.USE_BACK_GESTURE_ML_MODEL, false); + + if (newState == mUseMLModel) { + return; + } + + if (newState) { + String mlModelName = DeviceConfig.getString(DeviceConfig.NAMESPACE_SYSTEMUI, + SystemUiDeviceConfigFlags.BACK_GESTURE_ML_MODEL_NAME, "backgesture"); + mBackGestureTfClassifierProvider = SystemUIFactory.getInstance() + .createBackGestureTfClassifierProvider(mContext.getAssets(), mlModelName); + mMLModelThreshold = DeviceConfig.getFloat(DeviceConfig.NAMESPACE_SYSTEMUI, + SystemUiDeviceConfigFlags.BACK_GESTURE_ML_MODEL_THRESHOLD, 0.9f); + if (mBackGestureTfClassifierProvider.isActive()) { + mVocab = mBackGestureTfClassifierProvider.loadVocab(mContext.getAssets()); + mUseMLModel = true; + return; + } + } + + mUseMLModel = false; + if (mBackGestureTfClassifierProvider != null) { + mBackGestureTfClassifierProvider.release(); + mBackGestureTfClassifierProvider = null; + } + } + + private int getBackGesturePredictionsCategory(int x, int y, int app) { + if (app == -1) { + return -1; + } + + int distanceFromEdge; + int location; + if (x <= mDisplaySize.x / 2.0) { + location = 1; // left + distanceFromEdge = x; + } else { + location = 2; // right + distanceFromEdge = mDisplaySize.x - x; + } + + Object[] featuresVector = { + new long[]{(long) mDisplaySize.x}, + new long[]{(long) distanceFromEdge}, + new long[]{(long) location}, + new long[]{(long) app}, + new long[]{(long) y}, + }; + + mMLResults = mBackGestureTfClassifierProvider.predict(featuresVector); + if (mMLResults == -1) { + return -1; + } + + return mMLResults >= mMLModelThreshold ? 1 : 0; + } + private boolean isWithinTouchRegion(int x, int y) { // Disallow if we are in the bottom gesture area if (y >= (mDisplaySize.y - mBottomGestureHeight)) { return false; } - // If the point is way too far (twice the margin), it is // not interesting to us for logging purposes, nor we // should process it. Simply return false and keep @@ -446,11 +556,33 @@ public class EdgeBackGestureHandler extends CurrentUserTracker implements Displa return false; } - // Denotes whether we should proceed with the gesture. - // Even if it is false, we may want to log it assuming - // it is not invalid due to exclusion. - boolean withinRange = x <= mEdgeWidthLeft + mLeftInset - || x >= (mDisplaySize.x - mEdgeWidthRight - mRightInset); + int app = -1; + if (mVocab != null) { + app = mVocab.getOrDefault(mPackageName, -1); + } + // Check if we are within the tightest bounds beyond which + // we would not need to run the ML model. + boolean withinRange = x <= mMLEnableWidth + mLeftInset + || x >= (mDisplaySize.x - mMLEnableWidth - mRightInset); + if (!withinRange) { + int results = -1; + if (mUseMLModel && (results = getBackGesturePredictionsCategory(x, y, app)) != -1) { + withinRange = results == 1; + } else { + // Denotes whether we should proceed with the gesture. + // Even if it is false, we may want to log it assuming + // it is not invalid due to exclusion. + withinRange = x <= mEdgeWidthLeft + mLeftInset + || x >= (mDisplaySize.x - mEdgeWidthRight - mRightInset); + } + } + + // For debugging purposes + if (mPredictionLog.size() >= MAX_LOGGED_PREDICTIONS) { + mPredictionLog.removeFirst(); + } + mPredictionLog.addLast(String.format("[%d,%d,%d,%f,%d]", + x, y, app, mMLResults, withinRange ? 1 : 0)); // Always allow if the user is in a transient sticky immersive state if (mIsNavBarShownTransiently) { @@ -493,6 +625,11 @@ public class EdgeBackGestureHandler extends CurrentUserTracker implements Displa return; } mLogGesture = false; + String logPackageName = ""; + // Due to privacy, only top 100 most used apps by all users can be logged. + if (mUseMLModel && mVocab.containsKey(mPackageName) && mVocab.get(mPackageName) < 100) { + logPackageName = mPackageName; + } SysUiStatsLog.write(SysUiStatsLog.BACK_GESTURE_REPORTED_REPORTED, backType, (int) mDownPoint.y, mIsOnLeftEdge ? SysUiStatsLog.BACK_GESTURE__X_LOCATION__LEFT @@ -500,7 +637,8 @@ public class EdgeBackGestureHandler extends CurrentUserTracker implements Displa (int) mDownPoint.x, (int) mDownPoint.y, (int) mEndPoint.x, (int) mEndPoint.y, mEdgeWidthLeft + mLeftInset, - mDisplaySize.x - (mEdgeWidthRight + mRightInset)); + mDisplaySize.x - (mEdgeWidthRight + mRightInset), + mUseMLModel ? mMLResults : -2, logPackageName); } private void onMotionEvent(MotionEvent ev) { @@ -509,6 +647,7 @@ public class EdgeBackGestureHandler extends CurrentUserTracker implements Displa // Verify if this is in within the touch region and we aren't in immersive mode, and // either the bouncer is showing or the notification panel is hidden mIsOnLeftEdge = ev.getX() <= mEdgeWidthLeft + mLeftInset; + mMLResults = 0; mLogGesture = false; mInRejectedExclusion = false; mAllowGesture = !mDisabledForQuickstep && mIsBackGestureAllowed @@ -642,12 +781,19 @@ public class EdgeBackGestureHandler extends CurrentUserTracker implements Displa pw.println(" mIsAttached=" + mIsAttached); pw.println(" mEdgeWidthLeft=" + mEdgeWidthLeft); pw.println(" mEdgeWidthRight=" + mEdgeWidthRight); + pw.println(" mIsNavBarShownTransiently=" + mIsNavBarShownTransiently); + pw.println(" mPredictionLog=" + String.join(";", mPredictionLog)); } private boolean isGestureBlockingActivityRunning() { ActivityManager.RunningTaskInfo runningTask = ActivityManagerWrapper.getInstance().getRunningTask(); ComponentName topActivity = runningTask == null ? null : runningTask.topActivity; + if (topActivity != null) { + mPackageName = topActivity.getPackageName(); + } else { + mPackageName = "_UNKNOWN"; + } return topActivity != null && mGestureBlockingActivities.contains(topActivity); } diff --git a/packages/SystemUI/src/com/android/systemui/usb/UsbConfirmActivity.java b/packages/SystemUI/src/com/android/systemui/usb/UsbConfirmActivity.java index 286b7c049fc7..21d700e41a40 100644 --- a/packages/SystemUI/src/com/android/systemui/usb/UsbConfirmActivity.java +++ b/packages/SystemUI/src/com/android/systemui/usb/UsbConfirmActivity.java @@ -35,6 +35,8 @@ import android.os.UserHandle; import android.util.Log; import android.view.LayoutInflater; import android.view.View; +import android.view.Window; +import android.view.WindowManager; import android.widget.CheckBox; import android.widget.CompoundButton; import android.widget.TextView; @@ -58,6 +60,9 @@ public class UsbConfirmActivity extends AlertActivity @Override public void onCreate(Bundle icicle) { + getWindow().addSystemFlags( + WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS); + super.onCreate(icicle); Intent intent = getIntent(); diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java index 6fe11ed1792b..06a92272692d 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java +++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java @@ -88,6 +88,7 @@ import com.android.settingslib.Utils; import com.android.systemui.Dependency; import com.android.systemui.Prefs; import com.android.systemui.R; +import com.android.systemui.media.dialog.MediaOutputDialogFactory; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.VolumeDialog; import com.android.systemui.plugins.VolumeDialogController; @@ -470,6 +471,7 @@ public class VolumeDialogImpl implements VolumeDialog, Events.writeEvent(Events.EVENT_SETTINGS_CLICK); Intent intent = new Intent(Settings.Panel.ACTION_VOLUME); dismissH(DISMISS_REASON_SETTINGS_CLICKED); + Dependency.get(MediaOutputDialogFactory.class).dismiss(); Dependency.get(ActivityStarter.class).startActivity(intent, true /* dismissShade */); }); |