• 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.IBinder;
29 import android.util.Log;
30 import android.widget.Toast;
31 
32 /**
33  * Background started service that hosts messaging components.
34  * <p>
35  * The MapConnector manages connecting to the BT MAP service and the MapMessageMonitor listens for
36  * new incoming messages and publishes notifications. Actions in the notifications trigger command
37  * intents to this service (e.g. auto-reply, play message).
38  * <p>
39  * This service and its helper components run entirely in the main thread.
40  */
41 public class MessengerService extends Service {
42     static final String TAG = "MessengerService";
43     static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG);
44     static final boolean VDBG = Log.isLoggable(TAG, Log.VERBOSE);
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 clear notification state when user dismisses notification.
53     static final String ACTION_CLEAR_NOTIFICATION_STATE =
54             "com.android.car.messenger.ACTION_CLEAR_NOTIFICATION_STATE";
55     // Used to stop current play-out (invoked from Notification).
56     static final String ACTION_STOP_PLAYOUT = "com.android.car.messenger.ACTION_STOP_PLAYOUT";
57 
58     // Common extra for ACTION_AUTO_REPLY and ACTION_PLAY_MESSAGES.
59     static final String EXTRA_SENDER_KEY = "com.android.car.messenger.EXTRA_SENDER_KEY";
60 
61     private MapMessageMonitor mMessageMonitor;
62     private MapDeviceMonitor mDeviceMonitor;
63     private BluetoothMapClient mMapClient;
64 
65     @Override
onCreate()66     public void onCreate() {
67         if (DBG) {
68             Log.d(TAG, "onCreate");
69         }
70 
71         mMessageMonitor = new MapMessageMonitor(this);
72         mDeviceMonitor = new MapDeviceMonitor();
73         connectToMap();
74     }
75 
connectToMap()76     private void connectToMap() {
77         if (DBG) {
78             Log.d(TAG, "Connecting to MAP service");
79         }
80         BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
81         if (adapter == null) {
82             // This *should* never happen. Unless there's some severe internal error?
83             Log.wtf(TAG, "BluetoothAdapter is null! Internal error?");
84             return;
85         }
86 
87         if (!adapter.getProfileProxy(this, mMapServiceListener, BluetoothProfile.MAP_CLIENT)) {
88             // This *should* never happen.  Unless arguments passed are incorrect somehow...
89             Log.wtf(TAG, "Unable to get MAP profile! Possible programmer error?");
90             return;
91         }
92     }
93 
94     @Override
onStartCommand(Intent intent, int flags, int startId)95     public int onStartCommand(Intent intent, int flags, int startId) {
96         if (DBG) {
97             Log.d(TAG, "Handling intent: " + intent.getAction());
98         }
99 
100         // Service will be restarted even if its killed/dies. It will never stop itself.
101         // It may be restarted with null intent or one of the other intents e.g. REPLY, PLAY etc.
102         final int result = START_STICKY;
103 
104         if (intent == null || ACTION_START.equals(intent.getAction())) {
105             // These are NO-OP's since they're just used to bring up this service.
106             return result;
107         }
108 
109         if (!hasRequiredArgs(intent)) {
110             return result;
111         }
112         switch (intent.getAction()) {
113             case ACTION_AUTO_REPLY:
114                 boolean success;
115                 if (mMapClient != null) {
116                     success = mMessageMonitor.sendAutoReply(
117                             intent.getParcelableExtra(EXTRA_SENDER_KEY), mMapClient);
118                 } else {
119                     Log.e(TAG, "Unable to send reply; MAP profile disconnected!");
120                     success = false;
121                 }
122                 if (!success) {
123                     Toast.makeText(this, R.string.auto_reply_failed_message, Toast.LENGTH_SHORT)
124                             .show();
125                 }
126                 break;
127             case ACTION_PLAY_MESSAGES:
128                 mMessageMonitor.playMessages(intent.getParcelableExtra(EXTRA_SENDER_KEY));
129                 break;
130             case ACTION_STOP_PLAYOUT:
131                 mMessageMonitor.stopPlayout();
132                 break;
133             case ACTION_CLEAR_NOTIFICATION_STATE:
134                 mMessageMonitor.clearNotificationState(intent.getParcelableExtra(EXTRA_SENDER_KEY));
135                 break;
136             default:
137                 Log.e(TAG, "Ignoring unknown intent: " + intent.getAction());
138         }
139         return result;
140     }
141 
hasRequiredArgs(Intent intent)142     private boolean hasRequiredArgs(Intent intent) {
143         switch (intent.getAction()) {
144             case ACTION_AUTO_REPLY:
145             case ACTION_PLAY_MESSAGES:
146             case ACTION_CLEAR_NOTIFICATION_STATE:
147                 if (!intent.hasExtra(EXTRA_SENDER_KEY)) {
148                     Log.w(TAG, "Intent is missing sender-key extra: " + intent.getAction());
149                     return false;
150                 }
151                 return true;
152             case ACTION_STOP_PLAYOUT:
153                 // No args.
154                 return true;
155             default:
156                 // For unknown actions, default to true. We'll report error on these later.
157                 return true;
158         }
159     }
160 
161     @Override
onDestroy()162     public void onDestroy() {
163         if (DBG) {
164             Log.d(TAG, "onDestroy");
165         }
166         if (mMapClient != null) {
167             mMapClient.close();
168         }
169         mDeviceMonitor.cleanup();
170         mMessageMonitor.cleanup();
171     }
172 
173     @Override
onBind(Intent intent)174     public IBinder onBind(Intent intent) {
175         return null;
176     }
177 
178     // NOTE: These callbacks are invoked on the main thread.
179     private final BluetoothProfile.ServiceListener mMapServiceListener =
180             new BluetoothProfile.ServiceListener() {
181         @Override
182         public void onServiceConnected(int profile, BluetoothProfile proxy) {
183             mMapClient = (BluetoothMapClient) proxy;
184             if (MessengerService.DBG) {
185                 Log.d(TAG, "Connected to MAP service!");
186             }
187 
188             // Since we're connected, we will received broadcasts for any new messages
189             // in the MapMessageMonitor.
190         }
191 
192         @Override
193         public void onServiceDisconnected(int profile) {
194             if (MessengerService.DBG) {
195                 Log.d(TAG, "Disconnected from MAP service!");
196             }
197             mMapClient = null;
198             mMessageMonitor.handleMapDisconnect();
199         }
200     };
201 
202     private class MapDeviceMonitor extends BroadcastReceiver {
MapDeviceMonitor()203         MapDeviceMonitor() {
204             if (DBG) {
205                 Log.d(TAG, "Registering Map device monitor");
206             }
207             IntentFilter intentFilter = new IntentFilter();
208             intentFilter.addAction(BluetoothMapClient.ACTION_CONNECTION_STATE_CHANGED);
209             registerReceiver(this, intentFilter, android.Manifest.permission.BLUETOOTH, null);
210         }
211 
cleanup()212         void cleanup() {
213             unregisterReceiver(this);
214         }
215 
216         @Override
onReceive(Context context, Intent intent)217         public void onReceive(Context context, Intent intent) {
218             int state = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1);
219             int previousState = intent.getIntExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, -1);
220             BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
221             if (state == -1 || previousState == -1 || device == null) {
222                 Log.w(TAG, "Skipping broadcast, missing required extra");
223                 return;
224             }
225             if (previousState == BluetoothProfile.STATE_CONNECTED
226                     && state != BluetoothProfile.STATE_CONNECTED) {
227                 if (DBG) {
228                     Log.d(TAG, "Device losing MAP connection: " + device);
229                 }
230                 mMessageMonitor.handleDeviceDisconnect(device);
231             }
232         }
233     }
234 }
235