2
0
mirror of https://github.com/KDE/kdeconnect-android synced 2025-08-30 05:37:43 +00:00

[Android] New-style Message received handling

Summary:
Mark old-style SMS packet sending as deprecated (but still supported for backwards-compatibility with old Desktop apps)

Implement a ContentObserver on the Messages database, then send an update to the remote for all incoming or outgoing messages

Test Plan:
1. DBus
  - Connect Android to desktop
  - Subscribe to /modules/kdeconnect/devices/<device_id>/org.kdeconnect.device.conversations/conversationUpdated
  - Receive a new message (text yourself?)
  - Verify that something comes back on dbus (This endpoint is updated by D15409. With that patch you should see the new Message object, before you should just get the (meaningless) conversation ID)

2. SMS App
  - Relies on D15409 - See instructions there

Reviewers: #kde_connect, nicolasfella

Reviewed By: #kde_connect, nicolasfella

Subscribers: nicolasfella, kdeconnect

Tags: #kde_connect

Differential Revision: https://phabricator.kde.org/D15360
This commit is contained in:
Simon Redman 2018-09-17 08:52:54 -06:00
parent 0cc3639aa1
commit df42f992c8
2 changed files with 233 additions and 41 deletions

View File

@ -21,11 +21,14 @@
package org.kde.kdeconnect.Helpers;
import android.content.Context;
import android.database.ContentObserver;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.os.Looper;
import android.provider.Telephony;
import android.support.annotation.RequiresApi;
import android.util.Log;
import org.json.JSONException;
import org.json.JSONObject;
@ -34,6 +37,9 @@ import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class SMSHelper {
@ -87,13 +93,38 @@ public class SMSHelper {
* @return List of all messages in the thread
*/
public static List<Message> getMessagesInThread(Context context, ThreadID threadID) {
final String selection = ThreadID.lookupColumn + " == ?";
final String[] selectionArgs = new String[] { threadID.toString() };
return getMessagesWithFilter(context, selection, selectionArgs);
}
/**
* Get all messages which have a timestamp after the requested timestamp
*
* @param timestamp epoch in millis matching the timestamp to return
* @return null if no matching message is found, otherwise return a Message
*/
public static List<Message> getMessagesSinceTimestamp(Context context, long timestamp) {
final String selection = Message.DATE + " > ?";
final String[] selectionArgs = new String[] {Long.toString(timestamp)};
List<Message> messages = getMessagesWithFilter(context, selection, selectionArgs);
return messages;
}
/**
* Get all messages matching the passed filter. See documentation for Android's ContentResolver
*
* @param selection Parameterizable filter to use with the ContentResolver query. May be null.
* @param selectionArgs Parameters for selection. May be null.
* @return List of messages matching the filter
*/
private static List<Message> getMessagesWithFilter(Context context, String selection, String[] selectionArgs) {
List<Message> toReturn = new ArrayList<>();
Uri smsUri = getSMSUri();
final String selection = ThreadID.lookupColumn + " == ?";
final String[] selectionArgs = new String[] { threadID.toString() };
Cursor smsCursor = context.getContentResolver().query(
smsUri,
Message.smsColumns,
@ -102,10 +133,7 @@ public class SMSHelper {
null);
if (smsCursor != null && smsCursor.moveToFirst()) {
int threadColumn = smsCursor.getColumnIndexOrThrow(ThreadID.lookupColumn);
do {
int thread = smsCursor.getInt(threadColumn);
HashMap<String, String> messageInfo = new HashMap<>();
for (int columnIdx = 0; columnIdx < smsCursor.getColumnCount(); columnIdx++) {
String colName = smsCursor.getColumnName(columnIdx);
@ -168,6 +196,19 @@ public class SMSHelper {
return toReturn;
}
/**
* Register a ContentObserver for the Messages database
*
* @param observer ContentObserver to alert on Message changes
*/
public static void registerObserver(ContentObserver observer, Context context) {
context.getContentResolver().registerContentObserver(
SMSHelper.getSMSUri(),
true,
observer
);
}
/**
* Represent an ID used to uniquely identify a message thread
*/
@ -273,5 +314,65 @@ public class SMSHelper {
return this.m_body;
}
}
/**
* If anyone wants to subscribe to changes in the messages database, they will need a thread
* to handle callbacks on
* This singleton conveniently provides such a thread, accessed and used via its Looper object
*/
public static class MessageLooper extends Thread {
private static MessageLooper singleton = null;
private static Looper looper = null;
private static Lock looperReadyLock = new ReentrantLock();
private static Condition looperReady = looperReadyLock.newCondition();
private MessageLooper() {
setName("MessageHelperLooper");
}
/**
* Get the Looper object associated with this thread
*
* If the Looper has not been prepared, it is prepared as part of this method call.
* Since this means a thread has to be spawned, this method might block until that thread is
* ready to serve requests
*/
public static Looper getLooper() {
if (singleton == null) {
looperReadyLock.lock();
try {
singleton = new MessageLooper();
singleton.start();
while (looper == null) {
// Block until the looper is ready
looperReady.await();
}
} catch (InterruptedException e) {
// I don't know when this would happen
Log.e("SMSHelper", "Interrupted while waiting for Looper", e);
return null;
} finally {
looperReadyLock.unlock();
}
}
return looper;
}
public void run() {
looperReadyLock.lock();
try {
Looper.prepare();
looper = Looper.myLooper();
looperReady.signalAll();
} finally {
looperReadyLock.unlock();
}
Looper.loop();
}
}
}

View File

@ -27,7 +27,10 @@ import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.database.ContentObserver;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.preference.PreferenceManager;
import android.support.v4.content.ContextCompat;
import android.telephony.PhoneNumberUtils;
@ -47,8 +50,11 @@ import org.kde.kdeconnect_tp.BuildConfig;
import org.kde.kdeconnect_tp.R;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import static org.kde.kdeconnect.Plugins.TelephonyPlugin.TelephonyPlugin.PACKET_TYPE_TELEPHONY;
@ -130,13 +136,89 @@ public class SMSPlugin extends Plugin {
messages.add(SmsMessage.createFromPdu((byte[]) pdu));
}
smsBroadcastReceived(messages);
smsBroadcastReceivedDeprecated(messages);
}
}
};
private void smsBroadcastReceived(ArrayList<SmsMessage> messages) {
/**
* Keep track of the most-recently-seen message so that we can query for later ones as they arrive
*/
private long mostRecentTimestamp = 0;
// Since the mostRecentTimestamp is accessed both from the plugin's thread and the ContentObserver
// thread, make sure that access is coherent
private Lock mostRecentTimestampLock = new ReentrantLock();
private class MessageContentObserver extends ContentObserver {
SMSPlugin mPlugin;
/**
* Create a ContentObserver to watch the Messages database. onChange is called for
* every subscribed change
*
* @param parent Plugin which owns this observer
* @param handler Handler object used to make the callback
*/
public MessageContentObserver(SMSPlugin parent, Handler handler) {
super(handler);
mPlugin = parent;
}
@Override
/**
* The onChange method is called whenever the subscribed-to database changes
*
* In this case, this onChange expects to be called whenever *anything* in the Messages
* database changes and simply reports those updated messages to anyone who might be listening
*/
public void onChange(boolean selfChange) {
if (mPlugin.mostRecentTimestamp == 0) {
// Since the timestamp has not been initialized, we know that nobody else
// has requested a message. That being the case, there is most likely
// nobody listening for message updates, so just drop them
return;
}
mostRecentTimestampLock.lock();
// Grab the mostRecentTimestamp into the local stack because reading the Messages
// database could potentially be a long operation
long mostRecentTimestamp = mPlugin.mostRecentTimestamp;
mostRecentTimestampLock.unlock();
List<SMSHelper.Message> messages = SMSHelper.getMessagesSinceTimestamp(mPlugin.context, mostRecentTimestamp);
if (messages.size() == 0) {
// Our onChange often gets called many times for a single message. Don't make unnecessary
// noise
return;
}
// Update the most recent counter
mostRecentTimestampLock.lock();
for (SMSHelper.Message message : messages) {
if (message.m_date > mostRecentTimestamp) {
mPlugin.mostRecentTimestamp = message.m_date;
}
}
mostRecentTimestampLock.unlock();
// Send the alert about the update
device.sendPacket(constructBulkMessagePacket(messages));
}
}
@Deprecated
/**
* Deliver an old-style SMS packet in response to a new message arriving
*
* For backwards-compatibility with long-lived distro packages, this method needs to exist in
* order to support older desktop apps. However, note that it should no longer be used
*
* This comment is being written 30 August 2018. Distros will likely be running old versions for
* many years to come...
*
* @param messages Ordered list of parts of the message body which should be combined into a single message
*/
private void smsBroadcastReceivedDeprecated(ArrayList<SmsMessage> messages) {
if (BuildConfig.DEBUG) {
if (!(messages.size() > 0)) {
@ -189,6 +271,10 @@ public class SMSPlugin extends Plugin {
filter.setPriority(500);
context.registerReceiver(receiver, filter);
Looper helperLooper = SMSHelper.MessageLooper.getLooper();
ContentObserver messageObserver = new MessageContentObserver(this, new Handler(helperLooper));
SMSHelper.registerObserver(messageObserver, context);
return true;
}
@ -239,6 +325,35 @@ public class SMSPlugin extends Plugin {
return true;
}
/**
* Construct a proper packet of PACKET_TYPE_SMS_MESSAGE from the passed messages
*
* @param messages Messages to include in the packet
* @return NetworkPacket of type PACKET_TYPE_SMS_MESSAGE
*/
public static NetworkPacket constructBulkMessagePacket(Collection<SMSHelper.Message> messages) {
NetworkPacket reply = new NetworkPacket(PACKET_TYPE_SMS_MESSAGE);
JSONArray body = new JSONArray();
for (SMSHelper.Message message : messages) {
try {
JSONObject json = message.toJSONObject();
json.put("event", "sms");
body.put(json);
} catch (JSONException e) {
Log.e("Conversations", "Error serializing message");
}
}
reply.set("messages", body);
reply.set("event", "batch_messages");
return reply;
}
/**
* Respond to a request for all conversations
* <p>
@ -247,24 +362,17 @@ public class SMSPlugin extends Plugin {
private boolean handleRequestConversations(NetworkPacket packet) {
Map<SMSHelper.ThreadID, SMSHelper.Message> conversations = SMSHelper.getConversations(this.context);
NetworkPacket reply = new NetworkPacket(PACKET_TYPE_SMS_MESSAGE);
JSONArray messages = new JSONArray();
// Prepare the mostRecentTimestamp counter based on these messages, since they are the most
// recent in every conversation
mostRecentTimestampLock.lock();
for (SMSHelper.Message message : conversations.values()) {
try {
JSONObject json = message.toJSONObject();
json.put("event", "sms");
messages.put(json);
} catch (JSONException e) {
Log.e("Conversations", "Error serializing message");
if (message.m_date > mostRecentTimestamp) {
mostRecentTimestamp = message.m_date;
}
}
mostRecentTimestampLock.unlock();
reply.set("messages", messages);
reply.set("event", "batch_messages"); // Not really necessary, since this is implied by PACKET_TYPE_SMS_MESSAGE, but good for readability
NetworkPacket reply = constructBulkMessagePacket(conversations.values());
device.sendPacket(reply);
@ -276,24 +384,7 @@ public class SMSPlugin extends Plugin {
List<SMSHelper.Message> conversation = SMSHelper.getMessagesInThread(this.context, threadID);
NetworkPacket reply = new NetworkPacket(PACKET_TYPE_SMS_MESSAGE);
JSONArray messages = new JSONArray();
for (SMSHelper.Message message : conversation) {
try {
JSONObject json = message.toJSONObject();
json.put("event", "sms");
messages.put(json);
} catch (JSONException e) {
Log.e("Conversations", "Error serializing message");
}
}
reply.set("messages", messages);
reply.set("event", "batch_messages");
NetworkPacket reply = constructBulkMessagePacket(conversation);
device.sendPacket(reply);