Writing a Weather App for Android: Location

Over the past week or so, I’ve been writing a Weather App for Android using the cloud-based OpenWeatherMap API. The posts so far that cover this:

These have all been done with a static city name. I haven’t allowed editing of the city name, nor have I used the current location. Today I want to modify things so that the app becomes location-aware. Before I get started, I want to adjust the GetWeatherAsyncTask() method that I have been using all along so that it can handle any type of weather data. To do that, I’m going to create a WeatherRequest object:

package com.shellmonger.weatherapp.models;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

import android.location.Location;
import com.shellmonger.weatherapp.WeatherException;
import com.shellmonger.weatherapp.WeatherManager;

public class WeatherRequest {
    public WeatherRequestType requestType;
    public WeatherManager manager;
    public long cityId;
    public String cityData;
    public Location location;

    enum WeatherRequestType {
        BY_CITYNAME,
        BY_CITYID,
        BY_LOCATION,
        BY_ZIPCODE
    }
    
    public WeatherRequest(String request) {
        manager = new WeatherManager();
        cityData = request;
        requestType = isZipCode(request) ? WeatherRequestType.BY_ZIPCODE : WeatherRequestType.BY_CITYNAME;
    }

    public WeatherRequest(long request) {
        manager = new WeatherManager();
        cityId = request;
        requestType = WeatherRequestType.BY_CITYID;
    }

    public WeatherRequest(Location request) {
        manager = new WeatherManager();
        location = request;
        requestType = WeatherRequestType.BY_LOCATION;
    }

    public WeatherResponse getWeather() throws WeatherException {
        switch (requestType) {
            case BY_CITYID:
                return manager.getWeatherByCityId(cityId);
            case BY_CITYNAME:
                return manager.getWeatherByCityName(cityData);
            case BY_LOCATION:
                return manager.getWeatherByGps(location);
            case BY_ZIPCODE:
                return manager.getWeatherByZipCode(cityData);
        }
        return null;
    }

    private boolean isZipCode(String test) {
        Pattern p = Pattern.compile("^[0-9]{5}$");
        Matcher m = p.matcher(test);
        return m.matches();
    }
}

I can use this as follows:

WeatherResponse response = new WeatherRequest("Seattle,US").getWeather();

Similarly, I could replace the city name with a ZIP, a city ID or an Android Location and it will still work. The GetWeatherAsyncTask() now becomes the following:

    class GetWeatherAsyncTask extends AsyncTask<WeatherRequest,Void,WeatherResponse> {
        @Override
        protected void onPreExecute() {
            c_updated.setText("Updating...");
            setUpdateInProgress(true);
        }

        @Override
        protected WeatherResponse doInBackground(WeatherRequest... cities) {
            try {
                return cities[0].getWeather();
            } catch (WeatherException ex) {
                return null;
            }
        }
        
        // Rest of the GetWeatherAsyncTask class

I have to call task.execute() with an array of WeatherRequest objects now, which affects the onRefreshClick() method:

    public void onRefreshClick(View view) {
        if (!updateInProgress) {
            GetWeatherAsyncTask task = new GetWeatherAsyncTask();
            task.execute(new WeatherRequest[] { currentRequest });
        }
    }

Finally, I need to initialize the currentRequest. This is a class-level property of type WeatherRequest that is initialized in the onCreate() method:

        // Set up the current request
        currentRequest = new WeatherRequest("Seattle,US");

If I do nothing else, then I can change one line of code (the current request initializer) to get a new city or location.

Adding Location Awareness

So, what about location? There is an esoteric process for adding location awareness. Most people will abstract location awareness into a library, but I’m going to include it directly into the MainActivity. Let’s take a look at the process.

  1. Determine if you have permission to use the GPS.
    • If you do, then set up the location (step 2).
    • If not, then ask for permission (and set up location in the callback).
  2. Set up the location manager.
  3. When the location manager is ready, get the location.
  4. Once you have the location, create a new WeatherRequest object and refresh.

Let’s take a look at the permissions first. Here is the code, which is placed in the MainActivity.onCreate() method:

        // Determine if the app has permission - if so, then set up the location manager, otherwise ask for permission
        if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
            ActivityCompat.requestPermissions(this, new String[] { Manifest.permission.ACCESS_FINE_LOCATION }, PERMS_REQUEST_GPS_ACCESS);
        } else {
            setUpLocationManager();
        }

If the user has not allowed us to use the GPS, then ask for permission. If the user grants that permission, the onRequestPermissionsResult() method will be called:

    @Override
    public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) {
        if (requestCode == PERMS_REQUEST_GPS_ACCESS && grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
            Log.w("Location", "Permission was granted");
            setUpLocationManager();
        }
    }

So, eventually the app will call setUpLocationManager() if it is allowed to the use the GPS. There are just two paths – one for an already granted permission and one for when the user grants the permission during the first execution. In order to receive location updates, the activity needs to implement the LocationListener interface:

public class MainActivity extends AppCompatActivity implements LocationListener {
    // Constant for validating that permissions are available
    private static final int PERMS_REQUEST_GPS_ACCESS = 2001;

Note that I define the PERMS_REQUEST_GPS_ACCESS here as well – this is used in the permission granting callback process. The LocationListener requires certain callbacks, which we will get onto in a moment. First, let’s take a look at the setUpLocationManager() method:

    public void setUpLocationManager() {
        // Set up the location manager
        locationManager = (LocationManager)getSystemService(LOCATION_SERVICE);
        isLocationEnabled = locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER);
        if (isLocationEnabled) {
            this.onProviderEnabled(LocationManager.GPS_PROVIDER);
        } else {
            Log.w("Location", "Location/GPS is not enabled");
        }
    }

If the location manager has enabled the location provider already, we call the onProviderEnabled() method. This is actually a part of the LocationListener interface that we need to implement anyway. This line is just calling it the first time. Here is the completed interface:

    @Override
    public void onProviderEnabled(String provider) {
        if (provider == LocationManager.GPS_PROVIDER) {
            Log.w("Location", "Enabling GPS Provider");
            isLocationEnabled = true;
            try {
                locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 60000L, 100, this);
                onLocationChanged(locationManager.getLastKnownLocation(provider));
            } catch (SecurityException ex) {
                Toast.makeText(this, "You need the GPS permission", Toast.LENGTH_LONG);
            }
        }
    }

    @Override
    public void onProviderDisabled(String provider) {
        if (provider == LocationManager.GPS_PROVIDER) {
            Log.w("Location", "Disabling GPS Provider");
            isLocationEnabled = false;
        }
    }

    @Override
    public void onStatusChanged(String provider, int status, Bundle extras) {
    }

    @Override
    public void onLocationChanged(Location location) {
        Log.w("Location", "Location changed - refreshing data");
        currentRequest = new WeatherRequest(location);
        onRefreshClick(null);
    }

The onProviderEnabled() and onProviderDisabled() methods turn on and off the location requests based on the state of the GPS. The onLocationChanged() is called when the location changes. It is responsible for creating a new currentRequest object and then refreshing the data through the normal mechanism.

So far, so good. Just run the app and watch the data update with your current location!

Or not… I got a formatting error, which was implemented way back when I hooked up the UI to the data to catch any problems in the data retrieval. Firstly, I had a problem in the WeatherManager class. I had to do the following change:

    public WeatherResponse getWeatherByGps(double longitude, double latitude) throws WeatherException {
        return getWeather(String.format("lat=%.2f&lon=%.2f", latitude, longitude));
    }

Then I had a problem with the emulator. My house has a longitude of -122.28, which you cannot enter in the Android emulator. It works fine on a real device, however.

Wrap Up

That’s it for this app. There is, of course, a lot more you can do, and any perusal of the weather apps on the Google Play Store will get you plenty of ideas. I can also do a lot better job of handling the formatting and fetch failures, with user feedback like using the Toast object. Other considerations are around the act of running a public app – analytics, crash reporting and settings sync are all good topics to get to grips with. For now, the code for this app is on my GitHub repository.

One thought

  1. Pingback: Dew Drop - June 15, 2017 (#2501) - Morning Dew

Leave a Reply

Please log in using one of these methods to post your comment:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s