From 331813ce3c71e0c19e69e121d0d8559eaed6a72b Mon Sep 17 00:00:00 2001 From: mar-v-in Date: Tue, 4 Aug 2015 13:05:47 +0200 Subject: [PATCH] Update maps, rework MCS MCS rework related to #23 and #24 --- extern/GmsApi | 2 +- .../src/main/AndroidManifest.xml | 5 +- .../gms/maps/internal/CreatorImpl.java | 65 ++-- .../java/org/microg/gms/gcm/Constants.java | 9 + .../org/microg/gms/gcm/McsInputStream.java | 45 ++- .../org/microg/gms/gcm/McsOutputStream.java | 83 +++-- .../java/org/microg/gms/gcm/McsService.java | 307 +++++++++++------- .../org/microg/gms/gcm/TriggerReceiver.java | 20 +- .../java/org/microg/gms/maps/BackendMap.java | 11 +- .../org/microg/gms/maps/GoogleMapImpl.java | 16 +- .../java/org/microg/gms/maps/MapViewImpl.java | 27 ++ .../bitmap/BitmapDescriptorFactoryImpl.java | 11 + .../wearable/ConfigurationDatabaseHelper.java | 4 + .../microg/gms/wearable/DataItemInternal.java | 48 +++ .../microg/gms/wearable/DataItemRecord.java | 79 +++++ .../gms/wearable/NodeDatabaseHelper.java | 144 ++++++-- .../gms/wearable/WearableServiceImpl.java | 196 +++++++++-- 17 files changed, 829 insertions(+), 243 deletions(-) create mode 100644 play-services-core/src/main/java/org/microg/gms/wearable/DataItemInternal.java create mode 100644 play-services-core/src/main/java/org/microg/gms/wearable/DataItemRecord.java diff --git a/extern/GmsApi b/extern/GmsApi index f77b09dc..0890bf45 160000 --- a/extern/GmsApi +++ b/extern/GmsApi @@ -1 +1 @@ -Subproject commit f77b09dc0c3c750f7c99d901b6e5ced5f17d9465 +Subproject commit 0890bf454651e90274949af9dca09fbcfbf50d36 diff --git a/play-services-core/src/main/AndroidManifest.xml b/play-services-core/src/main/AndroidManifest.xml index d6e58971..b482ad8c 100644 --- a/play-services-core/src/main/AndroidManifest.xml +++ b/play-services-core/src/main/AndroidManifest.xml @@ -67,6 +67,7 @@ + @@ -174,8 +175,8 @@ + android:path="com.google.android.gms" + android:scheme="package" /> diff --git a/play-services-core/src/main/java/com/google/android/gms/maps/internal/CreatorImpl.java b/play-services-core/src/main/java/com/google/android/gms/maps/internal/CreatorImpl.java index 521dba43..47feb8e0 100644 --- a/play-services-core/src/main/java/com/google/android/gms/maps/internal/CreatorImpl.java +++ b/play-services-core/src/main/java/com/google/android/gms/maps/internal/CreatorImpl.java @@ -19,45 +19,58 @@ package com.google.android.gms.maps.internal; import android.app.Activity; import android.content.Context; import android.content.res.Resources; +import android.os.Parcel; import android.os.RemoteException; +import android.util.Log; + import com.google.android.gms.dynamic.IObjectWrapper; import com.google.android.gms.dynamic.ObjectWrapper; import com.google.android.gms.maps.GoogleMapOptions; -import org.microg.gms.maps.bitmap.BitmapDescriptorFactoryImpl; import com.google.android.gms.maps.model.internal.IBitmapDescriptorFactoryDelegate; -import org.microg.gms.maps.camera.CameraUpdateFactoryImpl; + import org.microg.gms.maps.MapFragmentImpl; import org.microg.gms.maps.MapViewImpl; import org.microg.gms.maps.ResourcesContainer; +import org.microg.gms.maps.bitmap.BitmapDescriptorFactoryImpl; +import org.microg.gms.maps.camera.CameraUpdateFactoryImpl; public class CreatorImpl extends ICreator.Stub { - @Override - public void init(IObjectWrapper resources) throws RemoteException { - initV2(resources, 0); - } + private static final String TAG = "GmsMapCreator"; - @Override - public IMapFragmentDelegate newMapFragmentDelegate(IObjectWrapper activity) throws RemoteException { - return new MapFragmentImpl((Activity)ObjectWrapper.unwrap(activity)); - } + @Override + public void init(IObjectWrapper resources) throws RemoteException { + initV2(resources, 0); + } - @Override - public IMapViewDelegate newMapViewDelegate(IObjectWrapper context, GoogleMapOptions options) throws RemoteException { - return new MapViewImpl((Context)ObjectWrapper.unwrap(context), options); - } + @Override + public IMapFragmentDelegate newMapFragmentDelegate(IObjectWrapper activity) throws RemoteException { + return new MapFragmentImpl((Activity) ObjectWrapper.unwrap(activity)); + } - @Override - public ICameraUpdateFactoryDelegate newCameraUpdateFactoryDelegate() throws RemoteException { - return new CameraUpdateFactoryImpl(); - } + @Override + public IMapViewDelegate newMapViewDelegate(IObjectWrapper context, GoogleMapOptions options) throws RemoteException { + return new MapViewImpl((Context) ObjectWrapper.unwrap(context), options); + } - @Override - public IBitmapDescriptorFactoryDelegate newBitmapDescriptorFactoryDelegate() throws RemoteException { - return new BitmapDescriptorFactoryImpl(); - } + @Override + public ICameraUpdateFactoryDelegate newCameraUpdateFactoryDelegate() throws RemoteException { + return new CameraUpdateFactoryImpl(); + } - @Override - public void initV2(IObjectWrapper resources, int flags) throws RemoteException { - ResourcesContainer.set((Resources) ObjectWrapper.unwrap(resources)); - } + @Override + public IBitmapDescriptorFactoryDelegate newBitmapDescriptorFactoryDelegate() throws RemoteException { + return new BitmapDescriptorFactoryImpl(); + } + + @Override + public void initV2(IObjectWrapper resources, int flags) throws RemoteException { + ResourcesContainer.set((Resources) ObjectWrapper.unwrap(resources)); + } + + @Override + public boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException { + if (super.onTransact(code, data, reply, flags)) return true; + Log.d(TAG, "onTransact [unknown]: " + code + ", " + data + ", " + flags); + return false; + } } diff --git a/play-services-core/src/main/java/org/microg/gms/gcm/Constants.java b/play-services-core/src/main/java/org/microg/gms/gcm/Constants.java index ea06c337..e8aa6833 100644 --- a/play-services-core/src/main/java/org/microg/gms/gcm/Constants.java +++ b/play-services-core/src/main/java/org/microg/gms/gcm/Constants.java @@ -26,4 +26,13 @@ public class Constants { public static final int MCS_DATA_MESSAGE_STANZA_TAG = 8; public static final int MCS_VERSION_CODE = 41; + + public static final int MSG_INPUT = 10; + public static final int MSG_INPUT_ERROR = 11; + public static final int MSG_OUTPUT = 20; + public static final int MSG_OUTPUT_ERROR = 21; + public static final int MSG_OUTPUT_READY = 22; + public static final int MSG_TEARDOWN = 30; + public static final int MSG_CONNECT = 40; + public static final int MSG_HEARTBEAT = 41; } diff --git a/play-services-core/src/main/java/org/microg/gms/gcm/McsInputStream.java b/play-services-core/src/main/java/org/microg/gms/gcm/McsInputStream.java index 32020e75..d8f825bc 100644 --- a/play-services-core/src/main/java/org/microg/gms/gcm/McsInputStream.java +++ b/play-services-core/src/main/java/org/microg/gms/gcm/McsInputStream.java @@ -16,6 +16,7 @@ package org.microg.gms.gcm; +import android.os.Handler; import android.util.Log; import com.squareup.wire.Message; @@ -39,8 +40,10 @@ import static org.microg.gms.gcm.Constants.MCS_HEARTBEAT_PING_TAG; import static org.microg.gms.gcm.Constants.MCS_IQ_STANZA_TAG; import static org.microg.gms.gcm.Constants.MCS_LOGIN_REQUEST_TAG; import static org.microg.gms.gcm.Constants.MCS_LOGIN_RESPONSE_TAG; +import static org.microg.gms.gcm.Constants.MSG_INPUT; +import static org.microg.gms.gcm.Constants.MSG_INPUT_ERROR; -public class McsInputStream { +public class McsInputStream extends Thread { private static final String TAG = "GmsGcmMcsInput"; private final InputStream is; @@ -48,14 +51,41 @@ public class McsInputStream { private int version = -1; private int lastStreamIdReported = -1; private int streamId = 0; + private long lastMsgTime = 0; + private Handler mainHandler; - public McsInputStream(InputStream is) { - this(is, false); + public McsInputStream(InputStream is, Handler mainHandler) { + this(is, mainHandler, false); } - public McsInputStream(InputStream is, boolean initialized) { + public McsInputStream(InputStream is, Handler mainHandler, boolean initialized) { this.is = is; + this.mainHandler = mainHandler; this.initialized = initialized; + setName("McsInputStream"); + } + + @Override + public void run() { + try { + while (!Thread.currentThread().isInterrupted()) { + Message message = read(); + if (message != null) { + lastMsgTime = System.currentTimeMillis(); + mainHandler.dispatchMessage(mainHandler.obtainMessage(MSG_INPUT, message)); + } + } + } catch (IOException e) { + try { + is.close(); + } catch (IOException ignored) { + } + mainHandler.dispatchMessage(mainHandler.obtainMessage(MSG_INPUT_ERROR, e)); + } + } + + public void close() { + interrupt(); } public int getStreamId() { @@ -63,6 +93,10 @@ public class McsInputStream { return streamId; } + public long getLastMsgTime() { + return lastMsgTime; + } + public boolean newStreamIdAvailable() { return lastStreamIdReported != streamId; } @@ -88,14 +122,12 @@ public class McsInputStream { ensureVersionRead(); int mcsTag = is.read(); int mcsSize = readVarint(); - Log.d(TAG, "Reading from MCS tag=" + mcsTag + " size=" + mcsSize); byte[] bytes = new byte[mcsSize]; int len = 0; while (len < mcsSize) { len += is.read(bytes, len, mcsSize - len); } Message read = read(mcsTag, bytes, len); - Log.d(TAG, "Read from MCS: " + read); streamId++; return read; } @@ -118,6 +150,7 @@ public class McsInputStream { case MCS_DATA_MESSAGE_STANZA_TAG: return wire.parseFrom(bytes, 0, len, DataMessageStanza.class); default: + Log.w(TAG, "Unknown tag: " + mcsTag); return null; } } diff --git a/play-services-core/src/main/java/org/microg/gms/gcm/McsOutputStream.java b/play-services-core/src/main/java/org/microg/gms/gcm/McsOutputStream.java index 99992611..0d33fa6f 100644 --- a/play-services-core/src/main/java/org/microg/gms/gcm/McsOutputStream.java +++ b/play-services-core/src/main/java/org/microg/gms/gcm/McsOutputStream.java @@ -16,6 +16,8 @@ package org.microg.gms.gcm; +import android.os.Handler; +import android.os.Looper; import android.util.Log; import com.squareup.wire.Message; @@ -28,9 +30,17 @@ import org.microg.gms.gcm.mcs.LoginRequest; import java.io.IOException; import java.io.OutputStream; -import static org.microg.gms.gcm.Constants.*; +import static org.microg.gms.gcm.Constants.MCS_DATA_MESSAGE_STANZA_TAG; +import static org.microg.gms.gcm.Constants.MCS_HEARTBEAT_ACK_TAG; +import static org.microg.gms.gcm.Constants.MCS_HEARTBEAT_PING_TAG; +import static org.microg.gms.gcm.Constants.MCS_LOGIN_REQUEST_TAG; +import static org.microg.gms.gcm.Constants.MCS_VERSION_CODE; +import static org.microg.gms.gcm.Constants.MSG_OUTPUT; +import static org.microg.gms.gcm.Constants.MSG_OUTPUT_ERROR; +import static org.microg.gms.gcm.Constants.MSG_OUTPUT_READY; +import static org.microg.gms.gcm.Constants.MSG_TEARDOWN; -public class McsOutputStream { +public class McsOutputStream extends Thread implements Handler.Callback { private static final String TAG = "GmsGcmMcsOutput"; private final OutputStream os; @@ -38,42 +48,73 @@ public class McsOutputStream { private int version = MCS_VERSION_CODE; private int streamId = 0; - public McsOutputStream(OutputStream os) { - this(os, false); + private Handler mainHandler; + private Handler myHandler; + + public McsOutputStream(OutputStream os, Handler mainHandler) { + this(os, mainHandler, false); } - public McsOutputStream(OutputStream os, boolean initialized) { + public McsOutputStream(OutputStream os, Handler mainHandler, boolean initialized) { this.os = os; + this.mainHandler = mainHandler; this.initialized = initialized; + setName("McsOutputStream"); } public int getStreamId() { return streamId; } - public void write(DataMessageStanza message) throws IOException { - write(message, MCS_DATA_MESSAGE_STANZA_TAG); + @Override + public void run() { + Looper.prepare(); + myHandler = new Handler(this); + mainHandler.dispatchMessage(mainHandler.obtainMessage(MSG_OUTPUT_READY)); + Looper.loop(); } - public void write(LoginRequest loginRequest) throws IOException { - write(loginRequest, MCS_LOGIN_REQUEST_TAG); + @Override + public boolean handleMessage(android.os.Message msg) { + switch (msg.what) { + case MSG_OUTPUT: + try { + Message message = (Message) msg.obj; + if (msg.obj instanceof DataMessageStanza) { + writeInternal(message, MCS_DATA_MESSAGE_STANZA_TAG); + } else if (msg.obj instanceof LoginRequest) { + writeInternal(message, MCS_LOGIN_REQUEST_TAG); + } else if (msg.obj instanceof HeartbeatAck) { + writeInternal(message, MCS_HEARTBEAT_ACK_TAG); + } else if (msg.obj instanceof HeartbeatPing) { + writeInternal(message, MCS_HEARTBEAT_PING_TAG); + } else { + Log.w(TAG, "Unknown message: " + msg.obj); + } + } catch (IOException e) { + mainHandler.dispatchMessage(mainHandler.obtainMessage(MSG_OUTPUT_ERROR, e)); + } + return true; + case MSG_TEARDOWN: + try { + os.close(); + } catch (IOException ignored) { + } + try { + Looper.myLooper().quit(); + } catch (Exception ignored) { + } + return true; + } + return false; } - public void write(HeartbeatAck ack) throws IOException { - write(ack, MCS_HEARTBEAT_ACK_TAG); - } - - public void write(HeartbeatPing ping) throws IOException{ - write(ping, MCS_HEARTBEAT_PING_TAG); - } - - public synchronized void write(Message message, int tag) throws IOException { + private synchronized void writeInternal(Message message, int tag) throws IOException { if (!initialized) { Log.d(TAG, "Write MCS version code: " + version); os.write(version); initialized = true; } - Log.d(TAG, "Write to MCS: " + message); os.write(tag); writeVarint(os, message.getSerializedSize()); os.write(message.toByteArray()); @@ -92,4 +133,8 @@ public class McsOutputStream { } } } + + public Handler getHandler() { + return myHandler; + } } diff --git a/play-services-core/src/main/java/org/microg/gms/gcm/McsService.java b/play-services-core/src/main/java/org/microg/gms/gcm/McsService.java index fc89ec33..056be7cf 100644 --- a/play-services-core/src/main/java/org/microg/gms/gcm/McsService.java +++ b/play-services-core/src/main/java/org/microg/gms/gcm/McsService.java @@ -16,11 +16,16 @@ package org.microg.gms.gcm; +import android.app.AlarmManager; import android.app.IntentService; +import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; -import android.text.TextUtils; +import android.os.Handler; +import android.os.Looper; +import android.os.PowerManager; +import android.os.SystemClock; import android.util.Log; import com.squareup.wire.Message; @@ -35,167 +40,132 @@ import org.microg.gms.gcm.mcs.LoginRequest; import org.microg.gms.gcm.mcs.LoginResponse; import org.microg.gms.gcm.mcs.Setting; -import java.io.IOException; import java.net.Socket; import java.util.Arrays; import java.util.Collections; -import java.util.concurrent.atomic.AtomicBoolean; import javax.net.ssl.SSLContext; import static android.os.Build.VERSION.SDK_INT; +import static org.microg.gms.gcm.Constants.MSG_CONNECT; +import static org.microg.gms.gcm.Constants.MSG_HEARTBEAT; +import static org.microg.gms.gcm.Constants.MSG_INPUT; +import static org.microg.gms.gcm.Constants.MSG_INPUT_ERROR; +import static org.microg.gms.gcm.Constants.MSG_OUTPUT; +import static org.microg.gms.gcm.Constants.MSG_OUTPUT_ERROR; +import static org.microg.gms.gcm.Constants.MSG_OUTPUT_READY; +import static org.microg.gms.gcm.Constants.MSG_TEARDOWN; -public class McsService extends IntentService { +public class McsService extends IntentService implements Handler.Callback { private static final String TAG = "GmsGcmMcsSvc"; + public static String ACTION_CONNECT = "org.microg.gms.gcm.mcs.CONNECT"; + public static String ACTION_HEARTBEAT = "org.microg.gms.gcm.mcs.HEARTBEAT"; + public static final String PREFERENCES_NAME = "mcs"; public static final String PREF_LAST_PERSISTENT_ID = "last_persistent_id"; - public static final String SERVICE_HOST = "mtalk.google.com"; - public static final int SERVICE_PORT = 5228; - public static final String SELF_CATEGORY = "com.google.android.gsf.gtalkservice"; public static final String IDLE_NOTIFICATION = "IdleNotification"; public static final String FROM_FIELD = "gcm@android.com"; - public static final int HEARTBEAT_MS = 60000; - public static final int HEARTBEAT_ALLOWED_OFFSET_MS = 2000; - private static final AtomicBoolean connecting = new AtomicBoolean(false); - private static final AtomicBoolean pending = new AtomicBoolean(false); - private static Thread connectionThread; - private static Thread heartbeatThread; + public static final String SERVICE_HOST = "mtalk.google.com"; + public static final int SERVICE_PORT = 5228; - private Socket sslSocket; - private McsInputStream inputStream; - private McsOutputStream outputStream; - private long lastMsgTime; + public static final int HEARTBEAT_MS = 60000; + + private static Socket sslSocket; + private static McsInputStream inputStream; + private static McsOutputStream outputStream; + + private PendingIntent heartbeatIntent; + + private static MainThread mainThread; + private static Handler mainHandler; + private boolean initialized = false; + + private AlarmManager alarmManager; + private PowerManager powerManager; + private static PowerManager.WakeLock wakeLock; public McsService() { super(TAG); } - public static AtomicBoolean getPending() { - return pending; + private class MainThread extends Thread { + @Override + public void run() { + Looper.prepare(); + mainHandler = new Handler(Looper.myLooper(), McsService.this); + mainHandler.dispatchMessage(mainHandler.obtainMessage(MSG_CONNECT)); + Looper.loop(); + } + } + + @Override + public void onCreate() { + super.onCreate(); + if (mainThread == null) { + mainThread = new MainThread(); + mainThread.start(); + } + heartbeatIntent = PendingIntent.getService(this, 0, new Intent(ACTION_HEARTBEAT, null, this, McsService.class), 0); + alarmManager = (AlarmManager) getSystemService(ALARM_SERVICE); + powerManager = (PowerManager) getSystemService(POWER_SERVICE); + wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "mcs"); + wakeLock.setReferenceCounted(false); + + alarmManager.setInexactRepeating(AlarmManager.ELAPSED_REALTIME, SystemClock.elapsedRealtime() + HEARTBEAT_MS, HEARTBEAT_MS, heartbeatIntent); + } + + public static boolean isConnected() { + return inputStream != null && inputStream.isAlive() && outputStream != null && outputStream.isAlive(); } @Override protected void onHandleIntent(Intent intent) { - if (!isConnected()) { - connectionThread = new Thread(new Runnable() { - @Override - public void run() { - connect(); - } - }); - connectionThread.start(); - } else { - Log.d(TAG, "MCS connection already started"); - } - pending.set(false); - } - - public static boolean isConnected() { - return connecting.get() || (connectionThread != null && connectionThread.isAlive()); - } - - private void heartbeatLoop() { - try { - while (!Thread.interrupted()) { - try { - long waitTime; - while ((waitTime = lastMsgTime + HEARTBEAT_MS - System.currentTimeMillis()) > HEARTBEAT_ALLOWED_OFFSET_MS) { - synchronized (heartbeatThread) { - Log.d(TAG, "Waiting for " + waitTime + "ms"); - heartbeatThread.wait(waitTime); - } - } - HeartbeatPing.Builder ping = new HeartbeatPing.Builder(); - if (inputStream.newStreamIdAvailable()) { - ping.last_stream_id_received(inputStream.getStreamId()); - } - outputStream.write(ping.build()); - lastMsgTime = System.currentTimeMillis(); - } catch (InterruptedException ie) { - Log.w(TAG, ie); - return; - } + wakeLock.acquire(); + if (mainHandler != null) { + if (ACTION_CONNECT.equals(intent.getAction())) { + mainHandler.dispatchMessage(mainHandler.obtainMessage(MSG_CONNECT, intent)); + } else if (ACTION_HEARTBEAT.equals(intent.getAction())) { + mainHandler.dispatchMessage(mainHandler.obtainMessage(MSG_HEARTBEAT, intent)); } - } catch (Exception e) { - Log.w(TAG, e); - connectionThread.interrupt(); } - if (heartbeatThread == Thread.currentThread()) { - heartbeatThread = null; - } - Log.d(TAG, "Heartbeating stopped"); } - private void connect() { - connecting.set(false); + private synchronized void connect() { try { Log.d(TAG, "Starting MCS connection..."); - LastCheckinInfo info = LastCheckinInfo.read(this); Socket socket = new Socket(SERVICE_HOST, SERVICE_PORT); Log.d(TAG, "Connected to " + SERVICE_HOST + ":" + SERVICE_PORT); - sslSocket = SSLContext.getDefault().getSocketFactory().createSocket(socket, "mtalk.google.com", 5228, true); + sslSocket = SSLContext.getDefault().getSocketFactory().createSocket(socket, SERVICE_HOST, SERVICE_PORT, true); Log.d(TAG, "Activated SSL with " + SERVICE_HOST + ":" + SERVICE_PORT); - inputStream = new McsInputStream(sslSocket.getInputStream()); - outputStream = new McsOutputStream(sslSocket.getOutputStream()); - LoginRequest loginRequest = buildLoginRequest(info); - Log.d(TAG, "Sending login request..."); - outputStream.write(loginRequest); - while (!Thread.interrupted()) { - Message o = inputStream.read(); - lastMsgTime = System.currentTimeMillis(); - if (o instanceof DataMessageStanza) { - handleMessage((DataMessageStanza) o); - } else if (o instanceof HeartbeatPing) { - handleHearbeatPing((HeartbeatPing) o); - } else if (o instanceof Close) { - handleClose((Close) o); - } else if (o instanceof LoginResponse) { - handleLoginresponse((LoginResponse) o); - } - } - sslSocket.close(); + inputStream = new McsInputStream(sslSocket.getInputStream(), mainHandler); + outputStream = new McsOutputStream(sslSocket.getOutputStream(), mainHandler); + inputStream.start(); + outputStream.start(); } catch (Exception e) { - Log.w(TAG, e); - try { - sslSocket.close(); - } catch (Exception ignored) { - } + Log.w(TAG, "Exception while connecting!", e); + mainHandler.dispatchMessage(mainHandler.obtainMessage(MSG_TEARDOWN, e)); } - if (heartbeatThread != null) { - heartbeatThread.interrupt(); - heartbeatThread = null; - } - Log.d(TAG, "Connection closed"); - sendBroadcast(new Intent("org.microg.gms.gcm.RECONNECT"), "org.microg.gms.STATUS_BROADCAST"); } - private void handleClose(Close close) throws IOException { - throw new IOException("Server requested close!"); + private void handleClose(Close close) { + throw new RuntimeException("Server requested close!"); } - private void handleLoginresponse(LoginResponse loginResponse) throws IOException { - getSharedPreferences().edit().putString(PREF_LAST_PERSISTENT_ID, "").apply(); + private void handleLoginResponse(LoginResponse loginResponse) { if (loginResponse.error == null) { + getSharedPreferences().edit().putString(PREF_LAST_PERSISTENT_ID, "").apply(); Log.d(TAG, "Logged in"); + wakeLock.release(); } else { - throw new IOException("Could not login: " + loginResponse.error); - } - if (heartbeatThread == null) { - heartbeatThread = new Thread(new Runnable() { - @Override - public void run() { - heartbeatLoop(); - } - }); - heartbeatThread.start(); + throw new RuntimeException("Could not login: " + loginResponse.error); } } - private void handleMessage(DataMessageStanza message) throws IOException { + private void handleCloudMessage(DataMessageStanza message) { if (message.persistent_id != null) { String old = getSharedPreferences().getString(PREF_LAST_PERSISTENT_ID, ""); if (!old.isEmpty()) { @@ -211,15 +181,20 @@ public class McsService extends IntentService { } } - private void handleHearbeatPing(HeartbeatPing ping) throws IOException { + private void handleHearbeatPing(HeartbeatPing ping) { HeartbeatAck.Builder ack = new HeartbeatAck.Builder().status(ping.status); if (inputStream.newStreamIdAvailable()) { ack.last_stream_id_received(inputStream.getStreamId()); } - outputStream.write(ack.build()); + send(ack.build()); } - private LoginRequest buildLoginRequest(LastCheckinInfo info) { + private void handleHeartbeatAck(HeartbeatAck ack) { + wakeLock.release(); + } + + private LoginRequest buildLoginRequest() { + LastCheckinInfo info = LastCheckinInfo.read(this); return new LoginRequest.Builder() .adaptive_heartbeat(false) .auth_service(LoginRequest.AuthService.ANDROID_ID) @@ -246,7 +221,7 @@ public class McsService extends IntentService { sendOrderedBroadcast(intent, msg.category + ".permission.C2D_MESSAGE"); } - private void handleSelfMessage(DataMessageStanza msg) throws IOException { + private void handleSelfMessage(DataMessageStanza msg) { for (AppData appData : msg.app_data) { if (IDLE_NOTIFICATION.equals(appData.key)) { DataMessageStanza.Builder msgResponse = new DataMessageStanza.Builder() @@ -258,7 +233,7 @@ public class McsService extends IntentService { if (inputStream.newStreamIdAvailable()) { msgResponse.last_stream_id_received(inputStream.getStreamId()); } - outputStream.write(msgResponse.build()); + send(msgResponse.build()); } } } @@ -266,4 +241,98 @@ public class McsService extends IntentService { private SharedPreferences getSharedPreferences() { return getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE); } + + private void send(Message message) { + mainHandler.dispatchMessage(mainHandler.obtainMessage(MSG_OUTPUT, message)); + } + + private void sendOutputStream(int what, Object obj) { + McsOutputStream os = outputStream; + if (os != null) { + Handler outputHandler = os.getHandler(); + if (outputHandler != null) + outputHandler.dispatchMessage(outputHandler.obtainMessage(what, obj)); + } + } + + @Override + public boolean handleMessage(android.os.Message msg) { + switch (msg.what) { + case MSG_INPUT: + Log.d(TAG, "Incoming message: " + msg.obj); + handleInput((Message) msg.obj); + return true; + case MSG_OUTPUT: + Log.d(TAG, "Outgoing message: " + msg.obj); + sendOutputStream(MSG_OUTPUT, msg.obj); + return true; + case MSG_INPUT_ERROR: + case MSG_OUTPUT_ERROR: + Log.d(TAG, "I/O error: " + msg.obj); + mainHandler.dispatchMessage(mainHandler.obtainMessage(MSG_TEARDOWN, msg.obj)); + return true; + case MSG_TEARDOWN: + Log.d(TAG, "Teardown initiated, reason: " + msg.obj); + handleTeardown(msg); + return true; + case MSG_CONNECT: + Log.d(TAG, "Connect initiated, reason: " + msg.obj); + if (!isConnected()) { + connect(); + } + return true; + case MSG_HEARTBEAT: + Log.d(TAG, "Heartbeat initiated, reason: " + msg.obj); + if (isConnected()) { + HeartbeatPing.Builder ping = new HeartbeatPing.Builder(); + if (inputStream.newStreamIdAvailable()) { + ping.last_stream_id_received(inputStream.getStreamId()); + } + send(ping.build()); + } else { + Log.d(TAG, "Ignoring heartbeat, not connected!"); + } + return true; + case MSG_OUTPUT_READY: + Log.d(TAG, "Sending login request..."); + send(buildLoginRequest()); + return true; + } + Log.w(TAG, "Unknown message: " + msg); + return false; + } + + private void handleInput(Message message) { + try { + if (message instanceof DataMessageStanza) { + handleCloudMessage((DataMessageStanza) message); + } else if (message instanceof HeartbeatPing) { + handleHearbeatPing((HeartbeatPing) message); + } else if (message instanceof Close) { + handleClose((Close) message); + } else if (message instanceof LoginResponse) { + handleLoginResponse((LoginResponse) message); + } else if (message instanceof HeartbeatAck) { + handleHeartbeatAck((HeartbeatAck) message); + } else { + Log.w(TAG, "Unknown message: " + message); + } + } catch (Exception e) { + mainHandler.dispatchMessage(mainHandler.obtainMessage(MSG_TEARDOWN, e)); + } + } + + private void handleTeardown(android.os.Message msg) { + sendOutputStream(MSG_TEARDOWN, msg.obj); + if (inputStream != null) { + inputStream.close(); + } + try { + sslSocket.close(); + } catch (Exception ignored) { + } + sendBroadcast(new Intent("org.microg.gms.gcm.RECONNECT"), "org.microg.gms.STATUS_BROADCAST"); + alarmManager.cancel(heartbeatIntent); + wakeLock.release(); + } } diff --git a/play-services-core/src/main/java/org/microg/gms/gcm/TriggerReceiver.java b/play-services-core/src/main/java/org/microg/gms/gcm/TriggerReceiver.java index 6cc5dec3..020cc0fe 100644 --- a/play-services-core/src/main/java/org/microg/gms/gcm/TriggerReceiver.java +++ b/play-services-core/src/main/java/org/microg/gms/gcm/TriggerReceiver.java @@ -34,21 +34,17 @@ public class TriggerReceiver extends BroadcastReceiver { public void onReceive(Context context, Intent intent) { boolean force = "android.provider.Telephony.SECRET_CODE".equals(intent.getAction()); - if (McsService.getPending().compareAndSet(false, true)) { - if (!McsService.isConnected() || force) { - if (PreferenceManager.getDefaultSharedPreferences(context).getBoolean(PREF_ENABLE_GCM, false) || force) { + if (!McsService.isConnected() || force) { + if (PreferenceManager.getDefaultSharedPreferences(context).getBoolean(PREF_ENABLE_GCM, false) || force) { - ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); - NetworkInfo networkInfo = cm.getActiveNetworkInfo(); - if (networkInfo != null && networkInfo.isConnected() || force) { - AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); - PendingIntent pendingIntent = PendingIntent.getService(context, 0, new Intent(context, McsService.class), 0); - alarmManager.set(AlarmManager.ELAPSED_REALTIME, SystemClock.elapsedRealtime() + pendingDelay, pendingIntent); - return; - } + ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo networkInfo = cm.getActiveNetworkInfo(); + if (networkInfo != null && networkInfo.isConnected() || force) { + AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); + PendingIntent pendingIntent = PendingIntent.getService(context, 0, new Intent(McsService.ACTION_CONNECT, null, context, McsService.class), 0); + alarmManager.set(AlarmManager.ELAPSED_REALTIME, SystemClock.elapsedRealtime() + pendingDelay, pendingIntent); } } - McsService.getPending().set(false); } } } diff --git a/play-services-core/src/main/java/org/microg/gms/maps/BackendMap.java b/play-services-core/src/main/java/org/microg/gms/maps/BackendMap.java index df680523..9da8ccfe 100644 --- a/play-services-core/src/main/java/org/microg/gms/maps/BackendMap.java +++ b/play-services-core/src/main/java/org/microg/gms/maps/BackendMap.java @@ -41,6 +41,7 @@ import org.oscim.map.Viewport; import org.oscim.theme.VtmThemes; import org.oscim.tiling.source.oscimap4.OSciMap4TileSource; +import java.lang.reflect.Method; import java.util.HashMap; public class BackendMap implements ItemizedLayer.OnItemGestureListener { @@ -83,27 +84,27 @@ public class BackendMap implements ItemizedLayer.OnItemGestureListener assets = new HashMap(); + + public DataItemInternal(String host, String path) { + this.host = host; + this.path = path; + this.uri = new Uri.Builder().scheme("wear").authority(host).path(path).build(); + } + + public DataItemInternal addAsset(String key, Asset asset) { + this.assets.put(key, asset); + return this; + } + + public Map getAssets() { + return Collections.unmodifiableMap(new HashMap(assets)); + } +} diff --git a/play-services-core/src/main/java/org/microg/gms/wearable/DataItemRecord.java b/play-services-core/src/main/java/org/microg/gms/wearable/DataItemRecord.java new file mode 100644 index 00000000..d9c0c8fd --- /dev/null +++ b/play-services-core/src/main/java/org/microg/gms/wearable/DataItemRecord.java @@ -0,0 +1,79 @@ +/* + * Copyright 2013-2015 µg Project Team + * + * 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 org.microg.gms.wearable; + +import android.content.ContentValues; +import android.database.Cursor; + +import com.google.android.gms.wearable.Asset; +import com.google.android.gms.wearable.internal.DataItemAssetParcelable; +import com.google.android.gms.wearable.internal.DataItemParcelable; + +import java.util.Map; + +public class DataItemRecord { + public DataItemInternal dataItem; + public String source; + public long seqId; + public long v1SeqId; + public long lastModified; + public boolean deleted; + public boolean assetsAreReady; + public String packageName; + public String signatureDigest; + + public ContentValues getContentValues() { + ContentValues contentValues = new ContentValues(); + contentValues.put("sourceNode", source); + contentValues.put("seqId", seqId); + contentValues.put("v1SourceNode", source); + contentValues.put("v1SeqId", v1SeqId); + contentValues.put("timestampMs", lastModified); + if (deleted) { + contentValues.put("deleted", 1); + contentValues.putNull("data"); + } else { + contentValues.put("deleted", 0); + contentValues.put("data", dataItem.data); + } + contentValues.put("assetsPresent", assetsAreReady ? 1 : 0); + return contentValues; + } + + public DataItemParcelable toParcelable() { + DataItemParcelable parcelable = new DataItemParcelable(dataItem.uri); + parcelable.data = dataItem.data; + for (Map.Entry entry : dataItem.getAssets().entrySet()) { + parcelable.getAssets().put(entry.getKey(), new DataItemAssetParcelable(entry.getValue().getDigest(), entry.getKey())); + } + return parcelable; + } + + public static DataItemRecord fromCursor(Cursor cursor) { + DataItemRecord record = new DataItemRecord(); + record.packageName = cursor.getString(1); + record.signatureDigest = cursor.getString(2); + record.dataItem = new DataItemInternal(cursor.getString(3), cursor.getString(4)); + record.seqId = cursor.getLong(5); + record.deleted = cursor.getLong(6) > 0; + record.source = cursor.getString(7); + record.dataItem.data = cursor.getBlob(8); + record.lastModified = cursor.getLong(9); + record.assetsAreReady = cursor.getLong(10) > 0; + return record; + } +} diff --git a/play-services-core/src/main/java/org/microg/gms/wearable/NodeDatabaseHelper.java b/play-services-core/src/main/java/org/microg/gms/wearable/NodeDatabaseHelper.java index a8478c64..2acdfe37 100644 --- a/play-services-core/src/main/java/org/microg/gms/wearable/NodeDatabaseHelper.java +++ b/play-services-core/src/main/java/org/microg/gms/wearable/NodeDatabaseHelper.java @@ -21,15 +21,18 @@ import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; -import android.net.Uri; +import android.util.Log; import com.google.android.gms.wearable.Asset; import java.util.Map; public class NodeDatabaseHelper extends SQLiteOpenHelper { + private static final String TAG = "GmsWearNodeDB"; + private static final String DB_NAME = "node.db"; - private static final int VERSION = 7; + private static final String[] GDIBHAP_FIELDS = new String[]{"dataitems_id", "packageName", "signatureDigest", "host", "path", "seqId", "deleted", "sourceNode", "data", "timestampMs", "assetsPresent", "assetname", "assets_digest", "v1SourceNode", "v1SeqId"}; + private static final int VERSION = 9; public NodeDatabaseHelper(Context context) { super(context, DB_NAME, null, VERSION); @@ -38,26 +41,29 @@ public class NodeDatabaseHelper extends SQLiteOpenHelper { @Override public void onCreate(SQLiteDatabase db) { db.execSQL("CREATE TABLE appkeys(_id INTEGER PRIMARY KEY AUTOINCREMENT,packageName TEXT NOT NULL,signatureDigest TEXT NOT NULL);"); - db.execSQL("CREATE TABLE dataitems(_id INTEGER PRIMARY KEY AUTOINCREMENT, appkeys_id INTEGER NOT NULL REFERENCES appkeys(_id), host TEXT NOT NULL, path TEXT NOT NULL, seqId INTEGER NOT NULL, deleted INTEGER NOT NULL, sourceNode TEXT NOT NULL, data BLOB, timestampMs INTEGER NOT NULL, assetsPresent INTEGER NOT NULL);"); + db.execSQL("CREATE TABLE dataitems(_id INTEGER PRIMARY KEY AUTOINCREMENT, appkeys_id INTEGER NOT NULL REFERENCES appkeys(_id), host TEXT NOT NULL, path TEXT NOT NULL, seqId INTEGER NOT NULL, deleted INTEGER NOT NULL, sourceNode TEXT NOT NULL, data BLOB, timestampMs INTEGER NOT NULL, assetsPresent INTEGER NOT NULL, v1SourceNode TEXT NOT NULL, v1SeqId INTEGER NOT NULL);"); db.execSQL("CREATE TABLE assets(digest TEXT PRIMARY KEY, dataPresent INTEGER NOT NULL DEFAULT 0, timestampMs INTEGER NOT NULL);"); db.execSQL("CREATE TABLE assetrefs(assetname TEXT NOT NULL, dataitems_id INTEGER NOT NULL REFERENCES dataitems(_id), assets_digest TEXT NOT NULL REFERENCES assets(digest));"); db.execSQL("CREATE TABLE assetsacls(appkeys_id INTEGER NOT NULL REFERENCES appkeys(_id), assets_digest TEXT NOT NULL);"); - db.execSQL("CREATE VIEW appKeyDataItems AS SELECT appkeys._id AS appkeys_id, appkeys.packageName AS packageName, appkeys.signatureDigest AS signatureDigest, dataitems._id AS dataitems_id, dataitems.host AS host, dataitems.path AS path, dataitems.seqId AS seqId, dataitems.deleted AS deleted, dataitems.sourceNode AS sourceNode, dataitems.data AS data, dataitems.timestampMs AS timestampMs, dataitems.assetsPresent AS assetsPresent FROM appkeys, dataitems WHERE appkeys._id=dataitems.appkeys_id"); + db.execSQL("CREATE TABLE nodeinfo(node TEXT NOT NULL PRIMARY KEY, seqId INTEGER, lastActivityMs INTEGER);"); + db.execSQL("CREATE VIEW appKeyDataItems AS SELECT appkeys._id AS appkeys_id, appkeys.packageName AS packageName, appkeys.signatureDigest AS signatureDigest, dataitems._id AS dataitems_id, dataitems.host AS host, dataitems.path AS path, dataitems.seqId AS seqId, dataitems.deleted AS deleted, dataitems.sourceNode AS sourceNode, dataitems.data AS data, dataitems.timestampMs AS timestampMs, dataitems.assetsPresent AS assetsPresent, dataitems.v1SourceNode AS v1SourceNode, dataitems.v1SeqId AS v1SeqId FROM appkeys, dataitems WHERE appkeys._id=dataitems.appkeys_id"); db.execSQL("CREATE VIEW appKeyAcls AS SELECT appkeys._id AS appkeys_id, appkeys.packageName AS packageName, appkeys.signatureDigest AS signatureDigest, assetsacls.assets_digest AS assets_digest FROM appkeys, assetsacls WHERE _id=appkeys_id"); - db.execSQL("CREATE VIEW dataItemsAndAssets AS SELECT appKeyDataItems.packageName AS packageName, appKeyDataItems.signatureDigest AS signatureDigest, appKeyDataItems.dataitems_id AS dataitems_id, appKeyDataItems.host AS host, appKeyDataItems.path AS path, appKeyDataItems.seqId AS seqId, appKeyDataItems.deleted AS deleted, appKeyDataItems.sourceNode AS sourceNode, appKeyDataItems.data AS data, appKeyDataItems.timestampMs AS timestampMs, appKeyDataItems.assetsPresent AS assetsPresent, assetrefs.assetname AS assetname, assetrefs.assets_digest AS assets_digest FROM appKeyDataItems LEFT OUTER JOIN assetrefs ON appKeyDataItems.dataitems_id=assetrefs.dataitems_id"); + db.execSQL("CREATE VIEW dataItemsAndAssets AS SELECT appKeyDataItems.packageName AS packageName, appKeyDataItems.signatureDigest AS signatureDigest, appKeyDataItems.dataitems_id AS dataitems_id, appKeyDataItems.host AS host, appKeyDataItems.path AS path, appKeyDataItems.seqId AS seqId, appKeyDataItems.deleted AS deleted, appKeyDataItems.sourceNode AS sourceNode, appKeyDataItems.data AS data, appKeyDataItems.timestampMs AS timestampMs, appKeyDataItems.assetsPresent AS assetsPresent, assetrefs.assetname AS assetname, assetrefs.assets_digest AS assets_digest, appKeyDataItems.v1SourceNode AS v1SourceNode, appKeyDataItems.v1SeqId AS v1SeqId FROM appKeyDataItems LEFT OUTER JOIN assetrefs ON appKeyDataItems.dataitems_id=assetrefs.dataitems_id"); db.execSQL("CREATE VIEW assetsReadyStatus AS SELECT dataitems_id AS dataitems_id, COUNT(*) = SUM(dataPresent) AS nowReady, assetsPresent AS markedReady FROM assetrefs, dataitems LEFT OUTER JOIN assets ON assetrefs.assets_digest = assets.digest WHERE assetrefs.dataitems_id=dataitems._id GROUP BY dataitems_id;"); db.execSQL("CREATE UNIQUE INDEX appkeys_NAME_AND_SIG ON appkeys(packageName,signatureDigest);"); db.execSQL("CREATE UNIQUE INDEX assetrefs_ASSET_REFS ON assetrefs(assets_digest,dataitems_id,assetname);"); db.execSQL("CREATE UNIQUE INDEX assets_DIGEST ON assets(digest);"); db.execSQL("CREATE UNIQUE INDEX assetsacls_APPKEY_AND_DIGEST ON assetsacls(appkeys_id,assets_digest);"); db.execSQL("CREATE UNIQUE INDEX dataitems_APPKEY_HOST_AND_PATH ON dataitems(appkeys_id,host,path);"); + db.execSQL("CREATE UNIQUE INDEX dataitems_SOURCENODE_AND_SEQID ON dataitems(sourceNode,seqId);"); + db.execSQL("CREATE UNIQUE INDEX dataitems_SOURCENODE_DELETED_AND_SEQID ON dataitems(sourceNode,deleted,seqId);"); } - public Cursor getDataItemsForDataHolder(String packageName, String signatureDigest) { + public synchronized Cursor getDataItemsForDataHolder(String packageName, String signatureDigest) { return getDataItemsForDataHolderByHostAndPath(packageName, signatureDigest, null, null); } - public Cursor getDataItemsForDataHolderByHostAndPath(String packageName, String signatureDigest, String host, String path) { + public synchronized Cursor getDataItemsForDataHolderByHostAndPath(String packageName, String signatureDigest, String host, String path) { String[] params; String selection; if (path == null) { @@ -76,32 +82,126 @@ public class NodeDatabaseHelper extends SQLiteOpenHelper { @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { - + if (oldVersion != VERSION) { + // TODO: Upgrade not supported, cleaning up + db.execSQL("DROP TABLE IF EXISTS appkeys;"); + db.execSQL("DROP TABLE IF EXISTS dataitems;"); + db.execSQL("DROP TABLE IF EXISTS assets;"); + db.execSQL("DROP TABLE IF EXISTS assetrefs;"); + db.execSQL("DROP TABLE IF EXISTS assetsacls;"); + db.execSQL("DROP TABLE IF EXISTS nodeinfo;"); + db.execSQL("DROP VIEW IF EXISTS appKeyDataItems;"); + db.execSQL("DROP VIEW IF EXISTS appKeyAcls;"); + db.execSQL("DROP VIEW IF EXISTS dataItemsAndAssets;"); + db.execSQL("DROP VIEW IF EXISTS assetsReadyStatus;"); + onCreate(db); + } } - private synchronized long getAppKey(String packageName, String signatureDigest) { - Cursor cursor = getReadableDatabase().rawQuery("SELECT _id FROM appkeys WHERE packageName=? AND signatureDigest=?", new String[]{packageName, signatureDigest}); + private synchronized long getAppKey(SQLiteDatabase db, String packageName, String signatureDigest) { + Cursor cursor = db.rawQuery("SELECT _id FROM appkeys WHERE packageName=? AND signatureDigest=?", new String[]{packageName, signatureDigest}); if (cursor != null) { - if (cursor.moveToNext()) { - return cursor.getLong(0); + try { + if (cursor.moveToNext()) { + return cursor.getLong(0); + } + } finally { + cursor.close(); } - cursor.close(); } ContentValues appKey = new ContentValues(); appKey.put("packageName", packageName); appKey.put("signatureDigest", signatureDigest); - return getWritableDatabase().insert("appkeys", null, appKey); + return db.insert("appkeys", null, appKey); } - public void putDataItem(String packageName, String signatureDigest, String host, String path, ContentValues data) { - ContentValues item = new ContentValues(data); - item.put("appkeys_id", getAppKey(packageName, signatureDigest)); - item.put("host", host); - item.put("path", path); - getWritableDatabase().insertWithOnConflict("dataitems", "host", item, SQLiteDatabase.CONFLICT_REPLACE); + public synchronized void putRecord(DataItemRecord record) { + SQLiteDatabase db = getWritableDatabase(); + db.beginTransaction(); + Cursor cursor = getDataItemsByHostAndPath(db, record.packageName, record.signatureDigest, record.dataItem.host, record.dataItem.path); + try { + String key; + if (cursor.moveToNext()) { + // update + key = cursor.getString(0); + updateRecord(db, key, record); + } else { + // insert + key = insertRecord(db, record); + } + if (record.assetsAreReady) { + ContentValues update = new ContentValues(); + update.put("assetsPresent", 1); + db.update("dataitems", update, "_id=?", new String[]{key}); + } + db.setTransactionSuccessful(); + } finally { + cursor.close(); + } + db.endTransaction(); } - public void deleteDataItem(String packageName, String signatureDigest, String host, String path) { - getWritableDatabase().delete("dataitems", "packageName=? AND signatureDigest=? AND host=? AND path=?", new String[]{packageName, signatureDigest, host, packageName}); + private static void updateRecord(SQLiteDatabase db, String key, DataItemRecord record) { + Log.d(TAG, "updateRecord: " + record); + } + + private String insertRecord(SQLiteDatabase db, DataItemRecord record) { + ContentValues contentValues = record.getContentValues(); + contentValues.put("appkeys_id", getAppKey(db, record.packageName, record.signatureDigest)); + contentValues.put("host", record.dataItem.host); + contentValues.put("path", record.dataItem.path); + String key = Long.toString(db.insert("dataitems", "host", contentValues)); + if (!record.deleted) { + for (Map.Entry asset : record.dataItem.getAssets().entrySet()) { + ContentValues assetValues = new ContentValues(); + assetValues.put("assets_digest", asset.getValue().getDigest()); + assetValues.put("dataitems_id", key); + assetValues.put("assetname", asset.getKey()); + db.insert("assetrefs", "assetname", assetValues); + } + Cursor status = db.query("assetsReadyStatus", new String[]{"nowReady"}, "dataitems_id=?", new String[]{key}, null, null, null); + if (status.moveToNext()) { + record.assetsAreReady = status.getLong(0) != 0; + } + status.close(); + } else { + record.assetsAreReady = false; + } + return key; + } + + private static Cursor getDataItemsByHostAndPath(SQLiteDatabase db, String packageName, String signatureDigest, String host, String path) { + String[] params; + String selection; + if (path == null) { + params = new String[]{packageName, signatureDigest}; + selection = "packageName =? AND signatureDigest =?"; + } else if (host == null) { + params = new String[]{packageName, signatureDigest, path}; + selection = "packageName =? AND signatureDigest =? AND path =?"; + } else { + params = new String[]{packageName, signatureDigest, host, path}; + selection = "packageName =? AND signatureDigest =? AND host =? AND path =?"; + } + selection += " AND deleted=0"; + return db.query("dataItemsAndAssets", GDIBHAP_FIELDS, selection, params, null, null, "packageName, signatureDigest, host, path"); + } + + public synchronized int deleteDataItems(String packageName, String signatureDigest, String host, String path) { + SQLiteDatabase db = getWritableDatabase(); + db.beginTransaction(); + Cursor cursor = getDataItemsByHostAndPath(db, packageName, signatureDigest, host, path); + int n = 0; + while (cursor.moveToNext()) { + DataItemRecord record = DataItemRecord.fromCursor(cursor); + record.deleted = true; + record.assetsAreReady = true; + record.dataItem.data = null; + updateRecord(db, cursor.getString(0), record); + n++; + } + db.setTransactionSuccessful(); + db.endTransaction(); + return n; } } diff --git a/play-services-core/src/main/java/org/microg/gms/wearable/WearableServiceImpl.java b/play-services-core/src/main/java/org/microg/gms/wearable/WearableServiceImpl.java index 9954575b..be64d065 100644 --- a/play-services-core/src/main/java/org/microg/gms/wearable/WearableServiceImpl.java +++ b/play-services-core/src/main/java/org/microg/gms/wearable/WearableServiceImpl.java @@ -16,7 +16,6 @@ package org.microg.gms.wearable; -import android.content.ContentValues; import android.content.Context; import android.content.SharedPreferences; import android.database.Cursor; @@ -24,21 +23,27 @@ import android.net.Uri; import android.os.Parcel; import android.os.RemoteException; import android.text.TextUtils; +import android.util.Base64; import android.util.Log; import com.google.android.gms.common.api.Status; import com.google.android.gms.common.data.DataHolder; -import com.google.android.gms.wearable.AddListenerRequest; +import com.google.android.gms.wearable.internal.AddListenerRequest; +import com.google.android.gms.wearable.Asset; import com.google.android.gms.wearable.ConnectionConfiguration; -import com.google.android.gms.wearable.GetConfigResponse; -import com.google.android.gms.wearable.GetConfigsResponse; -import com.google.android.gms.wearable.GetConnectedNodesResponse; -import com.google.android.gms.wearable.GetDataItemResponse; -import com.google.android.gms.wearable.GetLocalNodeResponse; -import com.google.android.gms.wearable.PutDataRequest; -import com.google.android.gms.wearable.PutDataResponse; -import com.google.android.gms.wearable.RemoveListenerRequest; -import com.google.android.gms.wearable.internal.DataItemAssetParcelable; +import com.google.android.gms.wearable.internal.CapabilityInfoParcelable; +import com.google.android.gms.wearable.internal.ChannelEventParcelable; +import com.google.android.gms.wearable.internal.DeleteDataItemsResponse; +import com.google.android.gms.wearable.internal.GetConfigResponse; +import com.google.android.gms.wearable.internal.GetConfigsResponse; +import com.google.android.gms.wearable.internal.GetConnectedNodesResponse; +import com.google.android.gms.wearable.internal.GetDataItemResponse; +import com.google.android.gms.wearable.internal.GetLocalNodeResponse; +import com.google.android.gms.wearable.internal.IWearableListener; +import com.google.android.gms.wearable.internal.MessageEventParcelable; +import com.google.android.gms.wearable.internal.PutDataRequest; +import com.google.android.gms.wearable.internal.PutDataResponse; +import com.google.android.gms.wearable.internal.RemoveListenerRequest; import com.google.android.gms.wearable.internal.DataItemParcelable; import com.google.android.gms.wearable.internal.IWearableCallbacks; import com.google.android.gms.wearable.internal.IWearableService; @@ -46,22 +51,33 @@ import com.google.android.gms.wearable.internal.NodeParcelable; import org.microg.gms.common.PackageUtils; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; import java.util.ArrayList; -import java.util.HashMap; +import java.util.HashSet; +import java.util.List; import java.util.Map; +import java.util.Set; import java.util.UUID; -public class WearableServiceImpl extends IWearableService.Stub { +public class WearableServiceImpl extends IWearableService.Stub implements IWearableListener { private static final String TAG = "GmsWearSvcImpl"; private static final String CLOCKWORK_NODE_PREFERENCES = "cw_node"; private static final String CLOCKWORK_NODE_PREFERENCE_NODE_ID = "node_id"; + private static final String CLOCKWORK_NODE_PREFERENCE_NEXT_SEQ_ID_BLOCK = "nextSeqIdBlock"; private final Context context; private final String packageName; private final NodeDatabaseHelper nodeDatabase; private final ConfigurationDatabaseHelper configDatabase; + private Set listeners = new HashSet(); + private long seqIdBlock; + private long seqIdInBlock = -1; public WearableServiceImpl(Context context, NodeDatabaseHelper nodeDatabase, ConfigurationDatabaseHelper configDatabase, String packageName) { this.context = context; @@ -80,28 +96,81 @@ public class WearableServiceImpl extends IWearableService.Stub { return nodeId; } + private synchronized long getNextSeqId() { + SharedPreferences preferences = context.getSharedPreferences(CLOCKWORK_NODE_PREFERENCES, Context.MODE_PRIVATE); + if (seqIdInBlock < 0) seqIdInBlock = 1000; + if (seqIdInBlock >= 1000) { + seqIdBlock = preferences.getLong(CLOCKWORK_NODE_PREFERENCE_NEXT_SEQ_ID_BLOCK, 100); + preferences.edit().putLong(CLOCKWORK_NODE_PREFERENCE_NEXT_SEQ_ID_BLOCK, seqIdBlock + seqIdInBlock).apply(); + seqIdInBlock = 0; + } + return seqIdBlock + seqIdInBlock++; + } + @Override public void putData(IWearableCallbacks callbacks, PutDataRequest request) throws RemoteException { Log.d(TAG, "putData: " + request); String host = request.getUri().getHost(); if (TextUtils.isEmpty(host)) host = getLocalNodeId(); - ContentValues prepared = new ContentValues(); - prepared.put("sourceNode", getLocalNodeId()); - prepared.put("deleted", false); - prepared.put("data", request.getData()); - prepared.put("timestampMs", System.currentTimeMillis()); - prepared.put("seqId", 0xFFFFFFFFL); - prepared.put("assetsPresent", false); - nodeDatabase.putDataItem(packageName, PackageUtils.firstSignatureDigest(context, packageName), host, request.getUri().getPath(), prepared); - Map assetMap = new HashMap(); - for (String key : request.getAssets().keySet()) { - assetMap.put(key, new DataItemAssetParcelable()); + DataItemInternal dataItem = new DataItemInternal(host, request.getUri().getPath()); + for (Map.Entry assetEntry : request.getAssets().entrySet()) { + Asset asset = prepareAsset(packageName, assetEntry.getValue()); + if (asset != null) { + dataItem.addAsset(assetEntry.getKey(), asset); + } } - if (!assetMap.isEmpty()) { - prepared.put("assetsPresent", true); - nodeDatabase.putDataItem(packageName, PackageUtils.firstSignatureDigest(context, packageName), host, request.getUri().getPath(), prepared); + dataItem.data = request.getData(); + DataItemParcelable parcelable = putDataItem(packageName, + PackageUtils.firstSignatureDigest(context, packageName), getLocalNodeId(), dataItem).toParcelable(); + callbacks.onPutDataResponse(new PutDataResponse(0, parcelable)); + } + + private DataItemRecord putDataItem(String packageName, String signatureDigest, String source, DataItemInternal dataItem) { + DataItemRecord record = new DataItemRecord(); + record.packageName = packageName; + record.signatureDigest = signatureDigest; + record.deleted = false; + record.source = source; + record.dataItem = dataItem; + record.v1SeqId = getNextSeqId(); + if (record.source.equals(getLocalNodeId())) record.seqId = record.v1SeqId; + nodeDatabase.putRecord(record); + return record; + } + + private Asset prepareAsset(String packageName, Asset asset) { + if (asset.data != null) { + String digest = calculateDigest(asset.data); + File assetFile = createAssetFile(digest); + boolean success = assetFile.exists(); + if (!success) { + File tmpFile = new File(assetFile.getParent(), assetFile.getName() + ".tmp"); + + try { + FileOutputStream stream = new FileOutputStream(tmpFile); + stream.write(asset.data); + stream.close(); + success = tmpFile.renameTo(assetFile); + } catch (IOException e) { + } + } + return Asset.createFromRef(digest); + } + return null; + } + + private File createAssetFile(String digest) { + File dir = new File(new File(context.getFilesDir(), "assets"), digest.substring(digest.length() - 2, digest.length())); + dir.mkdirs(); + return new File(dir, digest + ".asset"); + } + + private String calculateDigest(byte[] data) { + try { + return Base64.encodeToString(MessageDigest.getInstance("SHA1").digest(data), Base64.NO_WRAP | Base64.NO_PADDING | Base64.URL_SAFE); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); } - callbacks.onPutDataResponse(new PutDataResponse(0, new DataItemParcelable(request.getUri(), assetMap))); } @Override @@ -124,6 +193,11 @@ public class WearableServiceImpl extends IWearableService.Stub { public void getDataItems(IWearableCallbacks callbacks) throws RemoteException { Log.d(TAG, "getDataItems: " + callbacks); Cursor dataHolderItems = nodeDatabase.getDataItemsForDataHolder(packageName, PackageUtils.firstSignatureDigest(context, packageName)); + while (dataHolderItems.moveToNext()) { + Log.d(TAG, "getDataItems[]: path=" + Uri.parse(dataHolderItems.getString(1)).getPath()); + } + dataHolderItems.moveToFirst(); + dataHolderItems.moveToPrevious(); callbacks.onDataHolder(DataHolder.fromCursor(dataHolderItems, 0, null)); } @@ -137,7 +211,12 @@ public class WearableServiceImpl extends IWearableService.Stub { @Override public void deleteDataItems(IWearableCallbacks callbacks, Uri uri) throws RemoteException { Log.d(TAG, "deleteDataItems: " + uri); - nodeDatabase.deleteDataItem(packageName, PackageUtils.firstSignatureDigest(context, packageName), uri.getHost(), uri.getPath()); + int count = nodeDatabase.deleteDataItems(packageName, PackageUtils.firstSignatureDigest(context, packageName), uri.getHost(), uri.getPath()); + callbacks.onDeleteDataItemsResponse(new DeleteDataItemsResponse(0, count)); + } + + @Override + public void optInCloudSync(IWearableCallbacks callbacks, boolean enable) throws RemoteException { callbacks.onStatus(Status.SUCCESS); } @@ -159,11 +238,15 @@ public class WearableServiceImpl extends IWearableService.Stub { @Override public void addListener(IWearableCallbacks callbacks, AddListenerRequest request) throws RemoteException { Log.d(TAG, "addListener[nyp]: " + request); + listeners.add(request.listener); + callbacks.onStatus(Status.SUCCESS); } @Override public void removeListener(IWearableCallbacks callbacks, RemoveListenerRequest request) throws RemoteException { Log.d(TAG, "removeListener[nyp]: " + request); + listeners.remove(request.listener); + callbacks.onStatus(Status.SUCCESS); } @Override @@ -194,10 +277,65 @@ public class WearableServiceImpl extends IWearableService.Stub { } } + @Override + public void disableConnection(IWearableCallbacks callbacks, String name) throws RemoteException { + configDatabase.setEnabledState(name, false); + callbacks.onStatus(Status.SUCCESS); + } + @Override public boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException { if (super.onTransact(code, data, reply, flags)) return true; Log.d(TAG, "onTransact [unknown]: " + code + ", " + data + ", " + flags); return false; } + + @Override + public void onDataChanged(DataHolder data) throws RemoteException { + for (IWearableListener listener : listeners) { + listener.onDataChanged(data); + } + } + + @Override + public void onMessageReceived(MessageEventParcelable messageEvent) throws RemoteException { + for (IWearableListener listener : listeners) { + listener.onMessageReceived(messageEvent); + } + } + + @Override + public void onPeerConnected(NodeParcelable node) throws RemoteException { + for (IWearableListener listener : listeners) { + listener.onPeerConnected(node); + } + } + + @Override + public void onPeerDisconnected(NodeParcelable node) throws RemoteException { + for (IWearableListener listener : listeners) { + listener.onPeerDisconnected(node); + } + } + + @Override + public void onConnectedNodes(List nodes) throws RemoteException { + for (IWearableListener listener : listeners) { + listener.onConnectedNodes(nodes); + } + } + + @Override + public void onChannelEvent(ChannelEventParcelable channelEvent) throws RemoteException { + for (IWearableListener listener : listeners) { + listener.onChannelEvent(channelEvent); + } + } + + @Override + public void onConnectedCapabilityChanged(CapabilityInfoParcelable capabilityInfo) throws RemoteException { + for (IWearableListener listener : listeners) { + listener.onConnectedCapabilityChanged(capabilityInfo); + } + } }