1 /* 2 * Copyright 2023 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.bluetooth.notification; 18 19 import static java.util.Objects.requireNonNull; 20 21 import android.app.Notification; 22 import android.app.NotificationChannel; 23 import android.app.NotificationManager; 24 import android.app.PendingIntent; 25 import android.app.Service; 26 import android.content.Intent; 27 import android.net.Uri; 28 import android.os.IBinder; 29 import android.provider.Settings; 30 import android.service.notification.StatusBarNotification; 31 import android.util.Log; 32 import android.util.Pair; 33 34 import com.android.bluetooth.R; 35 import com.android.internal.messages.SystemMessageProto.SystemMessage; 36 37 import java.time.LocalDateTime; 38 import java.time.ZoneId; 39 import java.util.Map; 40 41 public class NotificationHelperService extends Service { 42 private static final String TAG = NotificationHelperService.class.getSimpleName(); 43 44 // Keeps track of whether wifi and bt remains on notification was shown 45 private static final String APM_WIFI_BT_NOTIFICATION = "apm_wifi_bt_notification"; 46 // Keeps track of whether bt remains on notification was shown 47 private static final String APM_BT_NOTIFICATION = "apm_bt_notification"; 48 // Keeps track of whether user enabling bt notification was shown 49 private static final String APM_BT_ENABLED_NOTIFICATION = "apm_bt_enabled_notification"; 50 // Keeps track of whether auto on enabling bt notification was shown 51 private static final String AUTO_ON_BT_ENABLED_NOTIFICATION = "auto_on_bt_enabled_notification"; 52 53 private static final String NOTIFICATION_TAG = "com.android.bluetooth"; 54 private static final String NOTIFICATION_CHANNEL = "notification_toggle_channel"; 55 private static final int NOTIFICATION_GROUP = R.string.bluetooth_notification_group; 56 57 private static final String NOTIFICATION_ACTION = 58 "android.bluetooth.notification.action.SEND_TOGGLE_NOTIFICATION"; 59 private static final String NOTIFICATION_EXTRA = 60 "android.bluetooth.notification.extra.NOTIFICATION_REASON"; 61 62 private static final Map<String, Pair<Integer /* titleId */, Integer /* messageId */>> 63 NOTIFICATION_MAP = 64 Map.of( 65 APM_WIFI_BT_NOTIFICATION, 66 Pair.create( 67 R.string.bluetooth_and_wifi_stays_on_title, 68 R.string.bluetooth_and_wifi_stays_on_message), 69 APM_BT_NOTIFICATION, 70 Pair.create( 71 R.string.bluetooth_stays_on_title, 72 R.string.bluetooth_stays_on_message), 73 APM_BT_ENABLED_NOTIFICATION, 74 Pair.create( 75 R.string.bluetooth_enabled_apm_title, 76 R.string.bluetooth_enabled_apm_message), 77 AUTO_ON_BT_ENABLED_NOTIFICATION, 78 Pair.create( 79 R.string.bluetooth_enabled_auto_on_title, 80 R.string.bluetooth_enabled_auto_on_message)); 81 82 @Override onBind(Intent intent)83 public IBinder onBind(Intent intent) { 84 return null; // This is not a bound service 85 } 86 87 @Override onStartCommand(Intent intent, int flags, int startId)88 public int onStartCommand(Intent intent, int flags, int startId) { 89 switch (intent.getAction()) { 90 case NOTIFICATION_ACTION -> { 91 sendToggleNotification(intent.getStringExtra(NOTIFICATION_EXTRA)); 92 } 93 } 94 return Service.START_NOT_STICKY; 95 } 96 sendToggleNotification(String notificationReason)97 private void sendToggleNotification(String notificationReason) { 98 String logHeader = "sendToggleNotification(" + notificationReason + "): "; 99 Pair<Integer, Integer> notificationContent = NOTIFICATION_MAP.get(notificationReason); 100 if (notificationContent == null) { 101 Log.e(TAG, logHeader + "unknown action"); 102 return; 103 } 104 105 if (!shouldDisplayNotification(notificationReason)) { 106 return; 107 } 108 109 NotificationManager notificationManager = 110 requireNonNull(getSystemService(NotificationManager.class)); 111 String tag = NOTIFICATION_TAG + "/" + notificationReason; 112 for (StatusBarNotification notification : notificationManager.getActiveNotifications()) { 113 if (tag.equals(notification.getTag())) { 114 notificationManager.cancel(tag, notification.getId()); 115 } 116 } 117 118 notificationManager.createNotificationChannel( 119 new NotificationChannel( 120 NOTIFICATION_CHANNEL, 121 getString(NOTIFICATION_GROUP), 122 NotificationManager.IMPORTANCE_LOW)); 123 124 String title = getString(notificationContent.first); 125 String message = getString(notificationContent.second); 126 127 Notification.Builder builder = 128 new Notification.Builder(this, NOTIFICATION_CHANNEL) 129 .setAutoCancel(true) 130 .setLocalOnly(true) 131 .setContentTitle(title) 132 .setContentText(message) 133 .setVisibility(Notification.VISIBILITY_PUBLIC) 134 .setStyle(new Notification.BigTextStyle().bigText(message)) 135 .setSmallIcon(android.R.drawable.stat_sys_data_bluetooth); 136 137 if (!notificationReason.equals(AUTO_ON_BT_ENABLED_NOTIFICATION)) { 138 // Do not display airplane link when the notification is due to auto_on feature 139 String helpLinkUrl = getString(R.string.config_apmLearnMoreLink); 140 builder.setContentIntent( 141 PendingIntent.getActivity( 142 this, 143 PendingIntent.FLAG_UPDATE_CURRENT, 144 new Intent(Intent.ACTION_VIEW) 145 .setData(Uri.parse(helpLinkUrl)) 146 .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK), 147 PendingIntent.FLAG_IMMUTABLE)); 148 } else { 149 // Open the setting page when the notification is clicked 150 builder.setContentIntent( 151 PendingIntent.getActivity( 152 this, 153 PendingIntent.FLAG_UPDATE_CURRENT, 154 new Intent("android.settings.BLUETOOTH_DASHBOARD_SETTINGS"), 155 PendingIntent.FLAG_IMMUTABLE)); 156 } 157 158 notificationManager.notify( 159 tag, SystemMessage.ID.NOTE_BT_APM_NOTIFICATION_VALUE, builder.build()); 160 } 161 shouldDisplayNotification(String countKey)162 private boolean shouldDisplayNotification(String countKey) { 163 final LocalDateTime now = LocalDateTime.now(ZoneId.systemDefault()); 164 final String dateKey = countKey + "_date"; 165 final String date = Settings.Secure.getString(getContentResolver(), dateKey); 166 final int countShown = Settings.Secure.getInt(getContentResolver(), countKey, 0); 167 168 // The notification is always displayed the first time and if it has been at least…: 169 // * … 1 week since the first display (aka recurring only once) 170 // * … 6 months since the last display (aka recurring forever) 171 172 LocalDateTime savedDate = null; 173 if (date != null) { 174 savedDate = LocalDateTime.parse(date); 175 if ((countShown == 1 && now.isBefore(savedDate.plusWeeks(1))) 176 || now.isBefore(savedDate.plusMonths(6))) { 177 Log.i( 178 TAG, 179 ("shouldDisplayNotification(" + countKey + "): Notification discarded.") 180 + (" countShown=" + countShown) 181 + (" savedDate=" + savedDate)); 182 return false; 183 } 184 } 185 186 Settings.Secure.putInt(getContentResolver(), countKey, Math.min(3, countShown + 1)); 187 Settings.Secure.putString(getContentResolver(), dateKey, now.toString()); 188 Log.i( 189 TAG, 190 ("shouldDisplayNotification(" + countKey + "): Notification is being shown.") 191 + (" countShown=" + countShown) 192 + (" savedDate=" + savedDate)); 193 return true; 194 } 195 } 196