Android Open Source - Udacity-Android-Course Sunshine Sync Adapter






From Project

Back to project page Udacity-Android-Course.

License

The source code is released under:

MIT License

If you think the Android project Udacity-Android-Course listed in this page is inappropriate, such as containing malicious code/tools or violating the copyright, please email info at java2s dot com, thanks.

Java Source Code

package com.example.assafg.sunshine.app.sync;
/*from   w ww  . j a v  a  2  s .co  m*/
import android.accounts.Account;
import android.accounts.AccountManager;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.AbstractThreadedSyncAdapter;
import android.content.ContentProviderClient;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SyncRequest;
import android.content.SyncResult;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.support.v4.app.NotificationCompat;
import android.support.v4.app.TaskStackBuilder;
import android.util.Log;

import com.example.assafg.sunshine.app.MainActivity;
import com.example.assafg.sunshine.app.R;
import com.example.assafg.sunshine.app.Utility.Utility;
import com.example.assafg.sunshine.app.data.WeatherContract;

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

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Calendar;
import java.util.Date;
import java.util.Vector;

/**
 * Created by assafg on 12/23/14.
 */
public class SunshineSyncAdapter extends AbstractThreadedSyncAdapter {
  private final String LOG_TAG = SunshineSyncAdapter.class.getSimpleName();

  private static final String[] NOTIFY_WEATHER_PROJECTION = new String[] {
      WeatherContract.WeatherEntry.COLUMN_WEATHER_ID,
      WeatherContract.WeatherEntry.COLUMN_MAX_TEMP,
      WeatherContract.WeatherEntry.COLUMN_MIN_TEMP,
      WeatherContract.WeatherEntry.COLUMN_SHORT_DESC
  };

  // these indices must match the projection
  private static final int INDEX_WEATHER_ID = 0;
  private static final int INDEX_MAX_TEMP = 1;
  private static final int INDEX_MIN_TEMP = 2;
  private static final int INDEX_SHORT_DESC = 3;

  private static final long DAY_IN_MILLIS = 1000 * 60 * 60 * 24;
  private static final int WEATHER_NOTIFICATION_ID = 3004;

  // Interval at which to sync with the weather, in milliseconds.
  // 60 seconds (1 minute) * 180 = 3 hours
  public static final int SYNC_INTERVAL = 30 * 180;
  public static final int SYNC_FLEXTIME = SYNC_INTERVAL/3;

  public SunshineSyncAdapter(Context context, boolean autoInitialize) {
    super(context, autoInitialize);
  }

  @Override
  public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) {
    Log.d(LOG_TAG, "Starting sync");
    // Getting the zipcode to send to the API
    String locationQuery = Utility.getPreferredLocation(getContext());

    // These two need to be declared outside the try/catch
    // so that they can be closed in the finally block.
    HttpURLConnection urlConnection = null;
    BufferedReader reader = null;

    // Will contain the raw JSON response as a string.
    String forecastJsonStr = null;

    String format = "json";
    String units = "metric";
    int numDays = 14;

    try {
      // Construct the URL for the OpenWeatherMap query
      // Possible parameters are avaiable at OWM's forecast API page, at
      // http://openweathermap.org/API#forecast
      final String FORECAST_BASE_URL =
          "http://api.openweathermap.org/data/2.5/forecast/daily?";
      final String QUERY_PARAM = "q";
      final String FORMAT_PARAM = "mode";
      final String UNITS_PARAM = "units";
      final String DAYS_PARAM = "cnt";

      Uri builtUri = Uri.parse(FORECAST_BASE_URL).buildUpon()
          .appendQueryParameter(QUERY_PARAM, locationQuery)
          .appendQueryParameter(FORMAT_PARAM, format)
          .appendQueryParameter(UNITS_PARAM, units)
          .appendQueryParameter(DAYS_PARAM, Integer.toString(numDays))
          .build();

      URL url = new URL(builtUri.toString());

      // Create the request to OpenWeatherMap, and open the connection
      urlConnection = (HttpURLConnection) url.openConnection();
      urlConnection.setRequestMethod("GET");
      urlConnection.connect();

      // Read the input stream into a String
      InputStream inputStream = urlConnection.getInputStream();
      StringBuffer buffer = new StringBuffer();
      if (inputStream == null) {
        // Nothing to do.
        return;
      }

      reader = new BufferedReader(new InputStreamReader(inputStream));
      String line;
      while ((line = reader.readLine()) != null) {
        // Since it's JSON, adding a newline isn't necessary (it won't affect parsing)
        // But it does make debugging a *lot* easier if you print out the completed
        // buffer for debugging.
        buffer.append(line + "\n");
      }

      if (buffer.length() == 0) {
        // Stream was empty.  No point in parsing.
        return;
      }
      forecastJsonStr = buffer.toString();
    } catch (IOException e) {
      Log.e(LOG_TAG, "Error ", e);
      // If the code didn't successfully get the weather data, there's no point in attemping
      // to parse it.
      return;
    } finally {
      if (urlConnection != null) {
        urlConnection.disconnect();
      }
      if (reader != null) {
        try {
          reader.close();
        } catch (final IOException e) {
          Log.e(LOG_TAG, "Error closing stream", e);
        }
      }
    }

    // Now we have a String representing the complete forecast in JSON Format.
    // Fortunately parsing is easy:  constructor takes the JSON string and converts it
    // into an Object hierarchy for us.

    // These are the names of the JSON objects that need to be extracted.

    // Location information
    final String OWM_CITY = "city";
    final String OWM_CITY_NAME = "name";
    final String OWM_COORD = "coord";

    // Location coordinate
    final String OWM_LATITUDE = "lat";
    final String OWM_LONGITUDE = "lon";

    // Weather information.  Each day's forecast info is an element of the "list" array.
    final String OWM_LIST = "list";

    final String OWM_DATETIME = "dt";
    final String OWM_PRESSURE = "pressure";
    final String OWM_HUMIDITY = "humidity";
    final String OWM_WINDSPEED = "speed";
    final String OWM_WIND_DIRECTION = "deg";

    // All temperatures are children of the "temp" object.
    final String OWM_TEMPERATURE = "temp";
    final String OWM_MAX = "max";
    final String OWM_MIN = "min";

    final String OWM_WEATHER = "weather";
    final String OWM_DESCRIPTION = "main";
    final String OWM_WEATHER_ID = "id";

    try {
      JSONObject forecastJson = new JSONObject(forecastJsonStr);
      JSONArray weatherArray = forecastJson.getJSONArray(OWM_LIST);

      JSONObject cityJson = forecastJson.getJSONObject(OWM_CITY);
      String cityName = cityJson.getString(OWM_CITY_NAME);

      JSONObject cityCoord = cityJson.getJSONObject(OWM_COORD);
      double cityLatitude = cityCoord.getDouble(OWM_LATITUDE);
      double cityLongitude = cityCoord.getDouble(OWM_LONGITUDE);

      long locationId = addLocation(locationQuery, cityName, cityLatitude, cityLongitude);

      // Insert the new weather information into the database
      Vector<ContentValues> cVVector = new Vector<>(weatherArray.length());

      for(int i = 0; i < weatherArray.length(); i++) {
        // These are the values that will be collected.

        long dateTime;
        double pressure;
        int humidity;
        double windSpeed;
        double windDirection;

        double high;
        double low;

        String description;
        int weatherId;

        // Get the JSON object representing the day
        JSONObject dayForecast = weatherArray.getJSONObject(i);

        // The date/time is returned as a long.  We need to convert that
        // into something human-readable, since most people won't read "1400356800" as
        // "this saturday".
        dateTime = dayForecast.getLong(OWM_DATETIME);

        pressure = dayForecast.getDouble(OWM_PRESSURE);
        humidity = dayForecast.getInt(OWM_HUMIDITY);
        windSpeed = dayForecast.getDouble(OWM_WINDSPEED);
        windDirection = dayForecast.getDouble(OWM_WIND_DIRECTION);

        // Description is in a child array called "weather", which is 1 element long.
        // That element also contains a weather code.
        JSONObject weatherObject =
            dayForecast.getJSONArray(OWM_WEATHER).getJSONObject(0);
        description = weatherObject.getString(OWM_DESCRIPTION);
        weatherId = weatherObject.getInt(OWM_WEATHER_ID);

        // Temperatures are in a child object called "temp".  Try not to name variables
        // "temp" when working with temperature.  It confuses everybody.
        JSONObject temperatureObject = dayForecast.getJSONObject(OWM_TEMPERATURE);
        high = temperatureObject.getDouble(OWM_MAX);
        low = temperatureObject.getDouble(OWM_MIN);

        ContentValues weatherValues = new ContentValues();

        weatherValues.put(WeatherContract.WeatherEntry.COLUMN_LOC_KEY, locationId);
        weatherValues.put(WeatherContract.WeatherEntry.COLUMN_DATETEXT, WeatherContract.getDbDateString(new Date(dateTime * 1000L)));
        weatherValues.put(WeatherContract.WeatherEntry.COLUMN_HUMIDITY, humidity);
        weatherValues.put(WeatherContract.WeatherEntry.COLUMN_PRESSURE, pressure);
        weatherValues.put(WeatherContract.WeatherEntry.COLUMN_WIND_SPEED, windSpeed);
        weatherValues.put(WeatherContract.WeatherEntry.COLUMN_DEGREES, windDirection);
        weatherValues.put(WeatherContract.WeatherEntry.COLUMN_MAX_TEMP, high);
        weatherValues.put(WeatherContract.WeatherEntry.COLUMN_MIN_TEMP, low);
        weatherValues.put(WeatherContract.WeatherEntry.COLUMN_SHORT_DESC, description);
        weatherValues.put(WeatherContract.WeatherEntry.COLUMN_WEATHER_ID, weatherId);

        cVVector.add(weatherValues);
      }
      if ( cVVector.size() > 0 ) {
        ContentValues[] cvArray = new ContentValues[cVVector.size()];
        cVVector.toArray(cvArray);
        getContext().getContentResolver().bulkInsert(WeatherContract.WeatherEntry.CONTENT_URI, cvArray);

        Calendar cal = Calendar.getInstance(); //Get's a calendar object with the current time.
        cal.add(Calendar.DATE, -1); //Signifies yesterday's date
        String yesterdayDate = WeatherContract.getDbDateString(cal.getTime());
        getContext().getContentResolver().delete(WeatherContract.WeatherEntry.CONTENT_URI,
            WeatherContract.WeatherEntry.COLUMN_DATETEXT + " <= ?",
            new String[] {yesterdayDate});

        notifyWeather();
      }
      Log.d(LOG_TAG, "FetchWeatherTask Complete. " + cVVector.size() + " Inserted");

    } catch (JSONException e) {
      Log.e(LOG_TAG, e.getMessage(), e);
      e.printStackTrace();
    }

    // This will only happen if there was an error getting or parsing the forecast.
    return;
  }

  /**
   * Helper method to handle insertion of a new location in the weather database.
   *
   * @param locationSetting The location string used to request updates from the server.
   * @param cityName A human-readable city name, e.g "Mountain View"
   * @param lat the latitude of the city
   * @param lon the longitude of the city
   * @return the row ID of the added location.
   */
  private long addLocation(String locationSetting, String cityName, double lat, double lon) {
    long locationId;

    Log.v(LOG_TAG, "inserting " + cityName + ", with coord: " + lat + ", " + lon);

    // First, check if the location with this city name exists in the db
    Cursor locationCursor = getContext().getContentResolver().query(
        WeatherContract.LocationEntry.CONTENT_URI,
        new String[]{WeatherContract.LocationEntry._ID},
        WeatherContract.LocationEntry.COLUMN_LOCATION_SETTING + " = ?",
        new String[]{locationSetting},
        null);

    if (locationCursor.moveToFirst()) {
      int locationIdIndex = locationCursor.getColumnIndex(WeatherContract.LocationEntry._ID);
      locationId = locationCursor.getLong(locationIdIndex);
    } else {
      // Now that the content provider is set up, inserting rows of data is pretty simple.
      // First create a ContentValues object to hold the data you want to insert.
      ContentValues locationValues = new ContentValues();

      // Then add the data, along with the corresponding name of the data type,
      // so the content provider knows what kind of value is being inserted.
      locationValues.put(WeatherContract.LocationEntry.COLUMN_CITY_NAME, cityName);
      locationValues.put(WeatherContract.LocationEntry.COLUMN_LOCATION_SETTING, locationSetting);
      locationValues.put(WeatherContract.LocationEntry.COLUMN_COORD_LAT, lat);
      locationValues.put(WeatherContract.LocationEntry.COLUMN_COORD_LONG, lon);

      // Finally, insert location data into the database.
      Uri insertedUri = getContext().getContentResolver().insert(
          WeatherContract.LocationEntry.CONTENT_URI,
          locationValues
      );

      // The resulting URI contains the ID for the row.  Extract the locationId from the Uri.
      locationId = ContentUris.parseId(insertedUri);
    }

    // Wait, that worked?  Yes!
    return locationId;
  }

  /**
   * Helper method to schedule the sync adapter periodic execution
   */
  public static void configurePeriodicSync(Context context, int syncInterval, int flexTime) {
    Account account = getSyncAccount(context);
    String authority = context.getString(R.string.content_authority);
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
      // we can enable inexact timers in our periodic sync
      SyncRequest request = new SyncRequest.Builder().
          syncPeriodic(syncInterval, flexTime).setExtras(new Bundle()).
          setSyncAdapter(account, authority).build();
      ContentResolver.requestSync(request);
    } else {
      ContentResolver.addPeriodicSync(account,
          authority, new Bundle(), syncInterval);
    }
  }

  private static void onAccountCreated(Account newAccount, Context context) {
        /*
         * Since we've created an account
         */
    SunshineSyncAdapter.configurePeriodicSync(context, SYNC_INTERVAL, SYNC_FLEXTIME);

        /*
         * Without calling setSyncAutomatically, our periodic sync will not be enabled.
         */
    ContentResolver.setSyncAutomatically(newAccount, context.getString(R.string.content_authority), true);

        /*
         * Finally, let's do a sync to get things started
         */
    syncImmediately(context);
  }

  public static void initializeSyncAdapter(Context context) {
    getSyncAccount(context);
  }

  /**
   * Helper method to have the sync adapter sync immediately
   * @param context The context used to access the account service
   */
  public static void syncImmediately(Context context) {
    Bundle bundle = new Bundle();
    bundle.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true);
    bundle.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true);
    ContentResolver.requestSync(getSyncAccount(context),
        context.getString(R.string.content_authority), bundle);
  }

  /**
   * Helper method to get the fake account to be used with SyncAdapter, or make a new one
   * if the fake account doesn't exist yet.  If we make a new account, we call the
   * onAccountCreated method so we can initialize things.
   *
   * @param context The context used to access the account service
   * @return a fake account.
   */
  public static Account getSyncAccount(Context context) {
    // Get an instance of the Android account manager
    AccountManager accountManager =
        (AccountManager) context.getSystemService(Context.ACCOUNT_SERVICE);

    // Create the account type and default account
    Account newAccount = new Account(
        context.getString(R.string.app_name), context.getString(R.string.sync_account_type));

    // If the password doesn't exist, the account doesn't exist
    if ( null == accountManager.getPassword(newAccount) ) {

        /*
         * Add the account and account type, no password or user data
         * If successful, return the Account object, otherwise report an error.
         */
      if (!accountManager.addAccountExplicitly(newAccount, "", null)) {
        return null;
      }
            /*
             * If you don't set android:syncable="true" in
             * in your <provider> element in the manifest,
             * then call ContentResolver.setIsSyncable(account, AUTHORITY, 1)
             * here.
             */

      onAccountCreated(newAccount, context);
    }
    return newAccount;
  }

  private void notifyWeather() {
    Context context = getContext();
    //checking the last update and notify if it' the first of the day
    SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
    String displayNotificationsKey = context.getString(R.string.pref_enable_notifications_key);
    boolean displayNotifications = prefs.getBoolean(displayNotificationsKey,
        Boolean.parseBoolean(context.getString(R.string.pref_enable_notifications_default)));

    if ( displayNotifications ) {

      String lastNotificationKey = context.getString(R.string.pref_last_notification);
      long lastSync = prefs.getLong(lastNotificationKey, 0);

      if (System.currentTimeMillis() - lastSync >= DAY_IN_MILLIS) {
        // Last sync was more than 1 day ago, let's send a notification with the weather.
        String locationQuery = Utility.getPreferredLocation(context);

        Uri weatherUri = WeatherContract.WeatherEntry.buildWeatherLocationWithDate(locationQuery, WeatherContract.getDbDateString(new Date()));

        // we'll query our contentProvider, as always
        Cursor cursor = context.getContentResolver().query(weatherUri, NOTIFY_WEATHER_PROJECTION, null, null, null);

        if (cursor.moveToFirst()) {
          int weatherId = cursor.getInt(INDEX_WEATHER_ID);
          double high = cursor.getDouble(INDEX_MAX_TEMP);
          double low = cursor.getDouble(INDEX_MIN_TEMP);
          String desc = cursor.getString(INDEX_SHORT_DESC);

          int iconId = Utility.getIconResourceForWeatherCondition(weatherId);
          String title = context.getString(R.string.app_name);

          boolean isMetric = Utility.isMetric(context);

          // Define the text of the forecast.
          String contentText = String.format(context.getString(R.string.format_notification),
              desc,
              Utility.formatTemperature(context, high, isMetric),
              Utility.formatTemperature(context, low, isMetric));

          // NotificationCompatBuilder is a very convenient way to build backward-compatible
          // notifications.  Just throw in some data.
          NotificationCompat.Builder mBuilder =
              new NotificationCompat.Builder(getContext())
                  .setSmallIcon(iconId)
                  .setContentTitle(title)
                  .setContentText(contentText);

          // Make something interesting happen when the user clicks on the notification.
          // In this case, opening the app is sufficient.
          Intent resultIntent = new Intent(context, MainActivity.class);

          // The stack builder object will contain an artificial back stack for the
          // started Activity.
          // This ensures that navigating backward from the Activity leads out of
          // your application to the Home screen.
          TaskStackBuilder stackBuilder = TaskStackBuilder.create(context);
          stackBuilder.addNextIntent(resultIntent);
          PendingIntent resultPendingIntent =
              stackBuilder.getPendingIntent(
                  0,
                  PendingIntent.FLAG_UPDATE_CURRENT
              );
          mBuilder.setContentIntent(resultPendingIntent);

          NotificationManager mNotificationManager =
              (NotificationManager) getContext().getSystemService(Context.NOTIFICATION_SERVICE);
          // WEATHER_NOTIFICATION_ID allows you to update the notification later on.
          mNotificationManager.notify(WEATHER_NOTIFICATION_ID, mBuilder.build());


          //refreshing last sync
          SharedPreferences.Editor editor = prefs.edit();
          editor.putLong(lastNotificationKey, System.currentTimeMillis());
          editor.commit();
        }
      }
    }

  }
}




Java Source Code List

com.example.assafg.sunshine.app.ApplicationTest.java
com.example.assafg.sunshine.app.DetailActivity.java
com.example.assafg.sunshine.app.DetailsFragment.java
com.example.assafg.sunshine.app.ForecastAdapter.java
com.example.assafg.sunshine.app.ForecastFragment.java
com.example.assafg.sunshine.app.MainActivity.java
com.example.assafg.sunshine.app.SettingsActivity.java
com.example.assafg.sunshine.app.Utility.Utility.java
com.example.assafg.sunshine.app.data.WeatherContract.java
com.example.assafg.sunshine.app.data.WeatherDbHelper.java
com.example.assafg.sunshine.app.data.WeatherProvider.java
com.example.assafg.sunshine.app.sync.SunshineAuthenticatorService.java
com.example.assafg.sunshine.app.sync.SunshineAuthenticator.java
com.example.assafg.sunshine.app.sync.SunshineSyncAdapter.java
com.example.assafg.sunshine.app.sync.SunshineSyncService.java