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))