Welcome to the Treehouse Community

Want to collaborate on code errors? Have bugs you need feedback on? Looking for an extra set of eyes on your latest project? Get support with fellow developers, designers, and programmers of all backgrounds and skill levels here with the Treehouse Community! While you're at it, check out some resources Treehouse students have shared here.

Looking to learn something new?

Treehouse offers a seven day free trial for new students. Get access to thousands of hours of content and join thousands of Treehouse students and alumni in the community today.

Start your free trial

Android Build a Weather App (2015) Hooking Up the Model to the View Wrapping Up

Binyamin Friedman
Binyamin Friedman
14,615 Points

I wanted to make the weather app detect your location...

I did some research and tried to add code to make the weather app use your location.

package com.teamtreehouse.stormy;

import android.*;
import android.Manifest;
import android.content.Context;
import android.content.pm.PackageManager;
import android.graphics.drawable.Drawable;
import android.location.Location;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.support.annotation.NonNull;
import android.support.v4.content.ContextCompat;
import android.support.v4.content.res.ResourcesCompat;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;

import com.google.android.gms.instantapps.ActivityCompat;
import com.google.android.gms.location.FusedLocationProviderClient;
import com.google.android.gms.location.LocationServices;
import com.google.android.gms.tasks.OnCompleteListener;
import com.google.android.gms.tasks.OnSuccessListener;
import com.google.android.gms.tasks.Task;

import org.json.JSONException;
import org.json.JSONObject;

import java.io.IOException;

import butterknife.ButterKnife;
import butterknife.InjectView;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;

public class MainActivity extends AppCompatActivity {

    private static final String TAG = MainActivity.class.getSimpleName();
    private static final int PERMISSION_REQUEST_CODE = 0;
    private CurrentWeather mCurrentWeather;
    private FusedLocationProviderClient mFusedLocationProviderClient;

    @InjectView(R.id.timeLabel) TextView mTimeLabel;
    @InjectView(R.id.temperatureLabel) TextView mTemperatureLabel;
    @InjectView(R.id.humidityValue) TextView mHumidityValue;
    @InjectView(R.id.precipValue) TextView mPrecipValue;
    @InjectView(R.id.summaryLabel) TextView mSummaryLabel;
    @InjectView(R.id.iconImageView) ImageView mIconImageView;
    @InjectView(R.id.refreshImageView) ImageView mRefreshImageView;
    @InjectView(R.id.progressBar) ProgressBar mProgressBar;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ButterKnife.inject(this);

        mFusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(this);
        mProgressBar.setVisibility(View.INVISIBLE);

        mRefreshImageView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                getForecast();
            }
        });

        getForecast();

        Log.d(TAG, "Main UI code is running!");
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
        if (grantResults[0] == PackageManager.PERMISSION_DENIED || permissions.length == 0) {
            Toast.makeText(this, "Stormy requires your location to function. Stormy won't ask for location permissions again, but you can always enable the permission in settings", Toast.LENGTH_LONG).show();
            Toast.makeText(this, "Stormy requires your location to function. Stormy won't ask for location permissions again, but you can always enable the permission in settings", Toast.LENGTH_LONG).show();
            Toast.makeText(this, "Stormy requires your location to function. Stormy won't ask for location permissions again, but you can always enable the permission in settings", Toast.LENGTH_LONG).show();
        }
    }

    private void getForecast() {
        toggleRefresh();

        Location location = null;

        int permissionCheck = ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION);
        if (permissionCheck == PackageManager.PERMISSION_GRANTED) {
            Task task = mFusedLocationProviderClient.getLastLocation();
            location = (Location) task.getResult();
        } else {
            requestPermissions(new String[]{Manifest.permission.ACCESS_COARSE_LOCATION}, PERMISSION_REQUEST_CODE);
        }

        if (location != null) {
            String apiKey = "bb8d7f69ca1b7c515db4409ff9c0a97c";
            double latitude = location.getLatitude();
            double longitude = location.getLongitude();
            String forecastUrl = "https://api.darksky.net/forecast/" +
                    apiKey + "/" + latitude + "," + longitude;

            if (isNetWorkAvailable()) {
                OkHttpClient client = new OkHttpClient();
                Request request = new Request.Builder()
                        .url(forecastUrl)
                        .build();

                Call call = client.newCall(request);
                call.enqueue(new Callback() {
                    @Override
                    public void onFailure(Call call, IOException e) {
                        runOnUiThread(new Runnable() {
                            @Override
                            public void run() {
                                toggleRefresh();
                            }
                        });
                        alertUserAboutError();
                    }

                    @Override
                    public void onResponse(Call call, Response response) throws IOException {
                        runOnUiThread(new Runnable() {
                            @Override
                            public void run() {
                                toggleRefresh();
                            }
                        });

                        try {
                            String jsonData = response.body().string();
                            Log.v(TAG, jsonData);
                            if (response.isSuccessful()) {
                                mCurrentWeather = getCurrentDetails(jsonData);
                                runOnUiThread(new Runnable() {
                                    @Override
                                    public void run() {
                                        updateDisplay();
                                    }
                                });
                            } else {
                                alertUserAboutError();
                            }
                        } catch (IOException | JSONException e) {
                            Log.e(TAG, "Exception caught: ", e);
                        }
                    }
                });
            } else {
                Toast.makeText(this, R.string.network_unavailable_message, Toast.LENGTH_LONG).show();
            }
        }
    }

    private void toggleRefresh() {
        if (mProgressBar.getVisibility() == View.INVISIBLE) {
            mProgressBar.setVisibility(View.VISIBLE);
            mRefreshImageView.setVisibility(View.INVISIBLE);
        } else {
            mProgressBar.setVisibility(View.INVISIBLE);
            mRefreshImageView.setVisibility(View.VISIBLE);
        }
    }

    //TODO one day add something that displays your location
    private void updateDisplay() {
        mTemperatureLabel.setText(mCurrentWeather.getTemperature() + "");
        mTimeLabel.setText("At " + mCurrentWeather.getFormattedTime() + " it will be");
        mHumidityValue.setText(mCurrentWeather.getHumidity() + "");
        mPrecipValue.setText(mCurrentWeather.getPrecipChance() + "%");
        mSummaryLabel.setText(mCurrentWeather.getSummary());
        Drawable drawable = ResourcesCompat.getDrawable(getResources(), mCurrentWeather.getIconId(), null);
        mIconImageView.setImageDrawable(drawable);
    }

    private CurrentWeather getCurrentDetails(String jsonData) throws JSONException {
        CurrentWeather currentWeather = new CurrentWeather();
        JSONObject forecast = new JSONObject(jsonData);
        currentWeather.setTimeZone(forecast.getString("timezone"));
        JSONObject currently = forecast.getJSONObject("currently");

        currentWeather.setHumidity(currently.getDouble("humidity"));
        currentWeather.setTime(currently.getLong("time"));
        currentWeather.setIcon(currently.getString("icon"));
        currentWeather.setPrecipChance(currently.getDouble("precipProbability"));
        currentWeather.setSummary(currently.getString("summary"));
        currentWeather.setTemperature(currently.getDouble("temperature"));

        return currentWeather;
    }

    private boolean isNetWorkAvailable() {
        ConnectivityManager manager = (ConnectivityManager)
                getSystemService(Context.CONNECTIVITY_SERVICE);
        NetworkInfo networkInfo = manager.getActiveNetworkInfo();
        boolean isAvailable = false;
        if (networkInfo != null && networkInfo.isConnected()) {
            isAvailable = true;
        }
        return isAvailable;
    }

    private void alertUserAboutError() {
        AlertDialogFragment dialog = new AlertDialogFragment();
        dialog.show(getFragmentManager(), "error_dialog");
    }
}

I am getting an error

java.lang.RuntimeException: Unable to start activity ComponentInfo{com.teamtreehouse.stormy/com.teamtreehouse.stormy.MainActivity}: java.lang.IllegalStateException: Task is not yet complete
                                                                              at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2817)
                                                                              at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2892)
                                                                              at android.app.ActivityThread.-wrap11(Unknown Source:0)
                                                                              at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1593)
                                                                              at android.os.Handler.dispatchMessage(Handler.java:105)
                                                                              at android.os.Looper.loop(Looper.java:164)
                                                                              at android.app.ActivityThread.main(ActivityThread.java:6541)
                                                                              at java.lang.reflect.Method.invoke(Native Method)
                                                                              at com.android.internal.os.Zygote$MethodAndArgsCaller.run(Zygote.java:240)
                                                                              at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:767)
                                                                           Caused by: java.lang.IllegalStateException: Task is not yet complete
                                                                              at com.google.android.gms.common.internal.zzbo.zza(Unknown Source:8)
                                                                              at com.google.android.gms.tasks.zzn.zzDG(Unknown Source:5)
                                                                              at com.google.android.gms.tasks.zzn.getResult(Unknown Source:3)
                                                                              at com.teamtreehouse.stormy.MainActivity.getForecast(MainActivity.java:97)
                                                                              at com.teamtreehouse.stormy.MainActivity.onCreate(MainActivity.java:75)

The last two lines are where it happens in my code. What am I doing wrong? I know it has to do with the task object.

1 Answer

Boban Talevski
Boban Talevski
24,793 Points

As the error points out, the problem lies in this part of your code

Task task = mFusedLocationProviderClient.getLastLocation();
location = (Location) task.getResult();

The thing is, getLastLocation() is starting its work in a new thread and since you immediately ask for the result in the next line, the result isn't there and the app crashes with the said exception.

You should set up a listener on the task object which will "listen" for when the method getLastLocation() is complete and call the onSucess(location location) method. The location parameter is the actual location result of the task. In other words, you call the method getLastLocation() and "move on", the method will not hand you the result instantly. So you write the code to be executed in the onSucess(location location) method. This is the direction you should be heading.

fusedLocationClient.getLastLocation()
                .addOnSuccessListener(this, new OnSuccessListener<Location>() {
                    @Override
                    public void onSuccess(Location location) {
                        if (location != null) {
                            Log.d(TAG, "Location returned.");
                            // write code here to to make the API call to the weather service and update the UI
                            // this code here is running on the Main UI thread as far as I understand
                        } else {
                            Log.d(TAG, "Null location returned.");
                        }
                    }
                });

I'm struggling with this challenge myself and have made some progress, but still I'm not fully satisfied with the results. Note that there is a difference in using the emulator (Android one) and a real device.

For example, in the emulator, I don't get any weather data displayed when the app is started and I have to tap the refresh button in order to get the data displayed. It is however displayed for whatever coordinates are fed in the emulator, so the location does work. It doesn't reflect any changes in the location if I feed it new coordinates, but I haven't yet done anything about getting location updates in the app. Even with the refresh button tapped, it doesn't get weather data for the new coordinates, just updates the weather for whatever the initial coordinates were. I would've expected that at least if I feed it new coordinates and tap refresh, it would change the location, but I'll troubleshoot that issue as well.

On my real device, when the app is started, the information is loaded without the need for tapping the refresh button and the information is for my actual current location. I haven't really checked if it will update the location on refresh as I would need to physically move around.

Working my way also to actually display the location information (city country etc.) instead of the hardcoded String we have there.

Binyamin Friedman
Binyamin Friedman
14,615 Points

I knew about the onSuccessListener, but since it is an anonymous inner class I wasn't sure how to set an outside variable to the task.getResult() since the anonymous inner classes can't change outside variables.

I've taken the Java Web Development Track and there was a course on debugging applications that used a weather app/website. It used the Google Geocoding Api to take coordinates and convert them to a readable address.

Boban Talevski
Boban Talevski
24,793 Points

About the issue for accessing outside variables inside an anonymous inner class (AIC), I solved it in the following way:

Create a StormyLocation class in the project which is just a POJO with longitude and latitude properties accessed through getters and setters. Then have an object of this type as a final member variable in MainActivity.java, which lets you access it in AIC, and change its properties through getters and setters. The properties of the object themselves are not final, so this is working fine. Not sure if it's the appropriate solution, but it works.

StormyLocation.java

public class StormyLocation {
    private double longitude;
    private double latitude;

    public double getLongitude() {
        return longitude;
    }

    public void setLongitude(double longitude) {
        this.longitude = longitude;
    }

    public double getLatitude() {
        return latitude;
    }

    public void setLatitude(double latitude) {
        this.latitude = latitude;
    }
}

MainActivity.java

    //..
    private final StormyLocation userLocation = new StormyLocation();

    //..

        fusedLocationClient.getLastLocation()
                .addOnSuccessListener(this, new OnSuccessListener<Location>() {
                    @Override
                    public void onSuccess(Location location) {
                        if (location != null) {
                            userLocation.setLatitude(location.getLatitude());
                            userLocation.setLongitude(location.getLongitude());
                            Log.d(TAG, "Location returned. About to call getForecast()");
                            getForecast(userLocation);
                        } else {
                            Log.d(TAG, "Null location returned.");
                        }
                    }
                });

So, inside onSucess, grab the values of the Location object and set them in your own StormyLocation object, and pass that StormyLocation object around to anything that needs latitude and longitude.

And i managed to get the actual location displayed instead of the hardcoded one, and yeah it was using the Geocoding API, thanks for suggesting that.

Still, all this works on a real device and not the AS emulator, but I see that the emulator can have issues accessing the location coordinates being fed to it. There are suggestions on how to make it work, but I didn't try too much as real device beats emulator I guess :). I tested it on three real devices of different brands and with different android versions and it worked on all of them.

Binyamin Friedman
Binyamin Friedman
14,615 Points

I used your idea for making a StormyLocation object, but now I'm getting a NullPointerException on the Location returned from task.getResult();

Task task = mFusedLocationProviderClient.getLastLocation();
            task.addOnCompleteListener(new OnCompleteListener() {
                @Override
                public void onComplete(@NonNull Task task) {
                    Location location1 = (Location) task.getResult();
                    location.setLatitude(location1.getLatitude());
                    location.setLongitude(location1.getLongitude());
                }
            });

The StormyLocation is just a simple longitude latitude object named location.

Why would the location object be null?

Boban Talevski
Boban Talevski
24,793 Points

Are you testing on an emulator or a real device? In my testing, I couldn't get a location from an emulator (it was always null). Even installed genymotion, but still nothing. So if you haven't tried, try on a real device to see if it works.

Though, as I see now, on the genymotion emulator my maps app from Ben's tutorial is getting the location updates (it doesn't on AS emulator), so it's definitely possible to get location info from an emulator, just it can be a little tricky and depends on various emulator settings I guess. I just gave up trying while working on the Stormy application as long as it worked on real devices.

Anyway, you could also try addonSuccessListener instead of addOnCompleteListener though I don't see much how it would change things as addOnCompleteListener seems to be called regardless of whether the task succeeds or fails. But maybe your task is failing for some reason and it'll give additional hints for troubleshooting. Or you could check in your oncomplete listener if the task.isSucessful() to eliminate that option.

Sorry I can't be of more help as I'm also new to all this, I feel like I scratched the surface on this subject (maps/locations) and I'm hoping to get back on it potentially in a future course on the Android track (if it is covered more in-depth at some point) or seek some other tutorial elsewhere if it isn't.