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.app.NotificationManager.Importance 26 import android.content.Context 27 import android.graphics.Color 28 import android.graphics.PixelFormat 29 import android.graphics.drawable.ColorDrawable 30 import android.graphics.drawable.Drawable 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.WindowInsets.Type.statusBars 38 import android.view.WindowManager 39 import android.widget.TextView 40 import com.android.internal.annotations.VisibleForTesting 41 import com.android.systemui.res.R 42 import com.android.systemui.dagger.SysUISingleton 43 import javax.inject.Inject 44 45 private 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 @SysUISingleton 60 class ChannelEditorDialogController @Inject constructor( 61 c: Context, 62 private val noMan: INotificationManager, 63 private val dialogBuilder: ChannelEditorDialog.Builder 64 ) { 65 val context: Context = c.applicationContext 66 67 private var prepared = false 68 private lateinit var dialog: ChannelEditorDialog 69 70 private var appIcon: Drawable? = null 71 private var appUid: Int? = null 72 private var packageName: String? = null 73 private var appName: String? = null 74 private var channel: NotificationChannel? = null 75 private var onSettingsClickListener: NotificationInfo.OnSettingsClickListener? = null 76 77 // Caller should set this if they care about when we dismiss 78 var onFinishListener: OnChannelEditorDialogFinishedListener? = null 79 80 // Channels handed to us from NotificationInfo 81 @VisibleForTesting 82 internal val channelList = mutableListOf<NotificationChannel>() 83 84 // Map from NotificationChannel to importance 85 private val edits = mutableMapOf<NotificationChannel, Int>() 86 private var appNotificationsEnabled = true 87 // System settings for app notifications 88 private var appNotificationsCurrentlyEnabled: Boolean? = null 89 90 // Keep a mapping of NotificationChannel.getGroup() to the actual group name for display 91 @VisibleForTesting 92 internal val groupNameLookup = hashMapOf<String, CharSequence>() 93 private val channelGroupList = mutableListOf<NotificationChannelGroup>() 94 95 /** 96 * Give the controller all the information it needs to present the dialog 97 * for a given app. Does a bunch of querying of NoMan, but won't present anything yet 98 */ 99 fun prepareDialogForApp( 100 appName: String, 101 packageName: String, 102 uid: Int, 103 channel: NotificationChannel, 104 appIcon: Drawable, 105 onSettingsClickListener: NotificationInfo.OnSettingsClickListener? 106 ) { 107 this.appName = appName 108 this.packageName = packageName 109 this.appUid = uid 110 this.appIcon = appIcon 111 this.appNotificationsEnabled = checkAreAppNotificationsOn() 112 this.onSettingsClickListener = onSettingsClickListener 113 this.channel = channel 114 115 // These will always start out the same 116 appNotificationsCurrentlyEnabled = appNotificationsEnabled 117 118 channelGroupList.clear() 119 channelGroupList.addAll(fetchNotificationChannelGroups()) 120 buildGroupNameLookup() 121 populateChannelList() 122 initDialog() 123 124 prepared = true 125 } 126 127 private fun buildGroupNameLookup() { 128 channelGroupList.forEach { group -> 129 if (group.id != null) { 130 groupNameLookup[group.id] = group.name 131 } 132 } 133 } 134 135 private fun populateChannelList() { 136 channelList.clear() 137 if (DEFAULT_CHANNEL_ID != channel!!.id) { 138 channelList.add(0, channel!!) 139 channelList.addAll(getDisplayableChannels(channelGroupList.asSequence()) 140 .filterNot { it.id == channel!!.id } 141 .distinct()) 142 } 143 } 144 145 private fun getDisplayableChannels( 146 groupList: Sequence<NotificationChannelGroup> 147 ): Sequence<NotificationChannel> { 148 val channels = groupList 149 .flatMap { group -> 150 group.channels.asSequence() 151 .sortedWith(compareBy {group.name?.toString() ?: group.id}) 152 .filterNot { channel -> 153 channel.isImportanceLockedByCriticalDeviceFunction 154 } 155 } 156 157 // TODO: sort these by avgSentWeekly, but for now let's just do alphabetical (why not) 158 return channels.sortedWith(compareBy { it.name?.toString() ?: it.id }) 159 } 160 161 fun show() { 162 if (!prepared) { 163 throw IllegalStateException("Must call prepareDialogForApp() before calling show()") 164 } 165 dialog.show() 166 } 167 168 /** 169 * Close the dialog without saving. For external callers 170 */ 171 fun close() { 172 done() 173 } 174 175 private fun done() { 176 resetState() 177 dialog.dismiss() 178 } 179 180 private fun resetState() { 181 appIcon = null 182 appUid = null 183 packageName = null 184 appName = null 185 appNotificationsCurrentlyEnabled = null 186 187 edits.clear() 188 channelList.clear() 189 groupNameLookup.clear() 190 } 191 192 fun groupNameForId(groupId: String?): CharSequence { 193 return groupNameLookup[groupId] ?: "" 194 } 195 196 fun proposeEditForChannel(channel: NotificationChannel, @Importance edit: Int) { 197 if (channel.importance == edit) { 198 edits.remove(channel) 199 } else { 200 edits[channel] = edit 201 } 202 203 dialog.updateDoneButtonText(hasChanges()) 204 } 205 206 fun proposeSetAppNotificationsEnabled(enabled: Boolean) { 207 appNotificationsEnabled = enabled 208 dialog.updateDoneButtonText(hasChanges()) 209 } 210 211 fun areAppNotificationsEnabled(): Boolean { 212 return appNotificationsEnabled 213 } 214 215 private fun hasChanges(): Boolean { 216 return edits.isNotEmpty() || (appNotificationsEnabled != appNotificationsCurrentlyEnabled) 217 } 218 219 @Suppress("unchecked_cast") 220 private fun fetchNotificationChannelGroups(): List<NotificationChannelGroup> { 221 return try { 222 noMan.getRecentBlockedNotificationChannelGroupsForPackage(packageName!!, appUid!!) 223 .list as? List<NotificationChannelGroup> ?: listOf() 224 } catch (e: Exception) { 225 Log.e(TAG, "Error fetching channel groups", e) 226 listOf() 227 } 228 } 229 230 private fun checkAreAppNotificationsOn(): Boolean { 231 return try { 232 noMan.areNotificationsEnabledForPackage(packageName!!, appUid!!) 233 } catch (e: Exception) { 234 Log.e(TAG, "Error calling NoMan", e) 235 false 236 } 237 } 238 239 private fun applyAppNotificationsOn(b: Boolean) { 240 try { 241 noMan.setNotificationsEnabledForPackage(packageName!!, appUid!!, b) 242 } catch (e: Exception) { 243 Log.e(TAG, "Error calling NoMan", e) 244 } 245 } 246 247 private fun setChannelImportance(channel: NotificationChannel, importance: Int) { 248 try { 249 channel.importance = importance 250 noMan.updateNotificationChannelForPackage(packageName!!, appUid!!, channel) 251 } catch (e: Exception) { 252 Log.e(TAG, "Unable to update notification importance", e) 253 } 254 } 255 256 @VisibleForTesting 257 fun apply() { 258 for ((channel, importance) in edits) { 259 if (channel.importance != importance) { 260 setChannelImportance(channel, importance) 261 } 262 } 263 264 if (appNotificationsEnabled != appNotificationsCurrentlyEnabled) { 265 applyAppNotificationsOn(appNotificationsEnabled) 266 } 267 } 268 269 @VisibleForTesting 270 fun launchSettings(sender: View) { 271 onSettingsClickListener?.onClick(sender, channel, appUid!!) 272 } 273 274 private fun initDialog() { 275 dialogBuilder.setContext(context) 276 dialog = dialogBuilder.build() 277 278 dialog.window?.requestFeature(Window.FEATURE_NO_TITLE) 279 // Prevent a11y readers from reading the first element in the dialog twice 280 dialog.setTitle("\u00A0") 281 dialog.apply { 282 setContentView(R.layout.notif_half_shelf) 283 setCanceledOnTouchOutside(true) 284 setOnDismissListener { onFinishListener?.onChannelEditorDialogFinished() } 285 286 val listView = findViewById<ChannelEditorListView>(R.id.half_shelf_container) 287 listView?.apply { 288 controller = this@ChannelEditorDialogController 289 appIcon = this@ChannelEditorDialogController.appIcon 290 appName = this@ChannelEditorDialogController.appName 291 channels = channelList 292 } 293 294 setOnShowListener { 295 // play a highlight animation for the given channel 296 listView?.highlightChannel(channel!!) 297 } 298 299 findViewById<TextView>(R.id.done_button)?.setOnClickListener { 300 apply() 301 done() 302 } 303 304 findViewById<TextView>(R.id.see_more_button)?.setOnClickListener { 305 launchSettings(it) 306 done() 307 } 308 309 window?.apply { 310 setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) 311 addFlags(wmFlags) 312 setType(WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL) 313 setWindowAnimations(com.android.internal.R.style.Animation_InputMethod) 314 315 attributes = attributes.apply { 316 format = PixelFormat.TRANSLUCENT 317 title = ChannelEditorDialogController::class.java.simpleName 318 gravity = Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL 319 fitInsetsTypes = attributes.fitInsetsTypes and statusBars().inv() 320 width = MATCH_PARENT 321 height = WRAP_CONTENT 322 } 323 } 324 } 325 } 326 327 private val wmFlags = (WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS 328 or WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH 329 or WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED) 330 } 331 332 class ChannelEditorDialog(context: Context, theme: Int) : Dialog(context, theme) { updateDoneButtonTextnull333 fun updateDoneButtonText(hasChanges: Boolean) { 334 findViewById<TextView>(R.id.done_button)?.setText( 335 if (hasChanges) 336 R.string.inline_ok_button 337 else 338 R.string.inline_done_button) 339 } 340 341 class Builder @Inject constructor() { 342 private lateinit var context: Context setContextnull343 fun setContext(context: Context): Builder { 344 this.context = context 345 return this 346 } 347 buildnull348 fun build(): ChannelEditorDialog { 349 return ChannelEditorDialog(context, R.style.Theme_SystemUI_Dialog) 350 } 351 } 352 } 353 354 interface OnChannelEditorDialogFinishedListener { onChannelEditorDialogFinishednull355 fun onChannelEditorDialogFinished() 356 } 357