1 /* <lambda>null2 * Copyright (C) 2019 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.row 18 19 import android.app.Dialog 20 import android.app.INotificationManager 21 import android.app.NotificationChannel 22 import android.app.NotificationChannel.DEFAULT_CHANNEL_ID 23 import android.app.NotificationChannelGroup 24 import android.app.NotificationManager.IMPORTANCE_NONE 25 import android.content.Context 26 import android.graphics.Color 27 import android.graphics.PixelFormat 28 import android.graphics.drawable.Drawable 29 import android.graphics.drawable.ColorDrawable 30 import android.util.Log 31 import android.view.Gravity 32 import android.view.View 33 import android.view.ViewGroup.LayoutParams.MATCH_PARENT 34 import android.view.ViewGroup.LayoutParams.WRAP_CONTENT 35 import android.view.Window 36 import android.view.WindowManager 37 import android.widget.TextView 38 import com.android.internal.annotations.VisibleForTesting 39 40 import com.android.systemui.R 41 42 import javax.inject.Inject 43 import javax.inject.Singleton 44 45 const val TAG = "ChannelDialogController" 46 47 /** 48 * ChannelEditorDialogController is the controller for the dialog half-shelf 49 * that allows users to quickly turn off channels. It is launched from the NotificationInfo 50 * guts view and displays controls for toggling app notifications as well as up to 4 channels 51 * from that app like so: 52 * 53 * APP TOGGLE <on/off> 54 * - Channel from which we launched <on/off> 55 * - <on/off> 56 * - the next 3 channels sorted alphabetically for that app <on/off> 57 * - <on/off> 58 */ 59 @Singleton 60 class ChannelEditorDialogController @Inject constructor( 61 c: Context, 62 private val noMan: INotificationManager 63 ) { 64 val context: Context = c.applicationContext 65 66 lateinit var dialog: Dialog 67 68 private var appIcon: Drawable? = null 69 private var appUid: Int? = null 70 private var packageName: String? = null 71 private var appName: String? = null 72 private var onSettingsClickListener: NotificationInfo.OnSettingsClickListener? = null 73 74 // Caller should set this if they care about when we dismiss 75 var onFinishListener: OnChannelEditorDialogFinishedListener? = null 76 77 // Channels handed to us from NotificationInfo 78 @VisibleForTesting 79 internal val providedChannels = mutableListOf<NotificationChannel>() 80 81 // Map from NotificationChannel to importance 82 private val edits = mutableMapOf<NotificationChannel, Int>() 83 var appNotificationsEnabled = true 84 85 // Keep a mapping of NotificationChannel.getGroup() to the actual group name for display 86 @VisibleForTesting 87 internal val groupNameLookup = hashMapOf<String, CharSequence>() 88 private val channelGroupList = mutableListOf<NotificationChannelGroup>() 89 90 /** 91 * Give the controller all of the information it needs to present the dialog 92 * for a given app. Does a bunch of querying of NoMan, but won't present anything yet 93 */ 94 fun prepareDialogForApp( 95 appName: String, 96 packageName: String, 97 uid: Int, 98 channels: Set<NotificationChannel>, 99 appIcon: Drawable, 100 onSettingsClickListener: NotificationInfo.OnSettingsClickListener? 101 ) { 102 this.appName = appName 103 this.packageName = packageName 104 this.appUid = uid 105 this.appIcon = appIcon 106 this.appNotificationsEnabled = checkAreAppNotificationsOn() 107 this.onSettingsClickListener = onSettingsClickListener 108 109 channelGroupList.clear() 110 channelGroupList.addAll(fetchNotificationChannelGroups()) 111 buildGroupNameLookup() 112 padToFourChannels(channels) 113 } 114 115 private fun buildGroupNameLookup() { 116 channelGroupList.forEach { group -> 117 if (group.id != null) { 118 groupNameLookup[group.id] = group.name 119 } 120 } 121 } 122 123 private fun padToFourChannels(channels: Set<NotificationChannel>) { 124 providedChannels.clear() 125 // First, add all of the given channels 126 providedChannels.addAll(channels.asSequence().take(4)) 127 128 // Then pad to 4 if we haven't been given that many 129 providedChannels.addAll(getDisplayableChannels(channelGroupList.asSequence()) 130 .filterNot { providedChannels.contains(it) } 131 .distinct() 132 .take(4 - providedChannels.size)) 133 134 // If we only got one channel and it has the default miscellaneous tag, then we actually 135 // are looking at an app with a targetSdk <= O, and it doesn't make much sense to show the 136 // channel 137 if (providedChannels.size == 1 && DEFAULT_CHANNEL_ID == providedChannels[0].id) { 138 providedChannels.clear() 139 } 140 } 141 142 private fun getDisplayableChannels( 143 groupList: Sequence<NotificationChannelGroup> 144 ): Sequence<NotificationChannel> { 145 146 val channels = groupList 147 .flatMap { group -> 148 group.channels.asSequence().filterNot { channel -> 149 channel.isImportanceLockedByOEM 150 || channel.importance == IMPORTANCE_NONE 151 || channel.isImportanceLockedByCriticalDeviceFunction 152 } 153 } 154 155 // TODO: sort these by avgSentWeekly, but for now let's just do alphabetical (why not) 156 return channels.sortedWith(compareBy { it.name?.toString() ?: it.id }) 157 } 158 159 fun show() { 160 initDialog() 161 dialog.show() 162 } 163 164 /** 165 * Close the dialog without saving. For external callers 166 */ 167 fun close() { 168 done() 169 } 170 171 private fun done() { 172 resetState() 173 dialog.dismiss() 174 onFinishListener?.onChannelEditorDialogFinished() 175 } 176 177 private fun resetState() { 178 appIcon = null 179 appUid = null 180 packageName = null 181 appName = null 182 183 edits.clear() 184 providedChannels.clear() 185 groupNameLookup.clear() 186 } 187 188 fun groupNameForId(groupId: String?): CharSequence { 189 return groupNameLookup[groupId] ?: "" 190 } 191 192 fun proposeEditForChannel(channel: NotificationChannel, edit: Int) { 193 if (channel.importance == edit) { 194 edits.remove(channel) 195 } else { 196 edits[channel] = edit 197 } 198 } 199 200 @Suppress("unchecked_cast") 201 private fun fetchNotificationChannelGroups(): List<NotificationChannelGroup> { 202 return try { 203 noMan.getNotificationChannelGroupsForPackage(packageName!!, appUid!!, false) 204 .list as? List<NotificationChannelGroup> ?: listOf() 205 } catch (e: Exception) { 206 Log.e(TAG, "Error fetching channel groups", e) 207 listOf() 208 } 209 } 210 211 private fun checkAreAppNotificationsOn(): Boolean { 212 return try { 213 noMan.areNotificationsEnabledForPackage(packageName!!, appUid!!) 214 } catch (e: Exception) { 215 Log.e(TAG, "Error calling NoMan", e) 216 false 217 } 218 } 219 220 private fun applyAppNotificationsOn(b: Boolean) { 221 try { 222 noMan.setNotificationsEnabledForPackage(packageName!!, appUid!!, b) 223 } catch (e: Exception) { 224 Log.e(TAG, "Error calling NoMan", e) 225 } 226 } 227 228 private fun setChannelImportance(channel: NotificationChannel, importance: Int) { 229 try { 230 channel.importance = importance 231 noMan.updateNotificationChannelForPackage(packageName!!, appUid!!, channel) 232 } catch (e: Exception) { 233 Log.e(TAG, "Unable to update notification importance", e) 234 } 235 } 236 237 @VisibleForTesting 238 fun apply() { 239 for ((channel, importance) in edits) { 240 if (channel.importance != importance) { 241 setChannelImportance(channel, importance) 242 } 243 } 244 245 if (appNotificationsEnabled != checkAreAppNotificationsOn()) { 246 applyAppNotificationsOn(appNotificationsEnabled) 247 } 248 } 249 250 @VisibleForTesting 251 fun launchSettings(sender: View) { 252 onSettingsClickListener?.onClick(sender, null, appUid!!) 253 } 254 255 private fun initDialog() { 256 dialog = Dialog(context) 257 258 dialog.window?.requestFeature(Window.FEATURE_NO_TITLE) 259 // Prevent a11y readers from reading the first element in the dialog twice 260 dialog.setTitle("\u00A0") 261 dialog.apply { 262 setContentView(R.layout.notif_half_shelf) 263 setCanceledOnTouchOutside(true) 264 findViewById<ChannelEditorListView>(R.id.half_shelf_container).apply { 265 controller = this@ChannelEditorDialogController 266 appIcon = this@ChannelEditorDialogController.appIcon 267 appName = this@ChannelEditorDialogController.appName 268 channels = providedChannels 269 } 270 271 findViewById<TextView>(R.id.done_button)?.setOnClickListener { 272 apply() 273 done() 274 } 275 276 findViewById<TextView>(R.id.see_more_button)?.setOnClickListener { 277 launchSettings(it) 278 done() 279 } 280 281 window?.apply { 282 setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) 283 addFlags(wmFlags) 284 setType(WindowManager.LayoutParams.TYPE_STATUS_BAR_PANEL) 285 setWindowAnimations(com.android.internal.R.style.Animation_InputMethod) 286 287 attributes = attributes.apply { 288 format = PixelFormat.TRANSLUCENT 289 title = ChannelEditorDialogController::class.java.simpleName 290 gravity = Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL 291 width = MATCH_PARENT 292 height = WRAP_CONTENT 293 } 294 } 295 } 296 } 297 298 private val wmFlags = (WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS 299 or WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN 300 or WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH 301 or WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED) 302 } 303 304 interface OnChannelEditorDialogFinishedListener { onChannelEditorDialogFinishednull305 fun onChannelEditorDialogFinished() 306 } 307