merge in oc-release history after reset to master
diff --git a/Android.mk b/Android.mk
new file mode 100644
index 0000000..6d173c5
--- /dev/null
+++ b/Android.mk
@@ -0,0 +1,39 @@
+#
+# 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.
+#
+
+
+LOCAL_PATH:= $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_MODULE_TAGS := optional
+
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_PACKAGE_NAME := CarMessengerApp
+
+LOCAL_OVERRIDES_PACKAGES := messaging
+
+LOCAL_MODULE_TAGS := optional
+
+LOCAL_PROGUARD_ENABLED := disabled
+
+LOCAL_PRIVILEGED_MODULE := true
+
+LOCAL_STATIC_JAVA_LIBRARIES += android-support-v4
+
+LOCAL_DEX_PREOPT := false
+
+include $(BUILD_PACKAGE)
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
new file mode 100644
index 0000000..d59587d
--- /dev/null
+++ b/AndroidManifest.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.car.messenger">
+
+    <uses-sdk android:minSdkVersion="25" android:targetSdkVersion="25"/>
+
+    <uses-permission android:name="android.permission.BLUETOOTH"/>
+    <uses-permission android:name="android.permission.SEND_SMS"/>
+    <uses-permission android:name="android.permission.READ_SMS"/>
+    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
+
+    <application android:label="CarMessenger">
+        <service android:name=".MessengerService" android:exported="false">
+        </service>
+
+        <receiver android:name=".MessengerReceiver" android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.BOOT_COMPLETED"/>
+            </intent-filter>
+        </receiver>
+    </application>
+</manifest>
diff --git a/res/values/strings.xml b/res/values/strings.xml
new file mode 100644
index 0000000..9fab76b
--- /dev/null
+++ b/res/values/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2007 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.
+-->
+
+<resources>
+    <plurals name="notification_new_message">
+        <item quantity="one">New message</item>
+        <item quantity="other">%d new messages</item>
+    </plurals>
+
+    <string name="auto_reply_message">I\'m driving right now</string>
+    <string name="auto_reply_failed_message">Unable to send reply. Please try again.</string>
+
+    <string name="tts_says_verb">says</string>
+
+    <string name="tts_failed_toast">Text playout failed!</string>
+</resources>
diff --git a/src/com/android/car/messenger/MapMessage.java b/src/com/android/car/messenger/MapMessage.java
new file mode 100644
index 0000000..2bca390
--- /dev/null
+++ b/src/com/android/car/messenger/MapMessage.java
@@ -0,0 +1,144 @@
+/*
+ * 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.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothMapClient;
+import android.content.Intent;
+import android.support.annotation.Nullable;
+
+/**
+ * Represents a message obtained via MAP service from a connected Bluetooth device.
+ */
+class MapMessage {
+    private BluetoothDevice mDevice;
+    private String mHandle;
+    private long mReceivedTimeMs;
+    private String mSenderName;
+    @Nullable
+    private String mSenderContactUri;
+    private String mText;
+
+    /**
+     * Constructs Message from {@code intent} that was received from MAP service via
+     * {@link BluetoothMapClient#ACTION_MESSAGE_RECEIVED} broadcast.
+     *
+     * @param intent Intent received from MAP service.
+     * @return Message constructed from extras in {@code intent}.
+     * @throws IllegalArgumentException If {@code intent} is missing any required fields.
+     */
+    public static MapMessage parseFrom(Intent intent) {
+        BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
+        String handle = intent.getStringExtra(BluetoothMapClient.EXTRA_MESSAGE_HANDLE);
+        String senderContactUri = intent.getStringExtra(
+                BluetoothMapClient.EXTRA_SENDER_CONTACT_URI);
+        String senderContactName = intent.getStringExtra(
+                BluetoothMapClient.EXTRA_SENDER_CONTACT_NAME);
+        String text = intent.getStringExtra(android.content.Intent.EXTRA_TEXT);
+        return new MapMessage(device, handle, System.currentTimeMillis(), senderContactName,
+                senderContactUri, text);
+    }
+
+    private MapMessage(BluetoothDevice device,
+            String handle,
+            long receivedTimeMs,
+            String senderName,
+            @Nullable String senderContactUri,
+            String text) {
+        boolean missingDevice = (device == null);
+        boolean missingHandle = (handle == null);
+        boolean missingSenderName = (senderName == null);
+        boolean missingText = (text == null);
+        if (missingDevice || missingHandle || missingText) {
+            StringBuilder builder = new StringBuilder("Missing required fields:");
+            if (missingDevice) {
+                builder.append(" device");
+            }
+            if (missingHandle) {
+                builder.append(" handle");
+            }
+            if (missingSenderName) {
+                builder.append(" senderName");
+            }
+            if (missingText) {
+                builder.append(" text");
+            }
+            throw new IllegalArgumentException(builder.toString());
+        }
+        mDevice = device;
+        mHandle = handle;
+        mReceivedTimeMs = receivedTimeMs;
+        mText = text;
+        mSenderContactUri = senderContactUri;
+        mSenderName = senderName;
+    }
+
+    public BluetoothDevice getDevice() {
+        return mDevice;
+    }
+
+    /**
+     * @return Unique handle for this message. NOTE: The handle is only required to be unique for
+     *      the lifetime of a single MAP session.
+     */
+    public String getHandle() {
+        return mHandle;
+    }
+
+    /**
+     * @return Milliseconds since epoch at which this message notification was received on the head-
+     *      unit.
+     */
+    public long getReceivedTimeMs() {
+        return mReceivedTimeMs;
+    }
+
+    /**
+     * @return Contact name as obtained from the device. If contact is in the device's address-book,
+     *       this is typically the contact name. Otherwise it will be the phone number.
+     */
+    public String getSenderName() {
+        return mSenderName;
+    }
+
+    /**
+     * @return Sender phone number available as a URI string. iPhone's don't provide these.
+     */
+    @Nullable
+    public String getSenderContactUri() {
+        return mSenderContactUri;
+    }
+
+    /**
+     * @return Actual content of the message.
+     */
+    public String getText() {
+        return mText;
+    }
+
+    @Override
+    public String toString() {
+        return "MapMessage{" +
+                "mDevice=" + mDevice +
+                ", mHandle='" + mHandle + '\'' +
+                ", mReceivedTimeMs=" + mReceivedTimeMs +
+                ", mText='" + mText + '\'' +
+                ", mSenderContactUri='" + mSenderContactUri + '\'' +
+                ", mSenderName='" + mSenderName + '\'' +
+                '}';
+    }
+}
diff --git a/src/com/android/car/messenger/MapMessageMonitor.java b/src/com/android/car/messenger/MapMessageMonitor.java
new file mode 100644
index 0000000..926579d
--- /dev/null
+++ b/src/com/android/car/messenger/MapMessageMonitor.java
@@ -0,0 +1,389 @@
+/*
+ * 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.NotificationManager;
+import android.app.PendingIntent;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothMapClient;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.net.Uri;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.support.annotation.Nullable;
+import android.support.v4.app.NotificationCompat;
+import android.util.Log;
+import android.widget.Toast;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.function.Predicate;
+
+/**
+ * Component that monitors for incoming messages and posts/updates notifications.
+ * <p>
+ * It also handles notifications requests e.g. sending auto-replies and message play-out.
+ * <p>
+ * It will receive broadcasts for new incoming messages as long as the MapClient is connected in
+ * {@link MessengerService}.
+ */
+class MapMessageMonitor {
+    private static final String TAG = "Messenger.MsgMonitor";
+    private static final boolean DBG = MessengerService.DBG;
+
+    private final Context mContext;
+    private final BluetoothMapReceiver mBluetoothMapReceiver;
+    private final NotificationManager mNotificationManager;
+    private final Map<MessageKey, MapMessage> mMessages = new HashMap<>();
+    private final Map<SenderKey, NotificationInfo> mNotificationInfos = new HashMap<>();
+    private final TTSHelper mTTSHelper;
+
+    MapMessageMonitor(Context context) {
+        mContext = context;
+        mBluetoothMapReceiver = new BluetoothMapReceiver();
+        mNotificationManager =
+                (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
+        mTTSHelper = new TTSHelper(mContext);
+    }
+
+    private void handleNewMessage(Intent intent) {
+        if (DBG) {
+            Log.d(TAG, "Handling new message");
+        }
+        try {
+            MapMessage message = MapMessage.parseFrom(intent);
+            if (MessengerService.VDBG) {
+                Log.v(TAG, "Parsed message: " + message);
+            }
+            MessageKey messageKey = new MessageKey(message);
+            boolean repeatMessage = mMessages.containsKey(messageKey);
+            mMessages.put(messageKey, message);
+            if (!repeatMessage) {
+                updateNotificationInfo(message, messageKey);
+            }
+        } catch (IllegalArgumentException e) {
+            Log.e(TAG, "Dropping invalid MAP message", e);
+        }
+    }
+
+    private void updateNotificationInfo(MapMessage message, MessageKey messageKey) {
+        SenderKey senderKey = new SenderKey(message);
+        NotificationInfo notificationInfo = mNotificationInfos.get(senderKey);
+        if (notificationInfo == null) {
+            notificationInfo =
+                    new NotificationInfo(message.getSenderName(), message.getSenderContactUri());
+            mNotificationInfos.put(senderKey, notificationInfo);
+        }
+        notificationInfo.mMessageKeys.add(messageKey);
+        updateNotificationFor(senderKey, notificationInfo, false /* ttsPlaying */);
+    }
+
+    private void updateNotificationFor(SenderKey senderKey,
+            NotificationInfo notificationInfo, boolean ttsPlaying) {
+        NotificationCompat.Builder builder = new NotificationCompat.Builder(mContext);
+        // TODO(sriniv): Use right icon when switching to correct layout. b/33280056.
+        builder.setSmallIcon(android.R.drawable.btn_plus);
+        builder.setContentTitle(notificationInfo.mSenderName);
+        builder.setContentText(mContext.getResources().getQuantityString(
+                R.plurals.notification_new_message, notificationInfo.mMessageKeys.size(),
+                notificationInfo.mMessageKeys.size()));
+
+        Intent deleteIntent = new Intent(mContext, MessengerService.class)
+                .setAction(MessengerService.ACTION_CLEAR_NOTIFICATION_STATE)
+                .putExtra(MessengerService.EXTRA_SENDER_KEY, senderKey);
+        builder.setDeleteIntent(
+                PendingIntent.getService(mContext, notificationInfo.mNotificationId, deleteIntent,
+                        PendingIntent.FLAG_UPDATE_CURRENT));
+
+        String messageActions[] = {
+                MessengerService.ACTION_AUTO_REPLY,
+                MessengerService.ACTION_PLAY_MESSAGES
+        };
+        // TODO(sriniv): Actual spec does not have any of these strings. Remove later. b/33280056.
+        // is implemented for notifications.
+        String actionTexts[] = { "Reply", "Play" };
+        if (ttsPlaying) {
+            messageActions[1] = MessengerService.ACTION_STOP_PLAYOUT;
+            actionTexts[1] = "Stop";
+        }
+        for (int i = 0; i < messageActions.length; i++) {
+            Intent intent = new Intent(mContext, MessengerService.class)
+                    .setAction(messageActions[i])
+                    .putExtra(MessengerService.EXTRA_SENDER_KEY, senderKey);
+            PendingIntent pendingIntent = PendingIntent.getService(mContext,
+                    notificationInfo.mNotificationId, intent, PendingIntent.FLAG_UPDATE_CURRENT);
+            builder.addAction(android.R.drawable.ic_media_play, actionTexts[i], pendingIntent);
+        }
+        mNotificationManager.notify(notificationInfo.mNotificationId, builder.build());
+    }
+
+    void clearNotificationState(SenderKey senderKey) {
+        if (DBG) {
+            Log.d(TAG, "Clearing notification state for: " + senderKey);
+        }
+        mNotificationInfos.remove(senderKey);
+    }
+
+    void playMessages(SenderKey senderKey) {
+        NotificationInfo notificationInfo = mNotificationInfos.get(senderKey);
+        if (notificationInfo == null) {
+            Log.e(TAG, "Unknown senderKey! " + senderKey);
+            return;
+        }
+
+        StringBuilder ttsMessage = new StringBuilder();
+        ttsMessage.append(notificationInfo.mSenderName)
+                .append(" ").append(mContext.getString(R.string.tts_says_verb));
+        for (MessageKey messageKey : notificationInfo.mMessageKeys) {
+            MapMessage message = mMessages.get(messageKey);
+            if (message != null) {
+                ttsMessage.append(". ").append(message.getText());
+            }
+        }
+
+        mTTSHelper.requestPlay(ttsMessage.toString(),
+                new TTSHelper.Listener() {
+            @Override
+            public void onTTSStarted() {
+                updateNotificationFor(senderKey, notificationInfo, true);
+            }
+
+            @Override
+            public void onTTSStopped() {
+                updateNotificationFor(senderKey, notificationInfo, false);
+            }
+
+            @Override
+            public void onTTSError() {
+                Toast.makeText(mContext, R.string.tts_failed_toast, Toast.LENGTH_SHORT).show();
+                onTTSStopped();
+            }
+        });
+    }
+
+    void stopPlayout() {
+        mTTSHelper.requestStop();
+    }
+
+    boolean sendAutoReply(SenderKey senderKey, BluetoothMapClient mapClient) {
+        if (DBG) {
+            Log.d(TAG, "Sending auto-reply to: " + senderKey);
+        }
+        BluetoothDevice device =
+                BluetoothAdapter.getDefaultAdapter().getRemoteDevice(senderKey.mDeviceAddress);
+        NotificationInfo notificationInfo = mNotificationInfos.get(senderKey);
+        if (notificationInfo == null) {
+            Log.w(TAG, "No notificationInfo found for senderKey: " + senderKey);
+            return false;
+        }
+        if (notificationInfo.mSenderContactUri == null) {
+            Log.w(TAG, "Do not have contact URI for sender!");
+            return false;
+        }
+        Uri recipientUris[] = { Uri.parse(notificationInfo.mSenderContactUri) };
+
+        final int requestCode = senderKey.hashCode();
+        PendingIntent sentIntent =
+                PendingIntent.getBroadcast(mContext, requestCode, new Intent(
+                        BluetoothMapClient.ACTION_MESSAGE_SENT_SUCCESSFULLY),
+                        PendingIntent.FLAG_ONE_SHOT);
+        String message = mContext.getString(R.string.auto_reply_message);
+        return mapClient.sendMessage(device, recipientUris, message, sentIntent, null);
+    }
+
+    void handleMapDisconnect() {
+        cleanupMessagesAndNotifications((key) -> true);
+    }
+
+    void handleDeviceDisconnect(BluetoothDevice device) {
+        cleanupMessagesAndNotifications((key) -> key.matches(device.getAddress()));
+    }
+
+    private void cleanupMessagesAndNotifications(Predicate<CompositeKey> predicate) {
+        Iterator<Map.Entry<MessageKey, MapMessage>> messageIt = mMessages.entrySet().iterator();
+        while (messageIt.hasNext()) {
+            if (predicate.test(messageIt.next().getKey())) {
+                messageIt.remove();
+            }
+        }
+        Iterator<Map.Entry<SenderKey, NotificationInfo>> notificationIt =
+                mNotificationInfos.entrySet().iterator();
+        while (notificationIt.hasNext()) {
+            Map.Entry<SenderKey, NotificationInfo> entry = notificationIt.next();
+            if (predicate.test(entry.getKey())) {
+                mNotificationManager.cancel(entry.getValue().mNotificationId);
+                notificationIt.remove();
+            }
+        }
+    }
+
+    void cleanup() {
+        mBluetoothMapReceiver.cleanup();
+        mTTSHelper.cleanup();
+    }
+
+    // Used to monitor for new incoming messages and sent-message broadcast.
+    private class BluetoothMapReceiver extends BroadcastReceiver {
+        BluetoothMapReceiver() {
+            if (DBG) {
+                Log.d(TAG, "Registering receiver for new messages");
+            }
+            IntentFilter intentFilter = new IntentFilter();
+            intentFilter.addAction(BluetoothMapClient.ACTION_MESSAGE_SENT_SUCCESSFULLY);
+            intentFilter.addAction(BluetoothMapClient.ACTION_MESSAGE_RECEIVED);
+            mContext.registerReceiver(this, intentFilter);
+        }
+
+        void cleanup() {
+            mContext.unregisterReceiver(this);
+        }
+
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            if (BluetoothMapClient.ACTION_MESSAGE_SENT_SUCCESSFULLY.equals(intent.getAction())) {
+                if (DBG) {
+                    Log.d(TAG, "SMS was sent successfully!");
+                }
+            } else if (BluetoothMapClient.ACTION_MESSAGE_RECEIVED.equals(intent.getAction())) {
+                handleNewMessage(intent);
+            } else {
+                Log.w(TAG, "Ignoring unknown broadcast " + intent.getAction());
+            }
+        }
+    }
+
+    /**
+     * Key used in HashMap that is composed from a BT device-address and device-specific "sub key"
+     */
+    private abstract static class CompositeKey {
+        final String mDeviceAddress;
+        final String mSubKey;
+
+        CompositeKey(String deviceAddress, String subKey) {
+            mDeviceAddress = deviceAddress;
+            mSubKey = subKey;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) {
+                return true;
+            }
+            if (o == null || getClass() != o.getClass()) {
+                return false;
+            }
+
+            CompositeKey that = (CompositeKey) o;
+            return Objects.equals(mDeviceAddress, that.mDeviceAddress)
+                    && Objects.equals(mSubKey, that.mSubKey);
+        }
+
+        boolean matches(String deviceAddress) {
+            return mDeviceAddress.equals(deviceAddress);
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(mDeviceAddress, mSubKey);
+        }
+
+        @Override
+        public String toString() {
+            return String.format("%s, deviceAddress: %s, subKey: %s",
+                    getClass().getSimpleName(), mDeviceAddress, mSubKey);
+        }
+    }
+
+    /**
+     * {@link CompositeKey} subclass used to identify specific messages; it uses message-handle as
+     * the secondary key.
+     */
+    private static class MessageKey extends CompositeKey {
+        MessageKey(MapMessage message) {
+            super(message.getDevice().getAddress(), message.getHandle());
+        }
+    }
+
+    /**
+     * CompositeKey used to identify Notification info for a sender; it uses a combination of
+     * senderContactUri and senderContactName as the secondary key.
+     */
+    static class SenderKey extends CompositeKey implements Parcelable {
+        private SenderKey(String deviceAddress, String key) {
+            super(deviceAddress, key);
+        }
+
+        SenderKey(MapMessage message) {
+            // Use a combination of senderName and senderContactUri for key. Ideally we would use
+            // only senderContactUri (which is encoded phone no.). However since some phones don't
+            // provide these, we fall back to senderName. Since senderName may not be unique, we
+            // include senderContactUri also to provide uniqueness in cases it is available.
+            this(message.getDevice().getAddress(),
+                    message.getSenderName() + "/" + message.getSenderContactUri());
+        }
+
+        @Override
+        public int describeContents() {
+            return 0;
+        }
+
+        @Override
+        public void writeToParcel(Parcel dest, int flags) {
+            dest.writeString(mDeviceAddress);
+            dest.writeString(mSubKey);
+        }
+
+        public static final Parcelable.Creator<SenderKey> CREATOR =
+                new Parcelable.Creator<SenderKey>() {
+            @Override
+            public SenderKey createFromParcel(Parcel source) {
+                return new SenderKey(source.readString(), source.readString());
+            }
+
+            @Override
+            public SenderKey[] newArray(int size) {
+                return new SenderKey[size];
+            }
+        };
+    }
+
+    /**
+     * Information about a single notification that is displayed.
+     */
+    private static class NotificationInfo {
+        private static int NEXT_NOTIFICATION_ID = 0;
+
+        final int mNotificationId = NEXT_NOTIFICATION_ID++;
+        final String mSenderName;
+        @Nullable
+        final String mSenderContactUri;
+        final List<MessageKey> mMessageKeys = new LinkedList<>();
+
+        NotificationInfo(String senderName, @Nullable String senderContactUri) {
+            mSenderName = senderName;
+            mSenderContactUri = senderContactUri;
+        }
+    }
+}
diff --git a/src/com/android/car/messenger/MessengerReceiver.java b/src/com/android/car/messenger/MessengerReceiver.java
new file mode 100644
index 0000000..c3af2f8
--- /dev/null
+++ b/src/com/android/car/messenger/MessengerReceiver.java
@@ -0,0 +1,34 @@
+/*
+ * 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.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.util.Log;
+
+/**
+ * Minimal receiver that starts up MessengerService on boot-completion.
+ */
+public class MessengerReceiver extends BroadcastReceiver {
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        Intent startIntent =
+                new Intent(MessengerService.ACTION_START).setClass(context, MessengerService.class);
+        context.startService(startIntent);
+    }
+}
diff --git a/src/com/android/car/messenger/MessengerService.java b/src/com/android/car/messenger/MessengerService.java
new file mode 100644
index 0000000..c2de6aa
--- /dev/null
+++ b/src/com/android/car/messenger/MessengerService.java
@@ -0,0 +1,234 @@
+/*
+ * 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.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);
+    static final boolean VDBG = Log.isLoggable(TAG, Log.VERBOSE);
+
+    // 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 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";
+
+    private MapMessageMonitor mMessageMonitor;
+    private MapDeviceMonitor mDeviceMonitor;
+    private BluetoothMapClient mMapClient;
+
+    @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.getAction());
+        }
+
+        // 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);
+                } 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_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;
+    }
+
+    private boolean hasRequiredArgs(Intent intent) {
+        switch (intent.getAction()) {
+            case ACTION_AUTO_REPLY:
+            case ACTION_PLAY_MESSAGES:
+            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 null;
+    }
+
+    // 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);
+            }
+        }
+    }
+}
diff --git a/src/com/android/car/messenger/TTSHelper.java b/src/com/android/car/messenger/TTSHelper.java
new file mode 100644
index 0000000..ff8ed3b
--- /dev/null
+++ b/src/com/android/car/messenger/TTSHelper.java
@@ -0,0 +1,181 @@
+/*
+ * 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.content.Context;
+import android.os.Handler;
+import android.speech.tts.TextToSpeech;
+import android.speech.tts.UtteranceProgressListener;
+import android.util.Log;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.Consumer;
+
+/**
+ * Component that wraps platform TTS engine and supports queued playout.
+ * <p>
+ * It takes care of initializing the TTS engine. TTS requests made are queued up and played when the
+ * engine is setup. It only supports one queued requests; any new requests will cause the existing
+ * one to be dropped. Similarly, if a new one is queued while an existing message is already playing
+ * the existing one will be stopped/interrupted and the new one will start playing.
+ */
+class TTSHelper {
+    interface Listener {
+        // Called when playout is about to start.
+        void onTTSStarted();
+
+        // The following two are terminal callbacks and no further callbacks should be expected.
+        // Called when playout finishes or playout is cancelled/never started because another TTS
+        // request was made.
+        void onTTSStopped();
+        // Called when there's an internal error.
+        void onTTSError();
+    }
+
+    private static final String TAG = "Messenger.TTSHelper";
+    private static final boolean DBG = MessengerService.DBG;
+
+    private final Handler mHandler = new Handler();
+    private final TextToSpeech mTextToSpeech;
+    private int mInitStatus;
+    private SpeechRequest mPendingRequest;
+    private final Map<String, Listener> mListeners = new HashMap<>();
+
+    TTSHelper(Context context) {
+        // OnInitListener will only set to SUCCESS/ERROR. So we initialize to STOPPED.
+        mInitStatus = TextToSpeech.STOPPED;
+        // TODO(sriniv): Init this only when needed and shutdown to free resources.
+        mTextToSpeech = new TextToSpeech(context, this::handleInitCompleted);
+        mTextToSpeech.setOnUtteranceProgressListener(mProgressListener);
+    }
+
+    private void handleInitCompleted(int initStatus) {
+        if (DBG) {
+            Log.d(TAG, "init completed: " + initStatus);
+        }
+        mInitStatus = initStatus;
+        if (mPendingRequest != null) {
+            playInternal(mPendingRequest.mTextToSpeak, mPendingRequest.mListener);
+            mPendingRequest = null;
+        }
+    }
+
+    void requestPlay(CharSequence textToSpeak, Listener listener) {
+        // Check if its still initializing.
+        if (mInitStatus == TextToSpeech.STOPPED) {
+            // Squash any already queued request.
+            if (mPendingRequest != null) {
+                mPendingRequest.mListener.onTTSStopped();
+            }
+            mPendingRequest = new SpeechRequest(textToSpeak, listener);
+        } else {
+            playInternal(textToSpeak, listener);
+        }
+    }
+
+    void requestStop() {
+        mTextToSpeech.stop();
+    }
+
+    private void playInternal(CharSequence textToSpeak, Listener listener) {
+        if (mInitStatus == TextToSpeech.ERROR) {
+            Log.e(TAG, "TTS setup failed!");
+            mHandler.post(listener::onTTSError);
+            return;
+        }
+
+        String id = Integer.toString(listener.hashCode());
+        if (DBG) {
+            Log.d(TAG, String.format("Queueing text in TTS: [%s], id=%s", textToSpeak, id));
+        }
+        if (mTextToSpeech.speak(textToSpeak, TextToSpeech.QUEUE_FLUSH, null, id)
+                != TextToSpeech.SUCCESS) {
+            Log.e(TAG, "Queuing text failed!");
+            mHandler.post(listener::onTTSError);
+            return;
+        }
+        mListeners.put(id, listener);
+    }
+
+    void cleanup() {
+        mTextToSpeech.stop();
+        mTextToSpeech.shutdown();
+    }
+
+    // The TTS engine will invoke onStart and then invoke either onDone, onStop or onError.
+    // Since these callbacks can come on other threads, we push updates back on to the TTSHelper's
+    // Handler.
+    private final UtteranceProgressListener mProgressListener = new UtteranceProgressListener() {
+        private void safeInvokeAsync(String id, boolean cleanup,
+                Consumer<Listener> callbackCaller) {
+            mHandler.post(() -> {
+                Listener listener = mListeners.get(id);
+                if (listener == null) {
+                    Log.e(TAG, "No listener found for: " + id);
+                    return;
+                }
+                callbackCaller.accept(listener);
+                if (cleanup) {
+                    mListeners.remove(id);
+                }
+            });
+        }
+
+        @Override
+        public void onStart(String id) {
+            if (DBG) {
+                Log.d(TAG, "TTS engine onStart: " + id);
+            }
+            safeInvokeAsync(id, false, Listener::onTTSStarted);
+        }
+
+        @Override
+        public void onDone(String id) {
+            if (DBG) {
+                Log.d(TAG, "TTS engine onDone: " + id);
+            }
+            safeInvokeAsync(id, true, Listener::onTTSStopped);
+        }
+
+        @Override
+        public void onStop(String id, boolean interrupted) {
+            if (DBG) {
+                Log.d(TAG, "TTS engine onStop: " + id);
+            }
+            safeInvokeAsync(id, true, Listener::onTTSStopped);
+        }
+
+        @Override
+        public void onError(String id) {
+            if (DBG) {
+                Log.d(TAG, "TTS engine onError: " + id);
+            }
+            safeInvokeAsync(id, true, Listener::onTTSError);
+        }
+   };
+
+    private static class SpeechRequest {
+        final CharSequence mTextToSpeak;
+        final Listener mListener;
+
+        public SpeechRequest(CharSequence textToSpeak, Listener listener) {
+            mTextToSpeak = textToSpeak;
+            mListener = listener;
+        }
+    };
+}