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.content.DialogInterface 27 import android.graphics.Color 28 import android.graphics.PixelFormat 29 import android.graphics.drawable.Drawable 30 import android.graphics.drawable.ColorDrawable 31 import android.util.Log 32 import android.view.Gravity 33 import android.view.View 34 import android.view.ViewGroup.LayoutParams.MATCH_PARENT 35 import android.view.ViewGroup.LayoutParams.WRAP_CONTENT 36 import android.view.Window 37 import android.view.WindowManager 38 import android.widget.TextView 39 import com.android.internal.annotations.VisibleForTesting 40 41 import com.android.systemui.R 42 43 import javax.inject.Inject 44 import javax.inject.Singleton 45 46 const val TAG = "ChannelDialogController" 47 48 /** 49 * ChannelEditorDialogController is the controller for the dialog half-shelf 50 * that allows users to quickly turn off channels. It is launched from the NotificationInfo 51 * guts view and displays controls for toggling app notifications as well as up to 4 channels 52 * from that app like so: 53 * 54 * APP TOGGLE <on/off> 55 * - Channel from which we launched <on/off> 56 * - <on/off> 57 * - the next 3 channels sorted alphabetically for that app <on/off> 58 * - <on/off> 59 */ 60 @Singleton 61 class ChannelEditorDialogController @Inject constructor( 62 c: Context, 63 private val noMan: INotificationManager 64 ) { 65 val context: Context = c.applicationContext 66 67 lateinit var dialog: Dialog 68 69 private var appIcon: Drawable? = null 70 private var appUid: Int? = null 71 private var packageName: String? = null 72 private var appName: String? = null 73 private var onSettingsClickListener: NotificationInfo.OnSettingsClickListener? = null 74 75 // Caller should set this if they care about when we dismiss 76 var onFinishListener: OnChannelEditorDialogFinishedListener? = null 77 78 // Channels handed to us from NotificationInfo 79 @VisibleForTesting 80 internal val providedChannels = mutableListOf<NotificationChannel>() 81 82 // Map from NotificationChannel to importance 83 private val edits = mutableMapOf<NotificationChannel, Int>() 84 var appNotificationsEnabled = true 85 86 // Keep a mapping of NotificationChannel.getGroup() to the actual group name for display 87 @VisibleForTesting 88 internal val groupNameLookup = hashMapOf<String, CharSequence>() 89 private val channelGroupList = mutableListOf<NotificationChannelGroup>() 90 91 /** 92 * Give the controller all of the information it needs to present the dialog 93 * for a given app. Does a bunch of querying of NoMan, but won't present anything yet 94 */ 95 fun prepareDialogForApp( 96 appName: String, 97 packageName: String, 98 uid: Int, 99 channels: Set<NotificationChannel>, 100 appIcon: Drawable, 101 onSettingsClickListener: NotificationInfo.OnSettingsClickListener? 102 ) { 103 this.appName = appName 104 this.packageName = packageName 105 this.appUid = uid 106 this.appIcon = appIcon 107 this.appNotificationsEnabled = checkAreAppNotificationsOn() 108 this.onSettingsClickListener = onSettingsClickListener 109 110 channelGroupList.clear() 111 channelGroupList.addAll(fetchNotificationChannelGroups()) 112 buildGroupNameLookup() 113 padToFourChannels(channels) 114 } 115 116 private fun buildGroupNameLookup() { 117 channelGroupList.forEach { group -> 118 if (group.id != null) { 119 groupNameLookup[group.id] = group.name 120 } 121 } 122 } 123 124 private fun padToFourChannels(channels: Set<NotificationChannel>) { 125 providedChannels.clear() 126 // First, add all of the given channels 127 providedChannels.addAll(channels.asSequence().take(4)) 128 129 // Then pad to 4 if we haven't been given that many 130 providedChannels.addAll(getDisplayableChannels(channelGroupList.asSequence()) 131 .filterNot { providedChannels.contains(it) } 132 .distinct() 133 .take(4 - providedChannels.size)) 134 135 // If we only got one channel and it has the default miscellaneous tag, then we actually 136 // are looking at an app with a targetSdk <= O, and it doesn't make much sense to show the 137 // channel 138 if (providedChannels.size == 1 && DEFAULT_CHANNEL_ID == providedChannels[0].id) { 139 providedChannels.clear() 140 } 141 } 142 143 private fun getDisplayableChannels( 144 groupList: Sequence<NotificationChannelGroup> 145 ): Sequence<NotificationChannel> { 146 147 val channels = groupList 148 .flatMap { group -> 149 group.channels.asSequence().filterNot { channel -> 150 channel.isImportanceLockedByOEM 151 || channel.importance == IMPORTANCE_NONE 152 || channel.isImportanceLockedByCriticalDeviceFunction 153 } 154 } 155 156 // TODO: sort these by avgSentWeekly, but for now let's just do alphabetical (why not) 157 return channels.sortedWith(compareBy { it.name?.toString() ?: it.id }) 158 } 159 160 fun show() { 161 initDialog() 162 dialog.show() 163 } 164 165 /** 166 * Close the dialog without saving. For external callers 167 */ 168 fun close() { 169 done() 170 } 171 172 private fun done() { 173 resetState() 174 dialog.dismiss() 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 setOnDismissListener(object : DialogInterface.OnDismissListener { 265 override fun onDismiss(dialog: DialogInterface?) { 266 onFinishListener?.onChannelEditorDialogFinished() 267 } 268 }) 269 findViewById<ChannelEditorListView>(R.id.half_shelf_container).apply { 270 controller = this@ChannelEditorDialogController 271 appIcon = this@ChannelEditorDialogController.appIcon 272 appName = this@ChannelEditorDialogController.appName 273 channels = providedChannels 274 } 275 276 findViewById<TextView>(R.id.done_button)?.setOnClickListener { 277 apply() 278 done() 279 } 280 281 findViewById<TextView>(R.id.see_more_button)?.setOnClickListener { 282 launchSettings(it) 283 done() 284 } 285 286 window?.apply { 287 setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) 288 addFlags(wmFlags) 289 setType(WindowManager.LayoutParams.TYPE_STATUS_BAR_PANEL) 290 setWindowAnimations(com.android.internal.R.style.Animation_InputMethod) 291 292 attributes = attributes.apply { 293 format = PixelFormat.TRANSLUCENT 294 title = ChannelEditorDialogController::class.java.simpleName 295 gravity = Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL 296 width = MATCH_PARENT 297 height = WRAP_CONTENT 298 } 299 } 300 } 301 } 302 303 private val wmFlags = (WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS 304 or WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN 305 or WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH 306 or WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED) 307 } 308 309 interface OnChannelEditorDialogFinishedListener { onChannelEditorDialogFinishednull310 fun onChannelEditorDialogFinished() 311 } 312