com.google.android.apps.santatracker.service.APIProcessor.java Source code

Java tutorial

Introduction

Here is the source code for com.google.android.apps.santatracker.service.APIProcessor.java

Source

/*
 * Copyright (C) 2016 Google Inc. All Rights Reserved.
 *
 * 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.google.android.apps.santatracker.service;

import android.database.sqlite.SQLiteDatabase;
import android.util.Log;

import com.google.android.apps.santatracker.data.DestinationDbHelper;
import com.google.android.apps.santatracker.data.GameDisabledState;
import com.google.android.apps.santatracker.data.SantaPreferences;
import com.google.android.apps.santatracker.data.StreamDbHelper;
import com.google.android.apps.santatracker.data.Switches;
import com.google.android.apps.santatracker.util.SantaLog;

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.util.Arrays;

/**
 * Abstracts access to the Santa API.
 * This class handles the processing and interpretation of received data from the API, including
 * parsing the result into {@link org.json.JSONObject}s and calling the
 * {@link com.google.android.apps.santatracker.service.APIProcessor.APICallback}
 * with any changed values.
 */
public abstract class APIProcessor {

    private static final String TAG = "SantaCommunicator";

    // JSON field names for parsing
    protected final static String FIELD_ROUTEOFFSET = "routeOffset";
    protected final static String FIELD_NOW = "now";
    protected final static String FIELD_TIMEOFFSET = "timeOffset";
    protected final static String FIELD_FINGERPRINT = "fingerprint";
    protected final static String FIELD_SWITCHOFF = "switchOff";
    protected final static String FIELD_REFRESH = "refresh";
    protected final static String FIELD_DISABLE_CASTBUTTON = "DisableCastButton";
    protected final static String FIELD_DISABLE_PHOTO = "DisableDestinationPhoto";
    protected final static String FIELD_DISABLE_GUMBALLGAME = "DisableGumballGame";
    protected final static String FIELD_DISABLE_JETPACKGAME = "DisableJetpackGame";
    protected final static String FIELD_DISABLE_MEMORYGAME = "DisableMemoryGame";
    protected final static String FIELD_DISABLE_ROCKETGAME = "DisableRocketGame";
    protected final static String FIELD_DISABLE_DANCERGAME = "DisableDancerGame";
    protected final static String FIELD_DISABLE_SNOWDOWNGAME = "DisableSnowdownGame";
    protected final static String FIELD_DISABLE_SWIMMINGGAME = "DisableSwimmingGame";
    protected final static String FIELD_DISABLE_BMXGAME = "DisableBmxGame";
    protected final static String FIELD_DISABLE_RUNNINGGAME = "DisableRunningGame";
    protected final static String FIELD_DISABLE_TENNISGAME = "DisableTennisGame";
    protected final static String FIELD_DISABLE_WATERPOLOGAME = "DisableWaterpoloGame";
    protected final static String FIELD_DISABLE_CITY_QUIZ = "DisableCityQuiz";
    protected final static String FIELD_DISABLE_PRESENTQUEST = "DisablePresentQuest";
    protected final static String FIELD_VIDEO_1 = "Video1";
    protected final static String FIELD_VIDEO_15 = "Video15";
    protected final static String FIELD_VIDEO_23 = "Video23";

    protected static final String FIELD_DESTINATIONS = "destinations";
    protected static final String FIELD_STATUS = "status";

    protected static final String FIELD_IDENTIFIER = "id";
    protected static final String FIELD_ARRIVAL = "arrival";
    protected static final String FIELD_DEPARTURE = "departure";

    protected static final String FIELD_DETAILS_CITY = "city";
    protected static final String FIELD_DETAILS_REGION = "region";
    protected static final String FIELD_DETAILS_COUNTRY = "country";

    protected static final String FIELD_DETAILS_LOCATION = "location";
    protected static final String FIELD_DETAILS_LOCATION_LAT = "lat";
    protected static final String FIELD_DETAILS_LOCATION_LNG = "lng";
    protected static final String FIELD_DETAILS_PRESENTSDELIVERED = "presentsDelivered";

    protected static final String FIELD_DETAILS_DETAILS = "details";
    protected static final String FIELD_DETAILS_ALTITUDE = "altitude";
    protected static final String FIELD_DETAILS_TIMEZONE = "timezone";
    protected static final String FIELD_DETAILS_PHOTOS = "photos";
    protected static final String FIELD_DETAILS_WEATHER = "weather";
    protected static final String FIELD_DETAILS_STREETVIEW = "streetView";
    protected static final String FIELD_DETAILS_GMMSTREETVIEW = "gmmStreetView";

    public static final String FIELD_PHOTO_URL = "url";
    public static final String FIELD_PHOTO_ATTRIBUTIONHTML = "attributionHtml";

    public static final String FIELD_WEATHER_URL = "url";
    public static final String FIELD_WEATHER_TEMPC = "tempC";
    public static final String FIELD_WEATHER_TEMPF = "tempF";

    public static final String FIELD_STREETVIEW_ID = "id";
    public static final String FIELD_STREETVIEW_LATITUDE = "latitude";
    public static final String FIELD_STREETVIEW_LONGITUDE = "longitude";
    public static final String FIELD_STREETVIEW_HEADING = "heading";

    public static final String FIELD_STREAM = "stream";
    public static final String FIELD_STREAMOFFSET = "streamOffset";
    public static final String FIELD_NOTIFICATIONSTREAM = "notificationStream";
    public static final String FIELD_STREAM_TIMESTAMP = "timestamp";
    public static final String FIELD_STREAM_STATUS = "status";
    public static final String FIELD_STREAM_DIDYOUKNOW = "didyouknow";
    public static final String FIELD_STREAM_IMAGEURL = "imageUrl";
    public static final String FIELD_STREAM_YOUTUBEID = "youtubeId";

    protected static final String FIELD_STATUS_OK = "OK";

    public static final long ERROR_CODE = Long.MIN_VALUE;

    private static final String EMPTY_STRING = "";

    // Preferences
    protected SantaPreferences mPreferences;
    // DB helpers
    protected DestinationDbHelper mDestinationDBHelper = null;
    protected StreamDbHelper mStreamDBHelper = null;

    // Callback
    private APICallback mCallback;

    public APIProcessor(SantaPreferences mPreferences, DestinationDbHelper mDBHelper,
            StreamDbHelper mStreamDBHelper, APICallback mCallback) {
        this.mPreferences = mPreferences;
        this.mDestinationDBHelper = mDBHelper;
        this.mStreamDBHelper = mStreamDBHelper;
        this.mCallback = mCallback;
    }

    /**
     * Load data from the API from the given URL and parse the returned data into a JSONObject.
     *
     * Implementations may ignore the URL parameter.
     */
    protected abstract JSONObject loadApi(String url);

    /** Loads remotely configurable switches and flags. */
    protected abstract Switches getSwitches();

    /**
     * Access the API from a URL and process its data.
     * If any values have changed, the appropriate callbacks in
     * {@link com.google.android.apps.santatracker.service.APIProcessor.APICallback} are called.
     * Returns {@link #ERROR_CODE} if the data could not be loaded or processed.
     * Returns the delay to the next API access if the access was successful.
     */
    public long accessAPI(String url) {
        SantaLog.d(TAG, "URL=" + url);
        // Get current values from mPreferences
        long offsetPref = mPreferences.getOffset();
        String fingerprintPref = mPreferences.getFingerprint();
        boolean switchOffPref = mPreferences.getSwitchOff();

        // load data as JSON
        JSONObject json = loadApi(url);

        if (json == null) {
            Log.d(TAG, "Santa Communication Error 3");
            return ERROR_CODE;
        }

        try {
            // Error if the status is not OK
            if (!FIELD_STATUS_OK.equals(json.getString(FIELD_STATUS))) {
                Log.d(TAG, "Santa Communication Error 4");
                return ERROR_CODE;
            }

            final int routeOffset = json.getInt(FIELD_ROUTEOFFSET);
            final long now = json.getLong(FIELD_NOW);
            final long offset = json.getLong(FIELD_TIMEOFFSET);
            final String fingerprint = json.getString(FIELD_FINGERPRINT);
            final long refresh = json.getLong(FIELD_REFRESH);
            final boolean switchOff = json.getBoolean(FIELD_SWITCHOFF);
            final JSONArray locations = json.getJSONArray(FIELD_DESTINATIONS);

            final int streamOffset = json.getInt(FIELD_STREAMOFFSET);
            final JSONArray stream = json.getJSONArray(FIELD_STREAM);

            // Notification stream parameters are optional
            final JSONArray notificationStream = json.has(FIELD_NOTIFICATIONSTREAM)
                    ? json.getJSONArray(FIELD_NOTIFICATIONSTREAM)
                    : null;

            // Fingerprint has changed, remove route and stream from db
            if (!fingerprint.equals(fingerprintPref)) {
                mCallback.notifyRouteUpdating();
                //empty the database and reset preferences
                mDestinationDBHelper.emptyDestinationTable();
                mStreamDBHelper.emptyCardTable();
                mPreferences.invalidateData();
            }

            // Destinations
            if (locations != null && locations.length() > 0) {
                int processedLocations = processRoute(locations);
                if (processedLocations > 0) {
                    final int newOffset = routeOffset + processedLocations;
                    mCallback.onNewRouteLoaded();
                    mPreferences.setFingerprint(fingerprint);
                    mPreferences.setRouteOffset(newOffset);
                    SantaLog.d(TAG, "Processed route - new details: " + newOffset + ", " + fingerprint);
                }
            }

            // Stream
            if (stream != null && stream.length() > 0) {
                // process non-notification cards
                int processedCards = processStream(stream, false);
                if (processedCards > 0) {
                    final int newOffset = streamOffset + processedCards;
                    mCallback.onNewStreamLoaded();
                    mPreferences.setStreamOffset(newOffset);
                    SantaLog.d(TAG, "Processed stream - new details: " + newOffset);
                }
            }

            // Notification Stream
            if (notificationStream != null && notificationStream.length() > 0) {
                // process notification cards
                int processedCards = processStream(notificationStream, true);
                if (processedCards > 0) {
                    mCallback.onNewNotificationStreamLoaded();
                    SantaLog.d(TAG, "Processed notification stream - count: " + processedCards);
                }
            }

            // Offset
            final long newOffset = now - System.currentTimeMillis() + offset;
            if (offsetPref != newOffset) {
                mPreferences.setOffset(newOffset);

                SantaLog.d(TAG, "New offset: " + newOffset + ", current=" + System.currentTimeMillis()
                        + ", new Santa=" + SantaPreferences.getCurrentTime());

                // Log.d(TAG, "new offset: new="+newOffset+", now="+now+", offset="+offset+",
                // prefOffset="+offsetPref+", time="+System.currentTimeMillis());
                // Notify only if offset varies significantly
                if ((newOffset > offsetPref + SantaPreferences.OFFSET_ACCEPTABLE_RANGE_DIFFERENCE
                        || newOffset < offsetPref - SantaPreferences.OFFSET_ACCEPTABLE_RANGE_DIFFERENCE)) {
                    mCallback.onNewOffset();
                }

            }

            if (switchOffPref != switchOff) {
                mPreferences.setSwitchOff(switchOff);
                mCallback.onNewSwitchOffState(switchOff);

            }

            // Check Switches for Changes
            checkSwitchesDiff(getSwitches());

            if (!fingerprint.equals(fingerprintPref)) {
                // new data has been processed and locations have been stored
                mCallback.onNewFingerprint();
            }

            return refresh;
        } catch (JSONException e) {
            Log.d(TAG, "Santa Communication Error 5");
            SantaLog.d(TAG, "JSON Exception", e);
            return ERROR_CODE;
        }

    }

    /**
     * Compare Switches to SharedPreferences and notify clients of any changes.
     */
    protected void checkSwitchesDiff(Switches s) {
        if (mPreferences.getCastDisabled() != s.disableCastButton) {
            // set cast preference
            mPreferences.setCastDisabled(s.disableCastButton);
            mCallback.onNewCastState(s.disableCastButton);
        }

        if (mPreferences.getDestinationPhotoDisabled() != s.disableDestinationPhoto) {
            // set destination photo preference
            mPreferences.setDestinationPhotoDisabled(s.disableDestinationPhoto);
            mCallback.onNewDestinationPhotoState(s.disableDestinationPhoto);
        }

        // Games
        if (!mPreferences.gameDisabledStateConsistent(s.gameState)) {
            // Overwrite game disabled state from Switches
            mPreferences.setGamesDisabled(s.gameState);

            // Notify of new game state
            mCallback.onNewGameState(s.gameState);
        }

        // Videos
        boolean videosConsistent = Arrays.equals(new String[] { s.video1, s.video15, s.video23 },
                mPreferences.getVideos());
        if (!videosConsistent) {
            mPreferences.setVideos(s.video1, s.video15, s.video23);
            mCallback.onNewVideos(s.video1, s.video15, s.video23);
        }
    }

    private int processRoute(JSONArray json) {
        SQLiteDatabase db = mDestinationDBHelper.getWritableDatabase();

        db.beginTransaction();
        try {
            // loop over each destination
            long previousPresents = mPreferences.getTotalPresents();

            int i;
            for (i = 0; i < json.length(); i++) {
                JSONObject dest = json.getJSONObject(i);

                JSONObject location = dest.getJSONObject(FIELD_DETAILS_LOCATION);

                long presentsTotal = dest.getLong(FIELD_DETAILS_PRESENTSDELIVERED);
                long presents = presentsTotal - previousPresents;
                previousPresents = presentsTotal;

                // Name
                String city = dest.getString(FIELD_DETAILS_CITY);
                String region = null;
                String country = null;

                if (dest.has(FIELD_DETAILS_REGION)) {
                    region = dest.getString(FIELD_DETAILS_REGION);
                    if (region.length() < 1) {
                        region = null;
                    }
                }
                if (dest.has(FIELD_DETAILS_COUNTRY)) {
                    country = dest.getString(FIELD_DETAILS_COUNTRY);
                    if (country.length() < 1) {
                        country = null;
                    }
                }

                //                if (mDebugLog) {
                //                    Log.d(TAG, "Location: " + city);
                //                }

                // Detail fields
                JSONObject details = dest.getJSONObject(FIELD_DETAILS_DETAILS);
                long timezone = details.isNull(FIELD_DETAILS_TIMEZONE) ? 0L
                        : details.getLong(FIELD_DETAILS_TIMEZONE);
                long altitude = details.getLong(FIELD_DETAILS_ALTITUDE);
                String photos = details.has(FIELD_DETAILS_PHOTOS) ? details.getString(FIELD_DETAILS_PHOTOS)
                        : EMPTY_STRING;
                String weather = details.has(FIELD_DETAILS_WEATHER) ? details.getString(FIELD_DETAILS_WEATHER)
                        : EMPTY_STRING;
                String streetview = details.has(FIELD_DETAILS_STREETVIEW)
                        ? details.getString(FIELD_DETAILS_STREETVIEW)
                        : EMPTY_STRING;
                String gmmStreetview = details.has(FIELD_DETAILS_GMMSTREETVIEW)
                        ? details.getString(FIELD_DETAILS_GMMSTREETVIEW)
                        : EMPTY_STRING;

                try {
                    // All parsed, insert into DB
                    mDestinationDBHelper.insertDestination(db, dest.getString(FIELD_IDENTIFIER),
                            dest.getLong(FIELD_ARRIVAL), dest.getLong(FIELD_DEPARTURE),

                            city, region, country,

                            location.getDouble(FIELD_DETAILS_LOCATION_LAT),
                            location.getDouble(FIELD_DETAILS_LOCATION_LNG), presentsTotal, presents, timezone,
                            altitude, photos, weather, streetview, gmmStreetview);
                } catch (android.database.sqlite.SQLiteConstraintException e) {
                    // ignore duplicate locations
                }
            }

            db.setTransactionSuccessful();
            // Update mPreferences
            mPreferences.setDBTimestamp(System.currentTimeMillis());
            mPreferences.setTotalPresents(previousPresents);
            return i;
        } catch (JSONException e) {
            Log.d(TAG, "Santa location tracking error 30");
            SantaLog.d(TAG, "JSON Exception", e);
        } finally {
            db.endTransaction();
        }

        return 0;
    }

    private int processStream(JSONArray json, boolean isWear) {
        SQLiteDatabase db = mStreamDBHelper.getWritableDatabase();

        db.beginTransaction();
        try {
            // loop over each card

            int i;
            for (i = 0; i < json.length(); i++) {
                JSONObject card = json.getJSONObject(i);

                final long timestamp = card.getLong(FIELD_STREAM_TIMESTAMP);
                final String status = getExistingJSONString(card, FIELD_STREAM_STATUS);
                final String didYouKnow = getExistingJSONString(card, FIELD_STREAM_DIDYOUKNOW);
                final String imageUrl = getExistingJSONString(card, FIELD_STREAM_IMAGEURL);
                final String youtubeId = getExistingJSONString(card, FIELD_STREAM_YOUTUBEID);

                //                if (mDebugLog) {
                //                    Log.d(TAG, "Notification: " + timestamp);
                //                }

                try {
                    // All parsed, insert into DB
                    mStreamDBHelper.insert(db, timestamp, status, didYouKnow, imageUrl, youtubeId, isWear);
                } catch (android.database.sqlite.SQLiteConstraintException e) {
                    // ignore duplicate cards
                }
            }

            db.setTransactionSuccessful();
            return i;
        } catch (JSONException e) {
            Log.d(TAG, "Santa location tracking error 31");
            SantaLog.d(TAG, "JSON Exception", e);
        } finally {
            db.endTransaction();
        }

        return 0;
    }

    // Reads an InputStream and converts it to a String.
    protected static StringBuilder read(InputStream stream) throws IOException {
        BufferedReader reader;
        StringBuilder builder = new StringBuilder();
        reader = new BufferedReader(new InputStreamReader(stream, "UTF-8"));

        for (String line; (line = reader.readLine()) != null;) {
            builder.append(line);
        }

        return builder;
    }

    /**
     * Returns the value of the JSON field identified by the name. Returns null if the field does
     * not exist.
     */
    public static String getExistingJSONString(JSONObject json, String name) throws JSONException {
        if (json.has(name)) {
            return json.getString(name);
        } else {
            return null;
        }
    }

    /**
     * Returns the double of the JSON object identified by the name. Returns {@link
     * java.lang.Double#MAX_VALUE} if the field does not exist.
     */
    public static double getExistingJSONDouble(JSONObject json, String name) throws JSONException {
        if (json.has(name)) {
            return json.getDouble(name);
        } else {
            return Double.MAX_VALUE;
        }
    }

    interface APICallback {

        void onNewSwitchOffState(boolean isOn);

        /**
         * Called when a new fingerprint has been detected and stored data
         * will be cleared to process the new route.
         *
         * @see #onNewRouteLoaded()
         */
        void onNewFingerprint();

        void onNewOffset();

        /**
         * Called when new data has been processed.
         */
        void onNewRouteLoaded();

        void onNewStreamLoaded();

        void onNewNotificationStreamLoaded();

        void notifyRouteUpdating();

        void onNewCastState(boolean disableCast);

        void onNewGameState(GameDisabledState state);

        void onNewVideos(String video1, String video15, String video23);

        void onNewDestinationPhotoState(boolean disableDestinationPhoto);

        void onNewApiDataAvailable();
    }

}