From 9148b7da5f4604dae1106d3932bfa7d3bfa63ad4 Mon Sep 17 00:00:00 2001 From: Mattias Eriksson Date: Wed, 14 Sep 2016 23:58:24 +0200 Subject: [PATCH] Initial Android Auto support This adds android auto support accordign to https://developer.android.com/training/auto/messaging/index.html#messaging However, since android auto is not officially supported in my country, the functionality is limited. Which means that I have not been able to fully test everything yet. What work is: * Message notification is shown. * When you click on it, the message is read. Closes #5880 --- AndroidManifest.xml | 18 +++ res/xml/automotive_app_desc.xml | 3 + .../AndroidAutoHeardReceiver.java | 79 +++++++++++++ .../AndroidAutoReplyReceiver.java | 111 ++++++++++++++++++ .../notifications/MessageNotifier.java | 2 + .../notifications/NotificationState.java | 29 +++++ .../SingleRecipientNotificationBuilder.java | 21 ++++ 7 files changed, 263 insertions(+) create mode 100644 res/xml/automotive_app_desc.xml create mode 100644 src/org/thoughtcrime/securesms/notifications/AndroidAutoHeardReceiver.java create mode 100644 src/org/thoughtcrime/securesms/notifications/AndroidAutoReplyReceiver.java diff --git a/AndroidManifest.xml b/AndroidManifest.xml index cee67568b..406a64d24 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -107,6 +107,9 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/org/thoughtcrime/securesms/notifications/AndroidAutoHeardReceiver.java b/src/org/thoughtcrime/securesms/notifications/AndroidAutoHeardReceiver.java new file mode 100644 index 000000000..fad0d0447 --- /dev/null +++ b/src/org/thoughtcrime/securesms/notifications/AndroidAutoHeardReceiver.java @@ -0,0 +1,79 @@ +/** + * Copyright (C) 2011 Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.thoughtcrime.securesms.notifications; + +import android.app.NotificationManager; +import android.content.Context; +import android.content.Intent; +import android.os.AsyncTask; +import android.support.annotation.Nullable; + +import org.thoughtcrime.securesms.ApplicationContext; +import org.thoughtcrime.securesms.crypto.MasterSecret; +import org.thoughtcrime.securesms.database.DatabaseFactory; +import org.thoughtcrime.securesms.database.MessagingDatabase.MarkedMessageInfo; +import org.thoughtcrime.securesms.database.MessagingDatabase.SyncMessageId; +import org.thoughtcrime.securesms.jobs.MultiDeviceReadUpdateJob; +import org.whispersystems.libsignal.logging.Log; + +import java.util.LinkedList; +import java.util.List; + +/** + * Marks an Android Auto as read after the driver have listened to it + */ +public class AndroidAutoHeardReceiver extends MasterSecretBroadcastReceiver { + + public static final String TAG = AndroidAutoHeardReceiver.class.getSimpleName(); + public static final String HEARD_ACTION = "org.thoughtcrime.securesms.notifications.ANDROID_AUTO_HEARD"; + public static final String THREAD_IDS_EXTRA = "car_heard_thread_ids"; + + @Override + protected void onReceive(final Context context, Intent intent, + @Nullable final MasterSecret masterSecret) + { + if (!HEARD_ACTION.equals(intent.getAction())) + return; + + final long[] threadIds = intent.getLongArrayExtra(THREAD_IDS_EXTRA); + + if (threadIds != null) { + ((NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE)) + .cancel(MessageNotifier.NOTIFICATION_ID); + + new AsyncTask() { + @Override + protected Void doInBackground(Void... params) { + List messageIdsCollection = new LinkedList<>(); + + for (long threadId : threadIds) { + Log.i(TAG, "Marking meassage as read: " + threadId); + List messageIds = DatabaseFactory.getThreadDatabase(context).setRead(threadId); + + messageIdsCollection.addAll(messageIds); + } + + MessageNotifier.updateNotification(context, masterSecret); + MarkReadReceiver.process(context, messageIdsCollection); + + return null; + } + }.execute(); + } + } +} diff --git a/src/org/thoughtcrime/securesms/notifications/AndroidAutoReplyReceiver.java b/src/org/thoughtcrime/securesms/notifications/AndroidAutoReplyReceiver.java new file mode 100644 index 000000000..5169b857e --- /dev/null +++ b/src/org/thoughtcrime/securesms/notifications/AndroidAutoReplyReceiver.java @@ -0,0 +1,111 @@ +/** + * Copyright (C) 2011 Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.thoughtcrime.securesms.notifications; + +import android.app.NotificationManager; +import android.content.Context; +import android.content.Intent; +import android.os.AsyncTask; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.v4.app.RemoteInput; + +import org.thoughtcrime.securesms.attachments.Attachment; +import org.thoughtcrime.securesms.crypto.MasterSecret; +import org.thoughtcrime.securesms.database.DatabaseFactory; +import org.thoughtcrime.securesms.database.MessagingDatabase; +import org.thoughtcrime.securesms.database.MessagingDatabase.MarkedMessageInfo; +import org.thoughtcrime.securesms.database.MessagingDatabase.SyncMessageId; +import org.thoughtcrime.securesms.database.RecipientPreferenceDatabase.RecipientsPreferences; +import org.thoughtcrime.securesms.mms.OutgoingMediaMessage; +import org.thoughtcrime.securesms.recipients.RecipientFactory; +import org.thoughtcrime.securesms.recipients.Recipients; +import org.thoughtcrime.securesms.sms.MessageSender; +import org.thoughtcrime.securesms.sms.OutgoingTextMessage; +import org.whispersystems.libsignal.logging.Log; +import org.whispersystems.libsignal.util.guava.Optional; + +import java.util.LinkedList; +import java.util.List; + +/** + * Get the response text from the Android Auto and sends an message as a reply + */ +public class AndroidAutoReplyReceiver extends MasterSecretBroadcastReceiver { + + public static final String TAG = AndroidAutoReplyReceiver.class.getSimpleName(); + public static final String REPLY_ACTION = "org.thoughtcrime.securesms.notifications.ANDROID_AUTO_REPLY"; + public static final String RECIPIENT_IDS_EXTRA = "car_recipient_ids"; + public static final String VOICE_REPLY_KEY = "car_voice_reply_key"; + public static final String THREAD_ID_EXTRA = "car_reply_thread_id"; + + @Override + protected void onReceive(final Context context, Intent intent, + final @Nullable MasterSecret masterSecret) + { + if (!REPLY_ACTION.equals(intent.getAction())) return; + + Bundle remoteInput = RemoteInput.getResultsFromIntent(intent); + + if (remoteInput == null) return; + + final long[] recipientIds = intent.getLongArrayExtra(RECIPIENT_IDS_EXTRA); + final long threadId = intent.getLongExtra(THREAD_ID_EXTRA, -1); + final CharSequence responseText = getMessageText(intent); + final Recipients recipients = RecipientFactory.getRecipientsForIds(context, recipientIds, false); + + if (responseText != null) { + new AsyncTask() { + @Override + protected Void doInBackground(Void... params) { + + long replyThreadId; + + Optional preferences = DatabaseFactory.getRecipientPreferenceDatabase(context).getRecipientsPreferences(recipientIds); + int subscriptionId = preferences.isPresent() ? preferences.get().getDefaultSubscriptionId().or(-1) : -1; + long expiresIn = preferences.isPresent() ? preferences.get().getExpireMessages() * 1000 : 0; + + if (recipients.isGroupRecipient()) { + Log.i("AndroidAutoReplyReceiver", "GroupRecipient, Sending media message"); + OutgoingMediaMessage reply = new OutgoingMediaMessage(recipients, responseText.toString(), new LinkedList(), System.currentTimeMillis(), subscriptionId, expiresIn, 0); + replyThreadId = MessageSender.send(context, masterSecret, reply, threadId, false); + } else { + Log.i("AndroidAutoReplyReceiver", "Sending regular message "); + OutgoingTextMessage reply = new OutgoingTextMessage(recipients, responseText.toString(), expiresIn, subscriptionId); + replyThreadId = MessageSender.send(context, masterSecret, reply, threadId, false); + } + + List messageIds = DatabaseFactory.getThreadDatabase(context).setRead(replyThreadId); + MessageNotifier.updateNotification(context, masterSecret); + MarkReadReceiver.process(context, messageIds); + + return null; + } + }.execute(); + } + } + + private CharSequence getMessageText(Intent intent) { + Bundle remoteInput = RemoteInput.getResultsFromIntent(intent); + if (remoteInput != null) { + return remoteInput.getCharSequence(VOICE_REPLY_KEY); + } + return null; + } + +} diff --git a/src/org/thoughtcrime/securesms/notifications/MessageNotifier.java b/src/org/thoughtcrime/securesms/notifications/MessageNotifier.java index 3af64a69b..0695001fa 100644 --- a/src/org/thoughtcrime/securesms/notifications/MessageNotifier.java +++ b/src/org/thoughtcrime/securesms/notifications/MessageNotifier.java @@ -258,6 +258,8 @@ public class MessageNotifier { notificationState.getMarkAsReadIntent(context), notificationState.getQuickReplyIntent(context, notifications.get(0).getRecipients()), notificationState.getWearableReplyIntent(context, notifications.get(0).getRecipients())); + builder.addAndroidAutoAction(notificationState.getAndroidAutoReplyIntent(context, notifications.get(0).getRecipients()), + notificationState.getAndroidAutoHeardIntent(context, notifications.get(0).getRecipients()), notifications.get(0).getTimestamp()); ListIterator iterator = notifications.listIterator(notifications.size()); diff --git a/src/org/thoughtcrime/securesms/notifications/NotificationState.java b/src/org/thoughtcrime/securesms/notifications/NotificationState.java index 51391b8d1..d0f42edea 100644 --- a/src/org/thoughtcrime/securesms/notifications/NotificationState.java +++ b/src/org/thoughtcrime/securesms/notifications/NotificationState.java @@ -102,6 +102,35 @@ public class NotificationState { return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); } + public PendingIntent getAndroidAutoReplyIntent(Context context, Recipients recipients) { + if (threads.size() != 1) throw new AssertionError("We only support replies to single thread notifications!"); + + Intent intent = new Intent(AndroidAutoReplyReceiver.REPLY_ACTION); + intent.putExtra(AndroidAutoReplyReceiver.RECIPIENT_IDS_EXTRA, recipients.getIds()); + intent.putExtra(AndroidAutoReplyReceiver.THREAD_ID_EXTRA, (long)threads.toArray()[0]); + intent.setPackage(context.getPackageName()); + + return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); + } + + public PendingIntent getAndroidAutoHeardIntent(Context context, Recipients recipients) { + long[] threadArray = new long[threads.size()]; + int index = 0; + for (long thread : threads) { + Log.w("NotificationState", "getAndroidAutoHeardIntent Added thread: " + thread); + threadArray[index++] = thread; + } + + Intent intent = new Intent(AndroidAutoHeardReceiver.HEARD_ACTION); + intent.putExtra(AndroidAutoHeardReceiver.THREAD_IDS_EXTRA, threadArray); + intent.setPackage(context.getPackageName()); + + Log.w("NotificationState", "getAndroidAutoHeardIntent - Pending array off intent length: " + + intent.getLongArrayExtra(AndroidAutoHeardReceiver.THREAD_IDS_EXTRA).length); + + return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); + } + public PendingIntent getQuickReplyIntent(Context context, Recipients recipients) { if (threads.size() != 1) throw new AssertionError("We only support replies to single thread notifications!"); diff --git a/src/org/thoughtcrime/securesms/notifications/SingleRecipientNotificationBuilder.java b/src/org/thoughtcrime/securesms/notifications/SingleRecipientNotificationBuilder.java index 3987c8dd1..4a2bc529e 100644 --- a/src/org/thoughtcrime/securesms/notifications/SingleRecipientNotificationBuilder.java +++ b/src/org/thoughtcrime/securesms/notifications/SingleRecipientNotificationBuilder.java @@ -101,6 +101,27 @@ public class SingleRecipientNotificationBuilder extends AbstractNotificationBuil } } + public void addAndroidAutoAction(@NonNull PendingIntent androidAutoReplyIntent, + @NonNull PendingIntent androidAutoHeardIntent, long timestamp) + { + + if (mContentTitle == null || mContentText == null) + return; + + RemoteInput remoteInput = new RemoteInput.Builder(AndroidAutoReplyReceiver.VOICE_REPLY_KEY) + .setLabel(context.getString(R.string.MessageNotifier_reply)) + .build(); + + NotificationCompat.CarExtender.UnreadConversation.Builder unreadConversationBuilder = + new NotificationCompat.CarExtender.UnreadConversation.Builder(mContentTitle.toString()) + .addMessage(mContentText.toString()) + .setLatestTimestamp(timestamp) + .setReadPendingIntent(androidAutoHeardIntent) + .setReplyAction(androidAutoReplyIntent, remoteInput); + + extend(new NotificationCompat.CarExtender().setUnreadConversation(unreadConversationBuilder.build())); + } + public void addActions(@Nullable MasterSecret masterSecret, @NonNull PendingIntent markReadIntent, @NonNull PendingIntent quickReplyIntent,