5

I am making a simple Android Wear app to control my thermostats, and I'm sending POST requests with Volley to control them. Everything works great in the Android Wear simulator (the request works), but, while the app does load on my Moto 360, the volley request gets called but invariably times out.

Why could my volley request be failing on my watch but working on the simulator? Other apps' requests succeed on my watch (for example, the built-in weather app can load up weather data in about 3 seconds). And, the weirdest part: I had the app working (successfully making volley requests) on my watch, and, about a day after I installed it to my watch from Android Studio, it suddenly stopped loading data for no apparent reason.

What I've tried so far:

  • I have requested the Internet permission in my manifest.xml.
  • I have increased the timeout to 30 seconds (see my code below), which didn't change anything.
  • I have tried tethering my computer and the simulator to my phone's connection via Bluetooth (to replicate the Bluetooth connection my physical watch has to my phone), and the simulator made the request successfully still (albeit with a two-second delay), ruling out the possibility of Bluetooth being too slow.
  • I made sure the API level is low enough for my Marshmallow-running watch (my watch and the app are both API level 23).
  • I tried doing a quick test request to Google before the request to the company's servers with my thermostat data, and while the Google request returns the site's HTML code in the simulator, it times out on my watch (thirty seconds after the request is initiated).
  • I tried putting some dummy data into the recycler view data should be loaded into, and the dummy data indeed showed up, ruling out that the recycler view is broken.
  • I deleted the app from my watch and reinstalled it, and deleted the companion from my phone, reinstalled it, and deleted it again, all to no avail.
  • A lengthy chat with Google Support did not produce anything meaningful.

Here's my code (from my main view's adapter):

public void refreshThermostatsRecyclerView(RequestQueue queue) {
    String url = "https://mobile.skyport.io:9090/login"; // login call to the thermostats server Skyport
     Log.w("myApp", "Starting /login call to Skyport"); // this gets called on simulator and watch
     // Request a string response from the provided URL.
     StringRequest stringRequest = new StringRequest(Request.Method.POST, url,
 Response.Listener<String>() {
       @Override
       public void onResponse(String response) {
           // Display the response string.
           Log.w("myApp", "Response is: " + response); // this gets called on the simulator but not the watch
           try {
               // there's some code to parse the data.
           } catch (JSONException e) {
                Log.w("myApp", "catching an error parsing the json."); // never gets called.
                e.printStackTrace();
            }
            }
      }, new Response.ErrorListener() {
            @Override
            public void onErrorResponse(VolleyError error) {
                Log.w("myApp", "Skyport request didn't work! " + error);  // this always gets called on the watch, with the error being a timeout error (com.Android.Volley.timeouterror) but never gets called in the simulator
            }
        }) {
            @Override
            public Map<String, String> getHeaders() throws AuthFailureError {
                Map<String, String> m = new HashMap<>();
                m.put("Referer", "app:/VenstarCloud.swf");
                // here I put some more headers
                return m;
            }

            @Override
            protected Map<String, String> getParams() throws AuthFailureError {
                Map<String, String> m = new HashMap<>();
                m.put("version", "3.0.5");
                m.put("email", userEmail);
                m.put("password", userToken);
                return m;
            }
        };
        // Add the request to the RequestQueue.
        int socketTimeout1 = 30000; // times out 30 seconds after the request starts on the watch
        RetryPolicy policy1 = new DefaultRetryPolicy(socketTimeout1, DefaultRetryPolicy.DEFAULT_MAX_RETRIES, DefaultRetryPolicy.DEFAULT_BACKOFF_MULT);
        stringRequest.setRetryPolicy(policy1);
        queue.add(stringRequest);
    }

Which is called from the onCreate() method in my Main Activity with this code:

RequestQueue queue = Volley.newRequestQueue(this);
refreshThermostatsRecyclerView(queue);

If you'd like to view the logs created by running this in the simulator and on the watch, they're on Google Drive here.


Edit 1: A reboot of my watch fixes the issue temporarily and allows the watch to make HTTP Requests again, but it breaks again once the watch disconnects from Bluetooth, connects to WiFi, disconnects from WiFi, and reconnects to Bluetooth (so it breaks every time I go across my apartment without my phone and then return).

Edit 2: I switched the volley requests all over to HTTPURLConnection Requests in an Async thread, and the same issues occur as with volley.


tl;dr: My app's Volley requests are working in the simulator but not on my Android Wear watch anymore (though Play Store-downloaded apps' similar requests work), how can I get a volley request to work again on my app on the watch?

owlswipe
  • 19,159
  • 9
  • 37
  • 82

4 Answers4

2

I am also using volley on an Android wear app I built and I am running it on a Moto 360, I have run into the same problem a couple o times. Try restarting the device. Go to Settings > Restart. It sounds silly but it has worked for me.

Melissa Rojas
  • 78
  • 1
  • 10
  • thank you for suggesting that :D. My watch had run out of charge and then booted up again once I plugged it in, so I thought a restart would be redundant. Somehow, though, those are different, and restarting just magically fixed it. – owlswipe Jan 21 '17 at 00:07
  • I am glad it worked. Next time it happens just restart device. – Melissa Rojas Jan 21 '17 at 00:49
  • So the issue came back. Restarting my watch fixes volley for a couple of hours, then it breaks again. Is there a permanent fix so my app will work without having to restart my watch every time? – owlswipe Jan 21 '17 at 16:22
  • I have not found another way to deal with it. I think this is a problem with the internet connection of the Moto 360, because it doesn´t only affect my app but every app installed that uses Internet. I have seen the same problem while using the Translate and maps app. Try it and you will see that it doesn't work at the same time your app doesn't work. – Melissa Rojas Jan 21 '17 at 19:52
  • Hmm! This isn't the case for me: my app's volley requests aren't working but Maps is loading up just fine and I just loaded a weather forecast too. – owlswipe Jan 21 '17 at 20:58
  • That is weird. Then, It must be a Volley problem. I am going to try something else to see if the problem persists on my app. – Melissa Rojas Jan 21 '17 at 22:03
  • switched my volley request to a HTTPURLConnection request and it breaks just the same still :/. – owlswipe Jan 28 '17 at 22:50
  • now have a bounty on this question, fyi. looking for something that fixes this permanently! – owlswipe Jan 29 '17 at 22:22
  • Updated my Moto 360 to Android Wear 2.0, ran my app on it, and two days in everything's still working flawlessly! Thanks for all the help—and all anyone else needs to do to fix the problem is update their watches to AW2.0. – owlswipe May 27 '17 at 13:44
2

As per these two conversations below, it seems that WiFi connectivity only allows Android Wear to connect to a phone over WiFi and not directly to the Internet. However, Android Wear 2.0 lets you use regular network APIs.

Direct internet connection on Android Wear?

Does Android Wear support direct access to the Internet?

So, for Android Wear 2.0+ Volley requests from wearable app should work.

If you want to use Android Wear <2.0, then:

On Wearable, in onCreate() add a key that indicates whether the phone should start collecting data.

PutDataMapRequest putDataMapReq = PutDataMapRequest.create("/shouldStart");
putDataMapReq.getDataMap().putBoolean(SHOULD_START_KEY, true);
PutDataRequest putDataReq = putDataMapReq.asPutDataRequest();
PendingResult pendingResult = Wearable.DataApi.putDataItem(mGoogleApiClient, putDataReq);

On phone, in onDataChanged, check if wearable wants to start collecting data. If yes, start Volley request.

for (DataEvent event : dataEvents) {
        if (event.getType() == DataEvent.TYPE_CHANGED) {
            // DataItem changed
            DataItem item = event.getDataItem();
            if (item.getUri().getPath().compareTo("/shouldStart") == 0) {
                DataMap dataMap = DataMapItem.fromDataItem(item).getDataMap();
                boolean shouldStart = dataMap.getBoolean(SHOULD_START_KEY));
                if(shouldStart) {
                    Volley.newRequestQueue(this).add(request);
                }

            }
        } else if (event.getType() == DataEvent.TYPE_DELETED) {
            // DataItem deleted
        }
    }

Then, your Volley request's onResponse should pass data back to Wearable.

public void onResponse(String response) {

    PutDataMapRequest putDataMapReq = PutDataMapRequest.create("/data");
    putDataMapReq.getDataMap().putString(DATA_KEY, true);
    PutDataRequest putDataReq = putDataMapReq.asPutDataRequest();
    PendingResult pendingResult = Wearable.DataApi.putDataItem(mGoogleApiClient, putDataReq);

 }

Finally, you can access data in your Wearable using onDataChanged and store it in your model for passing it onto adapter:

for (DataEvent event : dataEvents) {
    if (event.getType() == DataEvent.TYPE_CHANGED) {
         // DataItem changed
         DataItem item = event.getDataItem();
         if (item.getUri().getPath().compareTo("/data") == 0) {
             DataMap dataMap = DataMapItem.fromDataItem(item).getDataMap();
             parseAndpassToAdapter(dataMap.getString(DATA_KEY));
         }
    } else if (event.getType() == DataEvent.TYPE_DELETED) {
            // DataItem deleted
    }
}

You'll need Wearable.API to implement this and your class should implement DataApi.DataListener. For more information getting started, refer to Accessing the Wearable Data Layer and Syncing Data Items

Hope this helps.

Community
  • 1
  • 1
BhalchandraSW
  • 714
  • 5
  • 13
  • So you're saying that AW 2.0 will magically fix my issue? Do I need to add anything special to my code? – owlswipe Feb 05 '17 at 16:35
  • Please can you test following: Switch off your phone's WiFi, Bluetooth. Then, try to connect your watch to WiFi (if you need to put password in phone that's okay) . Download something totally new. For example, ask your friend to send you message or something like that. When you are receiving message your phone should be connected to wifi and phone's wifi bluetooth should be off. Please could you post result of this test here. – BhalchandraSW Feb 05 '17 at 17:47
  • Sorry! Watch's WiFi should be on. And, phone's WiFi, Bluetooth should be off. – BhalchandraSW Feb 05 '17 at 18:06
  • Indeed my watch works as it should when connected to WiFi rather than Bluetooth. **Just like when it's connected to Bluetooth**, my watch receives notifications, loads data in apps, and accesses Google just fine on WiFi. – owlswipe Feb 05 '17 at 21:54
  • And while your watch is using Internet, is your phone not connected to Internet? – BhalchandraSW Feb 05 '17 at 21:56
  • My phone must be connected to WiFi or cellular data for the whole Cloud Sync thing to work, so my phone is connected to the internet. – owlswipe Feb 05 '17 at 21:59
  • So if you're happy with using phone over Bluetooth or WiFi then you should use Data Layer API and not Volley. Have a look at - https://developer.android.com/training/wearables/data-layer/index.html. Hope this helps. – BhalchandraSW Feb 05 '17 at 22:03
  • Oh, that looks good! Can you post an answer with code for a sample request using that? – owlswipe Feb 05 '17 at 22:09
  • 1
    https://www.sitepoint.com/connecting-to-web-services-with-android-wear/ please try this guide. Hope this helps. – BhalchandraSW Feb 05 '17 at 23:17
  • I have the same problem but in my case, I am building a standalone app so I can´t use Data Layer API. I want to use Volley directly from the watch. I hope Android Wear 2.0 fixes this problem. – Melissa Rojas Feb 09 '17 at 15:14
  • @MelissaRojas Hope it does too! Given that it should hit my watch in a couple weeks, I think I'll wait for it. – owlswipe Feb 10 '17 at 02:09
  • @BhalchandraSW If you add info about using the data layer api to your answer (or wait for Android wear 2) I will accept it. – owlswipe Feb 10 '17 at 02:10
  • Thanks for your help!! – owlswipe Feb 13 '17 at 22:21
  • Updated my Moto 360 to Android Wear 2.0, ran my app on it, and two days in everything's working flawlessly! Thanks for all the help—and all anyone else needs to do to fix the problem is update their watches to AW2.0. – owlswipe May 27 '17 at 13:41
1

You could try an alternative to volley if you can rule out the connection as the problem:

compile 'com.android.support:appcompat-v7:23.1.1'
compile 'com.android.support:support-v4:23.1.0'
compile 'com.android.support:design:23.1.0'
compile 'com.google.code.gson:gson:2.2.4'
compile 'com.google.api-client:google-api-client:1.20.0'

The versions are important.

Then to your request:

Map<String, String> contentParams = new HashMap<>();
InputStream is = null;
NetHttpTransport transport = null;
HttpRequest request = null;
HttpResponse resp = null;
HttpHeaders headers = new HttpHeaders();
JSONObject json = null;

    try {
        transport = new NetHttpTransport();
        HttpRequestFactory factory = transport.createRequestFactory();
        request = factory.buildPostRequest(new GenericUrl(url), null);
        contentParams = getContentParameters();
        headers.putAll(getHeaderParameters());
        request.setHeaders(headers);
        request.getUrl().putAll(contentParams);
        resp = request.execute();
        is = resp.getContent();
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        try {
            if (is != null) {
                string = getJSONFromInputStream(is);
                json = new JSONObject(string);
            }
         } catch (Exception e) {
            e.printStackTrace();
        }
    }
    transport.shutdown();

protected Map<String, String> getContentParameters() {
     Map<String, String> m = new HashMap<>();
     m.put("version", "3.0.5");
     m.put("email", userEmail);
     m.put("password", userToken);
     return m;
}

protected Map<String, String> getHeaderParameters() {
     Map<String, String> m = new HashMap<>();
     m.put("Referer", "app:/VenstarCloud.swf");
     return m;
}

protected String getJSONFromInputStream(InputStream is) {
    if (is == null)
        throw new NullPointerException();
    //instantiates a reader with max size
    BufferedReader reader = new BufferedReader(new InputStreamReader(is), 8 * 1024);

    StringBuilder sb = new StringBuilder();

    try {
        //reads the response line by line (and separates by a line-break)
        String line;
        while ((line = reader.readLine()) != null) {
            sb.append(line + "\n");
        }
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        try {
            //closes the inputStream
            is.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    return sb.toString();
}

Then just execute your code from a thread/asynctask/have it delay your frontend slightly

Edit: Just in case there is a problem with appending a map:

for (Entry<String, String> entry : getHeaderParameters()) {
    headers.put(entry.getKey(), entry.getValue());
}

for (Entry<String, String> entry : getContentParameters()) {
    request.getUrl().put(entry.getKey(), entry.getValue());
}

Also as another note, make sure to change the return type from void on both those methods to Map

KoalaKoalified
  • 687
  • 4
  • 15
  • I tried HTTPURLConnection as an alternative to volley, do you think that your code here will work better? Nonetheless, I will try and get back to you. – owlswipe Feb 04 '17 at 03:00
  • I was thinking it just may have something to do with volley's versioning not being compatible with Android Wear's versioning. Hopefully it works! – KoalaKoalified Feb 04 '17 at 03:19
  • It is worth trying – Tom Feb 04 '17 at 11:07
  • @KoalaKoalified Trying to set headers from a Map, getting `java.lang.IllegalArgumentException: field com.google.api.client.http.HttpHeaders.contentType has type java.util.List, got java.lang.String` How can I fix that and set headers correctly? – owlswipe Feb 04 '17 at 16:32
  • Updated the response – KoalaKoalified Feb 04 '17 at 20:55
  • I get roughly four errors on your "Just in case" part. Do you mind testing this yourself to see the errors? For instance, entry doesn't have a method `.getKey()`. – owlswipe Feb 04 '17 at 23:02
  • Oh I see what the issue is: 4 of the HTTP headers I need cause that error, 4 don't. The one I put in my question (I left 7 out) doesn't cause that issue! Why would headers like `User Agent` -> `"Mozilla/5.0 (iOS; U; en-US) AppleWebKit/533.19.4 (KHTML, like Gecko) AdobeAIR/16.0"` cause that issue expecting List got String error? Skyport needs all 8 headers or else it's an invalid request, so just leaving the 4 ones in that work causes a 500 Internal Server Error. – owlswipe Feb 05 '17 at 02:21
  • If you modify your code to fix the header issue I mentioned above, I'll give you at least some rep (and a lot of thanks). – owlswipe Feb 05 '17 at 16:34
  • My best guess would be if you don't run the api and add a bunch of extra headers, it doesn't pick them up when the api that you are calling from requires a List of something instead of what you are passing in... Realistically that is an API issue as if one header works and the rest don't, it must be that something has changed. – KoalaKoalified Feb 05 '17 at 20:02
  • Also trying to navigate mobile.skyport.io:9090 just gives me the message OK wherever I go without credentials. – KoalaKoalified Feb 05 '17 at 20:05
  • @KoalaKoalified Those headers work just fine in equivalent requests from Volley and HTTPURLConnection so I don't think it's a problem with the API, though indeed it's weird that it gives the OK message. So, is there anything I can try to get those headers to work in NetHTTPTransport or should I give up on this route and hope Android Wear 2 fixes the issues? – owlswipe Feb 05 '17 at 21:53
  • 1
    I would hope Wear 2 fixes this issue. It sounds like it's a problem with the api's regex of headers to me though so idk. – KoalaKoalified Feb 05 '17 at 22:04
  • OK, I hope so too! Thanks for your help – owlswipe Feb 06 '17 at 12:28
  • @KoalaKoalified Updated my Moto 360 to Android Wear 2.0, ran my app on it, and two days in everything's still working flawlessly! Thanks for all the help—and all anyone else needs to do to fix the problem is update their watches to AW2.0. – owlswipe May 27 '17 at 13:44
0

Is this not just the case of when the watch is connected to the phone via bluetooth the internet will not work, as wifi is turned off. If the watch is using wifi to connect to the phone then it will work.

I'm working on wear 2.0 app and just turn blueooth off on my phone for my watch to get internet connection.

Craig J
  • 177
  • 1
  • 6
  • Nope, it works over Bluetooth *initially*, just not after switching to WiFi and back. The watch usually connects to the internet via bluetooth, that's how apps get their network connections. For instance—the Maps app loads maps while my watch is connected to Bluetooth, is there a way to load internet data with my app similarly? – owlswipe Feb 02 '17 at 18:28
  • The maps app will be making network connections from the phone though sending requests to the phone to get map data. It wont just be using network calls. If you look at the wear companion library then this is the method of getting network data. Check if WiFi available, if not pass it to the phone to get data. Or something like that off the top of my head. – Craig J Feb 03 '17 at 11:45
  • Interesting. What if I want to make a `standalone=true` app for Android Wear 2.0 and I needed to make a network request? Is there a way to do that? – owlswipe Feb 03 '17 at 12:29
  • Well, if you give me an answer that tells how to start a network request from the phone when the watch app is opened and sends the response to the watch, you can win the bounty. – owlswipe Feb 05 '17 at 03:09
  • You'd have to transfer internet data over the datalayer. Though if you're wanting just to support android wear 2.0 you can forget everything Ive just said. From 2.0 you can request to use wifi see the google docs: Network Access and Syncing https://developer.android.com/training/wearables/data-layer/network-access.html they have just been updated today – Craig J Feb 09 '17 at 22:20
  • Thanks! If you add that to your answer I will consider accepting it! – owlswipe Feb 10 '17 at 02:10
  • Updated my Moto 360 to Android Wear 2.0, ran my app on it, and two days in everything's working flawlessly! Thanks for all the help—and all anyone else needs to do to fix the problem is update their watches to AW2.0. – owlswipe May 27 '17 at 13:44
  • @owlswipe Thanks for the Update, I am going to update my Moto360 to Android Wear 2.0 now!!! – Melissa Rojas May 28 '17 at 15:40
  • @MelissaRojas You are welcome! Let me know if you have any problems/questions! – owlswipe May 28 '17 at 20:38
  • @MelissaRojas To force the update, connect your watch to WiFi (by turning off your Android phone's Bluetooth), drop it in the charging dock, and go to the system updates page (inside Settings > About). If it gives you the checkmark and says it's up to date, tap over and over again to force it to download the update. That, mysteriously enough, is the way to force the AW2 update if it doesn't come to you. And good luck! – owlswipe May 28 '17 at 20:43