Repo Created
This commit is contained in:
parent
eb305e2886
commit
a8c22c65db
4784 changed files with 329907 additions and 2 deletions
56
play-services-wearable/core/build.gradle
Normal file
56
play-services-wearable/core/build.gradle
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 microG Project Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
apply plugin: 'com.android.library'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'maven-publish'
|
||||
apply plugin: 'signing'
|
||||
|
||||
dependencies {
|
||||
implementation project(':play-services-base-core')
|
||||
|
||||
implementation project(':play-services-location')
|
||||
implementation project(':play-services-wearable')
|
||||
|
||||
implementation "org.microg:wearable:$wearableVersion"
|
||||
}
|
||||
|
||||
android {
|
||||
namespace "org.microg.gms.wearable.core"
|
||||
|
||||
compileSdkVersion androidCompileSdk
|
||||
buildToolsVersion "$androidBuildVersionTools"
|
||||
|
||||
defaultConfig {
|
||||
versionName version
|
||||
minSdkVersion androidMinSdk
|
||||
targetSdkVersion androidTargetSdk
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
dataBinding = true
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main.java.srcDirs += 'src/main/kotlin'
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
disable 'MissingTranslation'
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = 1.8
|
||||
targetCompatibility = 1.8
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = 1.8
|
||||
}
|
||||
}
|
||||
|
||||
apply from: '../../gradle/publish-android.gradle'
|
||||
|
||||
description = 'microG service implementation for play-services-wearable'
|
||||
11
play-services-wearable/core/src/main/AndroidManifest.xml
Normal file
11
play-services-wearable/core/src/main/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ SPDX-FileCopyrightText: 2022 microG Project Team
|
||||
~ SPDX-License-Identifier: Apache-2.0
|
||||
-->
|
||||
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<application>
|
||||
</application>
|
||||
</manifest>
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
/*
|
||||
* Copyright (C) 2019 microG 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.Context;
|
||||
import android.net.Uri;
|
||||
|
||||
import com.google.android.gms.common.api.CommonStatusCodes;
|
||||
import com.google.android.gms.common.data.DataHolder;
|
||||
import com.google.android.gms.wearable.Node;
|
||||
import com.google.android.gms.wearable.WearableStatusCodes;
|
||||
import com.google.android.gms.wearable.internal.NodeParcelable;
|
||||
|
||||
import org.microg.gms.common.PackageUtils;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
public class CapabilityManager {
|
||||
private static final Uri ROOT = Uri.parse("wear:/capabilities/");
|
||||
private final Context context;
|
||||
private final WearableImpl wearable;
|
||||
private final String packageName;
|
||||
|
||||
private Set<String> capabilities = new HashSet<String>();
|
||||
|
||||
public CapabilityManager(Context context, WearableImpl wearable, String packageName) {
|
||||
this.context = context;
|
||||
this.wearable = wearable;
|
||||
this.packageName = packageName;
|
||||
}
|
||||
|
||||
private Uri buildCapabilityUri(String capability, boolean withAuthority) {
|
||||
Uri.Builder builder = ROOT.buildUpon();
|
||||
if (withAuthority) builder.authority(wearable.getLocalNodeId());
|
||||
builder.appendPath(packageName);
|
||||
builder.appendPath(PackageUtils.firstSignatureDigest(context, packageName));
|
||||
builder.appendPath(Uri.encode(capability));
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
public Set<String> getNodesForCapability(String capability) {
|
||||
DataHolder dataHolder = wearable.getDataItemsByUriAsHolder(buildCapabilityUri(capability, false), packageName);
|
||||
Set<String> nodes = new HashSet<>();
|
||||
for (int i = 0; i < dataHolder.getCount(); i++) {
|
||||
nodes.add(dataHolder.getString("host", i, 0));
|
||||
}
|
||||
dataHolder.close();
|
||||
return nodes;
|
||||
}
|
||||
|
||||
public int add(String capability) {
|
||||
if (this.capabilities.contains(capability)) {
|
||||
return WearableStatusCodes.DUPLICATE_CAPABILITY;
|
||||
}
|
||||
DataItemInternal dataItem = new DataItemInternal(buildCapabilityUri(capability, true));
|
||||
DataItemRecord record = wearable.putDataItem(packageName, PackageUtils.firstSignatureDigest(context, packageName), wearable.getLocalNodeId(), dataItem);
|
||||
this.capabilities.add(capability);
|
||||
wearable.syncRecordToAll(record);
|
||||
return CommonStatusCodes.SUCCESS;
|
||||
}
|
||||
|
||||
public int remove(String capability) {
|
||||
if (!this.capabilities.contains(capability)) {
|
||||
return WearableStatusCodes.UNKNOWN_CAPABILITY;
|
||||
}
|
||||
wearable.deleteDataItems(buildCapabilityUri(capability, true), packageName);
|
||||
capabilities.remove(capability);
|
||||
return CommonStatusCodes.SUCCESS;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
* Copyright (C) 2013-2017 microG 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.Context;
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public class ClockworkNodePreferences {
|
||||
|
||||
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 static final Object lock = new Object();
|
||||
private static long seqIdBlock;
|
||||
private static long seqIdInBlock = -1;
|
||||
|
||||
private Context context;
|
||||
|
||||
public ClockworkNodePreferences(Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
public 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;
|
||||
}
|
||||
|
||||
public long getNextSeqId() {
|
||||
synchronized (lock) {
|
||||
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++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,124 @@
|
|||
/*
|
||||
* Copyright (C) 2013-2017 microG 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;
|
||||
import java.util.Random;
|
||||
|
||||
public class ConfigurationDatabaseHelper extends SQLiteOpenHelper {
|
||||
|
||||
public static final String NULL_STRING = "NULL_STRING";
|
||||
public static final String TABLE_NAME = "connectionConfigurations";
|
||||
public static final String BY_NAME = "name=?";
|
||||
|
||||
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.getColumnIndexOrThrow("name"));
|
||||
String pairedBtAddress = cursor.getString(cursor.getColumnIndexOrThrow("pairedBtAddress"));
|
||||
int connectionType = cursor.getInt(cursor.getColumnIndexOrThrow("connectionType"));
|
||||
int role = cursor.getInt(cursor.getColumnIndexOrThrow("role"));
|
||||
int enabled = cursor.getInt(cursor.getColumnIndexOrThrow("connectionEnabled"));
|
||||
String nodeId = cursor.getString(cursor.getColumnIndexOrThrow("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(TABLE_NAME, null, BY_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) {
|
||||
putConfiguration(config, null);
|
||||
}
|
||||
|
||||
public void putConfiguration(ConnectionConfiguration config, String oldNodeId) {
|
||||
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);
|
||||
if (oldNodeId == null) {
|
||||
getWritableDatabase().insert(TABLE_NAME, null, contentValues);
|
||||
} else {
|
||||
getWritableDatabase().update(TABLE_NAME, contentValues, "nodeId=?", new String[]{oldNodeId});
|
||||
}
|
||||
}
|
||||
|
||||
public ConnectionConfiguration[] getAllConfigurations() {
|
||||
Cursor cursor = getReadableDatabase().query(TABLE_NAME, null, null, null, null, null, null);
|
||||
if (cursor != null) {
|
||||
List<ConnectionConfiguration> configurations = new ArrayList<ConnectionConfiguration>();
|
||||
while (cursor.moveToNext()) {
|
||||
configurations.add(configFromCursor(cursor));
|
||||
}
|
||||
cursor.close();
|
||||
return configurations.toArray(new ConnectionConfiguration[configurations.size()]);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public void setEnabledState(String name, boolean enabled) {
|
||||
getWritableDatabase().execSQL("UPDATE connectionConfigurations SET connectionEnabled=? WHERE name=?", new String[]{enabled ? "1" : "0", name});
|
||||
}
|
||||
|
||||
public int deleteConfiguration(String name) {
|
||||
return getWritableDatabase().delete(TABLE_NAME, BY_NAME, new String[]{name});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* Copyright (C) 2013-2017 microG 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.net.Uri;
|
||||
|
||||
import com.google.android.gms.wearable.Asset;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class DataItemInternal {
|
||||
public final String host;
|
||||
public final String path;
|
||||
public final Uri uri;
|
||||
public byte[] data;
|
||||
private Map<String, Asset> assets = new HashMap<String, Asset>();
|
||||
|
||||
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(Uri uri) {
|
||||
this.uri = uri;
|
||||
this.host = uri.getAuthority();
|
||||
this.path = uri.getPath();
|
||||
}
|
||||
|
||||
public DataItemInternal addAsset(String key, Asset asset) {
|
||||
this.assets.put(key, asset);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Map<String, Asset> getAssets() {
|
||||
return Collections.unmodifiableMap(new HashMap<String, Asset>(assets));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
final StringBuilder sb = new StringBuilder("DataItemInternal{");
|
||||
sb.append("uri=").append(uri);
|
||||
sb.append(", assets=").append(assets.size());
|
||||
sb.append('}');
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,188 @@
|
|||
/*
|
||||
* Copyright (C) 2013-2017 microG 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 android.net.Uri;
|
||||
import android.util.Log;
|
||||
|
||||
import com.google.android.gms.common.data.DataHolder;
|
||||
import com.google.android.gms.wearable.Asset;
|
||||
import com.google.android.gms.wearable.internal.DataItemAssetParcelable;
|
||||
import com.google.android.gms.wearable.internal.DataItemParcelable;
|
||||
|
||||
import org.microg.wearable.proto.AssetEntry;
|
||||
import org.microg.wearable.proto.SetDataItem;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import okio.ByteString;
|
||||
|
||||
public class DataItemRecord {
|
||||
private static String[] EVENT_DATA_HOLDER_FIELDS = new String[] { "event_type", "path", "data", "tags", "asset_key", "asset_id" };
|
||||
|
||||
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 toContentValues() {
|
||||
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 DataHolder toEventDataHolder() {
|
||||
DataHolder.Builder builder = DataHolder.builder(EVENT_DATA_HOLDER_FIELDS);
|
||||
HashMap<String, Object> data = new HashMap<String, Object>();
|
||||
data.put("path", dataItem.uri.toString());
|
||||
if (deleted) {
|
||||
data.put("event_type", 2);
|
||||
builder.withRow(data);
|
||||
} else {
|
||||
data.put("event_type", 1);
|
||||
data.put("data", dataItem.data);
|
||||
data.put("tags", "");
|
||||
boolean added = false;
|
||||
for (Map.Entry<String, Asset> entry : dataItem.getAssets().entrySet()) {
|
||||
added = true;
|
||||
data.put("asset_id", entry.getValue().getDigest());
|
||||
data.put("asset_key", entry.getKey());
|
||||
builder.withRow(data);
|
||||
data = new HashMap<String, Object>();
|
||||
data.put("path", dataItem.uri.toString());
|
||||
}
|
||||
if (!added) {
|
||||
builder.withRow(data);
|
||||
}
|
||||
}
|
||||
return builder.build(0);
|
||||
}
|
||||
|
||||
public DataItemParcelable toParcelable() {
|
||||
Map<String, DataItemAssetParcelable> assets = new HashMap<>();
|
||||
for (Map.Entry<String, Asset> entry : dataItem.getAssets().entrySet()) {
|
||||
assets.put(entry.getKey(), new DataItemAssetParcelable(entry.getValue().getDigest(), entry.getKey()));
|
||||
}
|
||||
DataItemParcelable parcelable = new DataItemParcelable(dataItem.uri, assets);
|
||||
parcelable.data = dataItem.data;
|
||||
return parcelable;
|
||||
}
|
||||
|
||||
public SetDataItem toSetDataItem() {
|
||||
SetDataItem.Builder builder = new SetDataItem.Builder()
|
||||
.packageName(packageName)
|
||||
.signatureDigest(signatureDigest)
|
||||
.uri(dataItem.uri.toString())
|
||||
.seqId(seqId)
|
||||
.deleted(deleted)
|
||||
.lastModified(lastModified);
|
||||
if (source != null) builder.source(source);
|
||||
if (dataItem.data != null) builder.data(ByteString.of(dataItem.data));
|
||||
List<AssetEntry> protoAssets = new ArrayList<AssetEntry>();
|
||||
Map<String, Asset> assets = dataItem.getAssets();
|
||||
for (String key : assets.keySet()) {
|
||||
protoAssets.add(new AssetEntry.Builder()
|
||||
.key(key)
|
||||
.unknown3(4)
|
||||
.value(new org.microg.wearable.proto.Asset.Builder()
|
||||
.digest(assets.get(key).getDigest())
|
||||
.build()).build());
|
||||
}
|
||||
builder.assets(protoAssets);
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
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;
|
||||
if (cursor.getString(11) != null) {
|
||||
record.dataItem.addAsset(cursor.getString(11), Asset.createFromRef(cursor.getString(12)));
|
||||
while (cursor.moveToNext()) {
|
||||
if (cursor.getLong(5) == record.seqId) {
|
||||
record.dataItem.addAsset(cursor.getString(11), Asset.createFromRef(cursor.getString(12)));
|
||||
}
|
||||
}
|
||||
cursor.moveToPrevious();
|
||||
}
|
||||
return record;
|
||||
}
|
||||
|
||||
public static DataItemRecord fromSetDataItem(SetDataItem setDataItem) {
|
||||
DataItemRecord record = new DataItemRecord();
|
||||
record.dataItem = new DataItemInternal(Uri.parse(setDataItem.uri));
|
||||
if (setDataItem.data != null) record.dataItem.data = setDataItem.data.toByteArray();
|
||||
if (setDataItem.assets != null) {
|
||||
for (AssetEntry asset : setDataItem.assets) {
|
||||
record.dataItem.addAsset(asset.key, Asset.createFromRef(asset.value.digest));
|
||||
}
|
||||
}
|
||||
record.source = setDataItem.source;
|
||||
record.seqId = setDataItem.seqId;
|
||||
record.v1SeqId = -1;
|
||||
record.lastModified = setDataItem.lastModified;
|
||||
record.deleted = setDataItem.deleted == null ? false : setDataItem.deleted;
|
||||
record.packageName = setDataItem.packageName;
|
||||
record.signatureDigest = setDataItem.signatureDigest;
|
||||
return record;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
final StringBuilder sb = new StringBuilder("DataItemRecord{");
|
||||
sb.append("dataItem=").append(dataItem);
|
||||
sb.append(", source='").append(source).append('\'');
|
||||
sb.append(", seqId=").append(seqId);
|
||||
sb.append(", v1SeqId=").append(v1SeqId);
|
||||
sb.append(", lastModified=").append(lastModified);
|
||||
sb.append(", deleted=").append(deleted);
|
||||
sb.append(", assetsAreReady=").append(assetsAreReady);
|
||||
sb.append(", packageName='").append(packageName).append('\'');
|
||||
sb.append(", signatureDigest='").append(signatureDigest).append('\'');
|
||||
sb.append('}');
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,180 @@
|
|||
/*
|
||||
* Copyright (C) 2013-2017 microG 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.Context;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import com.google.android.gms.wearable.Asset;
|
||||
import com.google.android.gms.wearable.ConnectionConfiguration;
|
||||
import com.google.android.gms.wearable.internal.MessageEventParcelable;
|
||||
|
||||
import org.microg.gms.profile.Build;
|
||||
import org.microg.gms.settings.SettingsContract;
|
||||
import org.microg.wearable.ServerMessageListener;
|
||||
import org.microg.wearable.proto.AckAsset;
|
||||
import org.microg.wearable.proto.Connect;
|
||||
import org.microg.wearable.proto.FetchAsset;
|
||||
import org.microg.wearable.proto.FilePiece;
|
||||
import org.microg.wearable.proto.Heartbeat;
|
||||
import org.microg.wearable.proto.Request;
|
||||
import org.microg.wearable.proto.RootMessage;
|
||||
import org.microg.wearable.proto.SetAsset;
|
||||
import org.microg.wearable.proto.SetDataItem;
|
||||
import org.microg.wearable.proto.SyncStart;
|
||||
import org.microg.wearable.proto.SyncTableEntry;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
|
||||
public class MessageHandler extends ServerMessageListener {
|
||||
private static final String TAG = "GmsWearMsgHandler";
|
||||
private final WearableImpl wearable;
|
||||
private final String oldConfigNodeId;
|
||||
private String peerNodeId;
|
||||
|
||||
public MessageHandler(Context context, WearableImpl wearable, ConnectionConfiguration config) {
|
||||
this(wearable, config, Build.MODEL, config.nodeId, SettingsContract.getSettings(context, SettingsContract.CheckIn.INSTANCE.getContentUri(context), new String[]{SettingsContract.CheckIn.ANDROID_ID}, cursor -> cursor.getLong(0)));
|
||||
}
|
||||
|
||||
private MessageHandler(WearableImpl wearable, ConnectionConfiguration config, String name, String networkId, long androidId) {
|
||||
super(new Connect.Builder()
|
||||
.name(name)
|
||||
.id(wearable.getLocalNodeId())
|
||||
.networkId(networkId)
|
||||
.peerAndroidId(androidId)
|
||||
.unknown4(3)
|
||||
.peerVersion(1)
|
||||
.build());
|
||||
this.wearable = wearable;
|
||||
this.oldConfigNodeId = config.nodeId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConnect(Connect connect) {
|
||||
super.onConnect(connect);
|
||||
peerNodeId = connect.id;
|
||||
wearable.onConnectReceived(getConnection(), oldConfigNodeId, connect);
|
||||
try {
|
||||
getConnection().writeMessage(new RootMessage.Builder().syncStart(new SyncStart.Builder()
|
||||
.receivedSeqId(-1L)
|
||||
.version(2)
|
||||
.syncTable(Arrays.asList(
|
||||
new SyncTableEntry.Builder().key("cloud").value(1L).build(),
|
||||
new SyncTableEntry.Builder().key(wearable.getLocalNodeId()).value(wearable.getCurrentSeqId(wearable.getLocalNodeId())).build(), // TODO
|
||||
new SyncTableEntry.Builder().key(peerNodeId).value(wearable.getCurrentSeqId(peerNodeId)).build() // TODO
|
||||
)).build()).build());
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisconnected() {
|
||||
Connect connect = getRemoteConnect();
|
||||
if (connect == null)
|
||||
connect = new Connect.Builder().id(oldConfigNodeId).name("Wear device").build();
|
||||
wearable.onDisconnectReceived(getConnection(), connect);
|
||||
super.onDisconnected();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetAsset(SetAsset setAsset) {
|
||||
Log.d(TAG, "onSetAsset: " + setAsset);
|
||||
Asset asset;
|
||||
if (setAsset.data != null) {
|
||||
asset = Asset.createFromBytes(setAsset.data.toByteArray());
|
||||
} else {
|
||||
asset = Asset.createFromRef(setAsset.digest);
|
||||
}
|
||||
wearable.addAssetToDatabase(asset, setAsset.appkeys.appKeys);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAckAsset(AckAsset ackAsset) {
|
||||
Log.d(TAG, "onAckAsset: " + ackAsset);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFetchAsset(FetchAsset fetchAsset) {
|
||||
Log.d(TAG, "onFetchAsset: " + fetchAsset);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSyncStart(SyncStart syncStart) {
|
||||
Log.d(TAG, "onSyncStart: " + syncStart);
|
||||
if (syncStart.version < 2) {
|
||||
Log.d(TAG, "Sync uses version " + syncStart.version + " which is not supported (yet)");
|
||||
}
|
||||
boolean hasLocalNode = false;
|
||||
if (syncStart.syncTable != null) {
|
||||
for (SyncTableEntry entry : syncStart.syncTable) {
|
||||
wearable.syncToPeer(peerNodeId, entry.key, entry.value);
|
||||
if (wearable.getLocalNodeId().equals(entry.key)) hasLocalNode = true;
|
||||
}
|
||||
} else {
|
||||
Log.d(TAG, "No sync table given.");
|
||||
}
|
||||
if (!hasLocalNode) wearable.syncToPeer(peerNodeId, wearable.getLocalNodeId(), 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetDataItem(SetDataItem setDataItem) {
|
||||
Log.d(TAG, "onSetDataItem: " + setDataItem);
|
||||
wearable.putDataItem(DataItemRecord.fromSetDataItem(setDataItem));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRpcRequest(Request rpcRequest) {
|
||||
Log.d(TAG, "onRpcRequest: " + rpcRequest);
|
||||
if (TextUtils.isEmpty(rpcRequest.targetNodeId) || rpcRequest.targetNodeId.equals(wearable.getLocalNodeId())) {
|
||||
MessageEventParcelable messageEvent = new MessageEventParcelable();
|
||||
messageEvent.data = rpcRequest.rawData != null ? rpcRequest.rawData.toByteArray() : null;
|
||||
messageEvent.path = rpcRequest.path;
|
||||
messageEvent.requestId = rpcRequest.requestId + 31 * (rpcRequest.generation + 527);
|
||||
messageEvent.sourceNodeId = TextUtils.isEmpty(rpcRequest.sourceNodeId) ? peerNodeId : rpcRequest.sourceNodeId;
|
||||
|
||||
wearable.sendMessageReceived(rpcRequest.packageName, messageEvent);
|
||||
} else if (rpcRequest.targetNodeId.equals(peerNodeId)) {
|
||||
// Drop it
|
||||
} else {
|
||||
// TODO: find next hop
|
||||
}
|
||||
try {
|
||||
getConnection().writeMessage(new RootMessage.Builder().heartbeat(new Heartbeat()).build());
|
||||
} catch (IOException e) {
|
||||
onDisconnected();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHeartbeat(Heartbeat heartbeat) {
|
||||
Log.d(TAG, "onHeartbeat: " + heartbeat);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFilePiece(FilePiece filePiece) {
|
||||
Log.d(TAG, "onFilePiece: " + filePiece);
|
||||
wearable.handleFilePiece(getConnection(), filePiece.fileName, filePiece.piece.toByteArray(), filePiece.finalPiece ? filePiece.digest : null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChannelRequest(Request channelRequest) {
|
||||
Log.d(TAG, "onChannelRequest:" + channelRequest);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,297 @@
|
|||
/*
|
||||
* Copyright (C) 2013-2017 microG 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.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import com.google.android.gms.wearable.Asset;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
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 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;
|
||||
|
||||
private ClockworkNodePreferences clockworkNodePreferences;
|
||||
|
||||
public NodeDatabaseHelper(Context context) {
|
||||
super(context, DB_NAME, null, VERSION);
|
||||
clockworkNodePreferences = new ClockworkNodePreferences(context);
|
||||
}
|
||||
|
||||
@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, 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 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, 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 synchronized Cursor getDataItemsForDataHolder(String packageName, String signatureDigest) {
|
||||
return getDataItemsForDataHolderByHostAndPath(packageName, signatureDigest, null, null);
|
||||
}
|
||||
|
||||
public synchronized 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 (TextUtils.isEmpty(host)) {
|
||||
if (path.endsWith("/")) path = path + "%";
|
||||
path = path.replace("*", "%");
|
||||
params = new String[]{packageName, signatureDigest, path};
|
||||
selection = "packageName = ? AND signatureDigest = ? AND path LIKE ?";
|
||||
} else {
|
||||
if (path.endsWith("/")) path = path + "%";
|
||||
path = path.replace("*", "%");
|
||||
host = host.replace("*", "%");
|
||||
params = new String[]{packageName, signatureDigest, host, path};
|
||||
selection = "packageName = ? AND signatureDigest = ? AND host = ? AND path LIKE ?";
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
public synchronized Cursor getDataItemsByHostAndPath(String packageName, String signatureDigest, String host, String path) {
|
||||
Log.d(TAG, "getDataItemsByHostAndPath: " + packageName + ", " + signatureDigest + ", " + host + ", " + path);
|
||||
return getDataItemsByHostAndPath(getReadableDatabase(), packageName, signatureDigest, host, path);
|
||||
}
|
||||
|
||||
@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 static 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) {
|
||||
try {
|
||||
if (cursor.moveToNext()) {
|
||||
return cursor.getLong(0);
|
||||
}
|
||||
} finally {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
ContentValues appKey = new ContentValues();
|
||||
appKey.put("packageName", packageName);
|
||||
appKey.put("signatureDigest", signatureDigest);
|
||||
return db.insert("appkeys", null, appKey);
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
private static void updateRecord(SQLiteDatabase db, String key, DataItemRecord record) {
|
||||
ContentValues cv = record.toContentValues();
|
||||
db.update("dataitems", cv, "_id=?", new String[]{key});
|
||||
finishRecord(db, key, record);
|
||||
}
|
||||
|
||||
private static String insertRecord(SQLiteDatabase db, DataItemRecord record) {
|
||||
ContentValues contentValues = record.toContentValues();
|
||||
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.insertWithOnConflict("dataitems", "host", contentValues, SQLiteDatabase.CONFLICT_REPLACE));
|
||||
return finishRecord(db, key, record);
|
||||
}
|
||||
|
||||
private static String finishRecord(SQLiteDatabase db, String key, DataItemRecord record) {
|
||||
if (!record.deleted) {
|
||||
for (Map.Entry<String, Asset> 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.insertWithOnConflict("assetrefs", "assetname", assetValues, SQLiteDatabase.CONFLICT_IGNORE);
|
||||
}
|
||||
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 Cursor getModifiedDataItems(final String nodeId, final long seqId, final boolean excludeDeleted) {
|
||||
String selection = "sourceNode =? AND seqId >?" + (excludeDeleted ? " AND deleted =0" : "");
|
||||
return getReadableDatabase().query("dataItemsAndAssets", GDIBHAP_FIELDS, selection, new String[]{nodeId, Long.toString(seqId)}, null, null, "seqId", null);
|
||||
}
|
||||
|
||||
public synchronized List<DataItemRecord> deleteDataItems(String packageName, String signatureDigest, String host, String path) {
|
||||
List<DataItemRecord> updated = new ArrayList<DataItemRecord>();
|
||||
SQLiteDatabase db = getWritableDatabase();
|
||||
db.beginTransaction();
|
||||
Cursor cursor = getDataItemsByHostAndPath(db, packageName, signatureDigest, host, path);
|
||||
while (cursor.moveToNext()) {
|
||||
DataItemRecord record = DataItemRecord.fromCursor(cursor);
|
||||
record.deleted = true;
|
||||
record.assetsAreReady = true;
|
||||
record.dataItem.data = null;
|
||||
record.seqId = clockworkNodePreferences.getNextSeqId();
|
||||
record.v1SeqId = record.seqId;
|
||||
updateRecord(db, cursor.getString(0), record);
|
||||
updated.add(record);
|
||||
}
|
||||
db.setTransactionSuccessful();
|
||||
db.endTransaction();
|
||||
return updated;
|
||||
}
|
||||
|
||||
public long getCurrentSeqId(String sourceNode) {
|
||||
if (TextUtils.isEmpty(sourceNode)) return 1;
|
||||
return getCurrentSeqId(getReadableDatabase(), sourceNode);
|
||||
}
|
||||
|
||||
private long getCurrentSeqId(SQLiteDatabase db, String sourceNode) {
|
||||
Cursor cursor = db.query("dataItemsAndAssets", new String[]{"seqId"}, "sourceNode=?", new String[]{sourceNode}, null, null, "seqId DESC", "1");
|
||||
long res = 1;
|
||||
if (cursor != null) {
|
||||
if (cursor.moveToFirst()) {
|
||||
res = cursor.getLong(0);
|
||||
}
|
||||
cursor.close();
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
public synchronized void putAsset(Asset asset, boolean dataPresent) {
|
||||
ContentValues cv = new ContentValues();
|
||||
cv.put("digest", asset.getDigest());
|
||||
cv.put("dataPresent", dataPresent ? 1 : 0);
|
||||
cv.put("timestampMs", System.currentTimeMillis());
|
||||
getWritableDatabase().insertWithOnConflict("assets", null, cv, SQLiteDatabase.CONFLICT_REPLACE);
|
||||
}
|
||||
|
||||
public synchronized void allowAssetAccess(String digest, String packageName, String signatureDigest) {
|
||||
SQLiteDatabase db = getWritableDatabase();
|
||||
ContentValues cv = new ContentValues();
|
||||
cv.put("assets_digest", digest);
|
||||
cv.put("appkeys_id", getAppKey(db, packageName, signatureDigest));
|
||||
db.insertWithOnConflict("assetsacls", null, cv, SQLiteDatabase.CONFLICT_REPLACE);
|
||||
}
|
||||
|
||||
public Cursor listMissingAssets() {
|
||||
return getReadableDatabase().query("dataItemsAndAssets", GDIBHAP_FIELDS, "assetsPresent = 0 AND assets_digest NOT NULL", null, null, null, "packageName, signatureDigest, host, path");
|
||||
}
|
||||
|
||||
public boolean hasAsset(Asset asset) {
|
||||
Cursor cursor = getReadableDatabase().query("assets", new String[]{"dataPresent"}, "digest=?", new String[]{asset.getDigest()}, null, null, null);
|
||||
if (cursor == null) return false;
|
||||
try {
|
||||
return (cursor.moveToNext() && cursor.getInt(0) == 1);
|
||||
} finally {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void markAssetAsPresent(String digest) {
|
||||
ContentValues cv = new ContentValues();
|
||||
cv.put("dataPresent", 1);
|
||||
SQLiteDatabase db = getWritableDatabase();
|
||||
db.update("assets", cv, "digest=?", new String[]{digest});
|
||||
Cursor status = db.query("assetsReadyStatus", null, "nowReady != markedReady", null, null, null, null);
|
||||
while (status.moveToNext()) {
|
||||
cv = new ContentValues();
|
||||
cv.put("assetsPresent", status.getInt(status.getColumnIndexOrThrow("nowReady")));
|
||||
db.update("dataitems", cv, "_id=?", new String[]{Integer.toString(status.getInt(status.getColumnIndexOrThrow("dataitems_id")))});
|
||||
}
|
||||
status.close();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* Copyright (C) 2013-2017 microG 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.Context;
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class RpcHelper {
|
||||
private final Map<String, RpcConnectionState> rpcStateMap = new HashMap<String, RpcConnectionState>();
|
||||
private final SharedPreferences preferences;
|
||||
private final Context context;
|
||||
|
||||
public RpcHelper(Context context) {
|
||||
this.context = context;
|
||||
this.preferences = context.getSharedPreferences("wearable.rpc_service.settings", 0);
|
||||
}
|
||||
|
||||
private String getRpcConnectionId(String packageName, String targetNodeId, String path) {
|
||||
String mode = "lo";
|
||||
if (packageName.equals("com.google.android.wearable.app") && path.startsWith("/s3"))
|
||||
mode = "hi";
|
||||
return targetNodeId + ":" + mode;
|
||||
}
|
||||
|
||||
public RpcHelper.RpcConnectionState useConnectionState(String packageName, String targetNodeId, String path) {
|
||||
String rpcConnectionId = getRpcConnectionId(packageName, targetNodeId, path);
|
||||
synchronized (rpcStateMap) {
|
||||
if (!rpcStateMap.containsKey(rpcConnectionId)) {
|
||||
int g = preferences.getInt(rpcConnectionId, 1)+1;
|
||||
preferences.edit().putInt(rpcConnectionId, g).apply();
|
||||
rpcStateMap.put(rpcConnectionId, new RpcConnectionState(g));
|
||||
}
|
||||
RpcHelper.RpcConnectionState res = rpcStateMap.get(rpcConnectionId);
|
||||
res.lastRequestId++;
|
||||
return res.freeze();
|
||||
}
|
||||
}
|
||||
|
||||
public static class RpcConnectionState {
|
||||
public int generation;
|
||||
public int lastRequestId;
|
||||
|
||||
public RpcConnectionState(int generation) {
|
||||
this.generation = generation;
|
||||
}
|
||||
|
||||
public RpcConnectionState freeze() {
|
||||
RpcConnectionState res = new RpcConnectionState(generation);
|
||||
res.lastRequestId = lastRequestId;
|
||||
return res;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,642 @@
|
|||
/*
|
||||
* Copyright (C) 2013-2019 microG 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.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.os.RemoteException;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Base64;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.google.android.gms.common.data.DataHolder;
|
||||
import com.google.android.gms.wearable.Asset;
|
||||
import com.google.android.gms.wearable.ConnectionConfiguration;
|
||||
import com.google.android.gms.wearable.Node;
|
||||
import com.google.android.gms.wearable.internal.IWearableListener;
|
||||
import com.google.android.gms.wearable.internal.MessageEventParcelable;
|
||||
import com.google.android.gms.wearable.internal.NodeParcelable;
|
||||
import com.google.android.gms.wearable.internal.PutDataRequest;
|
||||
|
||||
import org.microg.gms.common.PackageUtils;
|
||||
import org.microg.gms.common.RemoteListenerProxy;
|
||||
import org.microg.gms.common.Utils;
|
||||
import org.microg.wearable.SocketConnectionThread;
|
||||
import org.microg.wearable.WearableConnection;
|
||||
import org.microg.wearable.proto.AckAsset;
|
||||
import org.microg.wearable.proto.AppKey;
|
||||
import org.microg.wearable.proto.AppKeys;
|
||||
import org.microg.wearable.proto.Connect;
|
||||
import org.microg.wearable.proto.FetchAsset;
|
||||
import org.microg.wearable.proto.FilePiece;
|
||||
import org.microg.wearable.proto.Request;
|
||||
import org.microg.wearable.proto.RootMessage;
|
||||
import org.microg.wearable.proto.SetAsset;
|
||||
import org.microg.wearable.proto.SetDataItem;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
|
||||
import okio.ByteString;
|
||||
|
||||
public class WearableImpl {
|
||||
|
||||
private static final String TAG = "GmsWear";
|
||||
|
||||
private static final int WEAR_TCP_PORT = 5601;
|
||||
|
||||
private final Context context;
|
||||
private final NodeDatabaseHelper nodeDatabase;
|
||||
private final ConfigurationDatabaseHelper configDatabase;
|
||||
private final Map<String, List<ListenerInfo>> listeners = new HashMap<String, List<ListenerInfo>>();
|
||||
private final Set<Node> connectedNodes = new HashSet<Node>();
|
||||
private final Map<String, WearableConnection> activeConnections = new HashMap<String, WearableConnection>();
|
||||
private RpcHelper rpcHelper;
|
||||
private SocketConnectionThread sct;
|
||||
private ConnectionConfiguration[] configurations;
|
||||
private boolean configurationsUpdated = false;
|
||||
private ClockworkNodePreferences clockworkNodePreferences;
|
||||
private CountDownLatch networkHandlerLock = new CountDownLatch(1);
|
||||
public Handler networkHandler;
|
||||
|
||||
public WearableImpl(Context context, NodeDatabaseHelper nodeDatabase, ConfigurationDatabaseHelper configDatabase) {
|
||||
this.context = context;
|
||||
this.nodeDatabase = nodeDatabase;
|
||||
this.configDatabase = configDatabase;
|
||||
this.clockworkNodePreferences = new ClockworkNodePreferences(context);
|
||||
this.rpcHelper = new RpcHelper(context);
|
||||
new Thread(() -> {
|
||||
Looper.prepare();
|
||||
networkHandler = new Handler(Looper.myLooper());
|
||||
networkHandlerLock.countDown();
|
||||
Looper.loop();
|
||||
}).start();
|
||||
}
|
||||
|
||||
public String getLocalNodeId() {
|
||||
return clockworkNodePreferences.getLocalNodeId();
|
||||
}
|
||||
|
||||
public 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 = clockworkNodePreferences.getNextSeqId();
|
||||
if (record.source.equals(getLocalNodeId())) record.seqId = record.v1SeqId;
|
||||
nodeDatabase.putRecord(record);
|
||||
return record;
|
||||
}
|
||||
|
||||
public DataItemRecord putDataItem(DataItemRecord record) {
|
||||
nodeDatabase.putRecord(record);
|
||||
if (!record.assetsAreReady) {
|
||||
for (Asset asset : record.dataItem.getAssets().values()) {
|
||||
if (!nodeDatabase.hasAsset(asset)) {
|
||||
Log.d(TAG, "Asset is missing: " + asset);
|
||||
}
|
||||
}
|
||||
}
|
||||
Intent intent = new Intent("com.google.android.gms.wearable.DATA_CHANGED");
|
||||
intent.setPackage(record.packageName);
|
||||
intent.setData(record.dataItem.uri);
|
||||
invokeListeners(intent, listener -> listener.onDataChanged(record.toEventDataHolder()));
|
||||
return record;
|
||||
}
|
||||
|
||||
private Asset prepareAsset(String packageName, Asset asset) {
|
||||
if (asset.getFd() != null && asset.data == null) {
|
||||
try {
|
||||
asset.data = Utils.readStreamToEnd(new FileInputStream(asset.getFd().getFileDescriptor()));
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
}
|
||||
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) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
}
|
||||
if (success) {
|
||||
Log.d(TAG, "Successfully created asset file " + assetFile);
|
||||
return Asset.createFromRef(digest);
|
||||
} else {
|
||||
Log.w(TAG, "Failed creating asset file " + assetFile);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public File createAssetFile(String digest) {
|
||||
File dir = new File(new File(context.getFilesDir(), "assets"), digest.substring(digest.length() - 2));
|
||||
dir.mkdirs();
|
||||
return new File(dir, digest + ".asset");
|
||||
}
|
||||
|
||||
private File createAssetReceiveTempFile(String name) {
|
||||
File dir = new File(context.getFilesDir(), "piece");
|
||||
dir.mkdirs();
|
||||
return new File(dir, name);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized ConnectionConfiguration[] getConfigurations() {
|
||||
if (configurations == null) {
|
||||
configurations = configDatabase.getAllConfigurations();
|
||||
}
|
||||
if (configurationsUpdated) {
|
||||
configurationsUpdated = false;
|
||||
ConnectionConfiguration[] newConfigurations = configDatabase.getAllConfigurations();
|
||||
for (ConnectionConfiguration configuration : configurations) {
|
||||
for (ConnectionConfiguration newConfiguration : newConfigurations) {
|
||||
if (newConfiguration.name.equals(configuration.name)) {
|
||||
newConfiguration.connected = configuration.connected;
|
||||
newConfiguration.peerNodeId = configuration.peerNodeId;
|
||||
newConfiguration.nodeId = configuration.nodeId;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
configurations = newConfigurations;
|
||||
}
|
||||
Log.d(TAG, "Configurations reported: " + Arrays.toString(configurations));
|
||||
return configurations;
|
||||
}
|
||||
|
||||
private void addConnectedNode(Node node) {
|
||||
connectedNodes.add(node);
|
||||
onConnectedNodes(getConnectedNodesParcelableList());
|
||||
}
|
||||
|
||||
private void removeConnectedNode(String nodeId) {
|
||||
for (Node connectedNode : new ArrayList<Node>(connectedNodes)) {
|
||||
if (connectedNode.getId().equals(nodeId))
|
||||
connectedNodes.remove(connectedNode);
|
||||
}
|
||||
onConnectedNodes(getConnectedNodesParcelableList());
|
||||
}
|
||||
|
||||
|
||||
public Context getContext() {
|
||||
return context;
|
||||
}
|
||||
|
||||
public void syncToPeer(String peerNodeId, String nodeId, long seqId) {
|
||||
Log.d(TAG, "-- Start syncing over to " + peerNodeId + ", nodeId " + nodeId + " starting with seqId " + seqId);
|
||||
Cursor cursor = nodeDatabase.getModifiedDataItems(nodeId, seqId, true);
|
||||
if (cursor != null) {
|
||||
while (cursor.moveToNext()) {
|
||||
if (!syncRecordToPeer(peerNodeId, DataItemRecord.fromCursor(cursor))) break;
|
||||
}
|
||||
cursor.close();
|
||||
}
|
||||
Log.d(TAG, "-- Done syncing over to " + peerNodeId + ", nodeId " + nodeId + " starting with seqId " + seqId);
|
||||
}
|
||||
|
||||
|
||||
void syncRecordToAll(DataItemRecord record) {
|
||||
for (String nodeId : new ArrayList<String>(activeConnections.keySet())) {
|
||||
syncRecordToPeer(nodeId, record);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean syncRecordToPeer(String nodeId, DataItemRecord record) {
|
||||
for (Asset asset : record.dataItem.getAssets().values()) {
|
||||
try {
|
||||
syncAssetToPeer(nodeId, record, asset);
|
||||
} catch (Exception e) {
|
||||
Log.w(TAG, "Could not sync asset " + asset + " for " + nodeId + " and " + record, e);
|
||||
closeConnection(nodeId);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
SetDataItem item = record.toSetDataItem();
|
||||
activeConnections.get(nodeId).writeMessage(new RootMessage.Builder().setDataItem(item).build());
|
||||
} catch (Exception e) {
|
||||
Log.w(TAG, e);
|
||||
closeConnection(nodeId);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void syncAssetToPeer(String nodeId, DataItemRecord record, Asset asset) throws IOException {
|
||||
RootMessage announceMessage = new RootMessage.Builder().setAsset(new SetAsset.Builder()
|
||||
.digest(asset.getDigest())
|
||||
.appkeys(new AppKeys(Collections.singletonList(new AppKey(record.packageName, record.signatureDigest))))
|
||||
.build()).hasAsset(true).build();
|
||||
activeConnections.get(nodeId).writeMessage(announceMessage);
|
||||
File assetFile = createAssetFile(asset.getDigest());
|
||||
String fileName = calculateDigest(announceMessage.encode());
|
||||
FileInputStream fis = new FileInputStream(assetFile);
|
||||
byte[] arr = new byte[12215];
|
||||
ByteString lastPiece = null;
|
||||
int c = 0;
|
||||
while ((c = fis.read(arr)) > 0) {
|
||||
if (lastPiece != null) {
|
||||
activeConnections.get(nodeId).writeMessage(new RootMessage.Builder().filePiece(new FilePiece(fileName, false, lastPiece, null)).build());
|
||||
}
|
||||
lastPiece = ByteString.of(arr, 0, c);
|
||||
}
|
||||
activeConnections.get(nodeId).writeMessage(new RootMessage.Builder().filePiece(new FilePiece(fileName, true, lastPiece, asset.getDigest())).build());
|
||||
}
|
||||
|
||||
public void addAssetToDatabase(Asset asset, List<AppKey> appKeys) {
|
||||
nodeDatabase.putAsset(asset, false);
|
||||
for (AppKey appKey : appKeys) {
|
||||
nodeDatabase.allowAssetAccess(asset.getDigest(), appKey.packageName, appKey.signatureDigest);
|
||||
}
|
||||
}
|
||||
|
||||
public long getCurrentSeqId(String nodeId) {
|
||||
return nodeDatabase.getCurrentSeqId(nodeId);
|
||||
}
|
||||
|
||||
public void handleFilePiece(WearableConnection connection, String fileName, byte[] bytes, String finalPieceDigest) {
|
||||
File file = createAssetReceiveTempFile(fileName);
|
||||
try {
|
||||
FileOutputStream fos = new FileOutputStream(file, true);
|
||||
fos.write(bytes);
|
||||
fos.close();
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
if (finalPieceDigest != null) {
|
||||
// This is a final piece. If digest matches we're so happy!
|
||||
try {
|
||||
String digest = calculateDigest(Utils.readStreamToEnd(new FileInputStream(file)));
|
||||
if (digest.equals(finalPieceDigest)) {
|
||||
if (file.renameTo(createAssetFile(digest))) {
|
||||
nodeDatabase.markAssetAsPresent(digest);
|
||||
connection.writeMessage(new RootMessage.Builder().ackAsset(new AckAsset(digest)).build());
|
||||
} else {
|
||||
Log.w(TAG, "Could not rename to target file name. delete=" + file.delete());
|
||||
}
|
||||
} else {
|
||||
Log.w(TAG, "Received digest does not match. delete=" + file.delete());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "Failed working with temp file. delete=" + file.delete(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void onConnectReceived(WearableConnection connection, String nodeId, Connect connect) {
|
||||
for (ConnectionConfiguration config : getConfigurations()) {
|
||||
if (config.nodeId.equals(nodeId)) {
|
||||
if (config.nodeId != nodeId) {
|
||||
config.nodeId = connect.id;
|
||||
configDatabase.putConfiguration(config, nodeId);
|
||||
}
|
||||
config.peerNodeId = connect.id;
|
||||
config.connected = true;
|
||||
}
|
||||
}
|
||||
Log.d(TAG, "Adding connection to list of open connections: " + connection + " with connect " + connect);
|
||||
activeConnections.put(connect.id, connection);
|
||||
onPeerConnected(new NodeParcelable(connect.id, connect.name));
|
||||
// Fetch missing assets
|
||||
Cursor cursor = nodeDatabase.listMissingAssets();
|
||||
if (cursor != null) {
|
||||
while (cursor.moveToNext()) {
|
||||
try {
|
||||
Log.d(TAG, "Fetch for " + cursor.getString(12));
|
||||
connection.writeMessage(new RootMessage.Builder()
|
||||
.fetchAsset(new FetchAsset.Builder()
|
||||
.assetName(cursor.getString(12))
|
||||
.packageName(cursor.getString(1))
|
||||
.signatureDigest(cursor.getString(2))
|
||||
.permission(false)
|
||||
.build()).build());
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
closeConnection(connect.id);
|
||||
}
|
||||
}
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
|
||||
public void onDisconnectReceived(WearableConnection connection, Connect connect) {
|
||||
for (ConnectionConfiguration config : getConfigurations()) {
|
||||
if (config.nodeId.equals(connect.id)) {
|
||||
config.connected = false;
|
||||
}
|
||||
}
|
||||
Log.d(TAG, "Removing connection from list of open connections: " + connection);
|
||||
activeConnections.remove(connect.id);
|
||||
onPeerDisconnected(new NodeParcelable(connect.id, connect.name));
|
||||
}
|
||||
|
||||
public List<NodeParcelable> getConnectedNodesParcelableList() {
|
||||
List<NodeParcelable> nodes = new ArrayList<NodeParcelable>();
|
||||
for (Node connectedNode : connectedNodes) {
|
||||
nodes.add(new NodeParcelable(connectedNode));
|
||||
}
|
||||
return nodes;
|
||||
}
|
||||
|
||||
interface ListenerInvoker {
|
||||
void invoke(IWearableListener listener) throws RemoteException;
|
||||
}
|
||||
|
||||
private void invokeListeners(@Nullable Intent intent, ListenerInvoker invoker) {
|
||||
for (String packageName : new ArrayList<>(listeners.keySet())) {
|
||||
List<ListenerInfo> listeners = this.listeners.get(packageName);
|
||||
if (listeners == null) continue;
|
||||
for (int i = 0; i < listeners.size(); i++) {
|
||||
boolean filterMatched = false;
|
||||
if (intent != null) {
|
||||
for (IntentFilter filter : listeners.get(i).filters) {
|
||||
filterMatched |= filter.match(context.getContentResolver(), intent, false, TAG) > 0;
|
||||
}
|
||||
}
|
||||
if (filterMatched || listeners.get(i).filters.length == 0) {
|
||||
try {
|
||||
invoker.invoke(listeners.get(i).listener);
|
||||
} catch (RemoteException e) {
|
||||
Log.w(TAG, "Registered listener at package " + packageName + " failed, removing.");
|
||||
listeners.remove(i);
|
||||
i--;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (listeners.isEmpty()) {
|
||||
this.listeners.remove(packageName);
|
||||
}
|
||||
}
|
||||
if (intent != null) {
|
||||
try {
|
||||
invoker.invoke(RemoteListenerProxy.get(context, intent, IWearableListener.class, "com.google.android.gms.wearable.BIND_LISTENER"));
|
||||
} catch (RemoteException e) {
|
||||
Log.w(TAG, "Failed to deliver message received to " + intent, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void onPeerConnected(NodeParcelable node) {
|
||||
Log.d(TAG, "onPeerConnected: " + node);
|
||||
invokeListeners(null, listener -> listener.onPeerConnected(node));
|
||||
addConnectedNode(node);
|
||||
}
|
||||
|
||||
public void onPeerDisconnected(NodeParcelable node) {
|
||||
Log.d(TAG, "onPeerDisconnected: " + node);
|
||||
invokeListeners(null, listener -> listener.onPeerDisconnected(node));
|
||||
removeConnectedNode(node.getId());
|
||||
}
|
||||
|
||||
public void onConnectedNodes(List<NodeParcelable> nodes) {
|
||||
Log.d(TAG, "onConnectedNodes: " + nodes);
|
||||
invokeListeners(null, listener -> listener.onConnectedNodes(nodes));
|
||||
}
|
||||
|
||||
public DataItemRecord putData(PutDataRequest request, String packageName) {
|
||||
DataItemInternal dataItem = new DataItemInternal(fixHost(request.getUri().getHost(), true), request.getUri().getPath());
|
||||
for (Map.Entry<String, Asset> assetEntry : request.getAssets().entrySet()) {
|
||||
Asset asset = prepareAsset(packageName, assetEntry.getValue());
|
||||
if (asset != null) {
|
||||
nodeDatabase.putAsset(asset, true);
|
||||
dataItem.addAsset(assetEntry.getKey(), asset);
|
||||
}
|
||||
}
|
||||
dataItem.data = request.getData();
|
||||
DataItemRecord record = putDataItem(packageName, PackageUtils.firstSignatureDigest(context, packageName), getLocalNodeId(), dataItem);
|
||||
syncRecordToAll(record);
|
||||
return record;
|
||||
}
|
||||
|
||||
public DataHolder getDataItemsAsHolder(String packageName) {
|
||||
Cursor dataHolderItems = nodeDatabase.getDataItemsForDataHolder(packageName, PackageUtils.firstSignatureDigest(context, packageName));
|
||||
return new DataHolder(dataHolderItems, 0, null);
|
||||
}
|
||||
|
||||
private String fixHost(String host, boolean nothingToLocal) {
|
||||
if (TextUtils.isEmpty(host) && nothingToLocal) return getLocalNodeId();
|
||||
if (TextUtils.isEmpty(host)) return null;
|
||||
if (host.equals("local")) return getLocalNodeId();
|
||||
return host;
|
||||
}
|
||||
|
||||
public DataHolder getDataItemsByUriAsHolder(Uri uri, String packageName) {
|
||||
String firstSignature;
|
||||
try {
|
||||
firstSignature = PackageUtils.firstSignatureDigest(context, packageName);
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
Cursor dataHolderItems = nodeDatabase.getDataItemsForDataHolderByHostAndPath(packageName, firstSignature, fixHost(uri.getHost(), false), uri.getPath());
|
||||
DataHolder dataHolder = new DataHolder(dataHolderItems, 0, null);
|
||||
Log.d(TAG, "Returning data holder of size " + dataHolder.getCount() + " for query " + uri);
|
||||
return dataHolder;
|
||||
}
|
||||
|
||||
public synchronized void addListener(String packageName, IWearableListener listener, IntentFilter[] filters) {
|
||||
if (!listeners.containsKey(packageName)) {
|
||||
listeners.put(packageName, new ArrayList<ListenerInfo>());
|
||||
}
|
||||
listeners.get(packageName).add(new ListenerInfo(listener, filters));
|
||||
}
|
||||
|
||||
public void removeListener(IWearableListener listener) {
|
||||
for (List<ListenerInfo> list : listeners.values()) {
|
||||
for (int i = 0; i < list.size(); i++) {
|
||||
if (list.get(i).listener.equals(listener)) {
|
||||
list.remove(i);
|
||||
i--;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void enableConnection(String name) {
|
||||
configDatabase.setEnabledState(name, true);
|
||||
configurationsUpdated = true;
|
||||
if (name.equals("server") && sct == null) {
|
||||
Log.d(TAG, "Starting server on :" + WEAR_TCP_PORT);
|
||||
(sct = SocketConnectionThread.serverListen(WEAR_TCP_PORT, new MessageHandler(context, this, configDatabase.getConfiguration(name)))).start();
|
||||
}
|
||||
}
|
||||
|
||||
public void disableConnection(String name) {
|
||||
configDatabase.setEnabledState(name, false);
|
||||
configurationsUpdated = true;
|
||||
if (name.equals("server") && sct != null) {
|
||||
activeConnections.remove(sct.getWearableConnection());
|
||||
sct.close();
|
||||
sct.interrupt();
|
||||
sct = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void deleteConnection(String name) {
|
||||
configDatabase.deleteConfiguration(name);
|
||||
configurationsUpdated = true;
|
||||
}
|
||||
|
||||
public void createConnection(ConnectionConfiguration config) {
|
||||
if (config.nodeId == null) config.nodeId = getLocalNodeId();
|
||||
Log.d(TAG, "putConfig[nyp]: " + config);
|
||||
configDatabase.putConfiguration(config);
|
||||
configurationsUpdated = true;
|
||||
}
|
||||
|
||||
public int deleteDataItems(Uri uri, String packageName) {
|
||||
List<DataItemRecord> records = nodeDatabase.deleteDataItems(packageName, PackageUtils.firstSignatureDigest(context, packageName), fixHost(uri.getHost(), false), uri.getPath());
|
||||
for (DataItemRecord record : records) {
|
||||
syncRecordToAll(record);
|
||||
}
|
||||
return records.size();
|
||||
}
|
||||
|
||||
public void sendMessageReceived(String packageName, MessageEventParcelable messageEvent) {
|
||||
Log.d(TAG, "onMessageReceived: " + messageEvent);
|
||||
Intent intent = new Intent("com.google.android.gms.wearable.MESSAGE_RECEIVED");
|
||||
intent.setPackage(packageName);
|
||||
intent.setData(Uri.parse("wear://" + getLocalNodeId() + "/" + messageEvent.getPath()));
|
||||
invokeListeners(intent, listener -> listener.onMessageReceived(messageEvent));
|
||||
}
|
||||
|
||||
public DataItemRecord getDataItemByUri(Uri uri, String packageName) {
|
||||
Cursor cursor = nodeDatabase.getDataItemsByHostAndPath(packageName, PackageUtils.firstSignatureDigest(context, packageName), fixHost(uri.getHost(), true), uri.getPath());
|
||||
DataItemRecord record = null;
|
||||
if (cursor != null) {
|
||||
if (cursor.moveToNext()) {
|
||||
record = DataItemRecord.fromCursor(cursor);
|
||||
}
|
||||
cursor.close();
|
||||
}
|
||||
Log.d(TAG, "getDataItem: " + record);
|
||||
return record;
|
||||
}
|
||||
|
||||
private IWearableListener getListener(String packageName, String action, Uri uri) {
|
||||
Intent intent = new Intent(action);
|
||||
intent.setPackage(packageName);
|
||||
intent.setData(uri);
|
||||
|
||||
return RemoteListenerProxy.get(context, intent, IWearableListener.class, "com.google.android.gms.wearable.BIND_LISTENER");
|
||||
}
|
||||
|
||||
private void closeConnection(String nodeId) {
|
||||
WearableConnection connection = activeConnections.get(nodeId);
|
||||
try {
|
||||
connection.close();
|
||||
} catch (IOException e1) {
|
||||
Log.w(TAG, e1);
|
||||
}
|
||||
if (connection == sct.getWearableConnection()) {
|
||||
sct.close();
|
||||
sct = null;
|
||||
}
|
||||
activeConnections.remove(nodeId);
|
||||
for (ConnectionConfiguration config : getConfigurations()) {
|
||||
if (nodeId.equals(config.nodeId) || nodeId.equals(config.peerNodeId)) {
|
||||
config.connected = false;
|
||||
}
|
||||
}
|
||||
onPeerDisconnected(new NodeParcelable(nodeId, "Wear device"));
|
||||
Log.d(TAG, "Closed connection to " + nodeId + " on error");
|
||||
}
|
||||
|
||||
public int sendMessage(String packageName, String targetNodeId, String path, byte[] data) {
|
||||
if (activeConnections.containsKey(targetNodeId)) {
|
||||
WearableConnection connection = activeConnections.get(targetNodeId);
|
||||
RpcHelper.RpcConnectionState state = rpcHelper.useConnectionState(packageName, targetNodeId, path);
|
||||
try {
|
||||
connection.writeMessage(new RootMessage.Builder().rpcRequest(new Request.Builder()
|
||||
.targetNodeId(targetNodeId)
|
||||
.path(path)
|
||||
.rawData(ByteString.of(data))
|
||||
.packageName(packageName)
|
||||
.signatureDigest(PackageUtils.firstSignatureDigest(context, packageName))
|
||||
.sourceNodeId(getLocalNodeId())
|
||||
.generation(state.generation)
|
||||
.requestId(state.lastRequestId)
|
||||
.build()).build());
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "Error while writing, closing link", e);
|
||||
closeConnection(targetNodeId);
|
||||
return -1;
|
||||
}
|
||||
return (state.generation + 527) * 31 + state.lastRequestId;
|
||||
}
|
||||
Log.d(TAG, targetNodeId + " seems not reachable");
|
||||
return -1;
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
try {
|
||||
this.networkHandlerLock.await();
|
||||
this.networkHandler.getLooper().quit();
|
||||
} catch (InterruptedException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
}
|
||||
|
||||
private class ListenerInfo {
|
||||
private IWearableListener listener;
|
||||
private IntentFilter[] filters;
|
||||
|
||||
private ListenerInfo(IWearableListener listener, IntentFilter[] filters) {
|
||||
this.listener = listener;
|
||||
this.filters = filters;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* Copyright (C) 2013-2017 microG 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.os.RemoteException;
|
||||
|
||||
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.GmsService;
|
||||
import org.microg.gms.common.PackageUtils;
|
||||
|
||||
public class WearableService extends BaseService {
|
||||
|
||||
private WearableImpl wearable;
|
||||
|
||||
public WearableService() {
|
||||
super("GmsWearSvc", GmsService.WEARABLE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
ConfigurationDatabaseHelper configurationDatabaseHelper = new ConfigurationDatabaseHelper(getApplicationContext());
|
||||
NodeDatabaseHelper nodeDatabaseHelper = new NodeDatabaseHelper(getApplicationContext());
|
||||
wearable = new WearableImpl(getApplicationContext(), nodeDatabaseHelper, configurationDatabaseHelper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
wearable.stop();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleServiceRequest(IGmsCallbacks callback, GetServiceRequest request, GmsService service) throws RemoteException {
|
||||
PackageUtils.getAndCheckCallingPackage(this, request.packageName);
|
||||
callback.onPostInitComplete(0, new WearableServiceImpl(this, wearable, request.packageName), null);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,507 @@
|
|||
/*
|
||||
* Copyright (C) 2013-2019 microG 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.Context;
|
||||
import android.net.Uri;
|
||||
import android.os.Handler;
|
||||
import android.os.Parcel;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.os.RemoteException;
|
||||
import android.util.Base64;
|
||||
import android.util.Log;
|
||||
|
||||
import com.google.android.gms.common.api.Status;
|
||||
import com.google.android.gms.wearable.Asset;
|
||||
import com.google.android.gms.wearable.ConnectionConfiguration;
|
||||
import com.google.android.gms.wearable.internal.*;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
public class WearableServiceImpl extends IWearableService.Stub {
|
||||
private static final String TAG = "GmsWearSvcImpl";
|
||||
|
||||
private final Context context;
|
||||
private final String packageName;
|
||||
private final WearableImpl wearable;
|
||||
private final Handler mainHandler;
|
||||
private final CapabilityManager capabilities;
|
||||
|
||||
public WearableServiceImpl(Context context, WearableImpl wearable, String packageName) {
|
||||
this.context = context;
|
||||
this.wearable = wearable;
|
||||
this.packageName = packageName;
|
||||
this.capabilities = new CapabilityManager(context, wearable, packageName);
|
||||
this.mainHandler = new Handler(context.getMainLooper());
|
||||
}
|
||||
|
||||
private void postMain(IWearableCallbacks callbacks, RemoteExceptionRunnable runnable) {
|
||||
mainHandler.post(new CallbackRunnable(callbacks) {
|
||||
@Override
|
||||
public void run(IWearableCallbacks callbacks) throws RemoteException {
|
||||
runnable.run();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void postNetwork(IWearableCallbacks callbacks, RemoteExceptionRunnable runnable) {
|
||||
this.wearable.networkHandler.post(new CallbackRunnable(callbacks) {
|
||||
@Override
|
||||
public void run(IWearableCallbacks callbacks) throws RemoteException {
|
||||
runnable.run();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
* Config
|
||||
*/
|
||||
|
||||
@Override
|
||||
public void putConfig(IWearableCallbacks callbacks, final ConnectionConfiguration config) throws RemoteException {
|
||||
postMain(callbacks, () -> {
|
||||
wearable.createConnection(config);
|
||||
callbacks.onStatus(Status.SUCCESS);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteConfig(IWearableCallbacks callbacks, final String name) throws RemoteException {
|
||||
postMain(callbacks, () -> {
|
||||
wearable.deleteConnection(name);
|
||||
callbacks.onStatus(Status.SUCCESS);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void getConfigs(IWearableCallbacks callbacks) throws RemoteException {
|
||||
Log.d(TAG, "getConfigs");
|
||||
postMain(callbacks, () -> {
|
||||
try {
|
||||
callbacks.onGetConfigsResponse(new GetConfigsResponse(0, wearable.getConfigurations()));
|
||||
} catch (Exception e) {
|
||||
callbacks.onGetConfigsResponse(new GetConfigsResponse(8, new ConnectionConfiguration[0]));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void enableConfig(IWearableCallbacks callbacks, final String name) throws RemoteException {
|
||||
Log.d(TAG, "enableConfig: " + name);
|
||||
postMain(callbacks, () -> {
|
||||
wearable.enableConnection(name);
|
||||
callbacks.onStatus(Status.SUCCESS);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void disableConfig(IWearableCallbacks callbacks, final String name) throws RemoteException {
|
||||
Log.d(TAG, "disableConfig: " + name);
|
||||
postMain(callbacks, () -> {
|
||||
wearable.disableConnection(name);
|
||||
callbacks.onStatus(Status.SUCCESS);
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
* DataItems
|
||||
*/
|
||||
|
||||
@Override
|
||||
public void putData(IWearableCallbacks callbacks, final PutDataRequest request) throws RemoteException {
|
||||
Log.d(TAG, "putData: " + request.toString(true));
|
||||
this.wearable.networkHandler.post(new CallbackRunnable(callbacks) {
|
||||
@Override
|
||||
public void run(IWearableCallbacks callbacks) throws RemoteException {
|
||||
DataItemRecord record = wearable.putData(request, packageName);
|
||||
callbacks.onPutDataResponse(new PutDataResponse(0, record.toParcelable()));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void getDataItem(IWearableCallbacks callbacks, final Uri uri) throws RemoteException {
|
||||
Log.d(TAG, "getDataItem: " + uri);
|
||||
postMain(callbacks, () -> {
|
||||
DataItemRecord record = wearable.getDataItemByUri(uri, packageName);
|
||||
if (record != null) {
|
||||
callbacks.onGetDataItemResponse(new GetDataItemResponse(0, record.toParcelable()));
|
||||
} else {
|
||||
callbacks.onGetDataItemResponse(new GetDataItemResponse(0, null));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void getDataItems(final IWearableCallbacks callbacks) throws RemoteException {
|
||||
Log.d(TAG, "getDataItems: " + callbacks);
|
||||
postMain(callbacks, () -> {
|
||||
callbacks.onDataItemChanged(wearable.getDataItemsAsHolder(packageName));
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void getDataItemsByUri(IWearableCallbacks callbacks, Uri uri) throws RemoteException {
|
||||
getDataItemsByUriWithFilter(callbacks, uri, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void getDataItemsByUriWithFilter(IWearableCallbacks callbacks, final Uri uri, int typeFilter) throws RemoteException {
|
||||
Log.d(TAG, "getDataItemsByUri: " + uri);
|
||||
postMain(callbacks, () -> {
|
||||
callbacks.onDataItemChanged(wearable.getDataItemsByUriAsHolder(uri, packageName));
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteDataItems(IWearableCallbacks callbacks, Uri uri) throws RemoteException {
|
||||
deleteDataItemsWithFilter(callbacks, uri, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteDataItemsWithFilter(IWearableCallbacks callbacks, final Uri uri, int typeFilter) throws RemoteException {
|
||||
Log.d(TAG, "deleteDataItems: " + uri);
|
||||
this.wearable.networkHandler.post(new CallbackRunnable(callbacks) {
|
||||
@Override
|
||||
public void run(IWearableCallbacks callbacks) throws RemoteException {
|
||||
callbacks.onDeleteDataItemsResponse(new DeleteDataItemsResponse(0, wearable.deleteDataItems(uri, packageName)));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendMessage(IWearableCallbacks callbacks, final String targetNodeId, final String path, final byte[] data) throws RemoteException {
|
||||
Log.d(TAG, "sendMessage: " + targetNodeId + " / " + path + ": " + (data == null ? null : Base64.encodeToString(data, Base64.NO_WRAP)));
|
||||
this.wearable.networkHandler.post(new CallbackRunnable(callbacks) {
|
||||
@Override
|
||||
public void run(IWearableCallbacks callbacks) throws RemoteException {
|
||||
SendMessageResponse sendMessageResponse = new SendMessageResponse();
|
||||
try {
|
||||
sendMessageResponse.requestId = wearable.sendMessage(packageName, targetNodeId, path, data);
|
||||
if (sendMessageResponse.requestId == -1) {
|
||||
sendMessageResponse.statusCode = 4000;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
sendMessageResponse.statusCode = 8;
|
||||
}
|
||||
mainHandler.post(() -> {
|
||||
try {
|
||||
callbacks.onSendMessageResponse(sendMessageResponse);
|
||||
} catch (RemoteException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void getFdForAsset(IWearableCallbacks callbacks, final Asset asset) throws RemoteException {
|
||||
Log.d(TAG, "getFdForAsset " + asset);
|
||||
postMain(callbacks, () -> {
|
||||
// TODO: Access control
|
||||
try {
|
||||
callbacks.onGetFdForAssetResponse(new GetFdForAssetResponse(0, ParcelFileDescriptor.open(wearable.createAssetFile(asset.getDigest()), ParcelFileDescriptor.MODE_READ_ONLY)));
|
||||
} catch (FileNotFoundException e) {
|
||||
callbacks.onGetFdForAssetResponse(new GetFdForAssetResponse(8, null));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void optInCloudSync(IWearableCallbacks callbacks, boolean enable) throws RemoteException {
|
||||
callbacks.onStatus(Status.SUCCESS);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Deprecated
|
||||
public void getCloudSyncOptInDone(IWearableCallbacks callbacks) throws RemoteException {
|
||||
Log.d(TAG, "unimplemented Method: getCloudSyncOptInDone");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setCloudSyncSetting(IWearableCallbacks callbacks, boolean enable) throws RemoteException {
|
||||
Log.d(TAG, "unimplemented Method: setCloudSyncSetting");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void getCloudSyncSetting(IWearableCallbacks callbacks) throws RemoteException {
|
||||
callbacks.onGetCloudSyncSettingResponse(new GetCloudSyncSettingResponse(0, false));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void getCloudSyncOptInStatus(IWearableCallbacks callbacks) throws RemoteException {
|
||||
Log.d(TAG, "unimplemented Method: getCloudSyncOptInStatus");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendRemoteCommand(IWearableCallbacks callbacks, byte b) throws RemoteException {
|
||||
Log.d(TAG, "unimplemented Method: sendRemoteCommand: " + b);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void getLocalNode(IWearableCallbacks callbacks) throws RemoteException {
|
||||
postMain(callbacks, () -> {
|
||||
try {
|
||||
callbacks.onGetLocalNodeResponse(new GetLocalNodeResponse(0, new NodeParcelable(wearable.getLocalNodeId(), wearable.getLocalNodeId())));
|
||||
} catch (Exception e) {
|
||||
callbacks.onGetLocalNodeResponse(new GetLocalNodeResponse(8, null));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void getConnectedNodes(IWearableCallbacks callbacks) throws RemoteException {
|
||||
postMain(callbacks, () -> {
|
||||
callbacks.onGetConnectedNodesResponse(new GetConnectedNodesResponse(0, wearable.getConnectedNodesParcelableList()));
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
* Capability
|
||||
*/
|
||||
|
||||
@Override
|
||||
public void getConnectedCapability(IWearableCallbacks callbacks, String capability, int nodeFilter) throws RemoteException {
|
||||
Log.d(TAG, "unimplemented Method: getConnectedCapability " + capability + ", " + nodeFilter);
|
||||
postMain(callbacks, () -> {
|
||||
List<NodeParcelable> nodes = new ArrayList<>();
|
||||
for (String host : capabilities.getNodesForCapability(capability)) {
|
||||
nodes.add(new NodeParcelable(host, host));
|
||||
}
|
||||
CapabilityInfoParcelable capabilityInfo = new CapabilityInfoParcelable(capability, nodes);
|
||||
callbacks.onGetCapabilityResponse(new GetCapabilityResponse(0, capabilityInfo));
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void getAllCapabilities(IWearableCallbacks callbacks, int nodeFilter) throws RemoteException {
|
||||
Log.d(TAG, "unimplemented Method: getConnectedCapaibilties: " + nodeFilter);
|
||||
callbacks.onGetAllCapabilitiesResponse(new GetAllCapabilitiesResponse());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addLocalCapability(IWearableCallbacks callbacks, String capability) throws RemoteException {
|
||||
Log.d(TAG, "unimplemented Method: addLocalCapability: " + capability);
|
||||
this.wearable.networkHandler.post(new CallbackRunnable(callbacks) {
|
||||
@Override
|
||||
public void run(IWearableCallbacks callbacks) throws RemoteException {
|
||||
callbacks.onAddLocalCapabilityResponse(new AddLocalCapabilityResponse(capabilities.add(capability)));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeLocalCapability(IWearableCallbacks callbacks, String capability) throws RemoteException {
|
||||
Log.d(TAG, "unimplemented Method: removeLocalCapability: " + capability);
|
||||
this.wearable.networkHandler.post(new CallbackRunnable(callbacks) {
|
||||
@Override
|
||||
public void run(IWearableCallbacks callbacks) throws RemoteException {
|
||||
callbacks.onRemoveLocalCapabilityResponse(new RemoveLocalCapabilityResponse(capabilities.remove(capability)));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addListener(IWearableCallbacks callbacks, AddListenerRequest request) throws RemoteException {
|
||||
if (request.listener != null) {
|
||||
wearable.addListener(packageName, request.listener, request.intentFilters);
|
||||
}
|
||||
callbacks.onStatus(Status.SUCCESS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeListener(IWearableCallbacks callbacks, RemoveListenerRequest request) throws RemoteException {
|
||||
wearable.removeListener(request.listener);
|
||||
callbacks.onStatus(Status.SUCCESS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void getStorageInformation(IWearableCallbacks callbacks) throws RemoteException {
|
||||
Log.d(TAG, "unimplemented Method: getStorageInformation");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearStorage(IWearableCallbacks callbacks) throws RemoteException {
|
||||
Log.d(TAG, "unimplemented Method: clearStorage");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void endCall(IWearableCallbacks callbacks) throws RemoteException {
|
||||
Log.d(TAG, "unimplemented Method: endCall");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void acceptRingingCall(IWearableCallbacks callbacks) throws RemoteException {
|
||||
Log.d(TAG, "unimplemented Method: acceptRingingCall");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void silenceRinger(IWearableCallbacks callbacks) throws RemoteException {
|
||||
Log.d(TAG, "unimplemented Method: silenceRinger");
|
||||
}
|
||||
|
||||
/*
|
||||
* Apple Notification Center Service
|
||||
*/
|
||||
|
||||
@Override
|
||||
public void injectAncsNotificationForTesting(IWearableCallbacks callbacks, AncsNotificationParcelable notification) throws RemoteException {
|
||||
Log.d(TAG, "unimplemented Method: injectAncsNotificationForTesting: " + notification);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doAncsPositiveAction(IWearableCallbacks callbacks, int i) throws RemoteException {
|
||||
Log.d(TAG, "unimplemented Method: doAncsPositiveAction: " + i);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doAncsNegativeAction(IWearableCallbacks callbacks, int i) throws RemoteException {
|
||||
Log.d(TAG, "unimplemented Method: doAncsNegativeAction: " + i);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void openChannel(IWearableCallbacks callbacks, String s1, String s2) throws RemoteException {
|
||||
Log.d(TAG, "unimplemented Method: openChannel; " + s1 + ", " + s2);
|
||||
}
|
||||
|
||||
/*
|
||||
* Channels
|
||||
*/
|
||||
|
||||
@Override
|
||||
public void closeChannel(IWearableCallbacks callbacks, String s) throws RemoteException {
|
||||
Log.d(TAG, "unimplemented Method: closeChannel: " + s);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void closeChannelWithError(IWearableCallbacks callbacks, String s, int errorCode) throws RemoteException {
|
||||
Log.d(TAG, "unimplemented Method: closeChannelWithError:" + s + ", " + errorCode);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void getChannelInputStream(IWearableCallbacks callbacks, IChannelStreamCallbacks channelCallbacks, String s) throws RemoteException {
|
||||
Log.d(TAG, "unimplemented Method: getChannelInputStream: " + s);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void getChannelOutputStream(IWearableCallbacks callbacks, IChannelStreamCallbacks channelCallbacks, String s) throws RemoteException {
|
||||
Log.d(TAG, "unimplemented Method: getChannelOutputStream: " + s);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeChannelInputToFd(IWearableCallbacks callbacks, String s, ParcelFileDescriptor fd) throws RemoteException {
|
||||
Log.d(TAG, "unimplemented Method: writeChannelInputToFd: " + s);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void readChannelOutputFromFd(IWearableCallbacks callbacks, String s, ParcelFileDescriptor fd, long l1, long l2) throws RemoteException {
|
||||
Log.d(TAG, "unimplemented Method: readChannelOutputFromFd: " + s + ", " + l1 + ", " + l2);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void syncWifiCredentials(IWearableCallbacks callbacks) throws RemoteException {
|
||||
Log.d(TAG, "unimplemented Method: syncWifiCredentials");
|
||||
}
|
||||
|
||||
/*
|
||||
* Connection deprecated
|
||||
*/
|
||||
|
||||
@Override
|
||||
@Deprecated
|
||||
public void putConnection(IWearableCallbacks callbacks, ConnectionConfiguration config) throws RemoteException {
|
||||
Log.d(TAG, "unimplemented Method: putConnection");
|
||||
}
|
||||
|
||||
@Override
|
||||
@Deprecated
|
||||
public void getConnection(IWearableCallbacks callbacks) throws RemoteException {
|
||||
Log.d(TAG, "getConfig");
|
||||
postMain(callbacks, () -> {
|
||||
ConnectionConfiguration[] configurations = wearable.getConfigurations();
|
||||
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
|
||||
@Deprecated
|
||||
public void enableConnection(IWearableCallbacks callbacks) throws RemoteException {
|
||||
postMain(callbacks, () -> {
|
||||
ConnectionConfiguration[] configurations = wearable.getConfigurations();
|
||||
if (configurations.length > 0) {
|
||||
enableConfig(callbacks, configurations[0].name);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
@Deprecated
|
||||
public void disableConnection(IWearableCallbacks callbacks) throws RemoteException {
|
||||
postMain(callbacks, () -> {
|
||||
ConnectionConfiguration[] configurations = wearable.getConfigurations();
|
||||
if (configurations.length > 0) {
|
||||
disableConfig(callbacks, configurations[0].name);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
|
||||
public abstract class CallbackRunnable implements Runnable {
|
||||
private IWearableCallbacks callbacks;
|
||||
|
||||
public CallbackRunnable(IWearableCallbacks callbacks) {
|
||||
this.callbacks = callbacks;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
run(callbacks);
|
||||
} catch (RemoteException e) {
|
||||
mainHandler.post(() -> {
|
||||
try {
|
||||
callbacks.onStatus(Status.CANCELED);
|
||||
} catch (RemoteException e2) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public abstract void run(IWearableCallbacks callbacks) throws RemoteException;
|
||||
}
|
||||
|
||||
public interface RemoteExceptionRunnable {
|
||||
void run() throws RemoteException;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,150 @@
|
|||
/*
|
||||
* Copyright (C) 2013-2017 microG 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.location;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
|
||||
import com.google.android.gms.common.api.GoogleApiClient;
|
||||
import com.google.android.gms.location.LocationRequest;
|
||||
import com.google.android.gms.common.internal.ClientIdentity;
|
||||
import com.google.android.gms.location.internal.LocationRequestInternal;
|
||||
import com.google.android.gms.wearable.DataMap;
|
||||
import com.google.android.gms.wearable.MessageEvent;
|
||||
import com.google.android.gms.wearable.Node;
|
||||
import com.google.android.gms.wearable.Wearable;
|
||||
import com.google.android.gms.wearable.WearableListenerService;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class WearableLocationService extends WearableListenerService {
|
||||
private static final String TAG = "GmsWearLocSvc";
|
||||
|
||||
public static final String PATH_LOCATION_REQUESTS = "com/google/android/location/fused/wearable/LOCATION_REQUESTS";
|
||||
public static final String PATH_CAPABILITY_QUERY = "com/google/android/location/fused/wearable/CAPABILITY_QUERY";
|
||||
public static final String PATH_CAPABILITY = "com/google/android/location/fused/wearable/CAPABILITY";
|
||||
|
||||
private GoogleApiClient apiClient;
|
||||
private Map<String, Collection<LocationRequestInternal>> requestMap = new HashMap<String, Collection<LocationRequestInternal>>();
|
||||
|
||||
@Override
|
||||
public void onMessageReceived(MessageEvent messageEvent) {
|
||||
if (messageEvent.getPath().equals(PATH_LOCATION_REQUESTS)) {
|
||||
DataMap dataMap = DataMap.fromByteArray(messageEvent.getData());
|
||||
onLocationRequests(messageEvent.getSourceNodeId(), readLocationRequestList(dataMap, this), dataMap.getBoolean("TRIGGER_UPDATE", false));
|
||||
} else if (messageEvent.getPath().equals(PATH_CAPABILITY_QUERY)) {
|
||||
onCapabilityQuery(messageEvent.getSourceNodeId());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPeerDisconnected(Node peer) {
|
||||
onLocationRequests(peer.getId(), null, false);
|
||||
}
|
||||
|
||||
public void onLocationRequests(String nodeId, Collection<LocationRequestInternal> requests, boolean triggerUpdate) {
|
||||
if (requests == null || requests.isEmpty()) {
|
||||
requestMap.remove(nodeId);
|
||||
} else {
|
||||
requestMap.put(nodeId, requests);
|
||||
}
|
||||
Log.d(TAG, "Requests: "+requestMap.entrySet());
|
||||
// TODO actually request
|
||||
}
|
||||
|
||||
public void onCapabilityQuery(String nodeId) {
|
||||
Wearable.MessageApi.sendMessage(getApiClient(), nodeId, PATH_CAPABILITY, writeLocationCapability(new DataMap(), true).toByteArray());
|
||||
}
|
||||
|
||||
private GoogleApiClient getApiClient() {
|
||||
if (apiClient == null) {
|
||||
apiClient = new GoogleApiClient.Builder(this).addApi(Wearable.API).build();
|
||||
}
|
||||
if (!apiClient.isConnected()) {
|
||||
apiClient.connect();
|
||||
}
|
||||
return apiClient;
|
||||
}
|
||||
|
||||
public static DataMap writeLocationCapability(DataMap dataMap, boolean locationCapable) {
|
||||
dataMap.putBoolean("CAPABILITY_LOCATION", locationCapable);
|
||||
return dataMap;
|
||||
}
|
||||
|
||||
public static Collection<LocationRequestInternal> readLocationRequestList(DataMap dataMap, Context context) {
|
||||
if (!dataMap.containsKey("REQUEST_LIST")) {
|
||||
Log.w(TAG, "malformed DataMap: missing key REQUEST_LIST");
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<DataMap> requestMapList = dataMap.getDataMapArrayList("REQUEST_LIST");
|
||||
List<LocationRequestInternal> locationRequests = new ArrayList<LocationRequestInternal>();
|
||||
for (DataMap map : requestMapList) {
|
||||
locationRequests.add(readLocationRequest(map, context));
|
||||
}
|
||||
return locationRequests;
|
||||
}
|
||||
|
||||
private static LocationRequestInternal readLocationRequest(DataMap dataMap, Context context) {
|
||||
LocationRequest locationRequest = new LocationRequest();
|
||||
LocationRequestInternal request = new LocationRequestInternal(locationRequest);
|
||||
request.triggerUpdate = true;
|
||||
request.clients = Collections.emptyList();
|
||||
|
||||
if (dataMap.containsKey("PRIORITY"))
|
||||
locationRequest.setPriority(dataMap.getInt("PRIORITY", 0));
|
||||
if (dataMap.containsKey("INTERVAL_MS"))
|
||||
locationRequest.setInterval(dataMap.getLong("INTERVAL_MS", 0));
|
||||
if (dataMap.containsKey("FASTEST_INTERVAL_MS"))
|
||||
locationRequest.setFastestInterval(dataMap.getLong("FASTEST_INTERVAL_MS", 0));
|
||||
if (dataMap.containsKey("MAX_WAIT_TIME_MS"))
|
||||
locationRequest.setMaxWaitTime(dataMap.getLong("MAX_WAIT_TIME_MS", 0));
|
||||
if (dataMap.containsKey("SMALLEST_DISPLACEMENT_METERS"))
|
||||
locationRequest.setSmallestDisplacement(dataMap.getFloat("SMALLEST_DISPLACEMENT_METERS", 0));
|
||||
if (dataMap.containsKey("NUM_UPDATES"))
|
||||
locationRequest.setNumUpdates(dataMap.getInt("NUM_UPDATES", 0));
|
||||
if (dataMap.containsKey("EXPIRATION_DURATION_MS"))
|
||||
locationRequest.setExpirationDuration(dataMap.getLong("EXPIRATION_DURATION_MS", 0));
|
||||
if (dataMap.containsKey("TAG"))
|
||||
request.tag = dataMap.getString("TAG");
|
||||
if (dataMap.containsKey("CLIENTS_PACKAGE_ARRAY")) {
|
||||
String[] packages = dataMap.getStringArray("CLIENTS_PACKAGE_ARRAY");
|
||||
if (packages != null) {
|
||||
request.clients = new ArrayList<ClientIdentity>();
|
||||
for (String packageName : packages) {
|
||||
request.clients.add(generateClientIdentity(packageName, context));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
private static ClientIdentity generateClientIdentity(String packageName, Context context) {
|
||||
return null;
|
||||
/*try {
|
||||
return new ClientIdentity(context.getPackageManager().getApplicationInfo(packageName, 0).uid, packageName);
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
Log.w(TAG, "Unknown client identity: " + packageName, e);
|
||||
return new ClientIdentity(context.getApplicationInfo().uid, context.getPackageName());
|
||||
}*/
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue