1 /*
<lambda>null2  * Copyright (C) 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.systemui.statusbar.notification.stack
18 
19 import android.content.Context
20 import android.service.notification.NotificationListenerService.REASON_APP_CANCEL
21 import android.service.notification.NotificationListenerService.REASON_APP_CANCEL_ALL
22 import android.service.notification.NotificationListenerService.REASON_CANCEL
23 import android.service.notification.NotificationListenerService.REASON_CANCEL_ALL
24 import android.service.notification.NotificationListenerService.REASON_CLICK
25 import android.service.notification.NotificationListenerService.REASON_GROUP_SUMMARY_CANCELED
26 import android.view.LayoutInflater
27 import android.view.View
28 import android.widget.LinearLayout
29 
30 import com.android.systemui.statusbar.notification.collection.NotificationEntry
31 import com.android.systemui.R
32 import com.android.systemui.statusbar.notification.ForegroundServiceDismissalFeatureController
33 import com.android.systemui.statusbar.notification.NotificationEntryListener
34 import com.android.systemui.statusbar.notification.NotificationEntryManager
35 import com.android.systemui.statusbar.notification.row.DungeonRow
36 import com.android.systemui.util.Assert
37 
38 import javax.inject.Inject
39 import javax.inject.Singleton
40 
41 /**
42  * Controller for the bottom area of NotificationStackScrollLayout. It owns swiped-away foreground
43  * service notifications and can reinstantiate them when requested.
44  */
45 @Singleton
46 class ForegroundServiceSectionController @Inject constructor(
47     val entryManager: NotificationEntryManager,
48     val featureController: ForegroundServiceDismissalFeatureController
49 ) {
50     private val TAG = "FgsSectionController"
51     private var context: Context? = null
52 
53     private val entries = mutableSetOf<NotificationEntry>()
54 
55     private var entriesView: View? = null
56 
57     init {
58         if (featureController.isForegroundServiceDismissalEnabled()) {
59             entryManager.addNotificationRemoveInterceptor(this::shouldInterceptRemoval)
60 
61             entryManager.addNotificationEntryListener(object : NotificationEntryListener {
62                 override fun onPostEntryUpdated(entry: NotificationEntry) {
63                     if (entries.contains(entry)) {
64                         removeEntry(entry)
65                         addEntry(entry)
66                         update()
67                     }
68                 }
69             })
70         }
71     }
72 
73     private fun shouldInterceptRemoval(
74         key: String,
75         entry: NotificationEntry?,
76         reason: Int
77     ): Boolean {
78         Assert.isMainThread()
79         val isClearAll = reason == REASON_CANCEL_ALL
80         val isUserDismiss = reason == REASON_CANCEL || reason == REASON_CLICK
81         val isAppCancel = reason == REASON_APP_CANCEL || reason == REASON_APP_CANCEL_ALL
82         val isSummaryCancel = reason == REASON_GROUP_SUMMARY_CANCELED
83 
84         if (entry == null) return false
85 
86         // We only want to retain notifications that the user dismissed
87         // TODO: centralize the entry.isClearable logic and this so that it's clear when a notif is
88         // clearable
89         if (isUserDismiss && !entry.sbn.isClearable) {
90             if (!hasEntry(entry)) {
91                 addEntry(entry)
92                 update()
93             }
94             // TODO: This isn't ideal. Slightly better would at least be to have NEM update the
95             // notif list when an entry gets intercepted
96             entryManager.updateNotifications(
97                     "FgsSectionController.onNotificationRemoveRequested")
98             return true
99         } else if ((isClearAll || isSummaryCancel) && !entry.sbn.isClearable) {
100             // In the case where a FGS notification is part of a group that is cleared or a clear
101             // all, we actually want to stop its removal but also not put it into the dungeon
102             return true
103         } else if (hasEntry(entry)) {
104             removeEntry(entry)
105             update()
106             return false
107         }
108 
109         return false
110     }
111 
112     private fun removeEntry(entry: NotificationEntry) {
113         Assert.isMainThread()
114         entries.remove(entry)
115     }
116 
117     private fun addEntry(entry: NotificationEntry) {
118         Assert.isMainThread()
119         entries.add(entry)
120     }
121 
122     fun hasEntry(entry: NotificationEntry): Boolean {
123         Assert.isMainThread()
124         return entries.contains(entry)
125     }
126 
127     fun initialize(context: Context) {
128         this.context = context
129     }
130 
131     fun createView(li: LayoutInflater): View {
132         entriesView = li.inflate(R.layout.foreground_service_dungeon, null)
133         // Start out gone
134         entriesView!!.visibility = View.GONE
135         return entriesView!!
136     }
137 
138     private fun update() {
139         Assert.isMainThread()
140         if (entriesView == null) {
141             throw IllegalStateException("ForegroundServiceSectionController is trying to show " +
142                     "dismissed fgs notifications without having been initialized!")
143         }
144 
145         // TODO: these views should be recycled and not inflating on the main thread
146         (entriesView!!.findViewById(R.id.entry_list) as LinearLayout).apply {
147             removeAllViews()
148             entries.sortedBy { it.ranking.rank }.forEach { entry ->
149                 val child = LayoutInflater.from(context)
150                         .inflate(R.layout.foreground_service_dungeon_row, null) as DungeonRow
151 
152                 child.entry = entry
153                 child.setOnClickListener {
154                     removeEntry(child.entry!!)
155                     update()
156                     entry.row.unDismiss()
157                     entry.row.resetTranslation()
158                     entryManager.updateNotifications("ForegroundServiceSectionController.onClick")
159                 }
160 
161                 addView(child)
162             }
163         }
164 
165         if (entries.isEmpty()) {
166             entriesView?.visibility = View.GONE
167         } else {
168             entriesView?.visibility = View.VISIBLE
169         }
170     }
171 }
172