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