Java tutorial
/* * Copyright (C) 2014 Jason M. Heim * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.jasonmheim.rollout.station; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.content.ContentProvider; import android.content.ContentResolver; import android.content.ContentValues; import android.content.Intent; import android.database.Cursor; import android.location.Location; import android.net.Uri; import android.os.Bundle; import android.support.v4.app.NotificationCompat; import android.util.Log; import com.google.gson.Gson; import com.jasonmheim.rollout.Constants; import com.jasonmheim.rollout.R; import com.jasonmheim.rollout.action.ActionIntentService; import com.jasonmheim.rollout.action.ActionManager; import com.jasonmheim.rollout.data.Station; import com.jasonmheim.rollout.data.StationDistance; import com.jasonmheim.rollout.data.StationDistanceRank; import com.jasonmheim.rollout.data.StationList; import com.jasonmheim.rollout.inject.ObjectGraphProvider; import com.jasonmheim.rollout.location.LocationManager; import com.jasonmheim.rollout.settings.Settings; import java.util.ArrayList; import java.util.List; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; import javax.inject.Inject; import static com.google.android.gms.location.LocationRequest.PRIORITY_BALANCED_POWER_ACCURACY; import static com.google.android.gms.location.LocationRequest.PRIORITY_HIGH_ACCURACY; import static com.jasonmheim.rollout.Constants.ACCOUNT; import static com.jasonmheim.rollout.Constants.ACTION_IDLE; import static com.jasonmheim.rollout.Constants.ACTION_RIDE; import static com.jasonmheim.rollout.Constants.ACTION_SEARCH; import static com.jasonmheim.rollout.Constants.ACTION_SILENCE; import static com.jasonmheim.rollout.Constants.AUTHORITY; import static com.jasonmheim.rollout.Constants.DESTINATION_NAME_HOME; import static com.jasonmheim.rollout.Constants.DESTINATION_NAME_WORK; import static com.jasonmheim.rollout.Constants.UPDATE_KEY_ACTION; import static com.jasonmheim.rollout.Constants.UPDATE_KEY_DESTINATION; /** * This class serves as the hub for all data and notification of updates to pretty much everything * in the application. * <p> * Normally, a content provider would be a front for a local MySQL store. Such a solution would be * too heavyweight in this case, since the service with bike share data does not provide any * mechanism for incremental updates; the service exposes a blob of JSON data with the current state * of the entire bike share system. * <p> * Given this we simply serialize the entire blob to disk using {@link StationDataStorage}. The insert * method expects to be invoked by the Sync Adapter that polls for updates. The query method exposes * a custom StationDataCursor that has one row of data with one column: the entire JSON blob. * <p> * In addition to the bike share data being fed here from the Sync Adapter, the update method is * invoked when a new current location is known, or the user has updated the application's action. * The former is to allow for notifications to be posted even if the main app is not running. The * latter is to be able to handle pending intents from notifications including the wearable. * <p> * By having all updates posted here, two things are accomplished: * <ul> * <li>This class becomes the hub of all notifications. * <li>The UI can listen for updates from a single STATION_URI to get the effects of all changes. * </ul> */ public class CoreContentProvider extends ContentProvider { @Inject Gson gson; @Inject StationDataStorage stationDataStorage; @Inject StationDataDownloader stationListDownloader; @Inject ExecutorService executorService; @Inject ActionManager actionManager; @Inject NotificationManager notificationManager; @Inject LocationManager locationManager; @Inject Settings settings; @Inject StationDataProcessor stationDataProcessor; private StationList stationList; private StationDistanceRank previousStationDistanceRank = null; private static final int[] FILL_COLOR = { R.drawable.ic_fill_0_color_48dp, R.drawable.ic_fill_1_color_48dp, R.drawable.ic_fill_2_color_48dp, R.drawable.ic_fill_3_color_48dp, R.drawable.ic_fill_4_color_48dp, R.drawable.ic_fill_5_color_48dp, R.drawable.ic_fill_6_color_48dp, R.drawable.ic_fill_7_color_48dp, R.drawable.ic_fill_8_color_48dp, R.drawable.ic_fill_0_red1_48dp, R.drawable.ic_fill_1_red1_48dp, R.drawable.ic_fill_2_red1_48dp, R.drawable.ic_fill_3_red1_48dp, R.drawable.ic_fill_4_red1_48dp, R.drawable.ic_fill_5_red1_48dp, R.drawable.ic_fill_6_red1_48dp, R.drawable.ic_fill_7_red1_48dp, R.drawable.ic_fill_8_red1_48dp, R.drawable.ic_fill_0_red2_48dp, R.drawable.ic_fill_1_red2_48dp, R.drawable.ic_fill_2_red2_48dp, R.drawable.ic_fill_3_red2_48dp, R.drawable.ic_fill_4_red2_48dp, R.drawable.ic_fill_5_red2_48dp, R.drawable.ic_fill_6_red2_48dp, R.drawable.ic_fill_7_red2_48dp, R.drawable.ic_fill_8_red2_48dp, R.drawable.ic_fill_0_red3_48dp, R.drawable.ic_fill_1_red3_48dp, R.drawable.ic_fill_2_red3_48dp, R.drawable.ic_fill_3_red3_48dp, R.drawable.ic_fill_4_red3_48dp, R.drawable.ic_fill_5_red3_48dp, R.drawable.ic_fill_6_red3_48dp, R.drawable.ic_fill_7_red3_48dp, R.drawable.ic_fill_8_red3_48dp, }; private static final int[] FILL_BW = { R.drawable.ic_fill_0_bw_24p, R.drawable.ic_fill_1_bw_24p, R.drawable.ic_fill_2_bw_24p, R.drawable.ic_fill_3_bw_24p, R.drawable.ic_fill_4_bw_24p, R.drawable.ic_fill_5_bw_24p, R.drawable.ic_fill_6_bw_24p, R.drawable.ic_fill_7_bw_24p, R.drawable.ic_fill_8_bw_24p, R.drawable.ic_fill_0_bw_red1_24p, R.drawable.ic_fill_1_bw_red1_24p, R.drawable.ic_fill_2_bw_red1_24p, R.drawable.ic_fill_3_bw_red1_24p, R.drawable.ic_fill_4_bw_red1_24p, R.drawable.ic_fill_5_bw_red1_24p, R.drawable.ic_fill_6_bw_red1_24p, R.drawable.ic_fill_7_bw_red1_24p, R.drawable.ic_fill_8_bw_red1_24p, R.drawable.ic_fill_0_bw_red2_24p, R.drawable.ic_fill_1_bw_red2_24p, R.drawable.ic_fill_2_bw_red2_24p, R.drawable.ic_fill_3_bw_red2_24p, R.drawable.ic_fill_4_bw_red2_24p, R.drawable.ic_fill_5_bw_red2_24p, R.drawable.ic_fill_6_bw_red2_24p, R.drawable.ic_fill_7_bw_red2_24p, R.drawable.ic_fill_8_bw_red2_24p, R.drawable.ic_fill_0_bw_red3_24p, R.drawable.ic_fill_1_bw_red3_24p, R.drawable.ic_fill_2_bw_red3_24p, R.drawable.ic_fill_3_bw_red3_24p, R.drawable.ic_fill_4_bw_red3_24p, R.drawable.ic_fill_5_bw_red3_24p, R.drawable.ic_fill_6_bw_red3_24p, R.drawable.ic_fill_7_bw_red3_24p, R.drawable.ic_fill_8_bw_red3_24p, }; // Use this to bump the priority of a notification without actually buzzing the device private static final long[] BUZZ_SILENT = { 0, 0, }; private static final long[] BUZZ_0 = { 0, 200, 100, 200, 100, 200, }; private static final long[] BUZZ_1 = { 0, 100, 150, 200, 150, 300, 150, 400, }; private static final long[] BUZZ_2 = { 0, 100, 150, 200, 150, 300, 150, 400, 250, 100, 150, 200, 150, 300, 150, 400, }; private static final long[] BUZZ_3 = { 0, 100, 150, 200, 150, 300, 150, 400, 250, 100, 150, 200, 150, 300, 150, 400, 250, 100, 150, 200, 150, 300, 150, 400, }; private static final long[][] BUZZ_RANKS = { BUZZ_0, BUZZ_1, BUZZ_2, BUZZ_3, }; @Override public int delete(Uri uri, String selection, String[] selectionArgs) { throw new UnsupportedOperationException("Not yet implemented"); } @Override public String getType(Uri uri) { throw new UnsupportedOperationException("Not yet implemented"); } @Override public Uri insert(Uri uri, ContentValues values) { // TODO: make this a constant, or even better, create static helper methods String valuesAsString = values.getAsString("StationList"); try { StationList newStationList = gson.fromJson(valuesAsString, StationList.class); internalInsert(newStationList); } catch (RuntimeException ex) { Log.e("Rollout", "Insert deserialization exception", ex); } return Constants.STATION_URI; } @Override public boolean onCreate() { ((ObjectGraphProvider) getContext().getApplicationContext()).get().inject(this); return true; } @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { StationDataCursor cursor = new StationDataCursor(); if (stationList == null) { Log.i("Rollout", "Provider not yet initialized, checking local storage..."); stationList = stationDataStorage.get(); } if (stationList == null) { Log.i("Rollout", "Local storage was empty."); Future<StationList> future = executorService.submit(stationListDownloader); try { StationList providerList = future.get(); if (providerList == null) { Log.i("Rollout", "Station list failed to download from query, requesting sync"); Bundle settingsBundle = new Bundle(); settingsBundle.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true); settingsBundle.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true); ContentResolver.requestSync(ACCOUNT, AUTHORITY, settingsBundle); } else { Log.i("Rollout", "Direct download succeeded."); // This implicitly sets stationList internalInsert(providerList); } } catch (InterruptedException ex) { Log.w("Rollout", "Content provider sync was interrupted", ex); } catch (ExecutionException ex) { Log.w("Rollout", "Content provider sync execution failed", ex); } } if (stationList != null) { cursor.setStationDataJson(gson.toJson(stationList)); } cursor.setNotificationUri(getContext().getContentResolver(), Constants.STATION_URI); return cursor; } @Override public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { // TODO: this is silly. Disambiguate by using different URIs for data, location, action, etc if (values.containsKey(Constants.UPDATE_KEY_LOCATION)) { Log.i("Rollout", "Updating location"); internalUpdate(); } else if (values.containsKey(Constants.UPDATE_KEY_ACTION)) { // TODO: Do this in the ActionManager. It's ridiculous to do this here. int action = actionManager.getAction(); Log.i("Rollout", "Updating action to " + action); switch (action) { case Constants.ACTION_SEARCH: Log.i("Rollout", "Active search action"); // Medium location speed locationManager.setLocationUpdateInterval(1, PRIORITY_HIGH_ACCURACY); // Fast periodic sync setSyncPeriod(1); break; case Constants.ACTION_RIDE: Log.i("Rollout", "Riding action"); // Fast location speed locationManager.setLocationUpdateInterval(0.5, PRIORITY_HIGH_ACCURACY); // Fast periodic sync setSyncPeriod(1); break; case Constants.ACTION_IDLE: Log.i("Rollout", "Passive search action"); // Slow location speed locationManager.setLocationUpdateInterval(10, PRIORITY_BALANCED_POWER_ACCURACY); // Slow periodic sync setSyncPeriod(5); break; case Constants.ACTION_SILENCE: Log.i("Rollout", "Muted action"); // Extra slow location speed locationManager.setLocationUpdateInterval(60, PRIORITY_BALANCED_POWER_ACCURACY); // Extra slow periodic sync setSyncPeriod(20); } internalUpdate(); } return 0; } private void setSyncPeriod(double periodInMinutes) { Log.i("Rollout", "Data update period in minutes: " + periodInMinutes); ContentResolver.addPeriodicSync(ACCOUNT, AUTHORITY, Bundle.EMPTY, (long) (periodInMinutes * Constants.SECONDS_PER_MINUTE)); } private synchronized void internalUpdate() { if (!settings.isDisclaimerAgreed()) { // The user has not yet agreed to the disclaimer. Post no notifications or URI changes. notificationManager.cancel(1); return; } try { int action = actionManager.getAction(); if (action == ACTION_SILENCE) { // Turn off notifications but still post URI changes via finally clause notificationManager.cancel(1); return; } if (stationList == null) { return; } Location lastLocation = locationManager.getLastLocation(); if (lastLocation == null) { return; } StationDistanceRank stationDistanceRank = stationDataProcessor.getClosestAvailableStation(stationList); if (stationDistanceRank == null) { return; } try { Station station = stationDistanceRank.getStationDistance().getStation(); int iconIndex = getIconIndex(stationDistanceRank); // The color icons look better on the wearable with their white background Notification.WearableExtender wearable = new Notification.WearableExtender() .setContentIcon(FILL_COLOR[iconIndex]).setHintHideIcon(true); // Don't bother setting large icon, it's redundant with the small icon especially once you // are on Lollipop. Notification.Builder builder = new Notification.Builder(getContext()) .setSmallIcon(FILL_BW[iconIndex]) .setColor(getContext().getResources().getColor(R.color.availableBikes)) .setContentTitle(station.stationName); switch (action) { case ACTION_RIDE: builder.setContentText(getDocks(stationDistanceRank)) .setPriority(NotificationCompat.PRIORITY_MAX) .setVibrate(getAppropriateBuzz(stationDistanceRank)); wearable.addAction(getIdleAction()).addAction(getSearchAction()).addAction(getSilenceAction()); break; case ACTION_SEARCH: builder.setContentText(getBikesAndDuds(stationDistanceRank)).setVibrate(BUZZ_SILENT) .setPriority(NotificationCompat.PRIORITY_MAX); wearable.addActions(getRideActions()).addAction(getIdleAction()).addAction(getSilenceAction()); break; case ACTION_IDLE: builder.setContentText(getBikesAndDuds(stationDistanceRank)) .setPriority(NotificationCompat.PRIORITY_DEFAULT); wearable.addAction(getSearchAction()).addActions(getRideActions()) .addAction(getSilenceAction()); break; } Intent resultIntent = new Intent(getContext(), StationDataActivity.class); PendingIntent pendingIntent = PendingIntent.getActivity(getContext(), 0, resultIntent, PendingIntent.FLAG_UPDATE_CURRENT); builder.setContentIntent(pendingIntent).extend(wearable); notificationManager.notify(1, builder.build()); } finally { previousStationDistanceRank = stationDistanceRank; } } finally { getContext().getContentResolver().notifyChange(Constants.STATION_URI, null); } } private long[] getAppropriateBuzz(StationDistanceRank nextStationDistanceRank) { if (settings.isVibrationEnabled() && actionManager.getDestinationName() != null) { if (previousStationDistanceRank == null) { return BUZZ_RANKS[nextStationDistanceRank.getLimitedRank()]; } Station previousStation = previousStationDistanceRank.getStationDistance().getStation(); Station nextStation = nextStationDistanceRank.getStationDistance().getStation(); if (previousStation.availableBikes != nextStation.availableBikes || previousStation.availableDocks != nextStation.availableDocks || previousStation.id != nextStation.id) { return BUZZ_RANKS[nextStationDistanceRank.getLimitedRank()]; } } return BUZZ_SILENT; } private static int getIconIndex(StationDistanceRank stationDistanceRank) { Station station = stationDistanceRank.getStationDistance().getStation(); int limitedRank = stationDistanceRank.getLimitedRank(); int iconIndex; if (station.availableBikes == 0) { iconIndex = 0; } else if (station.availableDocks == 0) { iconIndex = 8; } else { int max = station.availableDocks + station.availableBikes; iconIndex = ((station.availableBikes * 7) / max) + 1; } // Adjust index if the current rank is > 0. return iconIndex + (limitedRank * 9); } private static String getBikesAndDuds(StationDistanceRank stationDistanceRank) { StationDistance stationDistance = stationDistanceRank.getStationDistance(); Station station = stationDistance.getStation(); int duds = station.totalDocks - (station.availableDocks + station.availableBikes); return getRankString(stationDistanceRank.getRank()) + "Bikes: " + station.availableBikes + " Duds: " + duds + "\n" + "Go: " + stationDistance.getDistanceString(); } private static String getDocks(StationDistanceRank stationDistanceRank) { StationDistance stationDistance = stationDistanceRank.getStationDistance(); return getRankString(stationDistanceRank.getRank()) + "Docks: " + stationDistance.getStation().availableDocks + "\n" + "Go: " + stationDistance.getDistanceString(); } private static String getRankString(int rank) { return rank == 0 ? "" : "Rank: " + (rank + 1) + "\n"; } private void internalInsert(StationList newStationList) { if (newStationList == null) { return; } stationList = newStationList; stationDataStorage.set(stationList); internalUpdate(); } private Notification.Action getSearchAction() { return new Notification.Action(R.drawable.ic_search_white_24dp, "Search", getActionSettingIntent(ACTION_SEARCH)); } private Notification.Action getIdleAction() { return new Notification.Action(R.drawable.ic_pause_white_24dp, "Idle", getActionSettingIntent(ACTION_IDLE)); } private List<Notification.Action> getRideActions() { List<Notification.Action> actions = new ArrayList<Notification.Action>(); if (settings.isHomeDestinationActive()) { actions.add(new Notification.Action(R.drawable.ic_directions_bike_white_24dp, DESTINATION_NAME_HOME, getRideActionWithDestinationIntent(DESTINATION_NAME_HOME))); } if (settings.isWorkDestinationActive()) { actions.add(new Notification.Action(R.drawable.ic_directions_bike_white_24dp, DESTINATION_NAME_WORK, getRideActionWithDestinationIntent(DESTINATION_NAME_WORK))); } actions.add(new Notification.Action(R.drawable.ic_directions_bike_white_24dp, "Roam", getActionSettingIntent(ACTION_RIDE))); return actions; } private Notification.Action getSilenceAction() { return new Notification.Action(R.drawable.ic_volume_off_white_24dp, "Mute", getActionSettingIntent(ACTION_SILENCE)); } private PendingIntent getActionSettingIntent(int action) { Uri data = Constants.STATION_URI.buildUpon() // TODO: Don't use update keys for query param names .appendQueryParameter(UPDATE_KEY_ACTION, Integer.toString(action)).build(); Intent intent = new Intent(getContext(), ActionIntentService.class); intent.setData(data); PendingIntent pendingIntent = PendingIntent.getService(getContext(), 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); return pendingIntent; } private PendingIntent getRideActionWithDestinationIntent(String destination) { Uri data = Constants.STATION_URI.buildUpon() .appendQueryParameter(UPDATE_KEY_ACTION, Integer.toString(ACTION_RIDE)) .appendQueryParameter(UPDATE_KEY_DESTINATION, destination).build(); Intent intent = new Intent(getContext(), ActionIntentService.class); intent.setData(data); PendingIntent pendingIntent = PendingIntent.getService(getContext(), 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); return pendingIntent; } }