diff --git a/README.md b/README.md index d1e1e8a47c..4409852b76 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # AndroidAPS +ddd + * Check the wiki: http://wiki.androidaps.org * Everyone who’s been looping with AndroidAPS needs to fill out the form after 3 days of looping https://docs.google.com/forms/d/14KcMjlINPMJHVt28MDRupa4sz4DDIooI4SrW0P3HSN8/viewform?c=0&w=1 diff --git a/app/build.gradle b/app/build.gradle index b7ea47204b..8b50ad30ed 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -68,6 +68,7 @@ android { buildConfigField "String", "BUILDVERSION", '"' + generateGitBuild() + '-' + generateDate() + '"' buildConfigField "String", "HEAD", '"' + generateGitBuild() + '"' testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + // if you change minSdkVersion to less than 11, you need to change executeTask for wear ndk { moduleName "BleCommandUtil" @@ -77,7 +78,7 @@ android { // TODO remove once wear dependency com.google.android.gms:play-services-wearable:7.3.0 // has been upgraded (requiring significant code changes), which currently fails release // build with a deprecation warning - abortOnError false + // abortOnError false // (disabled entirely to avoid reports on the error, which would still be displayed // and it's easy to overlook that it's ignored) checkReleaseBuilds false @@ -194,7 +195,7 @@ dependencies { implementation "org.slf4j:slf4j-api:1.7.12" implementation "com.jjoe64:graphview:4.0.1" implementation "com.joanzapata.iconify:android-iconify-fontawesome:2.1.1" - implementation "com.google.android.gms:play-services-wearable:7.5.0" + implementation 'com.google.android.gms:play-services-wearable:10.2.1' implementation(name: "android-edittext-validator-v1.3.4-mod", ext: "aar") implementation(name: "sightparser-release", ext: "aar") implementation 'com.madgag.spongycastle:core:1.58.0.0' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 2ec11e5a33..dc9201d86c 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -169,7 +169,63 @@ android:name=".plugins.general.wear.wearintegration.WatchUpdaterService" android:exported="true"> - + + + + + + + + + + + + + + + + + + - \ No newline at end of file + diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/wear/WearPlugin.java b/app/src/main/java/info/nightscout/androidaps/plugins/general/wear/WearPlugin.java index ca24c4ed9e..e3545d88bf 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/general/wear/WearPlugin.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/wear/WearPlugin.java @@ -2,6 +2,7 @@ package info.nightscout.androidaps.plugins.general.wear; import android.content.Context; import android.content.Intent; +import android.util.Log; import com.squareup.otto.Subscribe; @@ -35,6 +36,8 @@ public class WearPlugin extends PluginBase { private final Context ctx; private static WearPlugin wearPlugin; + private static String TAG = "WearPlugin"; + public static WearPlugin getPlugin() { return wearPlugin; @@ -76,7 +79,10 @@ public class WearPlugin extends PluginBase { } private void sendDataToWatch(boolean status, boolean basals, boolean bgValue) { - if (isEnabled(getType())) { //only start service when this plugin is enabled + + //Log.d(TAG, "WR: WearPlugin:sendDataToWatch (status=" + status + ",basals=" + basals + ",bgValue=" + bgValue + ")"); + + if (isEnabled(getType())) { // only start service when this plugin is enabled if (bgValue) { ctx.startService(new Intent(ctx, WatchUpdaterService.class)); @@ -93,15 +99,20 @@ public class WearPlugin extends PluginBase { } void resendDataToWatch() { + //Log.d(TAG, "WR: WearPlugin:resendDataToWatch"); ctx.startService(new Intent(ctx, WatchUpdaterService.class).setAction(WatchUpdaterService.ACTION_RESEND)); } void openSettings() { + //Log.d(TAG, "WR: WearPlugin:openSettings"); ctx.startService(new Intent(ctx, WatchUpdaterService.class).setAction(WatchUpdaterService.ACTION_OPEN_SETTINGS)); } void requestNotificationCancel(String actionstring) { - Intent intent = new Intent(ctx, WatchUpdaterService.class).setAction(WatchUpdaterService.ACTION_CANCEL_NOTIFICATION); + //Log.d(TAG, "WR: WearPlugin:requestNotificationCancel"); + + Intent intent = new Intent(ctx, WatchUpdaterService.class) + .setAction(WatchUpdaterService.ACTION_CANCEL_NOTIFICATION); intent.putExtra("actionstring", actionstring); ctx.startService(intent); } diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/wear/wearintegration/SendToDataLayerThread.java b/app/src/main/java/info/nightscout/androidaps/plugins/general/wear/wearintegration/SendToDataLayerThread.java index d935bafec6..aa6498563d 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/general/wear/wearintegration/SendToDataLayerThread.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/wear/wearintegration/SendToDataLayerThread.java @@ -13,40 +13,127 @@ import com.google.android.gms.wearable.PutDataRequest; import com.google.android.gms.wearable.Wearable; import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.ReentrantLock; /** * Created by emmablack on 12/26/14. */ class SendToDataLayerThread extends AsyncTask { private GoogleApiClient googleApiClient; - private static final String TAG = "SendDataThread"; - String path; + private static final String TAG = "SendToDataLayerThread"; + private String path; + private String logPrefix = ""; // "WR: "; + private static int concurrency = 0; + private static int state = 0; + private static final ReentrantLock lock = new ReentrantLock(); + private static long lastlock = 0; + private static final boolean testlockup = false; // always false in production + SendToDataLayerThread(String path, GoogleApiClient pGoogleApiClient) { + // Log.d(TAG, logPrefix + "SendToDataLayerThread: " + path); this.path = path; googleApiClient = pGoogleApiClient; } + + @Override + protected void onPreExecute() { + concurrency++; + if ((concurrency > 12) || ((concurrency > 3 && (lastlock != 0) && (tsl() - lastlock) > 300000))) { + // error if 9 concurrent threads or lock held for >5 minutes with concurrency of 4 + final String err = "Wear Integration deadlock detected!! " + ((lastlock != 0) ? "locked" : "") + " state:" + + state + " @" + hourMinuteString(tsl()); + // Home.toaststaticnext(err); + Log.e(TAG, logPrefix + err); + } + if (concurrency < 0) + Log.d(TAG, logPrefix + "Wear Integration impossible concurrency!!"); + + Log.d(TAG, logPrefix + "SendDataToLayerThread pre-execute concurrency: " + concurrency); + } + + @Override protected Void doInBackground(DataMap... params) { - try { - final NodeApi.GetConnectedNodesResult nodes = Wearable.NodeApi.getConnectedNodes(googleApiClient).await(15, TimeUnit.SECONDS); - for (Node node : nodes.getNodes()) { - for (DataMap dataMap : params) { - PutDataMapRequest putDMR = PutDataMapRequest.create(path); - putDMR.getDataMap().putAll(dataMap); - PutDataRequest request = putDMR.asPutDataRequest(); - DataApi.DataItemResult result = Wearable.DataApi.putDataItem(googleApiClient, request).await(15, TimeUnit.SECONDS); - if (result.getStatus().isSuccess()) { - Log.d(TAG, "DataMap: " + dataMap + " sent to: " + node.getDisplayName()); - } else { - Log.d(TAG, "ERROR: failed to send DataMap"); - } - } + if (testlockup) { + try { + Log.e(TAG, logPrefix + "WARNING RUNNING TEST LOCK UP CODE - NEVER FOR PRODUCTION"); + Thread.sleep(1000000); // DEEEBBUUGGGG + } catch (Exception e) { } - } catch (Exception e) { - Log.e(TAG, "Got exception sending data to wear: " + e.toString()); } + sendToWear(params); + concurrency--; + Log.d(TAG, logPrefix + "SendDataToLayerThread post-execute concurrency: " + concurrency); return null; } + + + // Debug function to expose where it might be locking up + private synchronized void sendToWear(final DataMap... params) { + if (!lock.tryLock()) { + Log.d(TAG, logPrefix + "Concurrent access - waiting for thread unlock"); + lock.lock(); // enforce single threading + Log.d(TAG, logPrefix + "Thread unlocked - proceeding"); + } + lastlock = tsl(); + try { + if (state != 0) { + Log.e(TAG, logPrefix + "WEAR STATE ERROR: state=" + state); + } + state = 1; + final NodeApi.GetConnectedNodesResult nodes = Wearable.NodeApi.getConnectedNodes(googleApiClient).await(15, + TimeUnit.SECONDS); + + Log.d(TAG, logPrefix + "Nodes: " + nodes); + + state = 2; + for (Node node : nodes.getNodes()) { + state = 3; + for (DataMap dataMap : params) { + state = 4; + PutDataMapRequest putDMR = PutDataMapRequest.create(path); + state = 5; + putDMR.getDataMap().putAll(dataMap); + putDMR.setUrgent(); + state = 6; + PutDataRequest request = putDMR.asPutDataRequest(); + state = 7; + DataApi.DataItemResult result = Wearable.DataApi.putDataItem(googleApiClient, request).await(15, + TimeUnit.SECONDS); + state = 8; + if (result.getStatus().isSuccess()) { + Log.d(TAG, logPrefix + "DataMap: " + dataMap + " sent to: " + node.getDisplayName()); + } else { + Log.e(TAG, logPrefix + "ERROR: failed to send DataMap"); + result = Wearable.DataApi.putDataItem(googleApiClient, request).await(30, TimeUnit.SECONDS); + if (result.getStatus().isSuccess()) { + Log.d(TAG, logPrefix + "DataMap retry: " + dataMap + " sent to: " + node.getDisplayName()); + } else { + Log.e(TAG, logPrefix + "ERROR on retry: failed to send DataMap: " + + result.getStatus().toString()); + } + } + state = 9; + } + } + state = 0; + } catch (Exception e) { + Log.e(TAG, logPrefix + "Got exception in sendToWear: " + e.toString()); + } finally { + lastlock = 0; + lock.unlock(); + } + } + + + private static long tsl() { + return System.currentTimeMillis(); + } + + + private static String hourMinuteString(long timestamp) { + return android.text.format.DateFormat.format("kk:mm", timestamp).toString(); + } } diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/wear/wearintegration/WatchUpdaterService.java b/app/src/main/java/info/nightscout/androidaps/plugins/general/wear/wearintegration/WatchUpdaterService.java index 12dc116895..52dbeed75c 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/general/wear/wearintegration/WatchUpdaterService.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/wear/wearintegration/WatchUpdaterService.java @@ -1,9 +1,17 @@ package info.nightscout.androidaps.plugins.general.wear.wearintegration; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; +import android.os.AsyncTask; import android.os.BatteryManager; import android.os.Bundle; import android.os.Handler; @@ -14,8 +22,11 @@ import android.util.Log; import com.google.android.gms.common.ConnectionResult; import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.wearable.CapabilityApi; +import com.google.android.gms.wearable.CapabilityInfo; 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.PutDataMapRequest; import com.google.android.gms.wearable.PutDataRequest; import com.google.android.gms.wearable.Wearable; @@ -42,21 +53,24 @@ import info.nightscout.androidaps.plugins.iob.iobCobCalculator.IobCobCalculatorP import info.nightscout.androidaps.plugins.treatments.Treatment; import info.nightscout.androidaps.interfaces.PluginType; import info.nightscout.androidaps.interfaces.TreatmentsInterface; -import info.nightscout.androidaps.plugins.configBuilder.ConfigBuilderPlugin; import info.nightscout.androidaps.plugins.aps.loop.LoopPlugin; +import info.nightscout.androidaps.plugins.configBuilder.ConfigBuilderPlugin; +import info.nightscout.androidaps.plugins.configBuilder.ProfileFunctions; import info.nightscout.androidaps.plugins.general.nsclient.data.NSDeviceStatus; import info.nightscout.androidaps.plugins.general.overview.OverviewPlugin; import info.nightscout.androidaps.plugins.treatments.TreatmentsPlugin; import info.nightscout.androidaps.plugins.general.wear.ActionStringHandler; import info.nightscout.androidaps.plugins.general.wear.WearPlugin; +import info.nightscout.androidaps.plugins.iob.iobCobCalculator.IobCobCalculatorPlugin; +import info.nightscout.androidaps.plugins.treatments.Treatment; +import info.nightscout.androidaps.plugins.treatments.TreatmentsPlugin; import info.nightscout.androidaps.utils.DecimalFormatter; import info.nightscout.androidaps.utils.SP; import info.nightscout.androidaps.utils.SafeParse; import info.nightscout.androidaps.utils.ToastUtils; -public class WatchUpdaterService extends WearableListenerService implements - GoogleApiClient.ConnectionCallbacks, - GoogleApiClient.OnConnectionFailedListener { +public class WatchUpdaterService extends WearableListenerService implements GoogleApiClient.ConnectionCallbacks, GoogleApiClient.OnConnectionFailedListener { + public static final String ACTION_RESEND = WatchUpdaterService.class.getName().concat(".Resend"); public static final String ACTION_OPEN_SETTINGS = WatchUpdaterService.class.getName().concat(".OpenSettings"); public static final String ACTION_SEND_STATUS = WatchUpdaterService.class.getName().concat(".SendStatus"); @@ -91,6 +105,17 @@ public class WatchUpdaterService extends WearableListenerService implements private Handler handler; + // Phone + private static final String CAPABILITY_PHONE_APP = "phone_app_sync_bgs"; + private static final String MESSAGE_PATH_PHONE = "/phone_message_path"; + // Wear + private static final String CAPABILITY_WEAR_APP = "wear_app_sync_bgs"; + private static final String MESSAGE_PATH_WEAR = "/wear_message_path"; + private String mWearNodeId = null; + private String localnode = null; + private String logPrefix = ""; // "WR: " + + @Override public void onCreate() { mPrefs = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); @@ -112,24 +137,24 @@ public class WatchUpdaterService extends WearableListenerService implements public void setSettings() { wear_integration = WearPlugin.getPlugin().isEnabled(PluginType.GENERAL); + // Log.d(TAG, "WR: wear_integration=" + wear_integration); if (wear_integration) { googleApiConnect(); } } - public void googleApiConnect() { + + private void googleApiConnect() { if (googleApiClient != null && (googleApiClient.isConnected() || googleApiClient.isConnecting())) { googleApiClient.disconnect(); } - googleApiClient = new GoogleApiClient.Builder(this) - .addConnectionCallbacks(this) - .addOnConnectionFailedListener(this) - .addApi(Wearable.API) - .build(); + googleApiClient = new GoogleApiClient.Builder(this).addConnectionCallbacks(this) + .addOnConnectionFailedListener(this).addApi(Wearable.API).build(); Wearable.MessageApi.addListener(googleApiClient, this); if (googleApiClient.isConnected()) { - Log.d("WatchUpdater", "API client is connected"); + log.debug(logPrefix + "API client is connected"); } else { + // Log.d("WatchUpdater", logPrefix + "API client is not connected and is trying to connect"); googleApiClient.connect(); } } @@ -138,6 +163,8 @@ public class WatchUpdaterService extends WearableListenerService implements public int onStartCommand(Intent intent, int flags, int startId) { String action = intent != null ? intent.getAction() : null; + // Log.d(TAG, logPrefix + "onStartCommand: " + action); + if (wear_integration) { handler.post(() -> { if (googleApiClient.isConnected()) { @@ -177,13 +204,63 @@ public class WatchUpdaterService extends WearableListenerService implements } + private void updateWearSyncBgsCapability(CapabilityInfo capabilityInfo) { + Log.d("WatchUpdaterService", logPrefix + "CabilityInfo: " + capabilityInfo); + Set connectedNodes = capabilityInfo.getNodes(); + mWearNodeId = pickBestNodeId(connectedNodes); + } + + + private String pickBestNodeId(Set nodes) { + String bestNodeId = null; + // Find a nearby node or pick one arbitrarily + for (Node node : nodes) { + if (node.isNearby()) { + return node.getId(); + } + bestNodeId = node.getId(); + } + return bestNodeId; + } + + @Override public void onConnected(Bundle connectionHint) { + CapabilityApi.CapabilityListener capabilityListener = capabilityInfo -> { + updateWearSyncBgsCapability(capabilityInfo); + // Log.d(TAG, logPrefix + "onConnected onCapabilityChanged mWearNodeID:" + mWearNodeId); + // new CheckWearableConnected().execute(); + }; + + Wearable.CapabilityApi.addCapabilityListener(googleApiClient, capabilityListener, CAPABILITY_WEAR_APP); sendData(); } + + @Override + public void onPeerConnected(com.google.android.gms.wearable.Node peer) {// KS + super.onPeerConnected(peer); + String id = peer.getId(); + String name = peer.getDisplayName(); + // Log.d(TAG, logPrefix + "onPeerConnected peer name & ID: " + name + "|" + id); + } + + + @Override + public void onPeerDisconnected(com.google.android.gms.wearable.Node peer) {// KS + super.onPeerDisconnected(peer); + String id = peer.getId(); + String name = peer.getDisplayName(); + // Log.d(TAG, logPrefix + "onPeerDisconnected peer name & ID: " + name + "|" + id); + SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); + } + + @Override public void onMessageReceived(MessageEvent event) { + + // Log.d(TAG, logPrefix + "onMessageRecieved: " + event); + if (wear_integration) { if (event != null && event.getPath().equals(WEARABLE_RESEND_PATH)) { resendData(); @@ -214,6 +291,7 @@ public class WatchUpdaterService extends WearableListenerService implements private void sendData() { BgReading lastBG = DatabaseHelper.lastBg(); + // Log.d(TAG, logPrefix + "LastBg=" + lastBG); if (lastBG != null) { GlucoseStatus glucoseStatus = GlucoseStatus.getGlucoseStatusData(); @@ -228,18 +306,19 @@ public class WatchUpdaterService extends WearableListenerService implements return; } - new SendToDataLayerThread(WEARABLE_DATA_PATH, googleApiClient).execute(dataMap); + executeTask(new SendToDataLayerThread(WEARABLE_DATA_PATH, googleApiClient), dataMap); } } } + private DataMap dataMapSingleBG(BgReading lastBG, GlucoseStatus glucoseStatus) { String units = ProfileFunctions.getInstance().getProfileUnits(); Double lowLine = SafeParse.stringToDouble(mPrefs.getString("low_mark", "0")); Double highLine = SafeParse.stringToDouble(mPrefs.getString("high_mark", "0")); - //convert to mg/dl + // convert to mg/dl if (!units.equals(Constants.MGDL)) { lowLine *= Constants.MMOLL_TO_MGDL; highLine *= Constants.MMOLL_TO_MGDL; @@ -287,7 +366,6 @@ public class WatchUpdaterService extends WearableListenerService implements deltastring += "+"; } else { deltastring += "-"; - } boolean detailed = SP.getBoolean("wear_detailed_delta", false); @@ -352,7 +430,7 @@ public class WatchUpdaterService extends WearableListenerService implements } } entries.putDataMapArrayList("entries", dataMaps); - new SendToDataLayerThread(WEARABLE_DATA_PATH, googleApiClient).execute(entries); + executeTask(new SendToDataLayerThread(WEARABLE_DATA_PATH, googleApiClient), entries); } sendPreferences(); sendBasals(); @@ -501,7 +579,7 @@ public class WatchUpdaterService extends WearableListenerService implements dm.putDataMapArrayList("boluses", boluses); dm.putDataMapArrayList("predictions", predictions); - new SendToDataLayerThread(BASAL_DATA_PATH, googleApiClient).execute(dm); + executeTask(new SendToDataLayerThread(BASAL_DATA_PATH, googleApiClient), dm); } private DataMap tempDatamap(long startTime, double startBasal, long to, double toBasal, double amount) { @@ -548,6 +626,7 @@ public class WatchUpdaterService extends WearableListenerService implements dataMapRequest.getDataMap().putLong("timestamp", System.currentTimeMillis()); dataMapRequest.getDataMap().putString("openSettings", "openSettings"); PutDataRequest putDataRequest = dataMapRequest.asPutDataRequest(); + debugData("sendNotification", putDataRequest); Wearable.DataApi.putDataItem(googleApiClient, putDataRequest); } else { Log.e("OpenSettings", "No connection to wearable available!"); @@ -563,6 +642,7 @@ public class WatchUpdaterService extends WearableListenerService implements dataMapRequest.getDataMap().putString("progressstatus", status); dataMapRequest.getDataMap().putInt("progresspercent", progresspercent); PutDataRequest putDataRequest = dataMapRequest.asPutDataRequest(); + debugData("sendBolusProgress", putDataRequest); Wearable.DataApi.putDataItem(googleApiClient, putDataRequest); } else { Log.e("BolusProgress", "No connection to wearable available!"); @@ -582,6 +662,7 @@ public class WatchUpdaterService extends WearableListenerService implements log.debug("Requesting confirmation from wear: " + actionstring); PutDataRequest putDataRequest = dataMapRequest.asPutDataRequest(); + debugData("sendActionConfirmationRequest", putDataRequest); Wearable.DataApi.putDataItem(googleApiClient, putDataRequest); } else { Log.e("confirmationRequest", "No connection to wearable available!"); @@ -601,6 +682,7 @@ public class WatchUpdaterService extends WearableListenerService implements log.debug("Requesting confirmation from wear: " + actionstring); PutDataRequest putDataRequest = dataMapRequest.asPutDataRequest(); + debugData("sendChangeConfirmationRequest", putDataRequest); Wearable.DataApi.putDataItem(googleApiClient, putDataRequest); } else { Log.e("changeConfirmRequest", "No connection to wearable available!"); @@ -618,9 +700,10 @@ public class WatchUpdaterService extends WearableListenerService implements log.debug("Canceling notification on wear: " + actionstring); PutDataRequest putDataRequest = dataMapRequest.asPutDataRequest(); + debugData("sendCancelNotificationRequest", putDataRequest); Wearable.DataApi.putDataItem(googleApiClient, putDataRequest); } else { - Log.e("cancelNotificationRequest", "No connection to wearable available!"); + Log.e("cancelNotificationReq", "No connection to wearable available!"); } } @@ -683,6 +766,7 @@ public class WatchUpdaterService extends WearableListenerService implements dataMapRequest.getDataMap().putBoolean("showBgi", mPrefs.getBoolean("wear_showbgi", false)); dataMapRequest.getDataMap().putInt("batteryLevel", (phoneBattery >= 30) ? 1 : 0); PutDataRequest putDataRequest = dataMapRequest.asPutDataRequest(); + debugData("sendStatus", putDataRequest); Wearable.DataApi.putDataItem(googleApiClient, putDataRequest); } else { Log.e("SendStatus", "No connection to wearable available!"); @@ -699,12 +783,29 @@ public class WatchUpdaterService extends WearableListenerService implements dataMapRequest.getDataMap().putLong("timestamp", System.currentTimeMillis()); dataMapRequest.getDataMap().putBoolean("wearcontrol", wearcontrol); PutDataRequest putDataRequest = dataMapRequest.asPutDataRequest(); + debugData("sendPreferences", putDataRequest); Wearable.DataApi.putDataItem(googleApiClient, putDataRequest); } else { Log.e("SendStatus", "No connection to wearable available!"); } } + + private void debugData(String source, Object data) { + // Log.d(TAG, "WR: " + source + " " + data); + } + + + private void executeTask(AsyncTask task, DataMap... parameters) { + task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Object[])parameters); + // if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { + // task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + // } else { + // task.execute(); + // } + } + + @NonNull private String generateStatusString(Profile profile, String currentBasal, String iobSum, String iobDetail, String bgiString) { diff --git a/app/src/main/res/values/wear.xml b/app/src/main/res/values/wear.xml new file mode 100644 index 0000000000..41df8ef193 --- /dev/null +++ b/app/src/main/res/values/wear.xml @@ -0,0 +1,18 @@ + + + + + phone_app_sync_bgs + + \ No newline at end of file diff --git a/wear/build.gradle b/wear/build.gradle index be11cdbb5b..2a35c591b6 100644 --- a/wear/build.gradle +++ b/wear/build.gradle @@ -2,6 +2,7 @@ apply plugin: 'com.android.application' ext { wearableVersion = "2.0.1" + playServicesWearable = "10.2.1" } def generateGitBuild = { -> @@ -93,7 +94,7 @@ dependencies { //compile "com.ustwo.android:clockwise-wearable:1.0.2" compileOnly "com.google.android.wearable:wearable:${wearableVersion}" implementation "com.google.android.support:wearable:${wearableVersion}" - implementation "com.google.android.gms:play-services-wearable:7.3.0" + implementation "com.google.android.gms:play-services-wearable:${playServicesWearable}" implementation(name:"ustwo-clockwise-debug", ext:"aar") implementation "com.android.support:support-v4:27.0.1" implementation 'com.android.support:wear:27.0.1' diff --git a/wear/src/main/AndroidManifest.xml b/wear/src/main/AndroidManifest.xml index e6750c515d..f04e072bc7 100644 --- a/wear/src/main/AndroidManifest.xml +++ b/wear/src/main/AndroidManifest.xml @@ -168,9 +168,66 @@ + - + + + + + + + + + + + + + + + + + + diff --git a/wear/src/main/java/info/nightscout/androidaps/data/ListenerService.java b/wear/src/main/java/info/nightscout/androidaps/data/ListenerService.java index 52dfc97ad1..094b8aad76 100644 --- a/wear/src/main/java/info/nightscout/androidaps/data/ListenerService.java +++ b/wear/src/main/java/info/nightscout/androidaps/data/ListenerService.java @@ -1,5 +1,8 @@ package info.nightscout.androidaps.data; +import java.util.Set; +import java.util.concurrent.TimeUnit; + import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; @@ -7,16 +10,24 @@ import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.os.AsyncTask; +import android.os.Build; import android.os.Bundle; import android.os.SystemClock; import android.preference.PreferenceManager; import android.support.v4.content.LocalBroadcastManager; import android.support.v4.app.NotificationCompat; import android.support.v4.app.NotificationManagerCompat; +import android.support.v4.content.LocalBroadcastManager; +import android.util.Log; import com.google.android.gms.common.ConnectionResult; import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.common.api.PendingResult; +import com.google.android.gms.common.api.ResultCallback; +import com.google.android.gms.wearable.CapabilityApi; +import com.google.android.gms.wearable.CapabilityInfo; +import com.google.android.gms.wearable.ChannelApi; import com.google.android.gms.wearable.DataEvent; import com.google.android.gms.wearable.DataEventBuffer; import com.google.android.gms.wearable.DataMap; @@ -33,12 +44,15 @@ import info.nightscout.androidaps.R; import info.nightscout.androidaps.interaction.actions.AcceptActivity; import info.nightscout.androidaps.interaction.actions.CPPActivity; import info.nightscout.androidaps.interaction.utils.SafeParse; +import info.nightscout.androidaps.interaction.utils.WearUtil; + /** * Created by emmablack on 12/26/14. */ public class ListenerService extends WearableListenerService implements GoogleApiClient.ConnectionCallbacks, - GoogleApiClient.OnConnectionFailedListener { + GoogleApiClient.OnConnectionFailedListener, ChannelApi.ChannelListener { + private static final String WEARABLE_DATA_PATH = "/nightscout_watch_data"; private static final String WEARABLE_RESEND_PATH = "/nightscout_watch_data_resend"; private static final String WEARABLE_CANCELBOLUS_PATH = "/nightscout_watch_cancel_bolus"; @@ -67,49 +81,164 @@ public class ListenerService extends WearableListenerService implements GoogleAp private static final String ACTION_RESEND_BULK = "com.dexdrip.stephenblack.nightwatch.RESEND_BULK_DATA"; + GoogleApiClient googleApiClient; private long lastRequest = 0; private DismissThread confirmThread; private DismissThread bolusprogressThread; + private static final String TAG = "ListenerService"; + private DataRequester mDataRequester = null; + private static final int GET_CAPABILITIES_TIMEOUT_MS = 5000; + + // Phone + private static final String CAPABILITY_PHONE_APP = "phone_app_sync_bgs"; + private static final String MESSAGE_PATH_PHONE = "/phone_message_path"; + // Wear + private static final String CAPABILITY_WEAR_APP = "wear_app_sync_bgs"; + private static final String MESSAGE_PATH_WEAR = "/wear_message_path"; + private String mPhoneNodeId = null; + private String localnode = null; + private String logPrefix = ""; // "WR: " public class DataRequester extends AsyncTask { Context mContext; + String path; + byte[] payload; - DataRequester(Context context) { - mContext = context; + + DataRequester(Context context, String thispath, byte[] thispayload) { + path = thispath; + payload = thispayload; + // Log.d(TAG, logPrefix + "DataRequester DataRequester: " + thispath + " lastRequest:" + lastRequest); } + + @Override protected Void doInBackground(Void... params) { - if (googleApiClient.isConnected()) { - if (System.currentTimeMillis() - lastRequest > 20 * 1000) { // enforce 20-second debounce period - lastRequest = System.currentTimeMillis(); + // Log.d(TAG, logPrefix + "DataRequester: doInBack: " + params); - NodeApi.GetConnectedNodesResult nodes = - Wearable.NodeApi.getConnectedNodes(googleApiClient).await(); - for (Node node : nodes.getNodes()) { - Wearable.MessageApi.sendMessage(googleApiClient, node.getId(), WEARABLE_RESEND_PATH, null); - } + try { + + forceGoogleApiConnect(); + DataMap datamap; + + if (isCancelled()) { + Log.d(TAG, "doInBackground CANCELLED programmatically"); + return null; } - } else { - googleApiClient.blockingConnect(15, TimeUnit.SECONDS); - if (googleApiClient.isConnected()) { - if (System.currentTimeMillis() - lastRequest > 20 * 1000) { // enforce 20-second debounce period + + if (googleApiClient != null) { + if (!googleApiClient.isConnected()) + googleApiClient.blockingConnect(15, TimeUnit.SECONDS); + } + + // this code might not be needed in this way, but we need to see that later + if ((googleApiClient != null) && (googleApiClient.isConnected())) { + if ((System.currentTimeMillis() - lastRequest > 20 * 1000)) { + + // enforce 20-second debounce period lastRequest = System.currentTimeMillis(); - NodeApi.GetConnectedNodesResult nodes = - Wearable.NodeApi.getConnectedNodes(googleApiClient).await(); - for (Node node : nodes.getNodes()) { - Wearable.MessageApi.sendMessage(googleApiClient, node.getId(), WEARABLE_RESEND_PATH, null); + // NodeApi.GetConnectedNodesResult nodes = + // Wearable.NodeApi.getConnectedNodes(googleApiClient).await(); + if (localnode == null || (localnode != null && localnode.isEmpty())) + setLocalNodeName(); + + CapabilityInfo capabilityInfo = getCapabilities(); + + int count = 0; + Node phoneNode = null; + + if (capabilityInfo != null) { + phoneNode = updatePhoneSyncBgsCapability(capabilityInfo); + count = capabilityInfo.getNodes().size(); } + + Log.d(TAG, "doInBackground connected. CapabilityApi.GetCapabilityResult mPhoneNodeID=" + + (phoneNode != null ? phoneNode.getId() : "") + " count=" + count + " localnode=" + + localnode);// KS + + if (count > 0) { + + for (Node node : capabilityInfo.getNodes()) { + + // Log.d(TAG, "doInBackground path: " + path); + + switch (path) { + // simple send as is payloads + + case WEARABLE_RESEND_PATH: + Wearable.MessageApi.sendMessage(googleApiClient, node.getId(), + WEARABLE_RESEND_PATH, null); + break; + case WEARABLE_DATA_PATH: + case WEARABLE_CANCELBOLUS_PATH: + case WEARABLE_CONFIRM_ACTIONSTRING_PATH: + case WEARABLE_INITIATE_ACTIONSTRING_PATH: + case OPEN_SETTINGS: + case NEW_STATUS_PATH: + case NEW_PREFERENCES_PATH: + case BASAL_DATA_PATH: + case BOLUS_PROGRESS_PATH: + case ACTION_CONFIRMATION_REQUEST_PATH: + case NEW_CHANGECONFIRMATIONREQUEST_PATH: + case ACTION_CANCELNOTIFICATION_REQUEST_PATH: { + Log.w(TAG, logPrefix + "Unhandled path"); + // sendMessagePayload(node, path, path, payload); + } + + default:// SYNC_ALL_DATA + // this fall through is messy and non-deterministic for new paths + + } + } + } else { + + Log.d(TAG, logPrefix + "doInBackground connected but getConnectedNodes returns 0."); + + } + } else { + // no resend + Log.d(TAG, logPrefix + "Inside the timeout, will not be executed"); + + } + } else { + Log.d(TAG, logPrefix + "Not connected for sending: api " + + ((googleApiClient == null) ? "is NULL!" : "not null")); + if (googleApiClient != null) { + googleApiClient.connect(); + } else { + googleApiConnect(); } } + + } catch (Exception ex) { + Log.e(TAG, logPrefix + "Error executing DataRequester in background. Exception: " + ex.getMessage()); } + return null; } } + + public CapabilityInfo getCapabilities() { + + CapabilityApi.GetCapabilityResult capabilityResult = Wearable.CapabilityApi.getCapability(googleApiClient, + CAPABILITY_PHONE_APP, CapabilityApi.FILTER_REACHABLE).await(GET_CAPABILITIES_TIMEOUT_MS, + TimeUnit.MILLISECONDS); + + if (!capabilityResult.getStatus().isSuccess()) { + Log.e(TAG, logPrefix + "doInBackground Failed to get capabilities, status: " + + capabilityResult.getStatus().getStatusMessage()); + return null; + } + + return capabilityResult.getCapability(); + + } + public class BolusCancelTask extends AsyncTask { Context mContext; @@ -119,6 +248,8 @@ public class ListenerService extends WearableListenerService implements GoogleAp @Override protected Void doInBackground(Void... params) { + // Log.d(TAG, logPrefix + "BolusCancelTask: doInBack: " + params); + if (googleApiClient.isConnected()) { NodeApi.GetConnectedNodesResult nodes = Wearable.NodeApi.getConnectedNodes(googleApiClient).await(); @@ -154,6 +285,9 @@ public class ListenerService extends WearableListenerService implements GoogleAp @Override protected Void doInBackground(Void... params) { + + forceGoogleApiConnect(); + if (googleApiClient.isConnected()) { NodeApi.GetConnectedNodesResult nodes = Wearable.NodeApi.getConnectedNodes(googleApiClient).await(); @@ -176,7 +310,7 @@ public class ListenerService extends WearableListenerService implements GoogleAp } public void requestData() { - new DataRequester(this).execute(); + sendData(WEARABLE_RESEND_PATH, null); } public void cancelBolus() { @@ -191,7 +325,73 @@ public class ListenerService extends WearableListenerService implements GoogleAp new MessageActionTask(this, WEARABLE_INITIATE_ACTIONSTRING_PATH, actionstring).execute(); } - public void googleApiConnect() { + + private Node updatePhoneSyncBgsCapability(CapabilityInfo capabilityInfo) { + // Log.d(TAG, "CapabilityInfo: " + capabilityInfo); + + Set connectedNodes = capabilityInfo.getNodes(); + return pickBestNode(connectedNodes); + // mPhoneNodeId = pickBestNodeId(connectedNodes); + } + + + private Node pickBestNode(Set nodes) { + Node bestNode = null; + // Find a nearby node or pick one arbitrarily + for (Node node : nodes) { + if (node.isNearby()) { + return node; + } + bestNode = node; + } + return bestNode; + } + + + private synchronized void sendData(String path, byte[] payload) { + // Log.d(TAG, "WR: sendData: path: " + path + ", payload=" + payload); + + if (path == null) + return; + if (mDataRequester != null) { + // Log.d(TAG, logPrefix + "sendData DataRequester != null lastRequest:" + + // WearUtil.dateTimeText(lastRequest)); + if (mDataRequester.getStatus() != AsyncTask.Status.FINISHED) { + // Log.d(TAG, logPrefix + "sendData Should be canceled? Let run 'til finished."); + // mDataRequester.cancel(true); + } + } + + Log.d(TAG, logPrefix + "sendData: execute lastRequest:" + WearUtil.dateTimeText(lastRequest)); + mDataRequester = (DataRequester)new DataRequester(this, path, payload).execute(); + // executeTask(mDataRequester); + + // if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + // Log.d(TAG, "sendData SDK < M call execute lastRequest:" + WearUtil.dateTimeText(lastRequest)); + // mDataRequester = (DataRequester) new DataRequester(this, path, payload).execute(); + // } else { + // Log.d(TAG, "sendData SDK >= M call executeOnExecutor lastRequest:" + WearUtil.dateTimeText(lastRequest)); + // // TODO xdrip executor + // mDataRequester = (DataRequester) new DataRequester(this, path, payload).executeOnExecutor(xdrip.executor); + // } + } + + + private void googleApiConnect() { + if (googleApiClient != null) { + // Remove old listener(s) + try { + Wearable.ChannelApi.removeListener(googleApiClient, this); + } catch (Exception e) { + // + } + try { + Wearable.MessageApi.removeListener(googleApiClient, this); + } catch (Exception e) { + // + } + } + googleApiClient = new GoogleApiClient.Builder(this) .addConnectionCallbacks(this) .addOnConnectionFailedListener(this) @@ -201,8 +401,25 @@ public class ListenerService extends WearableListenerService implements GoogleAp } + + private void forceGoogleApiConnect() { + if ((googleApiClient != null && !googleApiClient.isConnected() && !googleApiClient.isConnecting()) || googleApiClient == null) { + try { + Log.d(TAG, "forceGoogleApiConnect: forcing google api reconnection"); + googleApiConnect(); + Thread.sleep(2000); + } catch (InterruptedException e) { + // + } + } + } + + @Override public int onStartCommand(Intent intent, int flags, int startId) { + + // Log.d(TAG, logPrefix + "onStartCommand: Intent: " + intent); + if (intent != null && ACTION_RESEND.equals(intent.getAction())) { googleApiConnect(); requestData(); @@ -256,13 +473,16 @@ public class ListenerService extends WearableListenerService implements GoogleAp public void onDataChanged(DataEventBuffer dataEvents) { DataMap dataMap; + // Log.d(TAG, logPrefix + "onDataChanged: DataEvents=" + dataEvents); for (DataEvent event : dataEvents) { if (event.getType() == DataEvent.TYPE_CHANGED) { - String path = event.getDataItem().getUri().getPath(); + + //Log.d(TAG, "WR: onDataChanged: Path: " + path + ", EventDataItem=" + event.getDataItem()); + if (path.equals(OPEN_SETTINGS)) { Intent intent = new Intent(this, AAPSPreferences.class); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); @@ -480,6 +700,21 @@ public class ListenerService extends WearableListenerService implements GoogleAp @Override public void onConnected(Bundle bundle) { + // Log.d(TAG, logPrefix + "onConnected call requestData"); + + CapabilityApi.CapabilityListener capabilityListener = new CapabilityApi.CapabilityListener() { + + @Override + public void onCapabilityChanged(CapabilityInfo capabilityInfo) { + updatePhoneSyncBgsCapability(capabilityInfo); + Log.d(TAG, logPrefix + "onConnected onCapabilityChanged mPhoneNodeID:" + mPhoneNodeId + + ", Capability: " + capabilityInfo); + } + }; + + Wearable.CapabilityApi.addCapabilityListener(googleApiClient, capabilityListener, CAPABILITY_PHONE_APP); + + Wearable.ChannelApi.addListener(googleApiClient, this); requestData(); } @@ -493,14 +728,38 @@ public class ListenerService extends WearableListenerService implements GoogleAp } + + private void setLocalNodeName() { + forceGoogleApiConnect(); + PendingResult result = Wearable.NodeApi.getLocalNode(googleApiClient); + result.setResultCallback(new ResultCallback() { + + @Override + public void onResult(NodeApi.GetLocalNodeResult getLocalNodeResult) { + if (!getLocalNodeResult.getStatus().isSuccess()) { + Log.e(TAG, "ERROR: failed to getLocalNode Status=" + + getLocalNodeResult.getStatus().getStatusMessage()); + } else { + Log.d(TAG, "getLocalNode Status=: " + getLocalNodeResult.getStatus().getStatusMessage()); + Node getnode = getLocalNodeResult.getNode(); + localnode = getnode != null ? getnode.getDisplayName() + "|" + getnode.getId() : ""; + Log.d(TAG, "setLocalNodeName. localnode=" + localnode); + } + } + }); + } + + @Override public void onDestroy() { super.onDestroy(); if (googleApiClient != null && googleApiClient.isConnected()) { googleApiClient.disconnect(); } + if (googleApiClient != null) { Wearable.MessageApi.removeListener(googleApiClient, this); + Wearable.ChannelApi.removeListener(googleApiClient, this); } - } + } } diff --git a/wear/src/main/java/info/nightscout/androidaps/interaction/utils/WearUtil.java b/wear/src/main/java/info/nightscout/androidaps/interaction/utils/WearUtil.java new file mode 100644 index 0000000000..ca63749888 --- /dev/null +++ b/wear/src/main/java/info/nightscout/androidaps/interaction/utils/WearUtil.java @@ -0,0 +1,19 @@ +package info.nightscout.androidaps.interaction.utils; + +import java.time.LocalDateTime; +import java.util.Date; + +/** + * Created by andy on 3/5/19. + */ + +public class WearUtil { + + + public static String dateTimeText(long timeInMs) { + Date d = new Date(timeInMs); + return "" + d.getDay() + "." + d.getMonth() + "." + d.getYear() + " " + d.getHours() + ":" + d.getMinutes() + ":" + d.getSeconds(); + } + + +} diff --git a/wear/src/main/res/values/wear.xml b/wear/src/main/res/values/wear.xml new file mode 100644 index 0000000000..41df8ef193 --- /dev/null +++ b/wear/src/main/res/values/wear.xml @@ -0,0 +1,18 @@ + + + + + phone_app_sync_bgs + + \ No newline at end of file