Home | History | Annotate | Download | only in messenger
      1 /*
      2  * Copyright (C) 2017 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.car.messenger;
     18 
     19 import android.app.Service;
     20 import android.bluetooth.BluetoothAdapter;
     21 import android.bluetooth.BluetoothDevice;
     22 import android.bluetooth.BluetoothMapClient;
     23 import android.bluetooth.BluetoothProfile;
     24 import android.content.BroadcastReceiver;
     25 import android.content.Context;
     26 import android.content.Intent;
     27 import android.content.IntentFilter;
     28 import android.os.Binder;
     29 import android.os.IBinder;
     30 import android.util.Log;
     31 import android.widget.Toast;
     32 
     33 /**
     34  * Background started service that hosts messaging components.
     35  * <p>
     36  * The MapConnector manages connecting to the BT MAP service and the MapMessageMonitor listens for
     37  * new incoming messages and publishes notifications. Actions in the notifications trigger command
     38  * intents to this service (e.g. auto-reply, play message).
     39  * <p>
     40  * This service and its helper components run entirely in the main thread.
     41  */
     42 public class MessengerService extends Service {
     43     static final String TAG = "MessengerService";
     44     static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG);
     45 
     46     // Used to start this service at boot-complete. Takes no arguments.
     47     static final String ACTION_START = "com.android.car.messenger.ACTION_START";
     48     // Used to auto-reply to messages from a sender (invoked from Notification).
     49     static final String ACTION_AUTO_REPLY = "com.android.car.messenger.ACTION_AUTO_REPLY";
     50     // Used to play-out messages from a sender (invoked from Notification).
     51     static final String ACTION_PLAY_MESSAGES = "com.android.car.messenger.ACTION_PLAY_MESSAGES";
     52     // Used to stop further audio notifications from the conversation.
     53     static final String ACTION_MUTE_CONVERSATION =
     54             "com.android.car.messenger.ACTION_MUTE_CONVERSATION";
     55     // Used to resume further audio notifications from the conversation.
     56     static final String ACTION_UNMUTE_CONVERSATION =
     57             "com.android.car.messenger.ACTION_UNMUTE_CONVERSATION";
     58     // Used to clear notification state when user dismisses notification.
     59     static final String ACTION_CLEAR_NOTIFICATION_STATE =
     60             "com.android.car.messenger.ACTION_CLEAR_NOTIFICATION_STATE";
     61     // Used to stop current play-out (invoked from Notification).
     62     static final String ACTION_STOP_PLAYOUT = "com.android.car.messenger.ACTION_STOP_PLAYOUT";
     63 
     64     // Common extra for ACTION_AUTO_REPLY and ACTION_PLAY_MESSAGES.
     65     static final String EXTRA_SENDER_KEY = "com.android.car.messenger.EXTRA_SENDER_KEY";
     66 
     67     static final String EXTRA_REPLY_MESSAGE = "com.android.car.messenger.EXTRA_REPLY_MESSAGE";
     68 
     69     // Used to notify that this service started to play out the messages.
     70     static final String ACTION_PLAY_MESSAGES_STARTED =
     71             "com.android.car.messenger.ACTION_PLAY_MESSAGES_STARTED";
     72 
     73     // Used to notify that this service finished playing out the messages.
     74     static final String ACTION_PLAY_MESSAGES_STOPPED =
     75             "com.android.car.messenger.ACTION_PLAY_MESSAGES_STOPPED";
     76 
     77     private MapMessageMonitor mMessageMonitor;
     78     private MapDeviceMonitor mDeviceMonitor;
     79     private BluetoothMapClient mMapClient;
     80     private final IBinder mBinder = new LocalBinder();
     81 
     82     public class LocalBinder extends Binder {
     83         MessengerService getService() {
     84             return MessengerService.this;
     85         }
     86     }
     87 
     88     @Override
     89     public void onCreate() {
     90         if (DBG) {
     91             Log.d(TAG, "onCreate");
     92         }
     93 
     94         mMessageMonitor = new MapMessageMonitor(this);
     95         mDeviceMonitor = new MapDeviceMonitor();
     96         connectToMap();
     97     }
     98 
     99     private void connectToMap() {
    100         if (DBG) {
    101             Log.d(TAG, "Connecting to MAP service");
    102         }
    103         BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
    104         if (adapter == null) {
    105             // This *should* never happen. Unless there's some severe internal error?
    106             Log.wtf(TAG, "BluetoothAdapter is null! Internal error?");
    107             return;
    108         }
    109 
    110         if (!adapter.getProfileProxy(this, mMapServiceListener, BluetoothProfile.MAP_CLIENT)) {
    111             // This *should* never happen.  Unless arguments passed are incorrect somehow...
    112             Log.wtf(TAG, "Unable to get MAP profile! Possible programmer error?");
    113             return;
    114         }
    115     }
    116 
    117     @Override
    118     public int onStartCommand(Intent intent, int flags, int startId) {
    119         if (DBG) {
    120             Log.d(TAG, "Handling intent: " + intent);
    121         }
    122 
    123         // Service will be restarted even if its killed/dies. It will never stop itself.
    124         // It may be restarted with null intent or one of the other intents e.g. REPLY, PLAY etc.
    125         final int result = START_STICKY;
    126 
    127         if (intent == null || ACTION_START.equals(intent.getAction())) {
    128             // These are NO-OP's since they're just used to bring up this service.
    129             return result;
    130         }
    131 
    132         if (!hasRequiredArgs(intent)) {
    133             return result;
    134         }
    135         switch (intent.getAction()) {
    136             case ACTION_AUTO_REPLY:
    137                 boolean success;
    138                 if (mMapClient != null) {
    139                     success = mMessageMonitor.sendAutoReply(
    140                             intent.getParcelableExtra(EXTRA_SENDER_KEY),
    141                             mMapClient,
    142                             intent.getStringExtra(EXTRA_REPLY_MESSAGE));
    143                 } else {
    144                     Log.e(TAG, "Unable to send reply; MAP profile disconnected!");
    145                     success = false;
    146                 }
    147                 if (!success) {
    148                     Toast.makeText(this, R.string.auto_reply_failed_message, Toast.LENGTH_SHORT)
    149                             .show();
    150                 }
    151                 break;
    152             case ACTION_PLAY_MESSAGES:
    153                 mMessageMonitor.playMessages(intent.getParcelableExtra(EXTRA_SENDER_KEY));
    154                 break;
    155             case ACTION_MUTE_CONVERSATION:
    156                 mMessageMonitor.toggleMuteConversation(
    157                         intent.getParcelableExtra(EXTRA_SENDER_KEY), true);
    158                 break;
    159             case ACTION_UNMUTE_CONVERSATION:
    160                 mMessageMonitor.toggleMuteConversation(
    161                         intent.getParcelableExtra(EXTRA_SENDER_KEY), false);
    162                 break;
    163             case ACTION_STOP_PLAYOUT:
    164                 mMessageMonitor.stopPlayout();
    165                 break;
    166             case ACTION_CLEAR_NOTIFICATION_STATE:
    167                 mMessageMonitor.clearNotificationState(intent.getParcelableExtra(EXTRA_SENDER_KEY));
    168                 break;
    169             default:
    170                 Log.e(TAG, "Ignoring unknown intent: " + intent.getAction());
    171         }
    172         return result;
    173     }
    174 
    175     /**
    176      * @return {code true} if the service is playing the TTS of the message.
    177      */
    178     public boolean isPlaying() {
    179         return mMessageMonitor.isPlaying();
    180     }
    181 
    182     private boolean hasRequiredArgs(Intent intent) {
    183         switch (intent.getAction()) {
    184             case ACTION_AUTO_REPLY:
    185             case ACTION_PLAY_MESSAGES:
    186             case ACTION_MUTE_CONVERSATION:
    187             case ACTION_CLEAR_NOTIFICATION_STATE:
    188                 if (!intent.hasExtra(EXTRA_SENDER_KEY)) {
    189                     Log.w(TAG, "Intent is missing sender-key extra: " + intent.getAction());
    190                     return false;
    191                 }
    192                 return true;
    193             case ACTION_STOP_PLAYOUT:
    194                 // No args.
    195                 return true;
    196             default:
    197                 // For unknown actions, default to true. We'll report error on these later.
    198                 return true;
    199         }
    200     }
    201 
    202     @Override
    203     public void onDestroy() {
    204         if (DBG) {
    205             Log.d(TAG, "onDestroy");
    206         }
    207         if (mMapClient != null) {
    208             mMapClient.close();
    209         }
    210         mDeviceMonitor.cleanup();
    211         mMessageMonitor.cleanup();
    212     }
    213 
    214     @Override
    215     public IBinder onBind(Intent intent) {
    216         return mBinder;
    217     }
    218 
    219     // NOTE: These callbacks are invoked on the main thread.
    220     private final BluetoothProfile.ServiceListener mMapServiceListener =
    221             new BluetoothProfile.ServiceListener() {
    222         @Override
    223         public void onServiceConnected(int profile, BluetoothProfile proxy) {
    224             mMapClient = (BluetoothMapClient) proxy;
    225             if (MessengerService.DBG) {
    226                 Log.d(TAG, "Connected to MAP service!");
    227             }
    228 
    229             // Since we're connected, we will received broadcasts for any new messages
    230             // in the MapMessageMonitor.
    231         }
    232 
    233         @Override
    234         public void onServiceDisconnected(int profile) {
    235             if (MessengerService.DBG) {
    236                 Log.d(TAG, "Disconnected from MAP service!");
    237             }
    238             mMapClient = null;
    239             mMessageMonitor.handleMapDisconnect();
    240         }
    241     };
    242 
    243     private class MapDeviceMonitor extends BroadcastReceiver {
    244         MapDeviceMonitor() {
    245             if (DBG) {
    246                 Log.d(TAG, "Registering Map device monitor");
    247             }
    248             IntentFilter intentFilter = new IntentFilter();
    249             intentFilter.addAction(BluetoothMapClient.ACTION_CONNECTION_STATE_CHANGED);
    250             registerReceiver(this, intentFilter, android.Manifest.permission.BLUETOOTH, null);
    251         }
    252 
    253         void cleanup() {
    254             unregisterReceiver(this);
    255         }
    256 
    257         @Override
    258         public void onReceive(Context context, Intent intent) {
    259             int state = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1);
    260             int previousState = intent.getIntExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, -1);
    261             BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
    262             if (state == -1 || previousState == -1 || device == null) {
    263                 Log.w(TAG, "Skipping broadcast, missing required extra");
    264                 return;
    265             }
    266             if (previousState == BluetoothProfile.STATE_CONNECTED
    267                     && state != BluetoothProfile.STATE_CONNECTED) {
    268                 if (DBG) {
    269                     Log.d(TAG, "Device losing MAP connection: " + device);
    270                 }
    271                 mMessageMonitor.handleDeviceDisconnect(device);
    272             }
    273         }
    274     }
    275 }
    276