• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 }