blob: 3ea681f8cc113ea9ef990acdaac29ea69801f8db [file] [log] [blame]
package com.android.car.messenger;
import android.app.Notification;
import android.app.Notification.Action;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.Service;
import android.content.Intent;
import android.media.AudioAttributes;
import android.os.Binder;
import android.os.Bundle;
import android.os.IBinder;
import android.provider.Settings;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
import androidx.core.app.NotificationCompat;
import androidx.core.app.RemoteInput;
import com.android.car.messenger.bluetooth.BluetoothMonitor;
import com.android.car.messenger.log.L;
/** Service responsible for handling SMS messaging events from paired Bluetooth devices. */
public class MessengerService extends Service {
private final static String TAG = "CM.MessengerService";
/* ACTIONS */
/** Used to start this service at boot-complete. Takes no arguments. */
public static final String ACTION_START = "com.android.car.messenger.ACTION_START";
/** Used to reply to message with voice input; triggered by an assistant. */
public static final String ACTION_VOICE_REPLY = "com.android.car.messenger.ACTION_VOICE_REPLY";
/** Used to clear notification state when user dismisses notification. */
public static final String ACTION_CLEAR_NOTIFICATION_STATE =
"com.android.car.messenger.ACTION_CLEAR_NOTIFICATION_STATE";
/** Used to mark a notification as read **/
public static final String ACTION_MARK_AS_READ =
"com.android.car.messenger.ACTION_MARK_AS_READ";
/** Used to notify when a sms is received. Takes no arguments. */
public static final String ACTION_RECEIVED_SMS =
"com.android.car.messenger.ACTION_RECEIVED_SMS";
/** Used to notify when a mms is received. Takes no arguments. */
public static final String ACTION_RECEIVED_MMS =
"com.android.car.messenger.ACTION_RECEIVED_MMS";
/* EXTRAS */
/** Key under which the {@link SenderKey} is provided. */
public static final String EXTRA_SENDER_KEY = "com.android.car.messenger.EXTRA_SENDER_KEY";
/**
* The resultKey of the {@link RemoteInput} which is sent in the reply callback {@link Action}.
*/
public static final String REMOTE_INPUT_KEY = "REMOTE_INPUT_KEY";
/* NOTIFICATIONS */
static final String SMS_CHANNEL_ID = "SMS_CHANNEL_ID";
private static final String APP_RUNNING_CHANNEL_ID = "APP_RUNNING_CHANNEL_ID";
private static final int SERVICE_STARTED_NOTIFICATION_ID = Integer.MAX_VALUE;
/** Delegate class used to handle this services' actions */
private MessengerDelegate mMessengerDelegate;
/** Notifies this service of new bluetooth actions */
private BluetoothMonitor mBluetoothMonitor;
/* Binding boilerplate */
private final IBinder mBinder = new LocalBinder();
public class LocalBinder extends Binder {
MessengerService getService() {
return MessengerService.this;
}
}
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
@Override
public void onCreate() {
super.onCreate();
L.d(TAG, "onCreate");
mMessengerDelegate = new MessengerDelegate(this);
mBluetoothMonitor = new BluetoothMonitor(this);
mBluetoothMonitor.registerListener(mMessengerDelegate);
sendServiceRunningNotification();
}
private void sendServiceRunningNotification() {
NotificationManager notificationManager = getSystemService(NotificationManager.class);
if (notificationManager == null) {
L.e(TAG, "Failed to get NotificationManager instance");
return;
}
// Create notification channel for app running notification
{
NotificationChannel appRunningNotificationChannel =
new NotificationChannel(APP_RUNNING_CHANNEL_ID,
getString(R.string.app_running_msg_channel_name),
NotificationManager.IMPORTANCE_MIN);
notificationManager.createNotificationChannel(appRunningNotificationChannel);
}
{
AudioAttributes attributes = new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_NOTIFICATION)
.build();
NotificationChannel smsChannel = new NotificationChannel(SMS_CHANNEL_ID,
getString(R.string.sms_channel_name),
NotificationManager.IMPORTANCE_HIGH);
smsChannel.setDescription(getString(R.string.sms_channel_description));
smsChannel.setSound(Settings.System.DEFAULT_NOTIFICATION_URI, attributes);
notificationManager.createNotificationChannel(smsChannel);
}
final Notification notification =
new NotificationCompat.Builder(this, APP_RUNNING_CHANNEL_ID)
.setSmallIcon(R.drawable.ic_message)
.setContentTitle(getString(R.string.app_running_msg_notification_title))
.setContentText(getString(R.string.app_running_msg_notification_content))
.build();
startForeground(SERVICE_STARTED_NOTIFICATION_ID, notification);
}
@Override
public void onDestroy() {
super.onDestroy();
L.d(TAG, "onDestroy");
mMessengerDelegate.cleanup();
mBluetoothMonitor.cleanup();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
final int result = START_STICKY;
if (intent == null || intent.getAction() == null) return result;
final String action = intent.getAction();
if (!hasRequiredArgs(intent)) {
L.e(TAG, "Dropping command: %s. Reason: Missing required argument.", action);
return result;
}
switch (action) {
case ACTION_START:
// NO-OP
break;
case ACTION_VOICE_REPLY:
voiceReply(intent);
break;
case ACTION_CLEAR_NOTIFICATION_STATE:
clearNotificationState(intent);
break;
case ACTION_MARK_AS_READ:
markAsRead(intent);
break;
case ACTION_RECEIVED_SMS:
// NO-OP
break;
case ACTION_RECEIVED_MMS:
// NO-OP
break;
case TelephonyManager.ACTION_RESPOND_VIA_MESSAGE:
respondViaMessage(intent);
break;
default:
L.w(TAG, "Unsupported action: %s", action);
}
return result;
}
/**
* Checks that the intent has all of the required arguments for its requested action.
*
* @param intent the intent to check
* @return true if the intent has all of the required {@link Bundle} args for its action
*/
private static boolean hasRequiredArgs(Intent intent) {
switch (intent.getAction()) {
case ACTION_VOICE_REPLY:
case ACTION_CLEAR_NOTIFICATION_STATE:
case ACTION_MARK_AS_READ:
if (!intent.hasExtra(EXTRA_SENDER_KEY)) {
L.w(TAG, "Intent %s missing sender-key extra.", intent.getAction());
return false;
}
return true;
default:
// For unknown actions, default to true. We'll report an error for these later.
return true;
}
}
/**
* Sends a reply, meant to be used from a caller originating from voice input.
*
* @param intent intent containing {@link MessengerService#EXTRA_SENDER_KEY} and
* a {@link RemoteInput} with {@link MessengerService#REMOTE_INPUT_KEY} resultKey
*/
public void voiceReply(Intent intent) {
final SenderKey senderKey = intent.getParcelableExtra(EXTRA_SENDER_KEY);
final Bundle bundle = RemoteInput.getResultsFromIntent(intent);
if (bundle == null) {
L.e(TAG, "Dropping voice reply. Received null RemoteInput result!");
return;
}
final CharSequence message = bundle.getCharSequence(REMOTE_INPUT_KEY);
L.d(TAG, "voiceReply");
if (!TextUtils.isEmpty(message)) {
mMessengerDelegate.sendMessage(senderKey, message.toString());
}
}
/**
* Clears notification(s) associated with a given sender key.
*
* @param intent intent containing {@link MessengerService#EXTRA_SENDER_KEY} bundle argument
*/
public void clearNotificationState(Intent intent) {
final SenderKey senderKey = intent.getParcelableExtra(EXTRA_SENDER_KEY);
L.d(TAG, "clearNotificationState");
mMessengerDelegate.clearNotifications(key -> key.equals(senderKey));
}
/**
* Mark a conversation associated with a given sender key as read.
*
* @param intent intent containing {@link MessengerService#EXTRA_SENDER_KEY} bundle argument
*/
public void markAsRead(Intent intent) {
final SenderKey senderKey = intent.getParcelableExtra(EXTRA_SENDER_KEY);
L.d(TAG, "markAsRead");
mMessengerDelegate.markAsRead(senderKey);
}
/**
* Respond to a call via text message.
*
* @param intent intent containing a URI describing the recipient and the URI schema
*/
public void respondViaMessage(Intent intent) {
Bundle extras = intent.getExtras();
if (extras == null) {
L.v(TAG, "Called to send SMS but no extras");
return;
}
// TODO: get senderKey from the recipient's address, and sendMessage() to it.
}
}