WearListenerService onDataChanged strange behavior
Asked Answered
I

1

7

I want to make bidirectional data transfer between Android Wear and Handheld. All seems to be good except triggering onDataChanged on Handheld. It triggers only then I plug in\out USB cable, connected to the PC. So I don't understand why it's happen.

Here is my code:

WearListenerService on Handheld:

import android.content.Intent;
import android.os.Bundle;
import android.support.v4.content.LocalBroadcastManager;

import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.api.GoogleApiClient;
import com.google.android.gms.wearable.DataApi;
import com.google.android.gms.wearable.DataEvent;
import com.google.android.gms.wearable.DataEventBuffer;
import com.google.android.gms.wearable.DataMap;
import com.google.android.gms.wearable.DataMapItem;
import com.google.android.gms.wearable.Node;
import com.google.android.gms.wearable.NodeApi;
import com.google.android.gms.wearable.PutDataMapRequest;
import com.google.android.gms.wearable.PutDataRequest;
import com.google.android.gms.wearable.Wearable;
import com.google.android.gms.wearable.WearableListenerService;

import java.util.List;

import ru.orangesoftware.financisto.db.DatabaseAdapter;
import ru.orangesoftware.financisto.model.Category;
import ru.orangesoftware.financisto.model.CategoryTree;
import ru.orangesoftware.financisto.utils.Log;

public class WearService extends WearableListenerService implements GoogleApiClient.ConnectionCallbacks,
        GoogleApiClient.OnConnectionFailedListener
{
    private static final String WEARABLE_DATA_PATH = "/wearable_data";
    private static final String HANDHELD_DATA_PATH = "/handheld_data";

    private SendToDataLayerThread s;
    GoogleApiClient googleClient;

    private DatabaseAdapter db;

    @Override
    public void onCreate()
    {
        super.onCreate();

        Log.d("WearService Created");
        db = new DatabaseAdapter(this);
        db.open();

        initGoogleApiClient();
    }

    @Override
    public void onDataChanged(DataEventBuffer dataEvents)
    {
        Log.d("In dataChanged");

        DataMap dataMap;
        for (DataEvent event : dataEvents)
        {

            // Check the data type
            if (event.getType() == DataEvent.TYPE_CHANGED)
            {
                // Check the data path
                String path = event.getDataItem().getUri().getPath();
                if (path.equals(HANDHELD_DATA_PATH))
                {
                    dataMap = DataMapItem.fromDataItem(event.getDataItem()).getDataMap();
                    Log.v("Path phone: " + path);
                    Log.v("DataMap received from watch: " + dataMap);

                    Intent messageIntent = new Intent();
                    messageIntent.setAction(Intent.ACTION_SEND);
                    messageIntent.putExtra("time", System.currentTimeMillis());
                    messageIntent.putExtra("DataMap", dataMap.toBundle());
                    LocalBroadcastManager.getInstance(this).sendBroadcast(messageIntent);

                    // Create a DataMap object and send it to the data layer
                    dataMap = new DataMap();
                    dataMap.putString("Pong", "Pong" + String.valueOf(System.currentTimeMillis()));
                    dataMap.putLong("time", System.currentTimeMillis());

                    //Requires a new thread to avoid blocking the UI
                    s = new SendToDataLayerThread(WEARABLE_DATA_PATH, dataMap);
                    s.start();
                }
            }
        }
    }

    private void initGoogleApiClient()
    {
        // Build a new GoogleApiClient for the the Wearable API

        Log.d("Initialaizing GoogleClient");

        if (googleClient == null)
        {
            googleClient = new GoogleApiClient.Builder(this)
                    .addApi(Wearable.API)
                    .addConnectionCallbacks(this)
                    .addOnConnectionFailedListener(this)
                    .build();
        }

        if (!googleClient.isConnected())
        {
            Log.d("Tring to connect to GoogleApi...");

            googleClient.connect();

        }

        Log.d("Google Client ID = " + googleClient.toString());
    }

    // Disconnect from the data layer when the Activity stops
    @Override
    public void onDestroy()
    {
        super.onDestroy();

        Log.d("WearService: onDestroy");

        if (null != googleClient && googleClient.isConnected())
        {
            googleClient.disconnect();
        }

        if (db != null)
        {
            db.close();
        }

    }



  @Override
public void onConnected(Bundle bundle)
{
    Log.d("onConnected entered");
    Log.d("GoogleAPI now status:" + googleClient.isConnected());
}

    @Override
    public void onConnectionSuspended(int i)
    {

    }

    @Override
    public void onConnectionFailed(ConnectionResult result) {
        Log.e("Connection to google api has failed. " + result.getErrorMessage());
    }

    class SendToDataLayerThread extends Thread
    {
        String path;
        DataMap dataMap;

        // Constructor for sending data objects to the data layer
        SendToDataLayerThread(String p, DataMap data)
        {
            path = p;
            dataMap = data;
        }

        public void run()
        {
            NodeApi.GetConnectedNodesResult nodes = Wearable.NodeApi.getConnectedNodes(googleClient).await();
            for (Node node : nodes.getNodes())
            {

                // Construct a DataRequest and send over the data layer
                PutDataMapRequest putDMR = PutDataMapRequest.create(path);
                putDMR.getDataMap().putAll(dataMap);
                PutDataRequest request = putDMR.asPutDataRequest();
                DataApi.DataItemResult result = Wearable.DataApi.putDataItem(googleClient, request).await();
                if (result.getStatus().isSuccess())
                {
                    Log.v("DataMap: " + dataMap + " sent to: " + node.getDisplayName() + "; path=" + path);
                } else
                {
                    // Log an error
                    Log.v("ERROR: failed to send DataMap");
                }
            }
        }
    }
}

WearListenerService on Wear:

import android.content.Intent;
import android.os.Bundle;

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.DataApi;
import com.google.android.gms.wearable.DataEvent;
import com.google.android.gms.wearable.DataEventBuffer;
import com.google.android.gms.wearable.DataMap;
import com.google.android.gms.wearable.DataMapItem;
import com.google.android.gms.wearable.Node;
import com.google.android.gms.wearable.NodeApi;
import com.google.android.gms.wearable.PutDataMapRequest;
import com.google.android.gms.wearable.PutDataRequest;
import com.google.android.gms.wearable.Wearable;
import com.google.android.gms.wearable.WearableListenerService;

public class ListenerService extends WearableListenerService implements GoogleApiClient.ConnectionCallbacks,
        GoogleApiClient.OnConnectionFailedListener
{
    private static final String WEARABLE_DATA_PATH = "/wearable_data";
    private static final String HANDHELD_DATA_PATH = "/handheld_data";

    GoogleApiClient googleClient;

    private SendToDataLayerThread s;

    @Override
    public void onDataChanged(DataEventBuffer dataEvents)
    {
        Log.d("In dataChanged");

        DataMap dataMap;
        for (DataEvent event : dataEvents)
        {
            // Check the data type
            if (event.getType() == DataEvent.TYPE_CHANGED)
            {
                // Check the data path
                String path = event.getDataItem().getUri().getPath();
                Log.d("DataChanged: " + "path = " + path);
                if (path.equals(WEARABLE_DATA_PATH))
                {
                    dataMap = DataMapItem.fromDataItem(event.getDataItem()).getDataMap();
                    Log.d("DataChanged: " + "dataMap received on watch: " + dataMap);
                }

            }
        }
    }

    private void initGoogleApiClient()
    {
        if (googleClient == null)
        {
            Log.d("Building google client id...");
            googleClient = new GoogleApiClient.Builder(this)
                    .addApi(Wearable.API)
                            .addConnectionCallbacks(this)
                            .addOnConnectionFailedListener(this)
                    .build();

            Log.d("Google client id = " + googleClient.toString());
        }

        if (!googleClient.isConnected())
        {
            googleClient.connect();
        }

        Log.d("Google Client ID = " + googleClient.toString());
    }

    // Placeholders for required connection callbacks
    @Override
    public void onConnectionSuspended(int cause)
    {
    }

    @Override
    public void onConnected(Bundle connectionHint)
    {
        Log.v("OnConnected entered");
    }

    @Override
    public void onConnectionFailed(ConnectionResult connectionResult)
    {
    }

    // Connect to the data layer when the Activity starts
    @Override
    public void onCreate()
    {
        super.onCreate();
        initGoogleApiClient();
    }

    // Disconnect from the data layer when the Activity stops
    @Override
    public void onDestroy()
    {
        if (null != googleClient && googleClient.isConnected())
        {
            Log.d("onDestroy: Disconnecting googleClient");
            googleClient.disconnect();
        }

        super.onDestroy();
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startid)
    {
        Log.d("onStartCommand: Service was started.");

        // Create a DataMap object and send it to the data layer
        DataMap dataMap = new DataMap();
        dataMap.putString("ping", "ping" + String.valueOf(System.currentTimeMillis()));
        dataMap.putLong("time", System.currentTimeMillis());
        //Requires a new thread to avoid blocking the UI

        s = new SendToDataLayerThread(HANDHELD_DATA_PATH, dataMap);
        s.start();

        return super.onStartCommand(intent, flags, startid);
    }

    class SendToDataLayerThread extends Thread
    {
        String path;
        DataMap dataMap;

        // Constructor for sending data objects to the data layer
        SendToDataLayerThread(String p, DataMap data)
        {
            path = p;
            dataMap = data;
        }

        public void run()
        {
            NodeApi.GetConnectedNodesResult nodes = Wearable.NodeApi.getConnectedNodes(googleClient).await();
            for (Node node : nodes.getNodes())
            {

                final Node node2 = node;

                // Construct a DataRequest and send over the data layer
                PutDataMapRequest putDMR = PutDataMapRequest.create(path);
                putDMR.getDataMap().putAll(dataMap);
                PutDataRequest request = putDMR.asPutDataRequest();

                PendingResult<DataApi.DataItemResult> pendingResult = Wearable.DataApi.putDataItem(googleClient, request);
                pendingResult.setResultCallback(new ResultCallback<DataApi.DataItemResult>()
                {
                    @Override
                    public void onResult(DataApi.DataItemResult dataItemResult)
                    {
                        if (dataItemResult.getStatus().isSuccess())
                        {
                            Log.v("DataMap: " + dataMap + " sent to: " + node2.getDisplayName());
                        } else
                        {
                            // Log an error
                            Log.v("ERROR: failed to send DataMap");
                        }
                    }
                });


            }
        }
    }
}

Main activities on both Handheld and Wear just start services. Data path is: 1) Wear service send data. onDataChanged on wear triggered as it should 2) Handheld triggers onDataChanged only after unplug or plug USB cable. Handheld send data to wear. 3) onDataChanged triggers on wear as it should and receive data.

Additional info. Manifest Handheld:

<?xml version="1.0" encoding="utf-8"?>
<manifest package="ru.orangesoftware.financisto"
          xmlns:android="http://schemas.android.com/apk/res/android"
          android:installLocation="internalOnly">

    <supports-screens
        android:anyDensity="true"
        android:largeScreens="true"
        android:normalScreens="true"
        android:resizeable="true"
        android:smallScreens="true"/>

    <uses-feature
        android:name="android.hardware.touchscreen"
        android:required="false"/>
    <uses-feature
        android:name="android.hardware.camera"
        android:required="false"/>
    <uses-feature
        android:name="android.hardware.location"
        android:required="false"/>
    <uses-feature
        android:name="android.hardware.location.network"
        android:required="false"/>
    <uses-feature
        android:name="android.hardware.location.gps"
        android:required="false"/>

    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
    <uses-permission android:name="android.permission.CAMERA"/>
    <uses-permission android:name="android.permission.INTERNET"/>
    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
    <uses-permission android:name="android.permission.VIBRATE"/>
    <uses-permission android:name="android.permission.WAKE_LOCK"/>
    <uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT"/>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.GET_ACCOUNTS"/>
    <uses-permission android:name="android.permission.USE_CREDENTIALS"/>
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>

    <application
        android:allowBackup="true"
        android:description="@string/app_description"
        android:icon="@drawable/icon"
        android:label="@string/app_name"
        android:theme="@android:style/Theme.DeviceDefault">


        <uses-library
            android:name="com.google.android.maps"
            android:required="false"/>
        <meta-data
            android:name="com.google.android.gms.version"
            android:value="@integer/google_play_services_version"/>
        <activity
        android:name=".activity.MainActivity"
        android:configChanges="orientation|keyboardHidden"
        android:label="@string/app_name"
        android:taskAffinity=".MainActivity">
        <intent-filter>
            <action android:name="android.intent.action.MAIN"/>
            <category android:name="android.intent.category.LAUNCHER"/>
        </intent-filter>
    </activity>

        <service
            android:name=".service.WearService">
            <intent-filter>
                <action android:name="com.google.android.gms.wearable.BIND_LISTENER"/>
            </intent-filter>
        </service>

    </application>

</manifest> 

Manifest Wear:

<?xml version="1.0" encoding="utf-8"?>
<manifest package="ru.orangesoftware.financisto"
          xmlns:android="http://schemas.android.com/apk/res/android">

    <uses-feature android:name="android.hardware.type.watch"/>

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@android:style/Theme.DeviceDefault">

        <meta-data
            android:name="com.google.android.gms.version"
            android:value="@integer/google_play_services_version"/>

        <activity
            android:name=".MainWearActivity"
            android:label="@string/app_name">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>

                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>
        <service
            android:name="ru.orangesoftware.financisto.ListenerService"
            android:enabled="true">
            <intent-filter>
                <action android:name="com.google.android.gms.wearable.BIND_LISTENER"/>
            </intent-filter>
        </service>


    </application>

</manifest>

Gradle Handheld:

buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:1.3.0'
        classpath 'com.neenbedankt.gradle.plugins:android-apt:1.4'
    }
}

apply plugin: 'com.android.application'
apply plugin: 'com.neenbedankt.android-apt'

repositories {
    maven { url "http://repo.commonsware.com.s3.amazonaws.com" }
    maven { url "https://repository-achartengine.forge.cloudbees.com/snapshot/" }
    mavenCentral()
}

android {
    compileSdkVersion 22
    buildToolsVersion "23.0.2"

    defaultConfig {
        applicationId "ru.orangesoftware.financisto"
        minSdkVersion 19
        targetSdkVersion 22
        versionCode 92
        versionName "1.6.8"
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
        }
    }

    packagingOptions {
        exclude 'META-INF/LICENSE'
        exclude 'META-INF/LICENSE.txt'
        exclude 'META-INF/NOTICE'
        exclude 'META-INF/NOTICE.txt'
    }
}

def googlePlayVersion = '8.3.0'

dependencies {
    compile "com.google.android.gms:play-services-base:$googlePlayVersion"
    compile "com.google.android.gms:play-services-drive:$googlePlayVersion"
    compile "com.google.android.gms:play-services-wearable:$googlePlayVersion"

    compile files('libs/dropbox-android-sdk-1.6.1/dropbox-android-sdk-1.6.1.jar')
    compile files('libs/google-rfc-2445/rfc2445-no-joda.jar')
    compile 'com.google.code.gson:gson:2.3'
    compile 'com.commonsware.cwac:wakeful:1.0.1'
    compile 'org.achartengine:achartengine:1.2.0'
    compile 'net.sf.trove4j:trove4j:3.0.3'
    compile 'com.wdullaer:materialdatetimepicker:2.0.0'
}

Gradle Wear:

apply plugin: 'com.android.application'


android {
    compileSdkVersion 22
    buildToolsVersion "23.0.2"

    defaultConfig {
        applicationId "ru.orangesoftware.financisto"
        minSdkVersion 20
        targetSdkVersion 22
        versionCode 1
        versionName "1.0"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

def googlePlayVersion = '8.3.0'

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.google.android.support:wearable:1.3.0'
    compile "com.google.android.gms:play-services-wearable:$googlePlayVersion"
}

Log on Handheld after app start:

11-15 12:20:20.616 29043-29043/? D/Financisto: WearService Created [WearService.onCreate:44]
11-15 12:20:20.616 29043-29043/? D/Financisto: Initialaizing GoogleClient [WearService.initGoogleApiClient:94]
11-15 12:20:20.636 29043-29043/? D/Financisto: Tring to connect to GoogleApi... [WearService.initGoogleApiClient:107]
11-15 12:20:20.636 29043-29043/? D/Financisto: Google Client ID = com.google.android.gms.internal.zzmg@4344d5c0 [WearService.initGoogleApiClient:113]
11-15 12:20:21.016 29043-29043/ru.orangesoftware.financisto D/Financisto: onConnected entered [WearService.onConnected:139]
11-15 12:20:21.016 29043-29043/ru.orangesoftware.financisto D/Financisto: GoogleAPI now status:true [WearService.onConnected:140]

-------now I send data from watch and unplug usb cable after 30 seconds

11-15 12:24:31.986 29043-29091/? D/Financisto: In dataChanged [WearService.onDataChanged:54]
11-15 12:24:31.986 29043-29091/? V/Financisto: Path phone: /handheld_data [WearService.onDataChanged:68]
11-15 12:24:31.986 29043-29091/? V/Financisto: DataMap received from watch: {time=1447565065308, ping=ping1447565065306} [WearService.onDataChanged:69]
11-15 12:24:32.036 29043-29091/? D/Financisto: In dataChanged [WearService.onDataChanged:54]
11-15 12:24:32.046 29043-1493/? V/Financisto: DataMap: {Pong=Pong1447565071992, time=1447565071992} sent to: Gear 2 76A1; path=/wearable_data [SendToDataLayerThread.run:179]

Log on Wear after app start and data sending:

11-15 12:24:25.301 2460-2460/ru.orangesoftware.financisto D/FinancistoWear: onStartCommand: Service was started.
11-15 12:24:25.377 2460-2460/ru.orangesoftware.financisto V/FinancistoWear: DataMap: {time=1447565065308, ping=ping1447565065306} sent to: Tolive GN3
11-15 12:24:25.379 2460-3309/ru.orangesoftware.financisto D/FinancistoWear: In dataChanged
11-15 12:24:25.379 2460-3309/ru.orangesoftware.financisto D/FinancistoWear: DataChanged: path = /handheld_data

Please, point me, what I'm doing wrong?

Solution

I rewrite SendToDataLayerThread. Now it is a regular class without extending Thread and DataRequest became Urgent:

class SendToDataLayerThread
    {
        String path;
        DataMap dataMap;

        // Constructor for sending data objects to the data layer
        SendToDataLayerThread(String p, DataMap data)
        {
            path = p;
            dataMap = data;
        }

        public void run()
        {
            //NodeApi.GetConnectedNodesResult nodes = Wearable.NodeApi.getConnectedNodes(googleClient);
            PendingResult<NodeApi.GetConnectedNodesResult> nodes = Wearable.NodeApi.getConnectedNodes(googleClient);
            nodes.setResultCallback(new ResultCallback<NodeApi.GetConnectedNodesResult>()
            {
                @Override
                public void onResult(NodeApi.GetConnectedNodesResult getConnectedNodesResult)
                {
                    for (Node node : getConnectedNodesResult.getNodes())
                    {

                        final Node node2 = node;

                        // Construct a DataRequest and send over the data layer
                        PutDataMapRequest putDMR = PutDataMapRequest.create(path);
                        putDMR.getDataMap().putAll(dataMap);
                        PutDataRequest request = putDMR.asPutDataRequest();
                        request.setUrgent();

                        PendingResult<DataApi.DataItemResult> pendingResult = Wearable.DataApi.putDataItem(googleClient, request);
                        pendingResult.setResultCallback(new ResultCallback<DataApi.DataItemResult>()
                        {
                            @Override
                            public void onResult(DataApi.DataItemResult dataItemResult)
                            {
                                if (dataItemResult.getStatus().isSuccess())
                                {
                                    Log.v("DataMap: " + dataMap + " sent to: " + node2.getDisplayName());
                                } else
                                {
                                    // Log an error
                                    Log.v("ERROR: failed to send DataMap");
                                }
                            }
                        });
                    }
                }
            });
        }
    }

Thanks for help!

Intrusion answered 15/11, 2015 at 5:32 Comment(0)
S
13

Per the Google Play services 8.3 blog post:

With Google Play services 8.3, we’ve updated the DataApi to allow for urgency in how data items are synced. Now, a priority can be added to the data item to determine when it should be synced. For example, if you are building an app that requires immediate syncing, such as a remote control app, it can still be done immediately by calling setUrgent(), but for something such as updating your contacts, you could tolerate some delay. Non-urgent DataItems may be delayed for up to 30 minutes, but you can expect that in most cases they will be delivered within a few minutes. Low priority is now the default, so setUrgent() is needed to obtain the previous timing.

So make sure you call setUrgent() on your data requests if you want them to immediately be synced to other connected devices.

Semitic answered 15/11, 2015 at 6:27 Comment(4)
Wow, thanks this solve problem on handheld! (I add this to both handheld and wear). Now it immediately triggers onDataChange. By my wear is silent after sending data to handheld. I'm trying to rewrite sending procedure on handheld like it is on wear, but nothing.Intrusion
Now I have silent onDataChanged on wear for handheld request. From log (it is like in question) I see that request was sent successfully to the right device, but onDataChanged on wear doesn't receive it (doesn't matter with or without setUrgent()). If I send from handheld message via MessageApi, it received by wear in added onMessageReceived. I want to know the secret, why MessageApi work well and DataAPI lost somewhere.Intrusion
I've found that data can reach wear in one of these modifications: after sending data make googleapi disconnect() and connect() on handheld, or replace Thread.start() to Thread.run()Intrusion
I'm not even sure why you are spinning up entire new Threads when there are asynchronous versions you could use instead of calling await() on a Thread.Semitic

© 2022 - 2024 — McMap. All rights reserved.