1 package com.android.car.messenger; 2 3 4 import android.app.Notification; 5 import android.app.Notification.Action; 6 import android.app.NotificationChannel; 7 import android.app.NotificationManager; 8 import android.app.Service; 9 import android.content.Intent; 10 import android.media.AudioAttributes; 11 import android.os.Binder; 12 import android.os.Bundle; 13 import android.os.IBinder; 14 import android.provider.Settings; 15 import android.telephony.TelephonyManager; 16 import android.text.TextUtils; 17 18 import androidx.core.app.NotificationCompat; 19 import androidx.core.app.RemoteInput; 20 21 import com.android.car.messenger.bluetooth.BluetoothMonitor; 22 import com.android.car.messenger.log.L; 23 24 /** Service responsible for handling SMS messaging events from paired Bluetooth devices. */ 25 public class MessengerService extends Service { 26 private final static String TAG = "CM.MessengerService"; 27 28 /* ACTIONS */ 29 /** Used to start this service at boot-complete. Takes no arguments. */ 30 public static final String ACTION_START = "com.android.car.messenger.ACTION_START"; 31 32 /** Used to reply to message with voice input; triggered by an assistant. */ 33 public static final String ACTION_VOICE_REPLY = "com.android.car.messenger.ACTION_VOICE_REPLY"; 34 35 /** Used to clear notification state when user dismisses notification. */ 36 public static final String ACTION_CLEAR_NOTIFICATION_STATE = 37 "com.android.car.messenger.ACTION_CLEAR_NOTIFICATION_STATE"; 38 39 /** Used to mark a notification as read **/ 40 public static final String ACTION_MARK_AS_READ = 41 "com.android.car.messenger.ACTION_MARK_AS_READ"; 42 43 /** Used to notify when a sms is received. Takes no arguments. */ 44 public static final String ACTION_RECEIVED_SMS = 45 "com.android.car.messenger.ACTION_RECEIVED_SMS"; 46 47 /** Used to notify when a mms is received. Takes no arguments. */ 48 public static final String ACTION_RECEIVED_MMS = 49 "com.android.car.messenger.ACTION_RECEIVED_MMS"; 50 51 /* EXTRAS */ 52 /** Key under which the {@link SenderKey} is provided. */ 53 public static final String EXTRA_SENDER_KEY = "com.android.car.messenger.EXTRA_SENDER_KEY"; 54 55 /** 56 * The resultKey of the {@link RemoteInput} which is sent in the reply callback {@link Action}. 57 */ 58 public static final String REMOTE_INPUT_KEY = "REMOTE_INPUT_KEY"; 59 60 /* NOTIFICATIONS */ 61 static final String SMS_CHANNEL_ID = "SMS_CHANNEL_ID"; 62 static final String SILENT_SMS_CHANNEL_ID = "SILENT_SMS_CHANNEL_ID"; 63 private static final String APP_RUNNING_CHANNEL_ID = "APP_RUNNING_CHANNEL_ID"; 64 private static final int SERVICE_STARTED_NOTIFICATION_ID = Integer.MAX_VALUE; 65 66 /** Delegate class used to handle this services' actions */ 67 private MessengerDelegate mMessengerDelegate; 68 69 /** Notifies this service of new bluetooth actions */ 70 private BluetoothMonitor mBluetoothMonitor; 71 72 /* Binding boilerplate */ 73 private final IBinder mBinder = new LocalBinder(); 74 75 public class LocalBinder extends Binder { getService()76 MessengerService getService() { 77 return MessengerService.this; 78 } 79 } 80 81 @Override onBind(Intent intent)82 public IBinder onBind(Intent intent) { 83 return mBinder; 84 } 85 86 @Override onCreate()87 public void onCreate() { 88 super.onCreate(); 89 L.d(TAG, "onCreate"); 90 91 mMessengerDelegate = new MessengerDelegate(this); 92 mBluetoothMonitor = new BluetoothMonitor(this); 93 mBluetoothMonitor.registerListener(mMessengerDelegate); 94 sendServiceRunningNotification(); 95 } 96 97 sendServiceRunningNotification()98 private void sendServiceRunningNotification() { 99 NotificationManager notificationManager = getSystemService(NotificationManager.class); 100 101 if (notificationManager == null) { 102 L.e(TAG, "Failed to get NotificationManager instance"); 103 return; 104 } 105 106 // Create notification channel for app running notification 107 { 108 NotificationChannel appRunningNotificationChannel = 109 new NotificationChannel(APP_RUNNING_CHANNEL_ID, 110 getString(R.string.app_running_msg_channel_name), 111 NotificationManager.IMPORTANCE_MIN); 112 notificationManager.createNotificationChannel(appRunningNotificationChannel); 113 } 114 115 // Create notification channel for notifications that should be posted silently in the 116 // notification center, without a heads up notification. 117 { 118 NotificationChannel silentNotificationChannel = 119 new NotificationChannel(SILENT_SMS_CHANNEL_ID, 120 getString(R.string.sms_channel_description), 121 NotificationManager.IMPORTANCE_LOW); 122 notificationManager.createNotificationChannel(silentNotificationChannel); 123 } 124 125 { 126 AudioAttributes attributes = new AudioAttributes.Builder() 127 .setUsage(AudioAttributes.USAGE_NOTIFICATION) 128 .build(); 129 NotificationChannel smsChannel = new NotificationChannel(SMS_CHANNEL_ID, 130 getString(R.string.sms_channel_name), 131 NotificationManager.IMPORTANCE_HIGH); 132 smsChannel.setDescription(getString(R.string.sms_channel_description)); 133 smsChannel.setSound(Settings.System.DEFAULT_NOTIFICATION_URI, attributes); 134 notificationManager.createNotificationChannel(smsChannel); 135 } 136 137 final Notification notification = 138 new NotificationCompat.Builder(this, APP_RUNNING_CHANNEL_ID) 139 .setSmallIcon(R.drawable.ic_message) 140 .setContentTitle(getString(R.string.app_running_msg_notification_title)) 141 .setContentText(getString(R.string.app_running_msg_notification_content)) 142 .build(); 143 startForeground(SERVICE_STARTED_NOTIFICATION_ID, notification); 144 } 145 146 @Override onDestroy()147 public void onDestroy() { 148 super.onDestroy(); 149 L.d(TAG, "onDestroy"); 150 mMessengerDelegate.onDestroy(); 151 mBluetoothMonitor.onDestroy(); 152 } 153 154 @Override onStartCommand(Intent intent, int flags, int startId)155 public int onStartCommand(Intent intent, int flags, int startId) { 156 final int result = START_STICKY; 157 158 if (intent == null || intent.getAction() == null) return result; 159 160 final String action = intent.getAction(); 161 162 if (!hasRequiredArgs(intent)) { 163 L.e(TAG, "Dropping command: %s. Reason: Missing required argument.", action); 164 return result; 165 } 166 167 switch (action) { 168 case ACTION_START: 169 // NO-OP 170 break; 171 case ACTION_VOICE_REPLY: 172 voiceReply(intent); 173 break; 174 case ACTION_CLEAR_NOTIFICATION_STATE: 175 clearNotificationState(intent); 176 break; 177 case ACTION_MARK_AS_READ: 178 markAsRead(intent); 179 break; 180 case ACTION_RECEIVED_SMS: 181 // NO-OP 182 break; 183 case ACTION_RECEIVED_MMS: 184 // NO-OP 185 break; 186 case TelephonyManager.ACTION_RESPOND_VIA_MESSAGE: 187 respondViaMessage(intent); 188 break; 189 default: 190 L.w(TAG, "Unsupported action: %s", action); 191 } 192 193 return result; 194 } 195 196 /** 197 * Checks that the intent has all of the required arguments for its requested action. 198 * 199 * @param intent the intent to check 200 * @return true if the intent has all of the required {@link Bundle} args for its action 201 */ hasRequiredArgs(Intent intent)202 private static boolean hasRequiredArgs(Intent intent) { 203 switch (intent.getAction()) { 204 case ACTION_VOICE_REPLY: 205 case ACTION_CLEAR_NOTIFICATION_STATE: 206 case ACTION_MARK_AS_READ: 207 if (!intent.hasExtra(EXTRA_SENDER_KEY)) { 208 L.w(TAG, "Intent %s missing sender-key extra.", intent.getAction()); 209 return false; 210 } 211 return true; 212 default: 213 // For unknown actions, default to true. We'll report an error for these later. 214 return true; 215 } 216 } 217 218 /** 219 * Sends a reply, meant to be used from a caller originating from voice input. 220 * 221 * @param intent intent containing {@link MessengerService#EXTRA_SENDER_KEY} and 222 * a {@link RemoteInput} with {@link MessengerService#REMOTE_INPUT_KEY} resultKey 223 */ voiceReply(Intent intent)224 public void voiceReply(Intent intent) { 225 final SenderKey senderKey = intent.getParcelableExtra(EXTRA_SENDER_KEY); 226 final Bundle bundle = RemoteInput.getResultsFromIntent(intent); 227 if (bundle == null) { 228 L.e(TAG, "Dropping voice reply. Received null RemoteInput result!"); 229 return; 230 } 231 final CharSequence message = bundle.getCharSequence(REMOTE_INPUT_KEY); 232 L.d(TAG, "voiceReply"); 233 if (!TextUtils.isEmpty(message)) { 234 mMessengerDelegate.sendMessage(senderKey, message.toString()); 235 } 236 } 237 238 /** 239 * Clears notification(s) associated with a given sender key. 240 * 241 * @param intent intent containing {@link MessengerService#EXTRA_SENDER_KEY} bundle argument 242 */ clearNotificationState(Intent intent)243 public void clearNotificationState(Intent intent) { 244 final SenderKey senderKey = intent.getParcelableExtra(EXTRA_SENDER_KEY); 245 L.d(TAG, "clearNotificationState"); 246 mMessengerDelegate.clearNotifications(key -> key.equals(senderKey)); 247 } 248 249 /** 250 * Mark a conversation associated with a given sender key as read. 251 * 252 * @param intent intent containing {@link MessengerService#EXTRA_SENDER_KEY} bundle argument 253 */ markAsRead(Intent intent)254 public void markAsRead(Intent intent) { 255 final SenderKey senderKey = intent.getParcelableExtra(EXTRA_SENDER_KEY); 256 L.d(TAG, "markAsRead"); 257 mMessengerDelegate.excludeFromNotification(senderKey); 258 } 259 260 /** 261 * Respond to a call via text message. 262 * 263 * @param intent intent containing a URI describing the recipient and the URI schema 264 */ respondViaMessage(Intent intent)265 public void respondViaMessage(Intent intent) { 266 Bundle extras = intent.getExtras(); 267 if (extras == null) { 268 L.v(TAG, "Called to send SMS but no extras"); 269 return; 270 } 271 272 // TODO: get senderKey from the recipient's address, and sendMessage() to it. 273 } 274 } 275