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
17package com.android.car.messenger;
18
19import android.app.Service;
20import android.bluetooth.BluetoothAdapter;
21import android.bluetooth.BluetoothDevice;
22import android.bluetooth.BluetoothMapClient;
23import android.bluetooth.BluetoothProfile;
24import android.content.BroadcastReceiver;
25import android.content.Context;
26import android.content.Intent;
27import android.content.IntentFilter;
28import android.os.Binder;
29import android.os.IBinder;
30import android.util.Log;
31import 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 */
42public 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