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