From 2cbafc1941f5225ec35a5ca9405a69dd46b1dc9e Mon Sep 17 00:00:00 2001 From: Caitlin Cassidy Date: Wed, 24 Mar 2021 15:26:00 +0000 Subject: [PATCH] [Ongoing Call Chip] Add an ongoing call chip in the status bar. Light mode: https://screenshot.googleplex.com/3uehhoJ4EYHQr47 Dark mode: https://screenshot.googleplex.com/7ymFjTNUhQmUhZA Bug: 183229367 Test: New tests added, atest, and manual Change-Id: Id9d9e8f0436e228beb3f2c5f5a9a81f6a07aa033 --- .../res/drawable/ongoing_call_chip_bg.xml | 20 +++ .../SystemUI/res/layout/ongoing_call_chip.xml | 50 ++++++ packages/SystemUI/res/layout/status_bar.xml | 2 + packages/SystemUI/res/values/dimens.xml | 7 + packages/SystemUI/res/values/flags.xml | 2 +- .../dagger/StatusBarDependenciesModule.java | 7 +- .../phone/CollapsedStatusBarFragment.java | 29 ++++ .../ongoingcall/OngoingCallController.kt | 55 ++++++- .../phone/ongoingcall/OngoingCallListener.kt | 26 +++ .../phone/CollapsedStatusBarFragmentTest.java | 50 +++++- .../ongoingcall/OngoingCallControllerTest.kt | 153 ++++++++++++++++++ 11 files changed, 391 insertions(+), 10 deletions(-) create mode 100644 packages/SystemUI/res/drawable/ongoing_call_chip_bg.xml create mode 100644 packages/SystemUI/res/layout/ongoing_call_chip.xml create mode 100644 packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallListener.kt create mode 100644 packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerTest.kt diff --git a/packages/SystemUI/res/drawable/ongoing_call_chip_bg.xml b/packages/SystemUI/res/drawable/ongoing_call_chip_bg.xml new file mode 100644 index 0000000000000..bdd6270bb50bb --- /dev/null +++ b/packages/SystemUI/res/drawable/ongoing_call_chip_bg.xml @@ -0,0 +1,20 @@ + + + + + + \ No newline at end of file diff --git a/packages/SystemUI/res/layout/ongoing_call_chip.xml b/packages/SystemUI/res/layout/ongoing_call_chip.xml new file mode 100644 index 0000000000000..c90fc3151ee74 --- /dev/null +++ b/packages/SystemUI/res/layout/ongoing_call_chip.xml @@ -0,0 +1,50 @@ + + + + + + + + + + diff --git a/packages/SystemUI/res/layout/status_bar.xml b/packages/SystemUI/res/layout/status_bar.xml index f8db97dbf800b..8b244c757649c 100644 --- a/packages/SystemUI/res/layout/status_bar.xml +++ b/packages/SystemUI/res/layout/status_bar.xml @@ -82,6 +82,8 @@ android:gravity="center_vertical|start" /> + + 24dp 32dp 50dp + + + 12dp + 16dp + + 4dp + 28dp diff --git a/packages/SystemUI/res/values/flags.xml b/packages/SystemUI/res/values/flags.xml index c39db941af38d..bbf204844e29a 100644 --- a/packages/SystemUI/res/values/flags.xml +++ b/packages/SystemUI/res/values/flags.xml @@ -49,7 +49,7 @@ false - false + true false diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java index 14c73b5cbb4c9..20383feff9bcc 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java @@ -63,6 +63,7 @@ import com.android.systemui.statusbar.policy.RemoteInputUriController; import com.android.systemui.tracing.ProtoTracer; import com.android.systemui.util.DeviceConfigProxy; import com.android.systemui.util.concurrency.DelayableExecutor; +import com.android.systemui.util.time.SystemClock; import com.android.wm.shell.bubbles.Bubbles; import java.util.Optional; @@ -234,9 +235,11 @@ public interface StatusBarDependenciesModule { @Provides @SysUISingleton static OngoingCallController provideOngoingCallController( - CommonNotifCollection notifCollection, FeatureFlags featureFlags) { + CommonNotifCollection notifCollection, + FeatureFlags featureFlags, + SystemClock systemClock) { OngoingCallController ongoingCallController = - new OngoingCallController(notifCollection, featureFlags); + new OngoingCallController(notifCollection, featureFlags, systemClock); ongoingCallController.init(); return ongoingCallController; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CollapsedStatusBarFragment.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CollapsedStatusBarFragment.java index f64a0e03698a9..9d1c609281060 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CollapsedStatusBarFragment.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CollapsedStatusBarFragment.java @@ -38,6 +38,7 @@ import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.phone.StatusBarIconController.DarkIconManager; import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallController; +import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallListener; import com.android.systemui.statusbar.policy.EncryptionHelper; import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.statusbar.policy.NetworkController; @@ -67,6 +68,7 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue private NetworkController mNetworkController; private LinearLayout mSystemIconArea; private View mClockView; + private ViewGroup mOngoingCallChip; private View mNotificationIconAreaInner; private View mCenteredIconArea; private int mDisabled1; @@ -86,6 +88,20 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue } }; + private final OngoingCallListener mOngoingCallListener = new OngoingCallListener() { + @Override + public void onOngoingCallStarted(boolean animate) { + disable(getContext().getDisplayId(), mDisabled1, mDisabled2, animate); + animateShow(mOngoingCallChip, animate); + } + + @Override + public void onOngoingCallEnded(boolean animate) { + disable(getContext().getDisplayId(), mDisabled1, mDisabled2, animate); + animateHiddenState(mOngoingCallChip, View.GONE, animate); + } + }; + @Inject public CollapsedStatusBarFragment(OngoingCallController ongoingCallController) { mOngoingCallController = ongoingCallController; @@ -142,6 +158,7 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue super.onResume(); mCommandQueue.addCallback(this); mStatusBarStateController.addCallback(this); + mOngoingCallController.addCallback(mOngoingCallListener); } @Override @@ -149,6 +166,7 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue super.onPause(); mCommandQueue.removeCallback(this); mStatusBarStateController.removeCallback(this); + mOngoingCallController.removeCallback(mOngoingCallListener); } @Override @@ -179,6 +197,8 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue } statusBarCenteredIconArea.addView(mCenteredIconArea); + initOngoingCallChip(); + // Default to showing until we know otherwise. showNotificationIconArea(false); } @@ -228,6 +248,10 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue state |= DISABLE_CLOCK; } + if (mOngoingCallController.getHasOngoingCall()) { + state |= DISABLE_NOTIFICATION_ICONS; + } + if (!mKeyguardStateController.isLaunchTransitionFadingAway() && !mKeyguardStateController.isKeyguardFadingAway() && shouldHideNotificationIcons() @@ -395,6 +419,11 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue } } + private void initOngoingCallChip() { + mOngoingCallChip = mStatusBar.findViewById(R.id.ongoing_call_chip); + mOngoingCallController.setChipView(mOngoingCallChip); + } + @Override public void onStateChanged(int newState) { 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 60d3ea3a70937..b55db6fd1e7d7 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 @@ -19,11 +19,16 @@ package com.android.systemui.statusbar.phone.ongoingcall import android.app.Notification import android.app.Notification.CallStyle.CALL_TYPE_ONGOING import android.util.Log +import android.view.ViewGroup +import android.widget.Chronometer +import com.android.systemui.R import com.android.systemui.dagger.SysUISingleton import com.android.systemui.statusbar.FeatureFlags 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.policy.CallbackController +import com.android.systemui.util.time.SystemClock import javax.inject.Inject /** @@ -32,19 +37,39 @@ import javax.inject.Inject @SysUISingleton class OngoingCallController @Inject constructor( private val notifCollection: CommonNotifCollection, - private val featureFlags: FeatureFlags -) { + private val featureFlags: FeatureFlags, + private val systemClock: SystemClock +) : CallbackController { + + var hasOngoingCall = false + private set + private var chipView: ViewGroup? = null + + private val mListeners: MutableList = mutableListOf() private val notifListener = object : NotifCollectionListener { override fun onEntryUpdated(entry: NotificationEntry) { - if (isOngoingCallNotification(entry) && DEBUG) { - Log.d(TAG, "Ongoing call notification updated") + if (isOngoingCallNotification(entry)) { + val timeView = chipView?.findViewById(R.id.ongoing_call_chip_time) + if (timeView != null) { + hasOngoingCall = true + val callStartTime = entry.sbn.notification.`when` + timeView.base = callStartTime - + System.currentTimeMillis() + + systemClock.elapsedRealtime() + timeView.start() + mListeners.forEach { l -> l.onOngoingCallStarted(animate = true) } + } else if (DEBUG) { + Log.w(TAG, "Ongoing call chip view could not be found; " + + "Not displaying chip in status bar") + } } } override fun onEntryRemoved(entry: NotificationEntry, reason: Int) { - if (isOngoingCallNotification(entry) && DEBUG) { - Log.d(TAG, "Ongoing call notification removed") + if (isOngoingCallNotification(entry)) { + hasOngoingCall = false + mListeners.forEach { l -> l.onOngoingCallEnded(animate = true) } } } } @@ -54,6 +79,24 @@ class OngoingCallController @Inject constructor( notifCollection.addCollectionListener(notifListener) } } + + fun setChipView(chipView: ViewGroup?) { + this.chipView = chipView + } + + override fun addCallback(listener: OngoingCallListener) { + synchronized(mListeners) { + if (!mListeners.contains(listener)) { + mListeners.add(listener) + } + } + } + + override fun removeCallback(listener: OngoingCallListener) { + synchronized(mListeners) { + mListeners.remove(listener) + } + } } private fun isOngoingCallNotification(entry: NotificationEntry): Boolean { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallListener.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallListener.kt new file mode 100644 index 0000000000000..7c583a1491e0d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallListener.kt @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2021 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 + +/** A listener that's notified when an ongoing call is started or ended. */ +interface OngoingCallListener { + /** Called when an ongoing call is started. */ + fun onOngoingCallStarted(animate: Boolean) + + /** Called when an ongoing call is ended. */ + fun onOngoingCallEnded(animate: Boolean) +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CollapsedStatusBarFragmentTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CollapsedStatusBarFragmentTest.java index 96cdaf98d964b..67fd5eb1acac5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CollapsedStatusBarFragmentTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CollapsedStatusBarFragmentTest.java @@ -38,12 +38,16 @@ import com.android.systemui.R; import com.android.systemui.SysuiBaseFragmentTest; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallController; +import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallListener; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; import org.mockito.Mockito; +import java.util.Objects; + @RunWith(AndroidTestingRunner.class) @RunWithLooper(setAsMainLooper = true) @SmallTest @@ -53,6 +57,7 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { private View mNotificationAreaInner; private View mCenteredNotificationAreaView; private StatusBarStateController mStatusBarStateController; + private OngoingCallController mOngoingCallController; public CollapsedStatusBarFragmentTest() { super(CollapsedStatusBarFragment.class); @@ -162,8 +167,51 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { Mockito.verify(mNotificationAreaInner, atLeast(1)).setVisibility(eq(View.VISIBLE)); } + @Test + public void onOngoingCallStarted_notificationsHiddenAndOngoingCallChipDisplayed() { + mFragments.dispatchResume(); + processAllMessages(); + + CollapsedStatusBarFragment fragment = (CollapsedStatusBarFragment) mFragment; + fragment.initNotificationIconArea(mMockNotificiationAreaController); + + ArgumentCaptor ongoingCallListenerCaptor = ArgumentCaptor.forClass( + OngoingCallListener.class); + Mockito.verify(mOngoingCallController).addCallback(ongoingCallListenerCaptor.capture()); + OngoingCallListener listener = Objects.requireNonNull(ongoingCallListenerCaptor.getValue()); + + when(mOngoingCallController.getHasOngoingCall()).thenReturn(true); + listener.onOngoingCallStarted(/* animate= */ false); + + assertEquals(View.VISIBLE, + mFragment.getView().findViewById(R.id.ongoing_call_chip).getVisibility()); + Mockito.verify(mNotificationAreaInner, atLeast(1)).setVisibility(eq(View.INVISIBLE)); + } + + @Test + public void onOngoingCallEnded_notificationsDisplayedAndOngoingCallChipHidden() { + mFragments.dispatchResume(); + processAllMessages(); + + CollapsedStatusBarFragment fragment = (CollapsedStatusBarFragment) mFragment; + fragment.initNotificationIconArea(mMockNotificiationAreaController); + + ArgumentCaptor ongoingCallListenerCaptor = ArgumentCaptor.forClass( + OngoingCallListener.class); + Mockito.verify(mOngoingCallController).addCallback(ongoingCallListenerCaptor.capture()); + OngoingCallListener listener = Objects.requireNonNull(ongoingCallListenerCaptor.getValue()); + + when(mOngoingCallController.getHasOngoingCall()).thenReturn(false); + listener.onOngoingCallEnded(/* animate= */ false); + + assertEquals(View.GONE, + mFragment.getView().findViewById(R.id.ongoing_call_chip).getVisibility()); + Mockito.verify(mNotificationAreaInner, atLeast(1)).setVisibility(eq(View.VISIBLE)); + } + @Override protected Fragment instantiate(Context context, String className, Bundle arguments) { - return new CollapsedStatusBarFragment(mock(OngoingCallController.class)); + mOngoingCallController = mock(OngoingCallController.class); + return new CollapsedStatusBarFragment(mOngoingCallController); } } 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 new file mode 100644 index 0000000000000..d87d1d1b92db3 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerTest.kt @@ -0,0 +1,153 @@ +/* + * Copyright (C) 2021 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 + +import android.app.Notification +import android.app.PendingIntent +import android.app.Person +import android.service.notification.NotificationListenerService.REASON_USER_STOPPED +import androidx.test.filters.SmallTest +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper +import android.view.LayoutInflater +import android.widget.LinearLayout +import com.android.systemui.R +import com.android.systemui.SysuiTestCase +import com.android.systemui.statusbar.FeatureFlags +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.util.time.FakeSystemClock +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor +import org.mockito.ArgumentMatchers.anyBoolean +import org.mockito.Mock +import org.mockito.Mockito.mock +import org.mockito.Mockito.verify +import org.mockito.Mockito.never +import org.mockito.Mockito.`when` +import org.mockito.MockitoAnnotations + +@SmallTest +@RunWith(AndroidTestingRunner::class) +@TestableLooper.RunWithLooper +class OngoingCallControllerTest : SysuiTestCase() { + + private lateinit var controller: OngoingCallController + private lateinit var notifCollectionListener: NotifCollectionListener + + @Mock private lateinit var mockOngoingCallListener: OngoingCallListener + + private lateinit var chipView: LinearLayout + + @Before + fun setUp() { + allowTestableLooperAsMainThread() + TestableLooper.get(this).runWithLooper { + chipView = LayoutInflater.from(mContext) + .inflate(R.layout.ongoing_call_chip, null) as LinearLayout + } + + MockitoAnnotations.initMocks(this) + val featureFlags = mock(FeatureFlags::class.java) + `when`(featureFlags.isOngoingCallStatusBarChipEnabled).thenReturn(true) + val notificationCollection = mock(CommonNotifCollection::class.java) + + controller = OngoingCallController(notificationCollection, featureFlags, FakeSystemClock()) + controller.init() + controller.addCallback(mockOngoingCallListener) + controller.setChipView(chipView) + + val collectionListenerCaptor = ArgumentCaptor.forClass(NotifCollectionListener::class.java) + verify(notificationCollection).addCollectionListener(collectionListenerCaptor.capture()) + notifCollectionListener = collectionListenerCaptor.value!! + } + + @Test + fun onEntryUpdated_isOngoingCallNotif_listenerNotifiedWithRightCallTime() { + notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) + + verify(mockOngoingCallListener).onOngoingCallStarted(anyBoolean()) + } + + @Test + fun onEntryUpdated_notOngoingCallNotif_listenerNotNotified() { + notifCollectionListener.onEntryUpdated(createNotCallNotifEntry()) + + verify(mockOngoingCallListener, never()).onOngoingCallStarted(anyBoolean()) + } + + @Test + fun onEntryRemoved_ongoingCallNotif_listenerNotified() { + notifCollectionListener.onEntryRemoved(createOngoingCallNotifEntry(), REASON_USER_STOPPED) + + verify(mockOngoingCallListener).onOngoingCallEnded(anyBoolean()) + } + + @Test + fun onEntryRemoved_notOngoingCallNotif_listenerNotNotified() { + notifCollectionListener.onEntryRemoved(createNotCallNotifEntry(), REASON_USER_STOPPED) + + verify(mockOngoingCallListener, never()).onOngoingCallEnded(anyBoolean()) + } + + @Test + fun hasOngoingCall_noOngoingCallNotifSent_returnsFalse() { + assertThat(controller.hasOngoingCall).isFalse() + } + + @Test + fun hasOngoingCall_ongoingCallNotifSentAndChipViewSet_returnsTrue() { + notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) + + assertThat(controller.hasOngoingCall).isTrue() + } + + @Test + fun hasOngoingCall_ongoingCallNotifSentButNoChipView_returnsFalse() { + controller.setChipView(null) + notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) + + assertThat(controller.hasOngoingCall).isFalse() + } + + @Test + fun hasOngoingCall_ongoingCallNotifSentThenRemoved_returnsFalse() { + val ongoingCallNotifEntry = createOngoingCallNotifEntry() + + notifCollectionListener.onEntryUpdated(ongoingCallNotifEntry) + notifCollectionListener.onEntryRemoved(ongoingCallNotifEntry, REASON_USER_STOPPED) + + assertThat(controller.hasOngoingCall).isFalse() + } + + private fun createOngoingCallNotifEntry(): NotificationEntry { + val notificationEntryBuilder = NotificationEntryBuilder() + notificationEntryBuilder.modifyNotification(context).style = ongoingCallStyle + return notificationEntryBuilder.build() + } + + private fun createNotCallNotifEntry() = NotificationEntryBuilder().build() +} + +private val ongoingCallStyle = Notification.CallStyle.forOngoingCall( + Person.Builder().setName("name").build(), + /* hangUpIntent= */ mock(PendingIntent::class.java))