diff --git a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java index 79925bad3cc78..9705551d2e33a 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java @@ -65,6 +65,7 @@ import android.view.accessibility.AccessibilityManager; import android.view.inputmethod.InputMethodManager; import com.android.internal.app.IBatteryStats; +import com.android.internal.appwidget.IAppWidgetService; import com.android.internal.statusbar.IStatusBarService; import com.android.internal.util.LatencyTracker; import com.android.systemui.dagger.qualifiers.DisplayId; @@ -188,6 +189,13 @@ public class FrameworkServicesModule { return context.getSystemService(InputMethodManager.class); } + @Provides + @Singleton + static IAppWidgetService provideIAppWidgetService() { + return IAppWidgetService.Stub.asInterface( + ServiceManager.getService(Context.APPWIDGET_SERVICE)); + } + @Provides @Singleton static IPackageManager provideIPackageManager() { diff --git a/packages/SystemUI/src/com/android/systemui/people/widget/PeopleSpaceWidgetManager.java b/packages/SystemUI/src/com/android/systemui/people/widget/PeopleSpaceWidgetManager.java new file mode 100644 index 0000000000000..8415368fe931c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/people/widget/PeopleSpaceWidgetManager.java @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.people.widget; + +import android.content.ComponentName; +import android.content.Context; +import android.os.ServiceManager; +import android.service.notification.NotificationListenerService; +import android.service.notification.StatusBarNotification; +import android.util.Log; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.appwidget.IAppWidgetService; +import com.android.systemui.R; +import com.android.systemui.people.PeopleSpaceUtils; +import com.android.systemui.statusbar.NotificationListener; +import com.android.systemui.statusbar.NotificationListener.NotificationHandler; + +import javax.inject.Inject; +import javax.inject.Singleton; + +/** Manager for People Space widget. */ +@Singleton +public class PeopleSpaceWidgetManager { + private static final String TAG = "PeopleSpaceWidgetMgr"; + private static final boolean DEBUG = PeopleSpaceUtils.DEBUG; + + private final Context mContext; + private IAppWidgetService mAppWidgetManager; + + @Inject + public PeopleSpaceWidgetManager(Context context, IAppWidgetService appWidgetService) { + if (DEBUG) Log.d(TAG, "constructor"); + mContext = context; + mAppWidgetManager = appWidgetService; + } + + /** Constructor used for testing. */ + @VisibleForTesting + protected PeopleSpaceWidgetManager(Context context) { + if (DEBUG) Log.d(TAG, "constructor"); + mContext = context; + mAppWidgetManager = IAppWidgetService.Stub.asInterface( + ServiceManager.getService(Context.APPWIDGET_SERVICE)); + } + + /** AppWidgetManager setter used for testing. */ + @VisibleForTesting + protected void setAppWidgetManager(IAppWidgetService appWidgetService) { + mAppWidgetManager = appWidgetService; + } + + /** Updates People Space widgets. */ + public void updateWidgets() { + try { + if (DEBUG) Log.d(TAG, "updateWidgets called"); + int[] widgetIds = mAppWidgetManager.getAppWidgetIds( + new ComponentName(mContext, PeopleSpaceWidgetProvider.class) + ); + + if (widgetIds.length == 0) { + if (DEBUG) Log.d(TAG, "no widgets to update"); + return; + } + + if (DEBUG) Log.d(TAG, "updating " + widgetIds.length + " widgets"); + mAppWidgetManager + .notifyAppWidgetViewDataChanged(mContext.getOpPackageName(), widgetIds, + R.id.widget_list_view); + } catch (Exception e) { + Log.e(TAG, "Exception: " + e); + } + } + + /** + * Attaches the manager to the pipeline, making it ready to receive events. Should only be + * called once. + */ + public void attach(NotificationListener listenerService) { + if (DEBUG) Log.d(TAG, "attach"); + listenerService.addNotificationHandler(mListener); + } + + private final NotificationHandler mListener = new NotificationHandler() { + @Override + public void onNotificationPosted( + StatusBarNotification sbn, NotificationListenerService.RankingMap rankingMap) { + if (DEBUG) Log.d(TAG, "onNotificationPosted"); + updateWidgets(); + } + + @Override + public void onNotificationRemoved( + StatusBarNotification sbn, + NotificationListenerService.RankingMap rankingMap + ) { + if (DEBUG) Log.d(TAG, "onNotificationRemoved"); + updateWidgets(); + } + + @Override + public void onNotificationRemoved( + StatusBarNotification sbn, + NotificationListenerService.RankingMap rankingMap, + int reason) { + if (DEBUG) Log.d(TAG, "onNotificationRemoved with reason " + reason); + updateWidgets(); + } + + @Override + public void onNotificationRankingUpdate( + NotificationListenerService.RankingMap rankingMap) { } + + @Override + public void onNotificationsInitialized() { + if (DEBUG) Log.d(TAG, "onNotificationsInitialized"); + updateWidgets(); + } + }; + +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/init/NotifPipelineInitializer.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/init/NotifPipelineInitializer.java index db49e4476a991..4fde1187477c6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/init/NotifPipelineInitializer.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/init/NotifPipelineInitializer.java @@ -21,6 +21,7 @@ import android.util.Log; import com.android.systemui.Dumpable; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dump.DumpManager; +import com.android.systemui.people.widget.PeopleSpaceWidgetManager; import com.android.systemui.statusbar.FeatureFlags; import com.android.systemui.statusbar.NotificationListener; import com.android.systemui.statusbar.notification.collection.NotifCollection; @@ -49,6 +50,7 @@ public class NotifPipelineInitializer implements Dumpable { private final ShadeListBuilder mListBuilder; private final NotifCoordinators mNotifPluggableCoordinators; private final NotifInflaterImpl mNotifInflater; + private final PeopleSpaceWidgetManager mPeopleSpaceWidgetManager; private final DumpManager mDumpManager; private final ShadeViewManagerFactory mShadeViewManagerFactory; private final FeatureFlags mFeatureFlags; @@ -62,6 +64,7 @@ public class NotifPipelineInitializer implements Dumpable { ShadeListBuilder listBuilder, NotifCoordinators notifCoordinators, NotifInflaterImpl notifInflater, + PeopleSpaceWidgetManager peopleSpaceWidgetManager, DumpManager dumpManager, ShadeViewManagerFactory shadeViewManagerFactory, FeatureFlags featureFlags) { @@ -72,6 +75,7 @@ public class NotifPipelineInitializer implements Dumpable { mNotifPluggableCoordinators = notifCoordinators; mDumpManager = dumpManager; mNotifInflater = notifInflater; + mPeopleSpaceWidgetManager = peopleSpaceWidgetManager; mShadeViewManagerFactory = shadeViewManagerFactory; mFeatureFlags = featureFlags; } @@ -99,6 +103,7 @@ public class NotifPipelineInitializer implements Dumpable { mListBuilder.attach(mNotifCollection); mNotifCollection.attach(mGroupCoalescer); mGroupCoalescer.attach(notificationService); + mPeopleSpaceWidgetManager.attach(notificationService); Log.d(TAG, "Notif pipeline initialized"); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/people/widget/PeopleSpaceWidgetManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/people/widget/PeopleSpaceWidgetManagerTest.java new file mode 100644 index 0000000000000..8ceebebe9a5b1 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/people/widget/PeopleSpaceWidgetManagerTest.java @@ -0,0 +1,152 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.people.widget; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import static java.util.Objects.requireNonNull; + +import android.content.Context; +import android.os.RemoteException; +import android.testing.AndroidTestingRunner; + +import androidx.test.filters.SmallTest; + +import com.android.internal.appwidget.IAppWidgetService; +import com.android.systemui.SysuiTestCase; +import com.android.systemui.statusbar.NotificationListener; +import com.android.systemui.statusbar.NotificationListener.NotificationHandler; +import com.android.systemui.statusbar.notification.collection.NoManSimulator; +import com.android.systemui.statusbar.notification.collection.NoManSimulator.NotifEvent; +import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder; +import com.android.systemui.util.time.FakeSystemClock; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@SmallTest +@RunWith(AndroidTestingRunner.class) +public class PeopleSpaceWidgetManagerTest extends SysuiTestCase { + private static final long MIN_LINGER_DURATION = 5; + + private static final String TEST_PACKAGE_A = "com.test.package_a"; + private static final String TEST_PACKAGE_B = "com.test.package_b"; + + private PeopleSpaceWidgetManager mManager; + + @Mock private NotificationListener mListenerService; + @Mock private IAppWidgetService mIAppWidgetService; + @Mock private Context mContext; + + @Captor private ArgumentCaptor mListenerCaptor; + + private final NoManSimulator mNoMan = new NoManSimulator(); + private final FakeSystemClock mClock = new FakeSystemClock(); + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + mManager = + new PeopleSpaceWidgetManager(mContext); + mManager.setAppWidgetManager(mIAppWidgetService); + mManager.attach(mListenerService); + + verify(mListenerService).addNotificationHandler(mListenerCaptor.capture()); + NotificationHandler serviceListener = requireNonNull(mListenerCaptor.getValue()); + mNoMan.addListener(serviceListener); + } + + + @Test + public void testDoNotNotifyAppWidgetIfNoWidgets() throws RemoteException { + int[] widgetIdsArray = {}; + when(mIAppWidgetService.getAppWidgetIds(any())).thenReturn(widgetIdsArray); + + NotifEvent notif1 = mNoMan.postNotif( + new NotificationEntryBuilder() + .setId(0) + .setPkg(TEST_PACKAGE_A)); + mClock.advanceTime(MIN_LINGER_DURATION); + + verify(mIAppWidgetService, times(1)).getAppWidgetIds(any()); + verify(mIAppWidgetService, never()).notifyAppWidgetViewDataChanged(any(), any(), anyInt()); + + } + + @Test + public void testNotifyAppWidgetIfWidgets() throws RemoteException { + int[] widgetIdsArray = {1}; + when(mIAppWidgetService.getAppWidgetIds(any())).thenReturn(widgetIdsArray); + + NotifEvent notif1 = mNoMan.postNotif( + new NotificationEntryBuilder() + .setId(0) + .setPkg(TEST_PACKAGE_A)); + mClock.advanceTime(MIN_LINGER_DURATION); + + verify(mIAppWidgetService, times(1)).getAppWidgetIds(any()); + verify(mIAppWidgetService, times(1)) + .notifyAppWidgetViewDataChanged(any(), eq(widgetIdsArray), anyInt()); + + } + + @Test + public void testNotifyAppWidgetTwiceIfTwoNotifications() throws RemoteException { + int[] widgetIdsArray = {1, 2}; + when(mIAppWidgetService.getAppWidgetIds(any())).thenReturn(widgetIdsArray); + + NotifEvent notif1 = mNoMan.postNotif(new NotificationEntryBuilder() + .setPkg(TEST_PACKAGE_A) + .setId(1)); + mClock.advanceTime(4); + NotifEvent notif2 = mNoMan.postNotif(new NotificationEntryBuilder() + .setPkg(TEST_PACKAGE_B) + .setId(2)); + + verify(mIAppWidgetService, times(2)).getAppWidgetIds(any()); + verify(mIAppWidgetService, times(2)) + .notifyAppWidgetViewDataChanged(any(), eq(widgetIdsArray), anyInt()); + } + + @Test + public void testNotifyAppWidgetTwiceIfNotificationPostedAndRemoved() throws RemoteException { + int[] widgetIdsArray = {1, 2}; + when(mIAppWidgetService.getAppWidgetIds(any())).thenReturn(widgetIdsArray); + + NotifEvent notif1 = mNoMan.postNotif(new NotificationEntryBuilder() + .setPkg(TEST_PACKAGE_A) + .setId(1)); + mClock.advanceTime(4); + NotifEvent notif1b = mNoMan.retractNotif(notif1.sbn, 0); + + verify(mIAppWidgetService, times(2)).getAppWidgetIds(any()); + verify(mIAppWidgetService, times(2)) + .notifyAppWidgetViewDataChanged(any(), eq(widgetIdsArray), anyInt()); + } +}