• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 {
getService()83         MessengerService getService() {
84             return MessengerService.this;
85         }
86     }
87 
88     @Override
onCreate()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 
connectToMap()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
onStartCommand(Intent intent, int flags, int startId)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      */
isPlaying()178     public boolean isPlaying() {
179         return mMessageMonitor.isPlaying();
180     }
181 
hasRequiredArgs(Intent intent)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
onDestroy()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
onBind(Intent intent)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 {
MapDeviceMonitor()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 
cleanup()253         void cleanup() {
254             unregisterReceiver(this);
255         }
256 
257         @Override
onReceive(Context context, Intent intent)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