diff options
20 files changed, 745 insertions, 225 deletions
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 0d23a6da0973..79be2b1f8632 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -330,6 +330,8 @@ <string name="share_to_app_stop_dialog_title">Stop sharing screen?</string> <!-- Text telling a user that they will stop sharing their screen if they click the "Stop sharing" button [CHAR LIMIT=100] --> <string name="share_to_app_stop_dialog_message">You will stop sharing your screen</string> + <!-- Text telling a user that they will stop sharing the contents of the specified [app_name] if they click the "Stop sharing" button. Note that the app name will appear in bold. [CHAR LIMIT=100] --> + <string name="share_to_app_stop_dialog_message_specific_app">You will stop sharing <b><xliff:g id="app_name" example="Photos App">%1$s</xliff:g></b></string> <!-- Button to stop screen sharing [CHAR LIMIT=35] --> <string name="share_to_app_stop_dialog_button">Stop sharing</string> @@ -337,6 +339,8 @@ <string name="cast_to_other_device_stop_dialog_title">Stop casting screen?</string> <!-- Text telling a user that they will stop casting their screen to a different device if they click the "Stop casting" button [CHAR LIMIT=100] --> <string name="cast_to_other_device_stop_dialog_message">You will stop casting your screen</string> + <!-- Text telling a user that they will stop casting the contents of the specified [app_name] to a different device if they click the "Stop casting" button. Note that the app name will appear in bold. [CHAR LIMIT=100] --> + <string name="cast_to_other_device_stop_dialog_message_specific_app">You will stop casting <b><xliff:g id="app_name" example="Photos App">%1$s</xliff:g></b></string> <!-- Button to stop screen casting to a different device [CHAR LIMIT=35] --> <string name="cast_to_other_device_stop_dialog_button">Stop casting</string> diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractor.kt index 6611434b661e..f6fbe38554a6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractor.kt @@ -30,8 +30,8 @@ import com.android.systemui.statusbar.chips.domain.interactor.OngoingActivityChi import com.android.systemui.statusbar.chips.domain.interactor.OngoingActivityChipInteractor.Companion.createDialogLaunchOnClickListener import com.android.systemui.statusbar.chips.domain.model.OngoingActivityChipModel import com.android.systemui.statusbar.chips.mediaprojection.ui.view.EndCastToOtherDeviceDialogDelegate +import com.android.systemui.statusbar.chips.mediaprojection.ui.view.EndMediaProjectionDialogHelper import com.android.systemui.statusbar.chips.mediaprojection.ui.view.EndShareToAppDialogDelegate -import com.android.systemui.statusbar.phone.SystemUIDialog import com.android.systemui.util.Utils import com.android.systemui.util.time.SystemClock import javax.inject.Inject @@ -60,8 +60,8 @@ constructor( private val mediaProjectionRepository: MediaProjectionRepository, private val packageManager: PackageManager, private val systemClock: SystemClock, - private val dialogFactory: SystemUIDialog.Factory, private val dialogTransitionAnimator: DialogTransitionAnimator, + private val endMediaProjectionDialogHelper: EndMediaProjectionDialogHelper, ) : OngoingActivityChipInteractor { override val chip: StateFlow<OngoingActivityChipModel> = mediaProjectionRepository.mediaProjectionState @@ -70,9 +70,9 @@ constructor( is MediaProjectionState.NotProjecting -> OngoingActivityChipModel.Hidden is MediaProjectionState.Projecting -> { if (isProjectionToOtherDevice(state.hostPackage)) { - createCastToOtherDeviceChip() + createCastToOtherDeviceChip(state) } else { - createShareToAppChip() + createShareToAppChip(state) } } } @@ -97,7 +97,9 @@ constructor( return Utils.isHeadlessRemoteDisplayProvider(packageManager, packageName) } - private fun createCastToOtherDeviceChip(): OngoingActivityChipModel.Shown { + private fun createCastToOtherDeviceChip( + state: MediaProjectionState.Projecting, + ): OngoingActivityChipModel.Shown { return OngoingActivityChipModel.Shown( icon = Icon.Resource( @@ -107,32 +109,39 @@ constructor( // TODO(b/332662551): Maybe use a MediaProjection API to fetch this time. startTimeMs = systemClock.elapsedRealtime(), createDialogLaunchOnClickListener( - castToOtherDeviceDialogDelegate, + createCastToOtherDeviceDialogDelegate(state), dialogTransitionAnimator, ), ) } - private val castToOtherDeviceDialogDelegate = + private fun createCastToOtherDeviceDialogDelegate(state: MediaProjectionState.Projecting) = EndCastToOtherDeviceDialogDelegate( - dialogFactory, + endMediaProjectionDialogHelper, this@MediaProjectionChipInteractor, + state, ) - private fun createShareToAppChip(): OngoingActivityChipModel.Shown { + private fun createShareToAppChip( + state: MediaProjectionState.Projecting, + ): OngoingActivityChipModel.Shown { return OngoingActivityChipModel.Shown( // TODO(b/332662551): Use the right content description. icon = Icon.Resource(SHARE_TO_APP_ICON, contentDescription = null), // TODO(b/332662551): Maybe use a MediaProjection API to fetch this time. startTimeMs = systemClock.elapsedRealtime(), - createDialogLaunchOnClickListener(shareToAppDialogDelegate, dialogTransitionAnimator), + createDialogLaunchOnClickListener( + createShareToAppDialogDelegate(state), + dialogTransitionAnimator + ), ) } - private val shareToAppDialogDelegate = + private fun createShareToAppDialogDelegate(state: MediaProjectionState.Projecting) = EndShareToAppDialogDelegate( - dialogFactory, + endMediaProjectionDialogHelper, this@MediaProjectionChipInteractor, + state, ) companion object { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndCastToOtherDeviceDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndCastToOtherDeviceDialogDelegate.kt index 33cec9755b1f..596fbf89a10d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndCastToOtherDeviceDialogDelegate.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndCastToOtherDeviceDialogDelegate.kt @@ -17,25 +17,33 @@ package com.android.systemui.statusbar.chips.mediaprojection.ui.view import android.os.Bundle +import com.android.systemui.mediaprojection.data.model.MediaProjectionState import com.android.systemui.res.R import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.MediaProjectionChipInteractor import com.android.systemui.statusbar.phone.SystemUIDialog /** A dialog that lets the user stop an ongoing cast-screen-to-other-device event. */ class EndCastToOtherDeviceDialogDelegate( - private val systemUIDialogFactory: SystemUIDialog.Factory, + private val endMediaProjectionDialogHelper: EndMediaProjectionDialogHelper, private val interactor: MediaProjectionChipInteractor, + private val state: MediaProjectionState.Projecting, ) : SystemUIDialog.Delegate { override fun createDialog(): SystemUIDialog { - return systemUIDialogFactory.create(this) + return endMediaProjectionDialogHelper.createDialog(this) } override fun beforeCreate(dialog: SystemUIDialog, savedInstanceState: Bundle?) { with(dialog) { setIcon(MediaProjectionChipInteractor.CAST_TO_OTHER_DEVICE_ICON) setTitle(R.string.cast_to_other_device_stop_dialog_title) - // TODO(b/332662551): Use a different message if they're sharing just a single app. - setMessage(R.string.cast_to_other_device_stop_dialog_message) + setMessage( + endMediaProjectionDialogHelper.getDialogMessage( + state, + genericMessageResId = R.string.cast_to_other_device_stop_dialog_message, + specificAppMessageResId = + R.string.cast_to_other_device_stop_dialog_message_specific_app, + ) + ) // No custom on-click, because the dialog will automatically be dismissed when the // button is clicked anyway. setNegativeButton(R.string.close_dialog_button, /* onClick= */ null) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelper.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelper.kt new file mode 100644 index 000000000000..347be02dbc60 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelper.kt @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2024 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.chips.mediaprojection.ui.view + +import android.annotation.StringRes +import android.content.Context +import android.content.pm.PackageManager +import android.text.Html +import android.text.Html.FROM_HTML_MODE_LEGACY +import android.text.TextUtils +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.mediaprojection.data.model.MediaProjectionState +import com.android.systemui.statusbar.phone.SystemUIDialog +import javax.inject.Inject + +/** Helper class for showing dialogs that let users end different types of media projections. */ +@SysUISingleton +class EndMediaProjectionDialogHelper +@Inject +constructor( + private val dialogFactory: SystemUIDialog.Factory, + private val packageManager: PackageManager, + private val context: Context +) { + /** Creates a new [SystemUIDialog] using the given delegate. */ + fun createDialog(delegate: SystemUIDialog.Delegate): SystemUIDialog { + return dialogFactory.create(delegate) + } + + /** + * Returns the message to show in the dialog based on the specific media projection state. + * + * @param genericMessageResId a res ID for a more generic "end projection" message + * @param specificAppMessageResId a res ID for an "end projection" message that also lets us + * specify which app is currently being projected. + */ + fun getDialogMessage( + state: MediaProjectionState.Projecting, + @StringRes genericMessageResId: Int, + @StringRes specificAppMessageResId: Int, + ): CharSequence { + when (state) { + is MediaProjectionState.Projecting.EntireScreen -> + return context.getString(genericMessageResId) + is MediaProjectionState.Projecting.SingleTask -> { + val packageName = + state.task.baseIntent.component?.packageName + ?: return context.getString(genericMessageResId) + try { + val appInfo = packageManager.getApplicationInfo(packageName, 0) + val appName = appInfo.loadLabel(packageManager) + return getSpecificAppMessageText(specificAppMessageResId, appName) + } catch (e: PackageManager.NameNotFoundException) { + // TODO(b/332662551): Log this error. + return context.getString(genericMessageResId) + } + } + } + } + + private fun getSpecificAppMessageText( + @StringRes specificAppMessageResId: Int, + appName: CharSequence, + ): CharSequence { + // https://developer.android.com/guide/topics/resources/string-resource#StylingWithHTML + val escapedAppName = TextUtils.htmlEncode(appName.toString()) + val text = context.getString(specificAppMessageResId, escapedAppName) + return Html.fromHtml(text, FROM_HTML_MODE_LEGACY) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndShareToAppDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndShareToAppDialogDelegate.kt index 3a863b10ef82..749a11f193c6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndShareToAppDialogDelegate.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndShareToAppDialogDelegate.kt @@ -17,25 +17,32 @@ package com.android.systemui.statusbar.chips.mediaprojection.ui.view import android.os.Bundle +import com.android.systemui.mediaprojection.data.model.MediaProjectionState import com.android.systemui.res.R import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.MediaProjectionChipInteractor import com.android.systemui.statusbar.phone.SystemUIDialog /** A dialog that lets the user stop an ongoing share-screen-to-app event. */ class EndShareToAppDialogDelegate( - private val systemUIDialogFactory: SystemUIDialog.Factory, + private val endMediaProjectionDialogHelper: EndMediaProjectionDialogHelper, private val interactor: MediaProjectionChipInteractor, + private val state: MediaProjectionState.Projecting, ) : SystemUIDialog.Delegate { override fun createDialog(): SystemUIDialog { - return systemUIDialogFactory.create(this) + return endMediaProjectionDialogHelper.createDialog(this) } override fun beforeCreate(dialog: SystemUIDialog, savedInstanceState: Bundle?) { with(dialog) { setIcon(MediaProjectionChipInteractor.SHARE_TO_APP_ICON) setTitle(R.string.share_to_app_stop_dialog_title) - // TODO(b/332662551): Use a different message if they're sharing just a single app. - setMessage(R.string.share_to_app_stop_dialog_message) + setMessage( + endMediaProjectionDialogHelper.getDialogMessage( + state, + genericMessageResId = R.string.share_to_app_stop_dialog_message, + specificAppMessageResId = R.string.share_to_app_stop_dialog_message_specific_app + ) + ) // No custom on-click, because the dialog will automatically be dismissed when the // button is clicked anyway. setNegativeButton(R.string.close_dialog_button, /* onClick= */ null) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/StatusBarModePerDisplayRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/StatusBarModePerDisplayRepository.kt index 11636bdf0f17..f95e0fb37921 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/StatusBarModePerDisplayRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/StatusBarModePerDisplayRepository.kt @@ -37,6 +37,7 @@ import com.android.systemui.statusbar.phone.BoundsPair import com.android.systemui.statusbar.phone.LetterboxAppearanceCalculator import com.android.systemui.statusbar.phone.StatusBarBoundsProvider import com.android.systemui.statusbar.phone.fragment.dagger.StatusBarFragmentComponent +import com.android.systemui.statusbar.phone.ongoingcall.data.model.OngoingCallModel import com.android.systemui.statusbar.phone.ongoingcall.data.repository.OngoingCallRepository import dagger.assisted.Assisted import dagger.assisted.AssistedFactory @@ -224,8 +225,8 @@ constructor( modifiedStatusBarAttributes, isTransientShown, isInFullscreenMode, - ongoingCallRepository.hasOngoingCall, - ) { modifiedAttributes, isTransientShown, isInFullscreenMode, hasOngoingCall -> + ongoingCallRepository.ongoingCallState, + ) { modifiedAttributes, isTransientShown, isInFullscreenMode, ongoingCallState -> if (modifiedAttributes == null) { null } else { @@ -234,7 +235,7 @@ constructor( modifiedAttributes.appearance, isTransientShown, isInFullscreenMode, - hasOngoingCall, + hasOngoingCall = ongoingCallState is OngoingCallModel.InCall, ) StatusBarAppearance( statusBarMode, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt index a7d4ce30a191..d128057d2010 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt @@ -29,35 +29,36 @@ import androidx.annotation.VisibleForTesting import com.android.internal.jank.InteractionJankMonitor import com.android.systemui.CoreStartable import com.android.systemui.Dumpable -import com.android.systemui.res.R import com.android.systemui.animation.ActivityTransitionAnimator import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.dump.DumpManager import com.android.systemui.plugins.ActivityStarter -import com.android.systemui.statusbar.chips.ui.view.ChipChronometer +import com.android.systemui.res.R import com.android.systemui.statusbar.chips.ui.view.ChipBackgroundContainer +import com.android.systemui.statusbar.chips.ui.view.ChipChronometer import com.android.systemui.statusbar.data.repository.StatusBarModeRepositoryStore import com.android.systemui.statusbar.gesture.SwipeStatusBarAwayGestureHandler import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener +import com.android.systemui.statusbar.phone.ongoingcall.data.model.OngoingCallModel import com.android.systemui.statusbar.phone.ongoingcall.data.repository.OngoingCallRepository import com.android.systemui.statusbar.policy.CallbackController import com.android.systemui.statusbar.window.StatusBarWindowController import com.android.systemui.util.time.SystemClock -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch import java.io.PrintWriter import java.util.concurrent.Executor import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch -/** - * A controller to handle the ongoing call chip in the collapsed status bar. - */ +/** A controller to handle the ongoing call chip in the collapsed status bar. */ @SysUISingleton -class OngoingCallController @Inject constructor( +class OngoingCallController +@Inject +constructor( @Application private val scope: CoroutineScope, private val context: Context, private val ongoingCallRepository: OngoingCallRepository, @@ -79,54 +80,61 @@ class OngoingCallController @Inject constructor( private val mListeners: MutableList<OngoingCallListener> = mutableListOf() private val uidObserver = CallAppUidObserver() - private val notifListener = object : NotifCollectionListener { - // Temporary workaround for b/178406514 for testing purposes. - // - // b/178406514 means that posting an incoming call notif then updating it to an ongoing call - // notif does not work (SysUI never receives the update). This workaround allows us to - // trigger the ongoing call chip when an ongoing call notif is *added* rather than - // *updated*, allowing us to test the chip. - // - // TODO(b/183229367): Remove this function override when b/178406514 is fixed. - override fun onEntryAdded(entry: NotificationEntry) { - onEntryUpdated(entry, true) - } + private val notifListener = + object : NotifCollectionListener { + // Temporary workaround for b/178406514 for testing purposes. + // + // b/178406514 means that posting an incoming call notif then updating it to an ongoing + // call notif does not work (SysUI never receives the update). This workaround allows us + // to trigger the ongoing call chip when an ongoing call notif is *added* rather than + // *updated*, allowing us to test the chip. + // + // TODO(b/183229367): Remove this function override when b/178406514 is fixed. + override fun onEntryAdded(entry: NotificationEntry) { + onEntryUpdated(entry, true) + } - override fun onEntryUpdated(entry: NotificationEntry) { - // We have a new call notification or our existing call notification has been updated. - // TODO(b/183229367): This likely won't work if you take a call from one app then - // switch to a call from another app. - if (callNotificationInfo == null && isCallNotification(entry) || - (entry.sbn.key == callNotificationInfo?.key)) { - val newOngoingCallInfo = CallNotificationInfo( - entry.sbn.key, - entry.sbn.notification.getWhen(), - entry.sbn.notification.contentIntent, - entry.sbn.uid, - entry.sbn.notification.extras.getInt( - Notification.EXTRA_CALL_TYPE, -1) == CALL_TYPE_ONGOING, - statusBarSwipedAway = callNotificationInfo?.statusBarSwipedAway ?: false - ) - if (newOngoingCallInfo == callNotificationInfo) { - return + override fun onEntryUpdated(entry: NotificationEntry) { + // We have a new call notification or our existing call notification has been + // updated. + // TODO(b/183229367): This likely won't work if you take a call from one app then + // switch to a call from another app. + if ( + callNotificationInfo == null && isCallNotification(entry) || + (entry.sbn.key == callNotificationInfo?.key) + ) { + val newOngoingCallInfo = + CallNotificationInfo( + entry.sbn.key, + entry.sbn.notification.getWhen(), + entry.sbn.notification.contentIntent, + entry.sbn.uid, + entry.sbn.notification.extras.getInt( + Notification.EXTRA_CALL_TYPE, + -1 + ) == CALL_TYPE_ONGOING, + statusBarSwipedAway = callNotificationInfo?.statusBarSwipedAway ?: false + ) + if (newOngoingCallInfo == callNotificationInfo) { + return + } + + callNotificationInfo = newOngoingCallInfo + if (newOngoingCallInfo.isOngoing) { + updateChip() + } else { + removeChip() + } } + } - callNotificationInfo = newOngoingCallInfo - if (newOngoingCallInfo.isOngoing) { - updateChip() - } else { + override fun onEntryRemoved(entry: NotificationEntry, reason: Int) { + if (entry.sbn.key == callNotificationInfo?.key) { removeChip() } } } - override fun onEntryRemoved(entry: NotificationEntry, reason: Int) { - if (entry.sbn.key == callNotificationInfo?.key) { - removeChip() - } - } - } - override fun start() { dumpManager.registerDumpable(this) notifCollection.addCollectionListener(notifListener) @@ -169,8 +177,21 @@ class OngoingCallController @Inject constructor( */ fun hasOngoingCall(): Boolean { return callNotificationInfo?.isOngoing == true && - // When the user is in the phone app, don't show the chip. - !uidObserver.isCallAppVisible + // When the user is in the phone app, don't show the chip. + !uidObserver.isCallAppVisible + } + + /** Creates the right [OngoingCallModel] based on the call state. */ + private fun getOngoingCallModel(): OngoingCallModel { + if (hasOngoingCall()) { + val currentInfo = + callNotificationInfo + // This shouldn't happen, but protect against it in case + ?: return OngoingCallModel.NoCall + return OngoingCallModel.InCall(currentInfo.callStartTime) + } else { + return OngoingCallModel.NoCall + } } override fun addCallback(listener: OngoingCallListener) { @@ -182,9 +203,7 @@ class OngoingCallController @Inject constructor( } override fun removeCallback(listener: OngoingCallListener) { - synchronized(mListeners) { - mListeners.remove(listener) - } + synchronized(mListeners) { mListeners.remove(listener) } } private fun updateChip() { @@ -196,8 +215,8 @@ class OngoingCallController @Inject constructor( if (currentChipView != null && timeView != null) { if (currentCallNotificationInfo.hasValidStartTime()) { timeView.setShouldHideText(false) - timeView.base = currentCallNotificationInfo.callStartTime - - systemClock.currentTimeMillis() + + timeView.base = + currentCallNotificationInfo.callStartTime - systemClock.currentTimeMillis() + systemClock.elapsedRealtime() timeView.start() } else { @@ -218,14 +237,19 @@ class OngoingCallController @Inject constructor( callNotificationInfo = null if (DEBUG) { - Log.w(TAG, "Ongoing call chip view could not be found; " + - "Not displaying chip in status bar") + Log.w( + TAG, + "Ongoing call chip view could not be found; " + + "Not displaying chip in status bar" + ) } } } private fun updateChipClickListener() { - if (callNotificationInfo == null) { return } + if (callNotificationInfo == null) { + return + } val currentChipView = chipView val backgroundView = currentChipView?.findViewById<View>(R.id.ongoing_activity_chip_background) @@ -237,7 +261,8 @@ class OngoingCallController @Inject constructor( intent, ActivityTransitionAnimator.Controller.fromView( backgroundView, - InteractionJankMonitor.CUJ_STATUS_BAR_APP_LAUNCH_FROM_CALL_CHIP) + InteractionJankMonitor.CUJ_STATUS_BAR_APP_LAUNCH_FROM_CALL_CHIP, + ) ) } } @@ -249,9 +274,11 @@ class OngoingCallController @Inject constructor( } private fun updateGestureListening() { - if (callNotificationInfo == null || - callNotificationInfo?.statusBarSwipedAway == true || - !isFullscreen) { + if ( + callNotificationInfo == null || + callNotificationInfo?.statusBarSwipedAway == true || + !isFullscreen + ) { swipeStatusBarAwayGestureHandler.removeOnGestureDetectedCallback(TAG) } else { swipeStatusBarAwayGestureHandler.addOnGestureDetectedCallback(TAG) { _ -> @@ -270,30 +297,31 @@ class OngoingCallController @Inject constructor( } /** Tear down anything related to the chip view to prevent leaks. */ - @VisibleForTesting - fun tearDownChipView() = chipView?.getTimeView()?.stop() + @VisibleForTesting fun tearDownChipView() = chipView?.getTimeView()?.stop() private fun View.getTimeView(): ChipChronometer? { return this.findViewById(R.id.ongoing_activity_chip_time) } /** - * If there's an active ongoing call, then we will force the status bar to always show, even if - * the user is in immersive mode. However, we also want to give users the ability to swipe away - * the status bar if they need to access the area under the status bar. - * - * This method updates the status bar window appropriately when the swipe away gesture is - * detected. - */ + * If there's an active ongoing call, then we will force the status bar to always show, even if + * the user is in immersive mode. However, we also want to give users the ability to swipe away + * the status bar if they need to access the area under the status bar. + * + * This method updates the status bar window appropriately when the swipe away gesture is + * detected. + */ private fun onSwipeAwayGestureDetected() { - if (DEBUG) { Log.d(TAG, "Swipe away gesture detected") } + if (DEBUG) { + Log.d(TAG, "Swipe away gesture detected") + } callNotificationInfo = callNotificationInfo?.copy(statusBarSwipedAway = true) statusBarWindowController.setOngoingProcessRequiresStatusBarVisible(false) swipeStatusBarAwayGestureHandler.removeOnGestureDetectedCallback(TAG) } private fun sendStateChangeEvent() { - ongoingCallRepository.setHasOngoingCall(hasOngoingCall()) + ongoingCallRepository.setOngoingCallState(getOngoingCallModel()) mListeners.forEach { l -> l.onOngoingCallStateChanged(animate = true) } } @@ -308,8 +336,8 @@ class OngoingCallController @Inject constructor( val statusBarSwipedAway: Boolean ) { /** - * Returns true if the notification information has a valid call start time. - * See b/192379214. + * Returns true if the notification information has a valid call start time. See + * b/192379214. */ fun hasValidStartTime(): Boolean = callStartTime > 0 } @@ -342,9 +370,10 @@ class OngoingCallController @Inject constructor( callAppUid = uid try { - isCallAppVisible = isProcessVisibleToUser( - iActivityManager.getUidProcessState(uid, context.opPackageName) - ) + isCallAppVisible = + isProcessVisibleToUser( + iActivityManager.getUidProcessState(uid, context.opPackageName) + ) if (isRegistered) { return } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/data/model/OngoingCallModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/data/model/OngoingCallModel.kt new file mode 100644 index 000000000000..aaa52a7b9e1c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/data/model/OngoingCallModel.kt @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2024 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.ongoingcall.data.model + +/** Represents the state of any ongoing calls. */ +sealed interface OngoingCallModel { + /** There is no ongoing call. */ + data object NoCall : OngoingCallModel + + /** + * There *is* an ongoing call. + * + * @property startTimeMs the time that the phone call started, based on the notification's + * `when` field. Importantly, this time is relative to + * [com.android.systemui.util.time.SystemClock.currentTimeMillis], **not** + * [com.android.systemui.util.time.SystemClock.elapsedRealtime]. + */ + data class InCall(val startTimeMs: Long) : OngoingCallModel +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/data/repository/OngoingCallRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/data/repository/OngoingCallRepository.kt index 886481e64dbe..554c47456871 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/data/repository/OngoingCallRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/data/repository/OngoingCallRepository.kt @@ -17,6 +17,7 @@ package com.android.systemui.statusbar.phone.ongoingcall.data.repository import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.statusbar.phone.ongoingcall.data.model.OngoingCallModel import javax.inject.Inject import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -32,15 +33,15 @@ import kotlinx.coroutines.flow.asStateFlow */ @SysUISingleton class OngoingCallRepository @Inject constructor() { - private val _hasOngoingCall = MutableStateFlow(false) - /** True if there's currently an ongoing call notification and false otherwise. */ - val hasOngoingCall: StateFlow<Boolean> = _hasOngoingCall.asStateFlow() + private val _ongoingCallState = MutableStateFlow<OngoingCallModel>(OngoingCallModel.NoCall) + /** The current ongoing call state. */ + val ongoingCallState: StateFlow<OngoingCallModel> = _ongoingCallState.asStateFlow() /** - * Sets whether there's currently an ongoing call notification. Should only be set from + * Sets the current ongoing call state, based on notifications. Should only be set from * [com.android.systemui.statusbar.phone.ongoingcall.OngoingCallController]. */ - fun setHasOngoingCall(hasOngoingCall: Boolean) { - _hasOngoingCall.value = hasOngoingCall + fun setOngoingCallState(state: OngoingCallModel) { + _ongoingCallState.value = state } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractorTest.kt index a4505a99cb77..327eec494b3c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractorTest.kt @@ -28,6 +28,7 @@ import com.android.systemui.animation.mockDialogTransitionAnimator import com.android.systemui.common.shared.model.Icon import com.android.systemui.coroutines.collectLastValue import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.testCase import com.android.systemui.kosmos.testScope import com.android.systemui.mediaprojection.data.model.MediaProjectionState import com.android.systemui.mediaprojection.data.repository.fakeMediaProjectionRepository @@ -55,7 +56,7 @@ import org.mockito.kotlin.whenever @SmallTest class MediaProjectionChipInteractorTest : SysuiTestCase() { - private val kosmos = Kosmos() + private val kosmos = Kosmos().also { it.testCase = this } private val testScope = kosmos.testScope private val mediaProjectionRepo = kosmos.fakeMediaProjectionRepository private val systemClock = kosmos.fakeSystemClock @@ -178,7 +179,7 @@ class MediaProjectionChipInteractorTest : SysuiTestCase() { } @Test - fun chip_castToOtherDevice_clickListenerShowsCastDialog() = + fun chip_castToOtherDevice_entireScreen_clickListenerShowsCastDialog() = testScope.runTest { val latest by collectLastValue(underTest.chip) mediaProjectionRepo.mediaProjectionState.value = @@ -200,7 +201,33 @@ class MediaProjectionChipInteractorTest : SysuiTestCase() { } @Test - fun chip_shareToApp_clickListenerShowsShareDialog() = + fun chip_castToOtherDevice_singleTask_clickListenerShowsCastDialog() = + testScope.runTest { + val latest by collectLastValue(underTest.chip) + + mediaProjectionRepo.mediaProjectionState.value = + MediaProjectionState.Projecting.SingleTask( + CAST_TO_OTHER_DEVICES_PACKAGE, + createTask(taskId = 1) + ) + + val clickListener = ((latest as OngoingActivityChipModel.Shown).onClickListener) + + // Dialogs must be created on the main thread + context.mainExecutor.execute { + clickListener.onClick(chipView) + verify(kosmos.mockDialogTransitionAnimator) + .showFromView( + eq(mockCastDialog), + eq(chipBackgroundView), + eq(null), + anyBoolean(), + ) + } + } + + @Test + fun chip_shareToApp_entireScreen_clickListenerShowsShareDialog() = testScope.runTest { val latest by collectLastValue(underTest.chip) mediaProjectionRepo.mediaProjectionState.value = @@ -221,6 +248,28 @@ class MediaProjectionChipInteractorTest : SysuiTestCase() { } } + @Test + fun chip_shareToApp_singleTask_clickListenerShowsShareDialog() = + testScope.runTest { + val latest by collectLastValue(underTest.chip) + mediaProjectionRepo.mediaProjectionState.value = + MediaProjectionState.Projecting.SingleTask(NORMAL_PACKAGE, createTask(taskId = 1)) + + val clickListener = ((latest as OngoingActivityChipModel.Shown).onClickListener) + + // Dialogs must be created on the main thread + context.mainExecutor.execute { + clickListener.onClick(chipView) + verify(kosmos.mockDialogTransitionAnimator) + .showFromView( + eq(mockShareDialog), + eq(chipBackgroundView), + eq(null), + anyBoolean(), + ) + } + } + companion object { const val CAST_TO_OTHER_DEVICES_PACKAGE = "other.devices.package" const val NORMAL_PACKAGE = "some.normal.package" diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndCastToOtherDeviceDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndCastToOtherDeviceDialogDelegateTest.kt index 9a2f545fa67e..7b676e2b34e4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndCastToOtherDeviceDialogDelegateTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndCastToOtherDeviceDialogDelegateTest.kt @@ -16,22 +16,28 @@ package com.android.systemui.statusbar.chips.mediaprojection.ui.view +import android.content.ComponentName import android.content.DialogInterface +import android.content.Intent +import android.content.packageManager +import android.content.pm.ApplicationInfo import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.testCase import com.android.systemui.kosmos.testScope +import com.android.systemui.mediaprojection.data.model.MediaProjectionState import com.android.systemui.mediaprojection.data.repository.fakeMediaProjectionRepository +import com.android.systemui.mediaprojection.taskswitcher.FakeActivityTaskManager.Companion.createTask import com.android.systemui.res.R import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.mediaProjectionChipInteractor import com.android.systemui.statusbar.phone.SystemUIDialog -import com.android.systemui.statusbar.phone.mockSystemUIDialogFactory import com.google.common.truth.Truth.assertThat import kotlin.test.Test import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest -import org.junit.Before +import org.mockito.kotlin.any import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.eq import org.mockito.kotlin.mock @@ -41,22 +47,14 @@ import org.mockito.kotlin.whenever @SmallTest @OptIn(ExperimentalCoroutinesApi::class) class EndCastToOtherDeviceDialogDelegateTest : SysuiTestCase() { - private val kosmos = Kosmos() + private val kosmos = Kosmos().also { it.testCase = this } private val sysuiDialog = mock<SystemUIDialog>() - private val sysuiDialogFactory = kosmos.mockSystemUIDialogFactory - private val underTest = - EndCastToOtherDeviceDialogDelegate( - sysuiDialogFactory, - kosmos.mediaProjectionChipInteractor, - ) - - @Before - fun setUp() { - whenever(sysuiDialogFactory.create(eq(underTest), eq(context))).thenReturn(sysuiDialog) - } + private lateinit var underTest: EndCastToOtherDeviceDialogDelegate @Test fun icon() { + createAndSetDelegate(ENTIRE_SCREEN) + underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null) verify(sysuiDialog).setIcon(R.drawable.ic_cast_connected) @@ -64,20 +62,52 @@ class EndCastToOtherDeviceDialogDelegateTest : SysuiTestCase() { @Test fun title() { + createAndSetDelegate(SINGLE_TASK) + underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null) verify(sysuiDialog).setTitle(R.string.cast_to_other_device_stop_dialog_title) } @Test - fun message() { + fun message_entireScreen() { + createAndSetDelegate(ENTIRE_SCREEN) + underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null) - verify(sysuiDialog).setMessage(R.string.cast_to_other_device_stop_dialog_message) + verify(sysuiDialog) + .setMessage(context.getString(R.string.cast_to_other_device_stop_dialog_message)) + } + + @Test + fun message_singleTask() { + val baseIntent = + Intent().apply { this.component = ComponentName("fake.task.package", "cls") } + val appInfo = mock<ApplicationInfo>() + whenever(appInfo.loadLabel(kosmos.packageManager)).thenReturn("Fake Package") + whenever(kosmos.packageManager.getApplicationInfo(eq("fake.task.package"), any<Int>())) + .thenReturn(appInfo) + + createAndSetDelegate( + MediaProjectionState.Projecting.SingleTask( + HOST_PACKAGE, + createTask(taskId = 1, baseIntent = baseIntent) + ) + ) + + underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null) + + // It'd be nice to use R.string.cast_to_other_device_stop_dialog_message_specific_app + // directly, but it includes the <b> tags which aren't in the returned string. + val result = argumentCaptor<CharSequence>() + verify(sysuiDialog).setMessage(result.capture()) + assertThat(result.firstValue.toString()).isEqualTo("You will stop casting Fake Package") } @Test fun negativeButton() { + createAndSetDelegate(ENTIRE_SCREEN) + underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null) verify(sysuiDialog).setNegativeButton(R.string.close_dialog_button, null) @@ -86,6 +116,8 @@ class EndCastToOtherDeviceDialogDelegateTest : SysuiTestCase() { @Test fun positiveButton() = kosmos.testScope.runTest { + createAndSetDelegate(SINGLE_TASK) + underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null) val clickListener = argumentCaptor<DialogInterface.OnClickListener>() @@ -105,4 +137,20 @@ class EndCastToOtherDeviceDialogDelegateTest : SysuiTestCase() { assertThat(kosmos.fakeMediaProjectionRepository.stopProjectingInvoked).isTrue() } + + private fun createAndSetDelegate(state: MediaProjectionState.Projecting) { + underTest = + EndCastToOtherDeviceDialogDelegate( + kosmos.endMediaProjectionDialogHelper, + kosmos.mediaProjectionChipInteractor, + state, + ) + } + + companion object { + private const val HOST_PACKAGE = "fake.host.package" + private val ENTIRE_SCREEN = MediaProjectionState.Projecting.EntireScreen(HOST_PACKAGE) + private val SINGLE_TASK = + MediaProjectionState.Projecting.SingleTask(HOST_PACKAGE, createTask(taskId = 1)) + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelperTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelperTest.kt new file mode 100644 index 000000000000..bbd1109c59e5 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelperTest.kt @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2024 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.chips.mediaprojection.ui.view + +import android.content.ComponentName +import android.content.Intent +import android.content.packageManager +import android.content.pm.ApplicationInfo +import android.content.pm.PackageManager +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.testCase +import com.android.systemui.mediaprojection.data.model.MediaProjectionState +import com.android.systemui.mediaprojection.taskswitcher.FakeActivityTaskManager.Companion.createTask +import com.android.systemui.res.R +import com.android.systemui.statusbar.phone.SystemUIDialog +import com.android.systemui.statusbar.phone.mockSystemUIDialogFactory +import com.google.common.truth.Truth.assertThat +import kotlin.test.Test +import org.mockito.kotlin.any +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +@SmallTest +class EndMediaProjectionDialogHelperTest : SysuiTestCase() { + private val kosmos = Kosmos().also { it.testCase = this } + + private val underTest = kosmos.endMediaProjectionDialogHelper + + @Test + fun createDialog_usesDelegateAndFactory() { + val dialog = mock<SystemUIDialog>() + val delegate = SystemUIDialog.Delegate { dialog } + whenever(kosmos.mockSystemUIDialogFactory.create(eq(delegate))).thenReturn(dialog) + + underTest.createDialog(delegate) + + verify(kosmos.mockSystemUIDialogFactory).create(delegate) + } + + @Test + fun getDialogMessage_entireScreen_isGenericMessage() { + val result = + underTest.getDialogMessage( + MediaProjectionState.Projecting.EntireScreen("host.package"), + R.string.accessibility_home, + R.string.cast_to_other_device_stop_dialog_message_specific_app + ) + + assertThat(result).isEqualTo(context.getString(R.string.accessibility_home)) + } + + @Test + fun getDialogMessage_singleTask_cannotFindPackage_isGenericMessage() { + val baseIntent = + Intent().apply { this.component = ComponentName("fake.task.package", "cls") } + whenever(kosmos.packageManager.getApplicationInfo(eq("fake.task.package"), any<Int>())) + .thenThrow(PackageManager.NameNotFoundException()) + + val projectionState = + MediaProjectionState.Projecting.SingleTask( + "host.package", + createTask(taskId = 1, baseIntent = baseIntent) + ) + + val result = + underTest.getDialogMessage( + projectionState, + R.string.accessibility_home, + R.string.cast_to_other_device_stop_dialog_message_specific_app + ) + + assertThat(result).isEqualTo(context.getString(R.string.accessibility_home)) + } + + @Test + fun getDialogMessage_singleTask_findsPackage_isSpecificMessageWithAppLabel() { + val baseIntent = + Intent().apply { this.component = ComponentName("fake.task.package", "cls") } + val appInfo = mock<ApplicationInfo>() + whenever(appInfo.loadLabel(kosmos.packageManager)).thenReturn("Fake Package") + whenever(kosmos.packageManager.getApplicationInfo(eq("fake.task.package"), any<Int>())) + .thenReturn(appInfo) + + val projectionState = + MediaProjectionState.Projecting.SingleTask( + "host.package", + createTask(taskId = 1, baseIntent = baseIntent) + ) + + val result = + underTest.getDialogMessage( + projectionState, + R.string.accessibility_home, + R.string.cast_to_other_device_stop_dialog_message_specific_app + ) + + // It'd be nice to use the R.string resources directly, but they include the <b> tags which + // aren't in the returned string. + assertThat(result.toString()).isEqualTo("You will stop casting Fake Package") + } + + @Test + fun getDialogMessage_appLabelHasSpecialCharacters_isEscaped() { + val baseIntent = + Intent().apply { this.component = ComponentName("fake.task.package", "cls") } + val appInfo = mock<ApplicationInfo>() + whenever(appInfo.loadLabel(kosmos.packageManager)).thenReturn("Fake & Package <Here>") + whenever(kosmos.packageManager.getApplicationInfo(eq("fake.task.package"), any<Int>())) + .thenReturn(appInfo) + + val projectionState = + MediaProjectionState.Projecting.SingleTask( + "host.package", + createTask(taskId = 1, baseIntent = baseIntent) + ) + + val result = + underTest.getDialogMessage( + projectionState, + R.string.accessibility_home, + R.string.cast_to_other_device_stop_dialog_message_specific_app + ) + + assertThat(result.toString()).isEqualTo("You will stop casting Fake & Package <Here>") + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndShareToAppDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndShareToAppDialogDelegateTest.kt index 1d6e8669274d..4ddca521abd3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndShareToAppDialogDelegateTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndShareToAppDialogDelegateTest.kt @@ -16,22 +16,28 @@ package com.android.systemui.statusbar.chips.mediaprojection.ui.view +import android.content.ComponentName import android.content.DialogInterface +import android.content.Intent +import android.content.packageManager +import android.content.pm.ApplicationInfo import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.testCase import com.android.systemui.kosmos.testScope +import com.android.systemui.mediaprojection.data.model.MediaProjectionState import com.android.systemui.mediaprojection.data.repository.fakeMediaProjectionRepository +import com.android.systemui.mediaprojection.taskswitcher.FakeActivityTaskManager.Companion.createTask import com.android.systemui.res.R import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.mediaProjectionChipInteractor import com.android.systemui.statusbar.phone.SystemUIDialog -import com.android.systemui.statusbar.phone.mockSystemUIDialogFactory import com.google.common.truth.Truth.assertThat import kotlin.test.Test import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest -import org.junit.Before +import org.mockito.kotlin.any import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.eq import org.mockito.kotlin.mock @@ -41,22 +47,14 @@ import org.mockito.kotlin.whenever @SmallTest @OptIn(ExperimentalCoroutinesApi::class) class EndShareToAppDialogDelegateTest : SysuiTestCase() { - private val kosmos = Kosmos() + private val kosmos = Kosmos().also { it.testCase = this } private val sysuiDialog = mock<SystemUIDialog>() - private val sysuiDialogFactory = kosmos.mockSystemUIDialogFactory - private val underTest = - EndShareToAppDialogDelegate( - sysuiDialogFactory, - kosmos.mediaProjectionChipInteractor, - ) - - @Before - fun setUp() { - whenever(sysuiDialogFactory.create(eq(underTest), eq(context))).thenReturn(sysuiDialog) - } + private lateinit var underTest: EndShareToAppDialogDelegate @Test fun icon() { + createAndSetDelegate(SINGLE_TASK) + underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null) verify(sysuiDialog).setIcon(R.drawable.ic_screenshot_share) @@ -64,20 +62,51 @@ class EndShareToAppDialogDelegateTest : SysuiTestCase() { @Test fun title() { + createAndSetDelegate(ENTIRE_SCREEN) + underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null) verify(sysuiDialog).setTitle(R.string.share_to_app_stop_dialog_title) } @Test - fun message() { + fun message_entireScreen() { + createAndSetDelegate(ENTIRE_SCREEN) + underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null) - verify(sysuiDialog).setMessage(R.string.share_to_app_stop_dialog_message) + verify(sysuiDialog).setMessage(context.getString(R.string.share_to_app_stop_dialog_message)) + } + + @Test + fun message_singleTask() { + val baseIntent = + Intent().apply { this.component = ComponentName("fake.task.package", "cls") } + val appInfo = mock<ApplicationInfo>() + whenever(appInfo.loadLabel(kosmos.packageManager)).thenReturn("Fake Package") + whenever(kosmos.packageManager.getApplicationInfo(eq("fake.task.package"), any<Int>())) + .thenReturn(appInfo) + + createAndSetDelegate( + MediaProjectionState.Projecting.SingleTask( + HOST_PACKAGE, + createTask(taskId = 1, baseIntent = baseIntent) + ) + ) + + underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null) + + // It'd be nice to use R.string.share_to_app_stop_dialog_message_specific_app directly, but + // it includes the <b> tags which aren't in the returned string. + val result = argumentCaptor<CharSequence>() + verify(sysuiDialog).setMessage(result.capture()) + assertThat(result.firstValue.toString()).isEqualTo("You will stop sharing Fake Package") } @Test fun negativeButton() { + createAndSetDelegate(SINGLE_TASK) + underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null) verify(sysuiDialog).setNegativeButton(R.string.close_dialog_button, null) @@ -86,6 +115,8 @@ class EndShareToAppDialogDelegateTest : SysuiTestCase() { @Test fun positiveButton() = kosmos.testScope.runTest { + createAndSetDelegate(ENTIRE_SCREEN) + underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null) val clickListener = argumentCaptor<DialogInterface.OnClickListener>() @@ -105,4 +136,20 @@ class EndShareToAppDialogDelegateTest : SysuiTestCase() { assertThat(kosmos.fakeMediaProjectionRepository.stopProjectingInvoked).isTrue() } + + private fun createAndSetDelegate(state: MediaProjectionState.Projecting) { + underTest = + EndShareToAppDialogDelegate( + kosmos.endMediaProjectionDialogHelper, + kosmos.mediaProjectionChipInteractor, + state, + ) + } + + companion object { + private const val HOST_PACKAGE = "fake.host.package" + private val ENTIRE_SCREEN = MediaProjectionState.Projecting.EntireScreen(HOST_PACKAGE) + private val SINGLE_TASK = + MediaProjectionState.Projecting.SingleTask(HOST_PACKAGE, createTask(taskId = 1)) + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt index 67129639efaf..65bf0bcf2741 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt @@ -22,6 +22,7 @@ import com.android.systemui.common.shared.model.ContentDescription import com.android.systemui.common.shared.model.Icon import com.android.systemui.coroutines.collectLastValue import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.testCase import com.android.systemui.kosmos.testScope import com.android.systemui.mediaprojection.data.model.MediaProjectionState import com.android.systemui.mediaprojection.data.repository.fakeMediaProjectionRepository @@ -39,8 +40,7 @@ import org.junit.Test @SmallTest class OngoingActivityChipsViewModelTest : SysuiTestCase() { - - private val kosmos = Kosmos() + private val kosmos = Kosmos().also { it.testCase = this } private val testScope = kosmos.testScope private val screenRecordState = kosmos.screenRecordRepository.screenRecordState diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/data/repository/StatusBarModeRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/data/repository/StatusBarModeRepositoryImplTest.kt index 057dcb2a156e..6af14e02ed62 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/data/repository/StatusBarModeRepositoryImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/data/repository/StatusBarModeRepositoryImplTest.kt @@ -35,6 +35,7 @@ import com.android.systemui.statusbar.phone.LetterboxAppearance import com.android.systemui.statusbar.phone.LetterboxAppearanceCalculator import com.android.systemui.statusbar.phone.StatusBarBoundsProvider import com.android.systemui.statusbar.phone.fragment.dagger.StatusBarFragmentComponent +import com.android.systemui.statusbar.phone.ongoingcall.data.model.OngoingCallModel import com.android.systemui.statusbar.phone.ongoingcall.data.repository.OngoingCallRepository import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.argumentCaptor @@ -390,7 +391,7 @@ class StatusBarModeRepositoryImplTest : SysuiTestCase() { testScope.runTest { val latest by collectLastValue(underTest.statusBarAppearance) - ongoingCallRepository.setHasOngoingCall(true) + ongoingCallRepository.setOngoingCallState(OngoingCallModel.InCall(startTimeMs = 34)) onSystemBarAttributesChanged( requestedVisibleTypes = WindowInsets.Type.navigationBars(), ) @@ -403,7 +404,7 @@ class StatusBarModeRepositoryImplTest : SysuiTestCase() { testScope.runTest { val latest by collectLastValue(underTest.statusBarAppearance) - ongoingCallRepository.setHasOngoingCall(true) + ongoingCallRepository.setOngoingCallState(OngoingCallModel.InCall(startTimeMs = 789)) onSystemBarAttributesChanged( requestedVisibleTypes = WindowInsets.Type.statusBars(), appearance = APPEARANCE_OPAQUE_STATUS_BARS, @@ -417,7 +418,7 @@ class StatusBarModeRepositoryImplTest : SysuiTestCase() { testScope.runTest { val latest by collectLastValue(underTest.statusBarAppearance) - ongoingCallRepository.setHasOngoingCall(false) + ongoingCallRepository.setOngoingCallState(OngoingCallModel.NoCall) onSystemBarAttributesChanged( requestedVisibleTypes = WindowInsets.Type.navigationBars(), appearance = APPEARANCE_OPAQUE_STATUS_BARS, diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerTest.kt index 4d6798be9211..feef9431c28c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerTest.kt @@ -32,16 +32,17 @@ import android.view.View import android.widget.LinearLayout import androidx.test.filters.SmallTest import com.android.internal.logging.testing.UiEventLoggerFake -import com.android.systemui.res.R import com.android.systemui.SysuiTestCase import com.android.systemui.dump.DumpManager import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.res.R +import com.android.systemui.statusbar.data.repository.FakeStatusBarModeRepository import com.android.systemui.statusbar.gesture.SwipeStatusBarAwayGestureHandler import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener -import com.android.systemui.statusbar.data.repository.FakeStatusBarModeRepository +import com.android.systemui.statusbar.phone.ongoingcall.data.model.OngoingCallModel import com.android.systemui.statusbar.phone.ongoingcall.data.repository.OngoingCallRepository import com.android.systemui.statusbar.window.StatusBarWindowController import com.android.systemui.util.concurrency.FakeExecutor @@ -60,13 +61,13 @@ import org.mockito.ArgumentMatchers.anyBoolean import org.mockito.ArgumentMatchers.anyString import org.mockito.ArgumentMatchers.nullable import org.mockito.Mock -import org.mockito.Mockito.`when` import org.mockito.Mockito.eq import org.mockito.Mockito.mock import org.mockito.Mockito.never import org.mockito.Mockito.reset import org.mockito.Mockito.times import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` import org.mockito.MockitoAnnotations private const val CALL_UID = 900 @@ -93,8 +94,8 @@ class OngoingCallControllerTest : SysuiTestCase() { private lateinit var controller: OngoingCallController private lateinit var notifCollectionListener: NotifCollectionListener - @Mock private lateinit var mockSwipeStatusBarAwayGestureHandler: - SwipeStatusBarAwayGestureHandler + @Mock + private lateinit var mockSwipeStatusBarAwayGestureHandler: SwipeStatusBarAwayGestureHandler @Mock private lateinit var mockOngoingCallListener: OngoingCallListener @Mock private lateinit var mockActivityStarter: ActivityStarter @Mock private lateinit var mockIActivityManager: IActivityManager @@ -112,21 +113,22 @@ class OngoingCallControllerTest : SysuiTestCase() { MockitoAnnotations.initMocks(this) val notificationCollection = mock(CommonNotifCollection::class.java) - controller = OngoingCallController( - testScope.backgroundScope, - context, - ongoingCallRepository, - notificationCollection, - clock, - mockActivityStarter, - mainExecutor, - mockIActivityManager, - OngoingCallLogger(uiEventLoggerFake), - DumpManager(), - mockStatusBarWindowController, - mockSwipeStatusBarAwayGestureHandler, - statusBarModeRepository, - ) + controller = + OngoingCallController( + testScope.backgroundScope, + context, + ongoingCallRepository, + notificationCollection, + clock, + mockActivityStarter, + mainExecutor, + mockIActivityManager, + OngoingCallLogger(uiEventLoggerFake), + DumpManager(), + mockStatusBarWindowController, + mockSwipeStatusBarAwayGestureHandler, + statusBarModeRepository, + ) controller.start() controller.addCallback(mockOngoingCallListener) controller.setChipView(chipView) @@ -136,7 +138,7 @@ class OngoingCallControllerTest : SysuiTestCase() { notifCollectionListener = collectionListenerCaptor.value!! `when`(mockIActivityManager.getUidProcessState(eq(CALL_UID), nullable(String::class.java))) - .thenReturn(PROC_STATE_INVISIBLE) + .thenReturn(PROC_STATE_INVISIBLE) } @After @@ -146,10 +148,14 @@ class OngoingCallControllerTest : SysuiTestCase() { @Test fun onEntryUpdated_isOngoingCallNotif_listenerAndRepoNotified() { - notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) + val notification = NotificationEntryBuilder(createOngoingCallNotifEntry()) + notification.modifyNotification(context).setWhen(567) + notifCollectionListener.onEntryUpdated(notification.build()) verify(mockOngoingCallListener).onOngoingCallStateChanged(anyBoolean()) - assertThat(ongoingCallRepository.hasOngoingCall.value).isTrue() + val repoState = ongoingCallRepository.ongoingCallState.value + assertThat(repoState).isInstanceOf(OngoingCallModel.InCall::class.java) + assertThat((repoState as OngoingCallModel.InCall).startTimeMs).isEqualTo(567) } @Test @@ -164,7 +170,8 @@ class OngoingCallControllerTest : SysuiTestCase() { notifCollectionListener.onEntryUpdated(createNotCallNotifEntry()) verify(mockOngoingCallListener, never()).onOngoingCallStateChanged(anyBoolean()) - assertThat(ongoingCallRepository.hasOngoingCall.value).isFalse() + assertThat(ongoingCallRepository.ongoingCallState.value) + .isInstanceOf(OngoingCallModel.NoCall::class.java) } @Test @@ -172,25 +179,27 @@ class OngoingCallControllerTest : SysuiTestCase() { notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) notifCollectionListener.onEntryUpdated(createScreeningCallNotifEntry()) - verify(mockOngoingCallListener, times(2)) - .onOngoingCallStateChanged(anyBoolean()) + verify(mockOngoingCallListener, times(2)).onOngoingCallStateChanged(anyBoolean()) } @Test fun onEntryUpdated_ongoingCallNotifThenScreeningCallNotif_repoUpdated() { notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) - assertThat(ongoingCallRepository.hasOngoingCall.value).isTrue() + assertThat(ongoingCallRepository.ongoingCallState.value) + .isInstanceOf(OngoingCallModel.InCall::class.java) notifCollectionListener.onEntryUpdated(createScreeningCallNotifEntry()) - assertThat(ongoingCallRepository.hasOngoingCall.value).isFalse() + assertThat(ongoingCallRepository.ongoingCallState.value) + .isInstanceOf(OngoingCallModel.NoCall::class.java) } /** Regression test for b/191472854. */ @Test fun onEntryUpdated_notifHasNullContentIntent_noCrash() { notifCollectionListener.onEntryUpdated( - createCallNotifEntry(ongoingCallStyle, nullContentIntent = true)) + createCallNotifEntry(ongoingCallStyle, nullContentIntent = true) + ) } /** Regression test for b/192379214. */ @@ -202,12 +211,12 @@ class OngoingCallControllerTest : SysuiTestCase() { notifCollectionListener.onEntryUpdated(notification.build()) chipView.measure( - View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), - View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED) + View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), + View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED) ) assertThat(chipView.findViewById<View>(R.id.ongoing_activity_chip_time)?.measuredWidth) - .isEqualTo(0) + .isEqualTo(0) } @Test @@ -218,12 +227,12 @@ class OngoingCallControllerTest : SysuiTestCase() { notifCollectionListener.onEntryUpdated(notification.build()) chipView.measure( - View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), - View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED) + View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), + View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED) ) assertThat(chipView.findViewById<View>(R.id.ongoing_activity_chip_time)?.measuredWidth) - .isGreaterThan(0) + .isGreaterThan(0) } @Test @@ -233,12 +242,12 @@ class OngoingCallControllerTest : SysuiTestCase() { notifCollectionListener.onEntryUpdated(notification.build()) chipView.measure( - View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), - View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED) + View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), + View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED) ) assertThat(chipView.findViewById<View>(R.id.ongoing_activity_chip_time)?.measuredWidth) - .isGreaterThan(0) + .isGreaterThan(0) } /** Regression test for b/194731244. */ @@ -250,15 +259,14 @@ class OngoingCallControllerTest : SysuiTestCase() { notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) } - verify(mockIActivityManager, times(1)) - .registerUidObserver(any(), any(), any(), any()) + verify(mockIActivityManager, times(1)).registerUidObserver(any(), any(), any(), any()) } /** Regression test for b/216248574. */ @Test fun entryUpdated_getUidProcessStateThrowsException_noCrash() { `when`(mockIActivityManager.getUidProcessState(eq(CALL_UID), nullable(String::class.java))) - .thenThrow(SecurityException()) + .thenThrow(SecurityException()) // No assert required, just check no crash notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) @@ -267,9 +275,15 @@ class OngoingCallControllerTest : SysuiTestCase() { /** Regression test for b/216248574. */ @Test fun entryUpdated_registerUidObserverThrowsException_noCrash() { - `when`(mockIActivityManager.registerUidObserver( - any(), any(), any(), nullable(String::class.java) - )).thenThrow(SecurityException()) + `when`( + mockIActivityManager.registerUidObserver( + any(), + any(), + any(), + nullable(String::class.java), + ) + ) + .thenThrow(SecurityException()) // No assert required, just check no crash notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) @@ -281,9 +295,8 @@ class OngoingCallControllerTest : SysuiTestCase() { notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) val packageNameCaptor = ArgumentCaptor.forClass(String::class.java) - verify(mockIActivityManager).registerUidObserver( - any(), any(), any(), packageNameCaptor.capture() - ) + verify(mockIActivityManager) + .registerUidObserver(any(), any(), any(), packageNameCaptor.capture()) assertThat(packageNameCaptor.value).isNotNull() } @@ -313,11 +326,13 @@ class OngoingCallControllerTest : SysuiTestCase() { fun onEntryRemoved_callNotifAddedThenRemoved_repoUpdated() { val ongoingCallNotifEntry = createOngoingCallNotifEntry() notifCollectionListener.onEntryAdded(ongoingCallNotifEntry) - assertThat(ongoingCallRepository.hasOngoingCall.value).isTrue() + assertThat(ongoingCallRepository.ongoingCallState.value) + .isInstanceOf(OngoingCallModel.InCall::class.java) notifCollectionListener.onEntryRemoved(ongoingCallNotifEntry, REASON_USER_STOPPED) - assertThat(ongoingCallRepository.hasOngoingCall.value).isFalse() + assertThat(ongoingCallRepository.ongoingCallState.value) + .isInstanceOf(OngoingCallModel.NoCall::class.java) } @Test @@ -360,7 +375,8 @@ class OngoingCallControllerTest : SysuiTestCase() { notifCollectionListener.onEntryRemoved(removedEntryBuilder.build(), REASON_USER_STOPPED) - assertThat(ongoingCallRepository.hasOngoingCall.value).isFalse() + assertThat(ongoingCallRepository.ongoingCallState.value) + .isInstanceOf(OngoingCallModel.NoCall::class.java) } @Test @@ -379,7 +395,8 @@ class OngoingCallControllerTest : SysuiTestCase() { notifCollectionListener.onEntryRemoved(createNotCallNotifEntry(), REASON_USER_STOPPED) - assertThat(ongoingCallRepository.hasOngoingCall.value).isTrue() + assertThat(ongoingCallRepository.ongoingCallState.value) + .isInstanceOf(OngoingCallModel.InCall::class.java) } @Test @@ -404,7 +421,7 @@ class OngoingCallControllerTest : SysuiTestCase() { @Test fun hasOngoingCall_ongoingCallNotifSentAndCallAppNotVisible_returnsTrue() { `when`(mockIActivityManager.getUidProcessState(eq(CALL_UID), nullable(String::class.java))) - .thenReturn(PROC_STATE_INVISIBLE) + .thenReturn(PROC_STATE_INVISIBLE) notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) @@ -414,7 +431,7 @@ class OngoingCallControllerTest : SysuiTestCase() { @Test fun hasOngoingCall_ongoingCallNotifSentButCallAppVisible_returnsFalse() { `when`(mockIActivityManager.getUidProcessState(eq(CALL_UID), nullable(String::class.java))) - .thenReturn(PROC_STATE_VISIBLE) + .thenReturn(PROC_STATE_VISIBLE) notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) @@ -472,10 +489,8 @@ class OngoingCallControllerTest : SysuiTestCase() { lateinit var newChipView: View TestableLooper.get(this).runWithLooper { - newChipView = LayoutInflater.from(mContext).inflate( - R.layout.ongoing_activity_chip, - null - ) + newChipView = + LayoutInflater.from(mContext).inflate(R.layout.ongoing_activity_chip, null) } // Change the chip view associated with the controller. @@ -488,13 +503,13 @@ class OngoingCallControllerTest : SysuiTestCase() { fun callProcessChangesToVisible_listenerNotified() { // Start the call while the process is invisible. `when`(mockIActivityManager.getUidProcessState(eq(CALL_UID), nullable(String::class.java))) - .thenReturn(PROC_STATE_INVISIBLE) + .thenReturn(PROC_STATE_INVISIBLE) notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) reset(mockOngoingCallListener) val captor = ArgumentCaptor.forClass(IUidObserver.Stub::class.java) - verify(mockIActivityManager).registerUidObserver( - captor.capture(), any(), any(), nullable(String::class.java)) + verify(mockIActivityManager) + .registerUidObserver(captor.capture(), any(), any(), nullable(String::class.java)) val uidObserver = captor.value // Update the process to visible. @@ -509,13 +524,13 @@ class OngoingCallControllerTest : SysuiTestCase() { fun callProcessChangesToInvisible_listenerNotified() { // Start the call while the process is visible. `when`(mockIActivityManager.getUidProcessState(eq(CALL_UID), nullable(String::class.java))) - .thenReturn(PROC_STATE_VISIBLE) + .thenReturn(PROC_STATE_VISIBLE) notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) reset(mockOngoingCallListener) val captor = ArgumentCaptor.forClass(IUidObserver.Stub::class.java) - verify(mockIActivityManager).registerUidObserver( - captor.capture(), any(), any(), nullable(String::class.java)) + verify(mockIActivityManager) + .registerUidObserver(captor.capture(), any(), any(), nullable(String::class.java)) val uidObserver = captor.value // Update the process to invisible. @@ -534,7 +549,7 @@ class OngoingCallControllerTest : SysuiTestCase() { assertThat(uiEventLoggerFake.numLogs()).isEqualTo(1) assertThat(uiEventLoggerFake.eventId(0)) - .isEqualTo(OngoingCallLogger.OngoingCallEvents.ONGOING_CALL_CLICKED.id) + .isEqualTo(OngoingCallLogger.OngoingCallEvents.ONGOING_CALL_CLICKED.id) } /** Regression test for b/212467440. */ @@ -556,8 +571,9 @@ class OngoingCallControllerTest : SysuiTestCase() { assertThat(uiEventLoggerFake.numLogs()).isEqualTo(1) assertThat(uiEventLoggerFake.eventId(0)) - .isEqualTo(OngoingCallLogger.OngoingCallEvents.ONGOING_CALL_VISIBLE.id) + .isEqualTo(OngoingCallLogger.OngoingCallEvents.ONGOING_CALL_VISIBLE.id) } + // Other tests for notifyChipVisibilityChanged are in [OngoingCallLogger], since // [OngoingCallController.notifyChipVisibilityChanged] just delegates to that class. @@ -621,8 +637,7 @@ class OngoingCallControllerTest : SysuiTestCase() { statusBarModeRepository.defaultDisplay.isInFullscreenMode.value = false testScope.runCurrent() - verify(mockSwipeStatusBarAwayGestureHandler) - .removeOnGestureDetectedCallback(anyString()) + verify(mockSwipeStatusBarAwayGestureHandler).removeOnGestureDetectedCallback(anyString()) } @Test @@ -635,8 +650,7 @@ class OngoingCallControllerTest : SysuiTestCase() { notifCollectionListener.onEntryRemoved(ongoingCallNotifEntry, REASON_USER_STOPPED) - verify(mockSwipeStatusBarAwayGestureHandler) - .removeOnGestureDetectedCallback(anyString()) + verify(mockSwipeStatusBarAwayGestureHandler).removeOnGestureDetectedCallback(anyString()) } // TODO(b/195839150): Add test @@ -675,5 +689,9 @@ private val person = Person.Builder().setName("name").build() private val hangUpIntent = mock(PendingIntent::class.java) private val ongoingCallStyle = Notification.CallStyle.forOngoingCall(person, hangUpIntent) -private val screeningCallStyle = Notification.CallStyle.forScreeningCall( - person, hangUpIntent, /* answerIntent= */ mock(PendingIntent::class.java))
\ No newline at end of file +private val screeningCallStyle = + Notification.CallStyle.forScreeningCall( + person, + hangUpIntent, + /* answerIntent= */ mock(PendingIntent::class.java), + ) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/data/repository/OngoingCallRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/data/repository/OngoingCallRepositoryTest.kt index 56aa7d6e3ca4..73a86a1b8511 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/data/repository/OngoingCallRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/data/repository/OngoingCallRepositoryTest.kt @@ -18,6 +18,7 @@ package com.android.systemui.statusbar.phone.ongoingcall.data.repository import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.statusbar.phone.ongoingcall.data.model.OngoingCallModel import com.google.common.truth.Truth.assertThat import org.junit.Test @@ -27,12 +28,13 @@ class OngoingCallRepositoryTest : SysuiTestCase() { @Test fun hasOngoingCall_matchesSet() { - underTest.setHasOngoingCall(true) + val inCallModel = OngoingCallModel.InCall(startTimeMs = 654) + underTest.setOngoingCallState(inCallModel) - assertThat(underTest.hasOngoingCall.value).isTrue() + assertThat(underTest.ongoingCallState.value).isEqualTo(inCallModel) - underTest.setHasOngoingCall(false) + underTest.setOngoingCallState(OngoingCallModel.NoCall) - assertThat(underTest.hasOngoingCall.value).isFalse() + assertThat(underTest.ongoingCallState.value).isEqualTo(OngoingCallModel.NoCall) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/CollapsedStatusBarViewModelImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/CollapsedStatusBarViewModelImplTest.kt index cdb2b883078a..b8299e5ef3ad 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/CollapsedStatusBarViewModelImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/CollapsedStatusBarViewModelImplTest.kt @@ -29,6 +29,7 @@ import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.keyguard.shared.model.TransitionStep import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.kosmos.testCase import com.android.systemui.kosmos.testDispatcher import com.android.systemui.kosmos.testScope import com.android.systemui.log.assertLogsWtf @@ -63,7 +64,10 @@ import org.junit.Test @SmallTest @OptIn(ExperimentalCoroutinesApi::class) class CollapsedStatusBarViewModelImplTest : SysuiTestCase() { - private val kosmos = Kosmos().apply { testDispatcher = UnconfinedTestDispatcher() } + private val kosmos = Kosmos().also { + it.testCase = this + it.testDispatcher = UnconfinedTestDispatcher() + } private val testScope = kosmos.testScope diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractorKosmos.kt index 062b4484044c..9d2281167fbf 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractorKosmos.kt @@ -21,7 +21,7 @@ import com.android.systemui.animation.mockDialogTransitionAnimator import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.mediaprojection.data.repository.fakeMediaProjectionRepository -import com.android.systemui.statusbar.phone.mockSystemUIDialogFactory +import com.android.systemui.statusbar.chips.mediaprojection.ui.view.endMediaProjectionDialogHelper import com.android.systemui.util.time.fakeSystemClock val Kosmos.mediaProjectionChipInteractor: MediaProjectionChipInteractor by @@ -31,7 +31,7 @@ val Kosmos.mediaProjectionChipInteractor: MediaProjectionChipInteractor by mediaProjectionRepository = fakeMediaProjectionRepository, packageManager = packageManager, systemClock = fakeSystemClock, - dialogFactory = mockSystemUIDialogFactory, + endMediaProjectionDialogHelper = endMediaProjectionDialogHelper, dialogTransitionAnimator = mockDialogTransitionAnimator, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelperKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelperKosmos.kt new file mode 100644 index 000000000000..4f82662fa673 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelperKosmos.kt @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2024 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.chips.mediaprojection.ui.view + +import android.content.applicationContext +import android.content.packageManager +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.statusbar.phone.mockSystemUIDialogFactory + +val Kosmos.endMediaProjectionDialogHelper: EndMediaProjectionDialogHelper by + Kosmos.Fixture { + EndMediaProjectionDialogHelper( + dialogFactory = mockSystemUIDialogFactory, + packageManager = packageManager, + context = applicationContext, + ) + } |