blob: 01fc9541b314c1cab06d8ad08e2cb3562beae47e [file] [log] [blame]
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.messenger;
import android.app.Service;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothMapClient;
import android.bluetooth.BluetoothProfile;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Binder;
import android.os.IBinder;
import android.util.Log;
import android.widget.Toast;
/**
* Background started service that hosts messaging components.
* <p>
* The MapConnector manages connecting to the BT MAP service and the MapMessageMonitor listens for
* new incoming messages and publishes notifications. Actions in the notifications trigger command
* intents to this service (e.g. auto-reply, play message).
* <p>
* This service and its helper components run entirely in the main thread.
*/
public class MessengerService extends Service {
static final String TAG = "MessengerService";
static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG);
// Used to start this service at boot-complete. Takes no arguments.
static final String ACTION_START = "com.android.car.messenger.ACTION_START";
// Used to auto-reply to messages from a sender (invoked from Notification).
static final String ACTION_AUTO_REPLY = "com.android.car.messenger.ACTION_AUTO_REPLY";
// Used to play-out messages from a sender (invoked from Notification).
static final String ACTION_PLAY_MESSAGES = "com.android.car.messenger.ACTION_PLAY_MESSAGES";
// Used to stop further audio notifications from the conversation.
static final String ACTION_MUTE_CONVERSATION =
"com.android.car.messenger.ACTION_MUTE_CONVERSATION";
// Used to resume further audio notifications from the conversation.
static final String ACTION_UNMUTE_CONVERSATION =
"com.android.car.messenger.ACTION_UNMUTE_CONVERSATION";
// Used to clear notification state when user dismisses notification.
static final String ACTION_CLEAR_NOTIFICATION_STATE =
"com.android.car.messenger.ACTION_CLEAR_NOTIFICATION_STATE";
// Used to stop current play-out (invoked from Notification).
static final String ACTION_STOP_PLAYOUT = "com.android.car.messenger.ACTION_STOP_PLAYOUT";
// Common extra for ACTION_AUTO_REPLY and ACTION_PLAY_MESSAGES.
static final String EXTRA_SENDER_KEY = "com.android.car.messenger.EXTRA_SENDER_KEY";
static final String EXTRA_REPLY_MESSAGE = "com.android.car.messenger.EXTRA_REPLY_MESSAGE";
// Used to notify that this service started to play out the messages.
static final String ACTION_PLAY_MESSAGES_STARTED =
"com.android.car.messenger.ACTION_PLAY_MESSAGES_STARTED";
// Used to notify that this service finished playing out the messages.
static final String ACTION_PLAY_MESSAGES_STOPPED =
"com.android.car.messenger.ACTION_PLAY_MESSAGES_STOPPED";
private MapMessageMonitor mMessageMonitor;
private MapDeviceMonitor mDeviceMonitor;
private BluetoothMapClient mMapClient;
private final IBinder mBinder = new LocalBinder();
public class LocalBinder extends Binder {
MessengerService getService() {
return MessengerService.this;
}
}
@Override
public void onCreate() {
if (DBG) {
Log.d(TAG, "onCreate");
}
mMessageMonitor = new MapMessageMonitor(this);
mDeviceMonitor = new MapDeviceMonitor();
connectToMap();
}
private void connectToMap() {
if (DBG) {
Log.d(TAG, "Connecting to MAP service");
}
BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
if (adapter == null) {
// This *should* never happen. Unless there's some severe internal error?
Log.wtf(TAG, "BluetoothAdapter is null! Internal error?");
return;
}
if (!adapter.getProfileProxy(this, mMapServiceListener, BluetoothProfile.MAP_CLIENT)) {
// This *should* never happen. Unless arguments passed are incorrect somehow...
Log.wtf(TAG, "Unable to get MAP profile! Possible programmer error?");
return;
}
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (DBG) {
Log.d(TAG, "Handling intent: " + intent);
}
// Service will be restarted even if its killed/dies. It will never stop itself.
// It may be restarted with null intent or one of the other intents e.g. REPLY, PLAY etc.
final int result = START_STICKY;
if (intent == null || ACTION_START.equals(intent.getAction())) {
// These are NO-OP's since they're just used to bring up this service.
return result;
}
if (!hasRequiredArgs(intent)) {
return result;
}
switch (intent.getAction()) {
case ACTION_AUTO_REPLY:
boolean success;
if (mMapClient != null) {
success = mMessageMonitor.sendAutoReply(
intent.getParcelableExtra(EXTRA_SENDER_KEY),
mMapClient,
intent.getStringExtra(EXTRA_REPLY_MESSAGE));
} else {
Log.e(TAG, "Unable to send reply; MAP profile disconnected!");
success = false;
}
if (!success) {
Toast.makeText(this, R.string.auto_reply_failed_message, Toast.LENGTH_SHORT)
.show();
}
break;
case ACTION_PLAY_MESSAGES:
mMessageMonitor.playMessages(intent.getParcelableExtra(EXTRA_SENDER_KEY));
break;
case ACTION_MUTE_CONVERSATION:
mMessageMonitor.toggleMuteConversation(
intent.getParcelableExtra(EXTRA_SENDER_KEY), true);
break;
case ACTION_UNMUTE_CONVERSATION:
mMessageMonitor.toggleMuteConversation(
intent.getParcelableExtra(EXTRA_SENDER_KEY), false);
break;
case ACTION_STOP_PLAYOUT:
mMessageMonitor.stopPlayout();
break;
case ACTION_CLEAR_NOTIFICATION_STATE:
mMessageMonitor.clearNotificationState(intent.getParcelableExtra(EXTRA_SENDER_KEY));
break;
default:
Log.e(TAG, "Ignoring unknown intent: " + intent.getAction());
}
return result;
}
/**
* @return {code true} if the service is playing the TTS of the message.
*/
public boolean isPlaying() {
return mMessageMonitor.isPlaying();
}
private boolean hasRequiredArgs(Intent intent) {
switch (intent.getAction()) {
case ACTION_AUTO_REPLY:
case ACTION_PLAY_MESSAGES:
case ACTION_MUTE_CONVERSATION:
case ACTION_CLEAR_NOTIFICATION_STATE:
if (!intent.hasExtra(EXTRA_SENDER_KEY)) {
Log.w(TAG, "Intent is missing sender-key extra: " + intent.getAction());
return false;
}
return true;
case ACTION_STOP_PLAYOUT:
// No args.
return true;
default:
// For unknown actions, default to true. We'll report error on these later.
return true;
}
}
@Override
public void onDestroy() {
if (DBG) {
Log.d(TAG, "onDestroy");
}
if (mMapClient != null) {
mMapClient.close();
}
mDeviceMonitor.cleanup();
mMessageMonitor.cleanup();
}
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
// NOTE: These callbacks are invoked on the main thread.
private final BluetoothProfile.ServiceListener mMapServiceListener =
new BluetoothProfile.ServiceListener() {
@Override
public void onServiceConnected(int profile, BluetoothProfile proxy) {
mMapClient = (BluetoothMapClient) proxy;
if (MessengerService.DBG) {
Log.d(TAG, "Connected to MAP service!");
}
// Since we're connected, we will received broadcasts for any new messages
// in the MapMessageMonitor.
}
@Override
public void onServiceDisconnected(int profile) {
if (MessengerService.DBG) {
Log.d(TAG, "Disconnected from MAP service!");
}
mMapClient = null;
mMessageMonitor.handleMapDisconnect();
}
};
private class MapDeviceMonitor extends BroadcastReceiver {
MapDeviceMonitor() {
if (DBG) {
Log.d(TAG, "Registering Map device monitor");
}
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(BluetoothMapClient.ACTION_CONNECTION_STATE_CHANGED);
registerReceiver(this, intentFilter, android.Manifest.permission.BLUETOOTH, null);
}
void cleanup() {
unregisterReceiver(this);
}
@Override
public void onReceive(Context context, Intent intent) {
int state = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1);
int previousState = intent.getIntExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, -1);
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
if (state == -1 || previousState == -1 || device == null) {
Log.w(TAG, "Skipping broadcast, missing required extra");
return;
}
if (previousState == BluetoothProfile.STATE_CONNECTED
&& state != BluetoothProfile.STATE_CONNECTED) {
if (DBG) {
Log.d(TAG, "Device losing MAP connection: " + device);
}
mMessageMonitor.handleDeviceDisconnect(device);
}
}
}
}