1 /* <lambda>null2 * Copyright (C) 2021 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 @file:Suppress("DEPRECATION") 17 18 package com.android.permissioncontroller.auto 19 20 import android.app.Notification 21 import android.app.NotificationChannel 22 import android.app.NotificationManager 23 import android.app.PendingIntent 24 import android.app.Service 25 import android.car.Car 26 import android.car.drivingstate.CarUxRestrictionsManager 27 import android.content.ComponentName 28 import android.content.Context 29 import android.content.Intent 30 import android.content.pm.PackageManager 31 import android.os.Bundle 32 import android.os.IBinder 33 import android.os.Process 34 import android.os.UserHandle 35 import android.permission.PermissionManager 36 import android.provider.Settings 37 import android.text.BidiFormatter 38 import androidx.annotation.VisibleForTesting 39 import com.android.permissioncontroller.Constants 40 import com.android.permissioncontroller.DumpableLog 41 import com.android.permissioncontroller.PermissionControllerStatsLog 42 import com.android.permissioncontroller.PermissionControllerStatsLog.PERMISSION_REMINDER_NOTIFICATION_INTERACTED__RESULT__NOTIFICATION_PRESENTED 43 import com.android.permissioncontroller.R 44 import com.android.permissioncontroller.permission.ui.auto.AutoReviewPermissionDecisionsFragment 45 import com.android.permissioncontroller.permission.utils.KotlinUtils 46 import com.android.permissioncontroller.permission.utils.KotlinUtils.getPackageLabel 47 import com.android.permissioncontroller.permission.utils.KotlinUtils.getPermGroupLabel 48 import com.android.permissioncontroller.permission.utils.StringUtils 49 import com.android.permissioncontroller.permission.utils.Utils 50 import java.util.Random 51 52 /** 53 * Service that collects permissions decisions made while driving and when the vehicle is no longer 54 * in a UX-restricted state shows a notification reminding the user of their decisions. 55 */ 56 class DrivingDecisionReminderService : Service() { 57 58 /** 59 * Information needed to show a reminder about a permission decisions. 60 */ 61 data class PermissionReminder( 62 val packageName: String, 63 val permissionGroup: String, 64 val user: UserHandle 65 ) 66 67 private var scheduled = false 68 private var carUxRestrictionsManager: CarUxRestrictionsManager? = null 69 private val permissionReminders: MutableSet<PermissionReminder> = mutableSetOf() 70 private var car: Car? = null 71 private var sessionId = Constants.INVALID_SESSION_ID 72 73 companion object { 74 private const val LOG_TAG = "DrivingDecisionReminderService" 75 private const val SETTINGS_PACKAGE_NAME_FALLBACK = "com.android.settings" 76 77 const val EXTRA_PACKAGE_NAME = "package_name" 78 const val EXTRA_PERMISSION_GROUP = "permission_group" 79 const val EXTRA_USER = "user" 80 81 /** 82 * Create an intent to launch [DrivingDecisionReminderService], including information about 83 * the permission decision to reminder the user about. 84 * 85 * @param context application context 86 * @param packageName package name of app effected by the permission decision 87 * @param permissionGroup permission group for the permission decision 88 * @param user user that made the permission decision 89 */ 90 fun createIntent( 91 context: Context, 92 packageName: String, 93 permissionGroup: String, 94 user: UserHandle 95 ): Intent { 96 val intent = Intent(context, DrivingDecisionReminderService::class.java) 97 intent.putExtra(EXTRA_PACKAGE_NAME, packageName) 98 intent.putExtra(EXTRA_PERMISSION_GROUP, permissionGroup) 99 intent.putExtra(EXTRA_USER, user) 100 return intent 101 } 102 103 /** 104 * Starts the [DrivingDecisionReminderService] if the vehicle currently requires distraction 105 * optimization. 106 */ 107 fun startServiceIfCurrentlyRestricted( 108 context: Context, 109 packageName: String, 110 permGroupName: String 111 ) { 112 Car.createCar( 113 context, 114 /* handler= */ null, 115 Car.CAR_WAIT_TIMEOUT_DO_NOT_WAIT) { car: Car, ready: Boolean -> 116 // just give up if we can't connect to the car 117 if (ready) { 118 val restrictionsManager = car.getCarManager( 119 Car.CAR_UX_RESTRICTION_SERVICE) as CarUxRestrictionsManager 120 if (restrictionsManager.currentCarUxRestrictions 121 .isRequiresDistractionOptimization) { 122 context.startService( 123 createIntent( 124 context, 125 packageName, 126 permGroupName, 127 Process.myUserHandle())) 128 } 129 } 130 car.disconnect() 131 } 132 } 133 } 134 135 override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { 136 val decisionReminder = parseStartIntent(intent) ?: return START_NOT_STICKY 137 permissionReminders.add(decisionReminder) 138 if (scheduled) { 139 DumpableLog.d(LOG_TAG, "Start service - reminder notification already scheduled") 140 return START_STICKY 141 } 142 scheduleNotificationForUnrestrictedState() 143 scheduled = true 144 while (sessionId == Constants.INVALID_SESSION_ID) { 145 sessionId = Random().nextLong() 146 } 147 return START_STICKY 148 } 149 150 override fun onDestroy() { 151 car?.disconnect() 152 } 153 154 override fun onBind(intent: Intent?): IBinder? { 155 return null 156 } 157 158 private fun scheduleNotificationForUnrestrictedState() { 159 Car.createCar(this, null, 160 Car.CAR_WAIT_TIMEOUT_DO_NOT_WAIT 161 ) { createdCar: Car?, ready: Boolean -> 162 car = createdCar 163 if (ready) { 164 onCarReady() 165 } else { 166 DumpableLog.w(LOG_TAG, 167 "Car service disconnected, no notification will be scheduled") 168 stopSelf() 169 } 170 } 171 } 172 173 private fun onCarReady() { 174 carUxRestrictionsManager = car?.getCarManager( 175 Car.CAR_UX_RESTRICTION_SERVICE) as CarUxRestrictionsManager 176 DumpableLog.d(LOG_TAG, "Registering UX restriction listener") 177 carUxRestrictionsManager?.registerListener { restrictions -> 178 if (!restrictions.isRequiresDistractionOptimization) { 179 DumpableLog.d(LOG_TAG, 180 "UX restrictions no longer required - showing reminder notification") 181 showRecentGrantDecisionsPostDriveNotification() 182 stopSelf() 183 } 184 } 185 } 186 187 private fun parseStartIntent(intent: Intent?): PermissionReminder? { 188 if (intent == null || 189 !intent.hasExtra(EXTRA_PACKAGE_NAME) || 190 !intent.hasExtra(EXTRA_PERMISSION_GROUP) || 191 !intent.hasExtra(EXTRA_USER)) { 192 DumpableLog.e(LOG_TAG, "Missing extras from intent $intent") 193 return null 194 } 195 val packageName = intent.getStringExtra(EXTRA_PACKAGE_NAME) 196 val permissionGroup = intent.getStringExtra(EXTRA_PERMISSION_GROUP) 197 val user = intent.getParcelableExtra<UserHandle>(EXTRA_USER) 198 return PermissionReminder(packageName!!, permissionGroup!!, user!!) 199 } 200 201 @VisibleForTesting 202 fun showRecentGrantDecisionsPostDriveNotification() { 203 val notificationManager = getSystemService(NotificationManager::class.java)!! 204 205 val permissionReminderChannel = NotificationChannel( 206 Constants.PERMISSION_REMINDER_CHANNEL_ID, getString(R.string.permission_reminders), 207 NotificationManager.IMPORTANCE_HIGH) 208 notificationManager.createNotificationChannel(permissionReminderChannel) 209 210 notificationManager.notify(DrivingDecisionReminderService::class.java.simpleName, 211 Constants.PERMISSION_DECISION_REMINDER_NOTIFICATION_ID, 212 createNotification(createNotificationTitle(), createNotificationContent())) 213 214 logNotificationPresented() 215 } 216 217 private fun createNotificationTitle(): String { 218 return applicationContext 219 .getString(R.string.post_drive_permission_decision_reminder_title) 220 } 221 222 @VisibleForTesting 223 fun createNotificationContent(): String { 224 val packageLabels: MutableList<String> = mutableListOf() 225 val permissionGroupNames: MutableList<String> = mutableListOf() 226 for (permissionReminder in permissionReminders) { 227 val packageLabel = getLabelForPackage(permissionReminder.packageName, 228 permissionReminder.user) 229 val permissionGroupLabel = getPermGroupLabel(applicationContext, 230 permissionReminder.permissionGroup).toString() 231 packageLabels.add(packageLabel) 232 permissionGroupNames.add(permissionGroupLabel) 233 } 234 val packageLabelsDistinct = packageLabels.distinct() 235 val permissionGroupNamesDistinct = permissionGroupNames.distinct() 236 return if (packageLabelsDistinct.size > 1) { 237 StringUtils.getIcuPluralsString(applicationContext, 238 R.string.post_drive_permission_decision_reminder_summary_multi_apps, 239 (packageLabels.size - 1), packageLabelsDistinct[0]) 240 } else if (permissionGroupNamesDistinct.size == 2) { 241 getString( 242 R.string.post_drive_permission_decision_reminder_summary_1_app_2_permissions, 243 packageLabelsDistinct[0], permissionGroupNamesDistinct[0], 244 permissionGroupNamesDistinct[1]) 245 } else if (permissionGroupNamesDistinct.size > 2) { 246 getString( 247 R.string.post_drive_permission_decision_reminder_summary_1_app_multi_permission, 248 permissionGroupNamesDistinct.size, packageLabelsDistinct[0]) 249 } else { 250 getString( 251 R.string.post_drive_permission_decision_reminder_summary_1_app_1_permission, 252 packageLabelsDistinct[0], permissionGroupNamesDistinct[0]) 253 } 254 } 255 256 @VisibleForTesting 257 fun getLabelForPackage(packageName: String, user: UserHandle): String { 258 return BidiFormatter.getInstance().unicodeWrap( 259 getPackageLabel(application, packageName, user)) 260 } 261 262 private fun createNotification(title: String, body: String): Notification { 263 val clickIntent = Intent(PermissionManager.ACTION_REVIEW_PERMISSION_DECISIONS).apply { 264 putExtra(Constants.EXTRA_SESSION_ID, sessionId) 265 putExtra(AutoReviewPermissionDecisionsFragment.EXTRA_SOURCE, 266 AutoReviewPermissionDecisionsFragment.EXTRA_SOURCE_NOTIFICATION) 267 flags = Intent.FLAG_ACTIVITY_NEW_TASK 268 } 269 val pendingIntent = PendingIntent.getActivity(this, 0, clickIntent, 270 PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_UPDATE_CURRENT or 271 PendingIntent.FLAG_IMMUTABLE) 272 273 val settingsDrawable = KotlinUtils.getBadgedPackageIcon( 274 application, 275 getSettingsPackageName(applicationContext.packageManager), 276 permissionReminders.first().user) 277 val settingsIcon = if (settingsDrawable != null) { 278 KotlinUtils.convertToBitmap(settingsDrawable) 279 } else { 280 null 281 } 282 283 val b = Notification.Builder(this, Constants.PERMISSION_REMINDER_CHANNEL_ID) 284 .setContentTitle(title) 285 .setContentText(body) 286 .setSmallIcon(R.drawable.ic_settings_24dp) 287 .setLargeIcon(settingsIcon) 288 .setColor(getColor(android.R.color.system_notification_accent_color)) 289 .setAutoCancel(true) 290 .setContentIntent(pendingIntent) 291 .addExtras(Bundle().apply { 292 putBoolean("com.android.car.notification.EXTRA_USE_LAUNCHER_ICON", false) 293 }) 294 // Auto doesn't show icons for actions 295 .addAction(Notification.Action.Builder(/* icon= */ null, 296 getString(R.string.go_to_settings), pendingIntent).build()) 297 Utils.getSettingsLabelForNotifications(applicationContext.packageManager)?.let { label -> 298 val extras = Bundle() 299 extras.putString(Notification.EXTRA_SUBSTITUTE_APP_NAME, label.toString()) 300 b.addExtras(extras) 301 } 302 return b.build() 303 } 304 305 private fun getSettingsPackageName(pm: PackageManager): String { 306 val settingsIntent = Intent(Settings.ACTION_SETTINGS) 307 val settingsComponent: ComponentName? = settingsIntent.resolveActivity(pm) 308 return settingsComponent?.packageName ?: SETTINGS_PACKAGE_NAME_FALLBACK 309 } 310 311 private fun logNotificationPresented() { 312 PermissionControllerStatsLog.write( 313 PermissionControllerStatsLog.PERMISSION_REMINDER_NOTIFICATION_INTERACTED, 314 sessionId, PERMISSION_REMINDER_NOTIFICATION_INTERACTED__RESULT__NOTIFICATION_PRESENTED) 315 } 316 }