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