diff --git a/extern/GmsApi b/extern/GmsApi index 2c847ce6..f77b09dc 160000 --- a/extern/GmsApi +++ b/extern/GmsApi @@ -1 +1 @@ -Subproject commit 2c847ce6e8ac1604356dc45fd672cfdd6ad4d73b +Subproject commit f77b09dc0c3c750f7c99d901b6e5ced5f17d9465 diff --git a/play-services-core/src/main/java/org/microg/gms/people/PeopleServiceImpl.java b/play-services-core/src/main/java/org/microg/gms/people/PeopleServiceImpl.java index c743f8dd..a237f25a 100644 --- a/play-services-core/src/main/java/org/microg/gms/people/PeopleServiceImpl.java +++ b/play-services-core/src/main/java/org/microg/gms/people/PeopleServiceImpl.java @@ -38,7 +38,7 @@ import java.io.File; public class PeopleServiceImpl extends IPeopleService.Stub { private static final String TAG = "GmsPeopleSvcImpl"; - private Context context; + private final Context context; public PeopleServiceImpl(Context context) { this.context = context; diff --git a/play-services-core/src/main/java/org/microg/gms/wearable/ConfigurationDatabaseHelper.java b/play-services-core/src/main/java/org/microg/gms/wearable/ConfigurationDatabaseHelper.java new file mode 100644 index 00000000..c0066602 --- /dev/null +++ b/play-services-core/src/main/java/org/microg/gms/wearable/ConfigurationDatabaseHelper.java @@ -0,0 +1,105 @@ +/* + * 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.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; + +import com.google.android.gms.wearable.ConnectionConfiguration; + +import java.util.ArrayList; +import java.util.List; + +public class ConfigurationDatabaseHelper extends SQLiteOpenHelper { + + public static final String NULL_STRING = "NULL_STRING"; + + public ConfigurationDatabaseHelper(Context context) { + super(context, "connectionconfig.db", null, 2); + } + + @Override + public void onCreate(SQLiteDatabase db) { + db.execSQL("CREATE TABLE connectionConfigurations (_id INTEGER PRIMARY KEY AUTOINCREMENT,androidId TEXT,name TEXT NOT NULL,pairedBtAddress TEXT NOT NULL,connectionType INTEGER NOT NULL,role INTEGER NOT NULL,connectionEnabled INTEGER NOT NULL,nodeId TEXT, UNIQUE(name) ON CONFLICT REPLACE);"); + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + + } + + private static ConnectionConfiguration configFromCursor(final Cursor cursor) { + String name = cursor.getString(cursor.getColumnIndex("name")); + String pairedBtAddress = cursor.getString(cursor.getColumnIndex("pairedBtAddress")); + int connectionType = cursor.getInt(cursor.getColumnIndex("connectionType")); + int role = cursor.getInt(cursor.getColumnIndex("role")); + int enabled = cursor.getInt(cursor.getColumnIndex("connectionEnabled")); + String nodeId = cursor.getString(cursor.getColumnIndex("nodeId")); + if (NULL_STRING.equals(name)) name = null; + if (NULL_STRING.equals(pairedBtAddress)) pairedBtAddress = null; + return new ConnectionConfiguration(name, pairedBtAddress, connectionType, role, enabled > 0, nodeId); + } + + public ConnectionConfiguration getConfiguration(String name) { + Cursor cursor = getReadableDatabase().query("connectionConfigurations", null, "name=?", new String[]{name}, null, null, null); + ConnectionConfiguration config = null; + if (cursor != null) { + if (cursor.moveToNext()) + config = configFromCursor(cursor); + cursor.close(); + } + return config; + } + + public void putConfiguration(ConnectionConfiguration config) { + ContentValues contentValues = new ContentValues(); + if (config.name != null) { + contentValues.put("name", config.name); + } else if (config.role == 2) { + contentValues.put("name", "server"); + } else { + contentValues.put("name", "NULL_STRING"); + } + if (config.address != null) { + contentValues.put("pairedBtAddress", config.address); + } else { + contentValues.put("pairedBtAddress", "NULL_STRING"); + } + contentValues.put("connectionType", config.type); + contentValues.put("role", config.role); + contentValues.put("connectionEnabled", true); + contentValues.put("nodeId", config.nodeId); + getWritableDatabase().insert("connectionConfigurations", null, contentValues); + } + + public ConnectionConfiguration[] getAllConfigurations() { + Cursor cursor = getReadableDatabase().query("connectionConfigurations", null, null, null, null, null, null); + if (cursor != null) { + List configurations = new ArrayList(); + while (cursor.moveToNext()) { + configurations.add(configFromCursor(cursor)); + } + cursor.close(); + return configurations.toArray(new ConnectionConfiguration[configurations.size()]); + } else { + return null; + } + } +} 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 new file mode 100644 index 00000000..a8478c64 --- /dev/null +++ b/play-services-core/src/main/java/org/microg/gms/wearable/NodeDatabaseHelper.java @@ -0,0 +1,107 @@ +/* + * 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.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; +import android.net.Uri; + +import com.google.android.gms.wearable.Asset; + +import java.util.Map; + +public class NodeDatabaseHelper extends SQLiteOpenHelper { + private static final String DB_NAME = "node.db"; + private static final int VERSION = 7; + + public NodeDatabaseHelper(Context context) { + super(context, DB_NAME, null, VERSION); + } + + @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 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 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 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);"); + } + + public Cursor getDataItemsForDataHolder(String packageName, String signatureDigest) { + return getDataItemsForDataHolderByHostAndPath(packageName, signatureDigest, null, null); + } + + public Cursor getDataItemsForDataHolderByHostAndPath(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 AND assetsPresent !=0"; + return getReadableDatabase().rawQuery("SELECT host AS host,path AS path,data AS data,\'\' AS tags,assetname AS asset_key,assets_digest AS asset_id FROM dataItemsAndAssets WHERE " + selection, params); + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + + } + + private synchronized long getAppKey(String packageName, String signatureDigest) { + Cursor cursor = getReadableDatabase().rawQuery("SELECT _id FROM appkeys WHERE packageName=? AND signatureDigest=?", new String[]{packageName, signatureDigest}); + if (cursor != null) { + if (cursor.moveToNext()) { + return cursor.getLong(0); + } + cursor.close(); + } + ContentValues appKey = new ContentValues(); + appKey.put("packageName", packageName); + appKey.put("signatureDigest", signatureDigest); + return getWritableDatabase().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 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}); + } +} diff --git a/play-services-core/src/main/java/org/microg/gms/wearable/WearableService.java b/play-services-core/src/main/java/org/microg/gms/wearable/WearableService.java index ec343f17..90ae6aa3 100644 --- a/play-services-core/src/main/java/org/microg/gms/wearable/WearableService.java +++ b/play-services-core/src/main/java/org/microg/gms/wearable/WearableService.java @@ -16,17 +16,35 @@ package org.microg.gms.wearable; -import android.app.Service; -import android.content.Intent; -import android.os.IBinder; -import android.util.Log; +import android.os.Binder; +import android.os.RemoteException; -public class WearableService extends Service { - private static final String TAG = "GmsWearSvc"; +import com.google.android.gms.common.internal.GetServiceRequest; +import com.google.android.gms.common.internal.IGmsCallbacks; + +import org.microg.gms.BaseService; +import org.microg.gms.common.PackageUtils; +import org.microg.gms.common.Services; + +public class WearableService extends BaseService { + + private ConfigurationDatabaseHelper configurationDatabaseHelper; + private NodeDatabaseHelper nodeDatabaseHelper; + + public WearableService() { + super("GmsWearSvc", Services.WEARABLE.SERVICE_ID); + } @Override - public IBinder onBind(Intent intent) { - Log.d(TAG, "onBind: " + intent); - return null; + public void onCreate() { + super.onCreate(); + configurationDatabaseHelper = new ConfigurationDatabaseHelper(this); + nodeDatabaseHelper = new NodeDatabaseHelper(this); + } + + @Override + public void handleServiceRequest(IGmsCallbacks callback, GetServiceRequest request) throws RemoteException { + PackageUtils.checkPackageUid(this, request.packageName, Binder.getCallingUid()); + callback.onPostInitComplete(0, new WearableServiceImpl(this, nodeDatabaseHelper, configurationDatabaseHelper, request.packageName), null); } } 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 new file mode 100644 index 00000000..9954575b --- /dev/null +++ b/play-services-core/src/main/java/org/microg/gms/wearable/WearableServiceImpl.java @@ -0,0 +1,203 @@ +/* + * 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.content.Context; +import android.content.SharedPreferences; +import android.database.Cursor; +import android.net.Uri; +import android.os.Parcel; +import android.os.RemoteException; +import android.text.TextUtils; +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.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.DataItemParcelable; +import com.google.android.gms.wearable.internal.IWearableCallbacks; +import com.google.android.gms.wearable.internal.IWearableService; +import com.google.android.gms.wearable.internal.NodeParcelable; + +import org.microg.gms.common.PackageUtils; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +public class WearableServiceImpl extends IWearableService.Stub { + 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 final Context context; + private final String packageName; + private final NodeDatabaseHelper nodeDatabase; + private final ConfigurationDatabaseHelper configDatabase; + + + public WearableServiceImpl(Context context, NodeDatabaseHelper nodeDatabase, ConfigurationDatabaseHelper configDatabase, String packageName) { + this.context = context; + this.nodeDatabase = nodeDatabase; + this.configDatabase = configDatabase; + this.packageName = packageName; + } + + private String getLocalNodeId() { + SharedPreferences preferences = context.getSharedPreferences(CLOCKWORK_NODE_PREFERENCES, Context.MODE_PRIVATE); + String nodeId = preferences.getString(CLOCKWORK_NODE_PREFERENCE_NODE_ID, null); + if (nodeId == null) { + nodeId = UUID.randomUUID().toString(); + preferences.edit().putString(CLOCKWORK_NODE_PREFERENCE_NODE_ID, nodeId).apply(); + } + return nodeId; + } + + @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()); + } + if (!assetMap.isEmpty()) { + prepared.put("assetsPresent", true); + nodeDatabase.putDataItem(packageName, PackageUtils.firstSignatureDigest(context, packageName), host, request.getUri().getPath(), prepared); + } + callbacks.onPutDataResponse(new PutDataResponse(0, new DataItemParcelable(request.getUri(), assetMap))); + } + + @Override + public void getDataItem(IWearableCallbacks callbacks, Uri uri) throws RemoteException { + Log.d(TAG, "getDataItem: " + uri); + Cursor cursor = nodeDatabase.getDataItemsForDataHolderByHostAndPath(packageName, PackageUtils.firstSignatureDigest(context, packageName), uri.getHost(), uri.getPath()); + if (cursor != null) { + if (cursor.moveToNext()) { + DataItemParcelable dataItem = new DataItemParcelable(new Uri.Builder().scheme("wear").authority(cursor.getString(0)).path(cursor.getString(1)).build()); + dataItem.data = cursor.getBlob(2); + // TODO: assets + callbacks.onGetDataItemResponse(new GetDataItemResponse(0, dataItem)); + } + cursor.close(); + } + // TODO: negative + } + + @Override + public void getDataItems(IWearableCallbacks callbacks) throws RemoteException { + Log.d(TAG, "getDataItems: " + callbacks); + Cursor dataHolderItems = nodeDatabase.getDataItemsForDataHolder(packageName, PackageUtils.firstSignatureDigest(context, packageName)); + callbacks.onDataHolder(DataHolder.fromCursor(dataHolderItems, 0, null)); + } + + @Override + public void getDataItemsByUri(IWearableCallbacks callbacks, Uri uri, int i) throws RemoteException { + Log.d(TAG, "getDataItemsByUri: " + uri); + Cursor dataHolderItems = nodeDatabase.getDataItemsForDataHolderByHostAndPath(packageName, PackageUtils.firstSignatureDigest(context, packageName), uri.getHost(), uri.getPath()); + callbacks.onDataHolder(DataHolder.fromCursor(dataHolderItems, 0, null)); + } + + @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()); + callbacks.onStatus(Status.SUCCESS); + } + + @Override + public void getLocalNode(IWearableCallbacks callbacks) throws RemoteException { + try { + callbacks.onGetLocalNodeResponse(new GetLocalNodeResponse(0, new NodeParcelable(getLocalNodeId(), getLocalNodeId()))); + } catch (Exception e) { + callbacks.onGetLocalNodeResponse(new GetLocalNodeResponse(8, null)); + } + } + + @Override + public void getConnectedNodes(IWearableCallbacks callbacks) throws RemoteException { + Log.d(TAG, "getConnectedNodes[fak]"); + callbacks.onGetConnectedNodesResponse(new GetConnectedNodesResponse(0, new ArrayList())); + } + + @Override + public void addListener(IWearableCallbacks callbacks, AddListenerRequest request) throws RemoteException { + Log.d(TAG, "addListener[nyp]: " + request); + } + + @Override + public void removeListener(IWearableCallbacks callbacks, RemoveListenerRequest request) throws RemoteException { + Log.d(TAG, "removeListener[nyp]: " + request); + } + + @Override + public void putConfig(IWearableCallbacks callbacks, ConnectionConfiguration config) throws RemoteException { + Log.d(TAG, "putConfig[nyp]: " + config); + configDatabase.putConfiguration(config); + callbacks.onStatus(Status.SUCCESS); + } + + @Override + public void getConfig(IWearableCallbacks callbacks) throws RemoteException { + Log.d(TAG, "getConfig"); + ConnectionConfiguration[] configurations = configDatabase.getAllConfigurations(); + if (configurations == null || configurations.length == 0) { + callbacks.onGetConfigResponse(new GetConfigResponse(1, new ConnectionConfiguration(null, null, 0, 0, false))); + } else { + callbacks.onGetConfigResponse(new GetConfigResponse(0, configurations[0])); + } + } + + @Override + public void getConfigs(IWearableCallbacks callbacks) throws RemoteException { + Log.d(TAG, "getConfigs"); + try { + callbacks.onGetConfigsResponse(new GetConfigsResponse(0, configDatabase.getAllConfigurations())); + } catch (Exception e) { + callbacks.onGetConfigsResponse(new GetConfigsResponse(8, new ConnectionConfiguration[0])); + } + } + + @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; + } +}