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