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