com.jasonmheim.rollout.station.CoreContentProvider.java Source code

Java tutorial

Introduction

Here is the source code for com.jasonmheim.rollout.station.CoreContentProvider.java

Source

/*
 * 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;
    }
}