diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml index 7120cc21c8219..52b41a43c63e1 100644 --- a/packages/SystemUI/AndroidManifest.xml +++ b/packages/SystemUI/AndroidManifest.xml @@ -530,6 +530,14 @@ androidprv:alwaysFocusable="true" android:excludeFromRecents="true" /> + + + + + + + + + + + + diff --git a/packages/SystemUI/res/layout/tv_notification_panel.xml b/packages/SystemUI/res/layout/tv_notification_panel.xml new file mode 100644 index 0000000000000..8f00a727b9123 --- /dev/null +++ b/packages/SystemUI/res/layout/tv_notification_panel.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + diff --git a/packages/SystemUI/res/values-television/config.xml b/packages/SystemUI/res/values-television/config.xml index 09ec439b183e0..0c8c5c443c4ad 100644 --- a/packages/SystemUI/res/values-television/config.xml +++ b/packages/SystemUI/res/values-television/config.xml @@ -29,7 +29,8 @@ com.android.systemui.util.NotificationChannels com.android.systemui.volume.VolumeUI com.android.systemui.statusbar.tv.TvStatusBar - com.android.systemui.statusbar.tv.TvNotificationPanel + com.android.systemui.statusbar.tv.notifications.TvNotificationPanel + com.android.systemui.statusbar.tv.notifications.TvNotificationHandler com.android.systemui.statusbar.tv.VpnStatusObserver com.android.systemui.usb.StorageNotification com.android.systemui.power.PowerUI diff --git a/packages/SystemUI/res/values/colors_tv.xml b/packages/SystemUI/res/values/colors_tv.xml index 9b0ae1d61de68..0961f503e7d43 100644 --- a/packages/SystemUI/res/values/colors_tv.xml +++ b/packages/SystemUI/res/values/colors_tv.xml @@ -33,4 +33,7 @@ #A03C4043 #FFF8F9FA #FFDADCE0 + + #383838 + #FFFFFF diff --git a/packages/SystemUI/res/values/dimens_tv.xml b/packages/SystemUI/res/values/dimens_tv.xml new file mode 100644 index 0000000000000..9545bfd088a0c --- /dev/null +++ b/packages/SystemUI/res/values/dimens_tv.xml @@ -0,0 +1,19 @@ + + + + 360dp + \ No newline at end of file diff --git a/packages/SystemUI/res/values/strings_tv.xml b/packages/SystemUI/res/values/strings_tv.xml index 13271d6f7f2e3..b51cb5619f0c5 100644 --- a/packages/SystemUI/res/values/strings_tv.xml +++ b/packages/SystemUI/res/values/strings_tv.xml @@ -26,4 +26,6 @@ Via %1$s + Notifications + No Notifications diff --git a/packages/SystemUI/res/values/styles_tv.xml b/packages/SystemUI/res/values/styles_tv.xml index 0c4fd23ca107c..cb433f3a60090 100644 --- a/packages/SystemUI/res/values/styles_tv.xml +++ b/packages/SystemUI/res/values/styles_tv.xml @@ -23,4 +23,12 @@ false true + + diff --git a/packages/SystemUI/src/com/android/systemui/dagger/DefaultActivityBinder.java b/packages/SystemUI/src/com/android/systemui/dagger/DefaultActivityBinder.java index 13ff3f5c4e6cd..ec4a91c244644 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/DefaultActivityBinder.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/DefaultActivityBinder.java @@ -22,6 +22,7 @@ import com.android.systemui.ForegroundServicesDialog; import com.android.systemui.keyguard.WorkLockActivity; import com.android.systemui.screenrecord.ScreenRecordDialog; import com.android.systemui.settings.brightness.BrightnessDialog; +import com.android.systemui.statusbar.tv.notifications.TvNotificationPanelActivity; import com.android.systemui.tuner.TunerActivity; import com.android.systemui.usb.UsbDebuggingActivity; import com.android.systemui.usb.UsbDebuggingSecondaryUserActivity; @@ -85,4 +86,10 @@ public abstract class DefaultActivityBinder { @IntoMap @ClassKey(CreateUserActivity.class) public abstract Activity bindCreateUserActivity(CreateUserActivity activity); + + /** Inject into TvNotificationPanelActivity. */ + @Binds + @IntoMap + @ClassKey(TvNotificationPanelActivity.class) + public abstract Activity bindTvNotificationPanelActivity(TvNotificationPanelActivity activity); } diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIBinder.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIBinder.java index c0013d8cb981c..9f6c19bdf06fe 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIBinder.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIBinder.java @@ -34,8 +34,8 @@ import com.android.systemui.shortcut.ShortcutKeyDispatcher; import com.android.systemui.statusbar.dagger.StatusBarModule; import com.android.systemui.statusbar.notification.InstantAppNotifier; import com.android.systemui.statusbar.phone.StatusBar; -import com.android.systemui.statusbar.tv.TvNotificationPanel; import com.android.systemui.statusbar.tv.TvStatusBar; +import com.android.systemui.statusbar.tv.notifications.TvNotificationPanel; import com.android.systemui.theme.ThemeOverlayController; import com.android.systemui.toast.ToastUI; import com.android.systemui.util.leak.GarbageMonitor; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/tv/TvStatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/tv/TvStatusBar.java index 2795857383e8f..bdf2b0c24ba54 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/tv/TvStatusBar.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/tv/TvStatusBar.java @@ -71,7 +71,6 @@ public class TvStatusBar extends SystemUI implements CommandQueue.Callbacks { // Creating AudioRecordingDisclosureBar and just letting it run new AudioRecordingDisclosureBar(mContext); } - } @Override diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/tv/notifications/TvNotificationAdapter.java b/packages/SystemUI/src/com/android/systemui/statusbar/tv/notifications/TvNotificationAdapter.java new file mode 100644 index 0000000000000..3b1a4db067a35 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/tv/notifications/TvNotificationAdapter.java @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.tv.notifications; + +import android.app.Notification; +import android.app.PendingIntent; +import android.service.notification.StatusBarNotification; +import android.util.Log; +import android.util.SparseArray; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.android.systemui.R; + +/** + * Adapter for the VerticalGridView of the TvNotificationsPanelView. + */ +public class TvNotificationAdapter extends RecyclerView.Adapter { + private static final String TAG = "TvNotificationAdapter"; + private SparseArray mNotifications; + + public TvNotificationAdapter() { + setHasStableIds(true); + } + + @NonNull + @Override + public TvNotificationViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.tv_notification_item, + parent, false); + return new TvNotificationViewHolder(view); + } + + @Override + public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) { + if (mNotifications == null) { + Log.e(TAG, "Could not bind view holder because the notification is missing"); + return; + } + + TvNotificationViewHolder holder = (TvNotificationViewHolder) viewHolder; + Notification notification = mNotifications.valueAt(position).getNotification(); + holder.mTitle.setText(notification.extras.getString(Notification.EXTRA_TITLE)); + holder.mDetails.setText(notification.extras.getString(Notification.EXTRA_TEXT)); + holder.mPendingIntent = notification.contentIntent; + } + + @Override + public int getItemCount() { + return mNotifications == null ? 0 : mNotifications.size(); + } + + @Override + public long getItemId(int position) { + // the item id is the notification id + return mNotifications.keyAt(position); + } + + /** + * Updates the notifications and calls notifyDataSetChanged(). + */ + public void setNotifications(SparseArray notifications) { + this.mNotifications = notifications; + notifyDataSetChanged(); + } + + private static class TvNotificationViewHolder extends RecyclerView.ViewHolder implements + View.OnClickListener { + final TextView mTitle; + final TextView mDetails; + PendingIntent mPendingIntent; + + protected TvNotificationViewHolder(View itemView) { + super(itemView); + mTitle = itemView.findViewById(R.id.tv_notification_title); + mDetails = itemView.findViewById(R.id.tv_notification_details); + itemView.setOnClickListener(this); + } + + @Override + public void onClick(View v) { + try { + if (mPendingIntent != null) { + mPendingIntent.send(); + } + } catch (PendingIntent.CanceledException e) { + Log.d(TAG, "Pending intent canceled for : " + mPendingIntent); + } + } + } + +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/tv/notifications/TvNotificationHandler.java b/packages/SystemUI/src/com/android/systemui/statusbar/tv/notifications/TvNotificationHandler.java new file mode 100644 index 0000000000000..d985803c2b39e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/tv/notifications/TvNotificationHandler.java @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.tv.notifications; + +import android.annotation.Nullable; +import android.app.Notification; +import android.content.Context; +import android.service.notification.NotificationListenerService; +import android.service.notification.StatusBarNotification; +import android.util.Log; +import android.util.SparseArray; + +import com.android.systemui.SystemUI; +import com.android.systemui.statusbar.NotificationListener; + +import javax.inject.Inject; + +/** + * Keeps track of the notifications on TV. + */ +public class TvNotificationHandler extends SystemUI implements + NotificationListener.NotificationHandler { + private static final String TAG = "TvNotificationHandler"; + private final NotificationListener mNotificationListener; + private final SparseArray mNotifications = new SparseArray<>(); + @Nullable + private Listener mUpdateListener; + + @Inject + public TvNotificationHandler(Context context, NotificationListener notificationListener) { + super(context); + mNotificationListener = notificationListener; + } + + public SparseArray getCurrentNotifications() { + return mNotifications; + } + + public void setTvNotificationListener(Listener listener) { + mUpdateListener = listener; + } + + @Override + public void start() { + mNotificationListener.addNotificationHandler(this); + mNotificationListener.registerAsSystemService(); + } + + @Override + public void onNotificationPosted(StatusBarNotification sbn, + NotificationListenerService.RankingMap rankingMap) { + if (!new Notification.TvExtender(sbn.getNotification()).isAvailableOnTv()) { + Log.v(TAG, "Notification not added because it isn't relevant for tv"); + return; + } + + mNotifications.put(sbn.getId(), sbn); + if (mUpdateListener != null) { + mUpdateListener.notificationsUpdated(mNotifications); + } + Log.d(TAG, "Notification added"); + } + + @Override + public void onNotificationRemoved(StatusBarNotification sbn, + NotificationListenerService.RankingMap rankingMap) { + + if (mNotifications.contains(sbn.getId())) { + mNotifications.remove(sbn.getId()); + Log.d(TAG, "Notification removed"); + + if (mUpdateListener != null) { + mUpdateListener.notificationsUpdated(mNotifications); + } + } + } + + @Override + public void onNotificationRemoved(StatusBarNotification sbn, + NotificationListenerService.RankingMap rankingMap, int reason) { + onNotificationRemoved(sbn, rankingMap); + } + + @Override + public void onNotificationRankingUpdate(NotificationListenerService.RankingMap rankingMap) { + // noop + } + + @Override + public void onNotificationsInitialized() { + // noop + } + + /** + * Get notified when the notifications are updated. + */ + interface Listener { + void notificationsUpdated(SparseArray sbns); + } + +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/tv/TvNotificationPanel.java b/packages/SystemUI/src/com/android/systemui/statusbar/tv/notifications/TvNotificationPanel.java similarity index 86% rename from packages/SystemUI/src/com/android/systemui/statusbar/tv/TvNotificationPanel.java rename to packages/SystemUI/src/com/android/systemui/statusbar/tv/notifications/TvNotificationPanel.java index 0bd36240a3664..477424c55a737 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/tv/TvNotificationPanel.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/tv/notifications/TvNotificationPanel.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.statusbar.tv; +package com.android.systemui.statusbar.tv.notifications; import android.Manifest; import android.app.NotificationManager; @@ -59,9 +59,8 @@ public class TvNotificationPanel extends SystemUI implements CommandQueue.Callba startNotificationHandlerActivity( new Intent(NotificationManager.ACTION_TOGGLE_NOTIFICATION_HANDLER_PANEL)); } else { - Log.w(TAG, - "Not toggling notification panel: config_notificationHandlerPackage is " - + "empty"); + openInternalNotificationPanel( + NotificationManager.ACTION_TOGGLE_NOTIFICATION_HANDLER_PANEL); } } @@ -71,9 +70,8 @@ public class TvNotificationPanel extends SystemUI implements CommandQueue.Callba startNotificationHandlerActivity( new Intent(NotificationManager.ACTION_OPEN_NOTIFICATION_HANDLER_PANEL)); } else { - Log.w(TAG, - "Not expanding notification panel: config_notificationHandlerPackage is " - + "empty"); + openInternalNotificationPanel( + NotificationManager.ACTION_OPEN_NOTIFICATION_HANDLER_PANEL); } } @@ -86,11 +84,17 @@ public class TvNotificationPanel extends SystemUI implements CommandQueue.Callba closeNotificationIntent.setPackage(mNotificationHandlerPackage); mContext.sendBroadcastAsUser(closeNotificationIntent, UserHandle.CURRENT); } else { - Log.w(TAG, - "Not closing notification panel: config_notificationHandlerPackage is empty"); + openInternalNotificationPanel( + NotificationManager.ACTION_CLOSE_NOTIFICATION_HANDLER_PANEL); } } + private void openInternalNotificationPanel(String action) { + Intent intent = new Intent(mContext, TvNotificationPanelActivity.class); + intent.setAction(action); + mContext.startActivityAsUser(intent, UserHandle.SYSTEM); + } + /** * Starts the activity intent if all of the following are true *
    diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/tv/notifications/TvNotificationPanelActivity.java b/packages/SystemUI/src/com/android/systemui/statusbar/tv/notifications/TvNotificationPanelActivity.java new file mode 100644 index 0000000000000..30f401b91d25a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/tv/notifications/TvNotificationPanelActivity.java @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.tv.notifications; + +import android.annotation.NonNull; +import android.app.Activity; +import android.app.NotificationManager; +import android.content.Intent; +import android.os.Bundle; +import android.service.notification.StatusBarNotification; +import android.util.SparseArray; +import android.view.View; + +import androidx.leanback.widget.VerticalGridView; + +import com.android.systemui.R; + +import javax.inject.Inject; + +/** + * This Activity shows a notification panel for tv. It is used if no other app (e.g. a launcher) can + * be found to show the notifications. + */ +public class TvNotificationPanelActivity extends Activity implements + TvNotificationHandler.Listener { + private final TvNotificationHandler mTvNotificationHandler; + private TvNotificationAdapter mTvNotificationAdapter; + private VerticalGridView mNotificationListView; + private View mNotificationPlaceholder; + private boolean mPanelAlreadyOpen = false; + + @Inject + public TvNotificationPanelActivity(TvNotificationHandler tvNotificationHandler) { + super(); + mTvNotificationHandler = tvNotificationHandler; + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + if (maybeClosePanel(getIntent())) { + return; + } + mPanelAlreadyOpen = true; + + setContentView(R.layout.tv_notification_panel); + + mNotificationPlaceholder = findViewById(R.id.no_tv_notifications); + mTvNotificationAdapter = new TvNotificationAdapter(); + + mNotificationListView = findViewById(R.id.notifications_list); + mNotificationListView.setAdapter(mTvNotificationAdapter); + mNotificationListView.setColumnWidth(R.dimen.tv_notification_panel_width); + + mTvNotificationHandler.setTvNotificationListener(this); + notificationsUpdated(mTvNotificationHandler.getCurrentNotifications()); + } + + @Override + public void notificationsUpdated(@NonNull SparseArray notificationList) { + mTvNotificationAdapter.setNotifications(notificationList); + + boolean noNotifications = notificationList.size() == 0; + mNotificationListView.setVisibility(noNotifications ? View.GONE : View.VISIBLE); + mNotificationPlaceholder.setVisibility(noNotifications ? View.VISIBLE : View.GONE); + } + + @Override + public void onNewIntent(Intent intent) { + super.onNewIntent(intent); + maybeClosePanel(intent); + } + + /** + * Handles intents from onCreate and onNewIntent. + * + * @return true if the panel is being closed, false if it is being opened + */ + private boolean maybeClosePanel(Intent intent) { + if (NotificationManager.ACTION_CLOSE_NOTIFICATION_HANDLER_PANEL.equals(intent.getAction()) + || (mPanelAlreadyOpen + && NotificationManager.ACTION_TOGGLE_NOTIFICATION_HANDLER_PANEL.equals( + intent.getAction()))) { + finish(); + return true; + } + return false; + } + + @Override + public void onDestroy() { + super.onDestroy(); + mTvNotificationHandler.setTvNotificationListener(null); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/tv/TvSystemUIBinder.java b/packages/SystemUI/src/com/android/systemui/tv/TvSystemUIBinder.java index 2c3ea4f452bb7..353333f714d49 100644 --- a/packages/SystemUI/src/com/android/systemui/tv/TvSystemUIBinder.java +++ b/packages/SystemUI/src/com/android/systemui/tv/TvSystemUIBinder.java @@ -16,14 +16,22 @@ package com.android.systemui.tv; +import com.android.systemui.SystemUI; import com.android.systemui.dagger.GlobalRootComponent; -import com.android.systemui.wmshell.TvPipModule; +import com.android.systemui.statusbar.tv.notifications.TvNotificationHandler; import dagger.Binds; import dagger.Module; +import dagger.multibindings.ClassKey; +import dagger.multibindings.IntoMap; @Module interface TvSystemUIBinder { @Binds GlobalRootComponent bindGlobalRootComponent(TvGlobalRootComponent globalRootComponent); + + @Binds + @IntoMap + @ClassKey(TvNotificationHandler.class) + SystemUI bindTvNotificationHandler(TvNotificationHandler systemui); } diff --git a/packages/SystemUI/src/com/android/systemui/tv/TvSystemUIModule.java b/packages/SystemUI/src/com/android/systemui/tv/TvSystemUIModule.java index 8ffc7cf568ffe..56a4c203e8402 100644 --- a/packages/SystemUI/src/com/android/systemui/tv/TvSystemUIModule.java +++ b/packages/SystemUI/src/com/android/systemui/tv/TvSystemUIModule.java @@ -43,6 +43,7 @@ import com.android.systemui.qs.tileimpl.QSFactoryImpl; import com.android.systemui.recents.Recents; import com.android.systemui.recents.RecentsImplementation; import com.android.systemui.statusbar.CommandQueue; +import com.android.systemui.statusbar.NotificationListener; import com.android.systemui.statusbar.NotificationLockscreenUserManager; import com.android.systemui.statusbar.NotificationLockscreenUserManagerImpl; import com.android.systemui.statusbar.NotificationShadeWindowController; @@ -62,6 +63,7 @@ import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.DeviceProvisionedController; import com.android.systemui.statusbar.policy.DeviceProvisionedControllerImpl; import com.android.systemui.statusbar.policy.HeadsUpManager; +import com.android.systemui.statusbar.tv.notifications.TvNotificationHandler; import javax.inject.Named; @@ -164,4 +166,11 @@ public abstract class TvSystemUIModule { @Binds abstract DozeHost provideDozeHost(DozeServiceHost dozeServiceHost); + + @Provides + @SysUISingleton + static TvNotificationHandler provideTvNotificationHandler(Context context, + NotificationListener notificationListener) { + return new TvNotificationHandler(context, notificationListener); + } }