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