/**
* StockService.java
* Copyright (C)2014 Nicholas Killewald
*
* This file is distributed under the terms of the BSD license.
* The source package should have a LICENCE file at the toplevel.
*/
package net.exclaimindustries.geohashdroid.services;
import android.content.Intent;
import android.os.Bundle;
import android.os.Parcelable;
import android.util.Log;
import com.commonsware.cwac.wakeful.WakefulIntentService;
import net.exclaimindustries.geohashdroid.util.Graticule;
import net.exclaimindustries.geohashdroid.util.HashBuilder;
import net.exclaimindustries.geohashdroid.util.HashBuilder.StockRunner;
import net.exclaimindustries.geohashdroid.util.Info;
import net.exclaimindustries.tools.AndroidUtil;
import java.io.Serializable;
import java.util.Calendar;
import java.util.LinkedList;
import java.util.List;
/**
* <p>
* StockService, whose wakefulness is made possible by the sleepless efforts of
* Mark "CommonsGuy" Murphy, handles all stock retrieval duties. You ask it for
* a stock, it'll later broadcast an Intent either with that stock or some
* error.
* </p>
*
* <p>
* This is going to be similar to the old StockService of ages past, but just
* the business end of it. The alarm is handled elsewhere, as well as all
* involved reschedule-if-not-connected tomfoolery. We'll report errors back,
* of course, so that any callers know what's going on, but otherwise we'll just
* try to get the stock and convert it to a hash (either to the web or just from
* the stock cache).
* </p>
*
* @author Nicholas Killewald
*/
public class StockService extends WakefulIntentService {
private static final String DEBUG_TAG = "StockService";
/**
* <p>
* Action to send out when you want stock data and the associated Info
* object for a Graticule and date. You want to make sure this at least has
* a Calendar for {@link #EXTRA_DATE}. If {@link #EXTRA_GRATICULE} is
* given, it'll look for an Info object for a single-Graticule expedition.
* If there isn't or the given Graticule is null, it'll assume it's a
* Globalhash.
* </p>
*
* <p>
* If the date is null or isn't a Calendar, or if the Graticule extra exists
* but isn't a Graticule object (null counts as a Graticule), StockService
* will ignore and discard the request, even if a request ID was sent.
* </p>
*/
public static final String ACTION_STOCK_REQUEST = "net.exclaimindustries.geohashdroid.STOCK_REQUEST";
/**
* Action that gets broadcast whenever StockService is returning a stock
* result. The intent will have a motley assortment of extras with it, each
* of which are mentioned in this class, most of which were supplied with
* the {@link #ACTION_STOCK_REQUEST} that started this.
*/
public static final String ACTION_STOCK_RESULT = "net.exclaimindustries.geohashdroid.STOCK_RESULT";
/**
* <p>
* Key for the extra stuff Bundle. This Bundle will contain all the needed
* Extras to put StockService together. This is needed because not all
* devices seem to apply the correct ClassLoader when dealing with Intents
* being sent across remote services (i.e. broadcasts), resulting in
* problems when custom Parcelables are used (i.e. Graticule and Info). A
* Bundle, on the other hand, doesn't try to unmarshall Parcelables until
* needed, and we can properly assign the ClassLoader then.
* </p>
*
* <p>
* Note that this also implies you should call {@link Bundle#setClassLoader(ClassLoader)}
* on this Bundle with whatever the current ClassLoader is any time you deal
* with data from StockService.
* </p>
*/
public static final String EXTRA_STUFF = "net.exclaimindustries.geohashdroid.EXTRA_STUFF";
/**
* Key for an ID extra on the response. This isn't actually used and is not
* required, but whatever is stored here (so long as it's a long) will be
* put in the broadcast Intent when done. If this isn't specified, it will
* come back as -1.
*/
public static final String EXTRA_REQUEST_ID = "net.exclaimindustries.geohashdroid.EXTRA_REQUEST_ID";
/**
* Key for additional flags in the request. For the most part, these don't
* change how the request is handled, like the request ID, and will simply
* be passed back in the resulting broadcast Intent. Some flags, however,
* like {@link #FLAG_INCLUDE_NEARBY_POINTS}, will add more data. This helps
* BroadcastReceivers know what led to this request, which can come in handy
* if there's some case where you want to ignore responses the came from,
* say, the stock alarm.
*/
public static final String EXTRA_REQUEST_FLAGS = "net.exclaimindustries.geohashdroid.EXTRA_REQUEST_FLAGS";
/**
* Key for additional flags in the response. These give additional info as
* to what happened during the request.
*/
public static final String EXTRA_RESPONSE_FLAGS = "net.exclaimindustries.geohashdroid.EXTRA_RESPONSE_FLAGS";
/**
* Key for a Graticule extra. This must be defined, though it can be null
* if you're requesting a Globalhash.
*/
public static final String EXTRA_GRATICULE = "net.exclaimindustries.geohashdroid.EXTRA_GRATICULE";
/**
* Key for a Calendar extra. This must be defined and not null. And a
* Calendar.
*/
public static final String EXTRA_DATE = "net.exclaimindustries.geohashdroid.EXTRA_DATE";
/**
* Key for an Info extra. This comes back in the broadcast. Note that the
* data will be null if there was an error.
*/
public static final String EXTRA_INFO = "net.exclaimindustries.geohashdroid.EXTRA_INFO";
/**
* Key for the response code extra. This will be an int.
*/
public static final String EXTRA_RESPONSE_CODE = "net.exclaimindustries.geohashdroid.EXTRA_RESPONSE_CODE";
/**
* Key for nearby points, if {@link #FLAG_INCLUDE_NEARBY_POINTS} was
* specified. This will be an array of Info objects. The order of the
* array is arbitrary. There will usually be eight elements in it, though
* there may be fewer if the request is either at the poles or in rare
* 30W-related cases.
*/
public static final String EXTRA_NEARBY_POINTS = "net.exclaimindustries.geohashdroid.EXTRA_NEARBY_POINTS";
/**
* Flag meaning this request came from the stock alarm around 9:30am EST.
* This is for pre-cache stuff.
*/
public static final int FLAG_ALARM = 0x1;
/**
* Flag meaning this request was manually initiated by the user. This is
* for if the user specifically wants a certain date or Graticule.
*/
public static final int FLAG_USER_INITIATED = 0x2;
/**
* Flag meaning this request was automatically initiated. This is used for
* odd cases where we need to make requests behind the user's back, like
* coming back in from preferences and we need to re-read the "place nearby
* flags" setting.
*/
public static final int FLAG_AUTO_INITIATED = 0x4;
/**
* Flag meaning this request is due to the user wanting to find the closest
* point to some location. As you can tell from the value, this will imply
* {@link #FLAG_INCLUDE_NEARBY_POINTS}.
*/
public static final int FLAG_FIND_CLOSEST = 0x28;
/**
* Flag meaning this request came from Select-A-Graticule mode. CentralMap
* should know what to do with it.
*/
public static final int FLAG_SELECT_A_GRATICULE = 0x10;
/**
* Flag meaning that, in addition to the point requested, the (up to) eight
* surrounding points should also be included in the response.
*/
public static final int FLAG_INCLUDE_NEARBY_POINTS = 0x20;
/**
* Flag meaning this response was found in the cache. If not set, it was
* either found on the web or it wasn't found at all, the latter of which
* implying you really ought to have checked the response code first.
*/
public static final int FLAG_CACHED = 0x1;
/** All okay response. */
public static final int RESPONSE_OKAY = 0;
/** Error response if the requested stock wasn't posted yet. */
public static final int RESPONSE_NOT_POSTED_YET = -1;
/**
* Error response if we needed to go to the network for a stock lookup, but
* we didn't have any network connection at all.
*/
public static final int RESPONSE_NO_CONNECTION = -2;
/** Error response if there was some network error involved. */
public static final int RESPONSE_NETWORK_ERROR = -3;
public StockService() {
super("StockService");
}
@Override
protected void doWakefulWork(Intent intent) {
// Gee, thanks, WakefulIntentService, for covering all that confusing
// WakeLock stuff! You're even off the main thread, too, so I don't
// have to spawn a new thread to not screw up the UI! So let's get that
// data right in hand, shall we?
if(!intent.hasExtra(EXTRA_DATE)) {
Log.e(DEBUG_TAG, "BAILING OUT: There's no date!");
return;
}
// Maybe we have a request ID!
long requestId = intent.getLongExtra(EXTRA_REQUEST_ID, -1L);
// Maybe we have flags!
int flags = intent.getIntExtra(EXTRA_REQUEST_FLAGS, 0);
// Maybe we'll respond with flags!
int respFlags = 0;
// Oh, man, can we ever parcelize a Graticule!
Parcelable p = intent.getParcelableExtra(EXTRA_GRATICULE);
// Remember, the Graticule MIGHT be null if it's a globalhash.
if(p != null && !(p instanceof Graticule)) {
Log.e(DEBUG_TAG, "BAILING OUT: p is not null and isn't a Graticule!");
return;
}
Graticule graticule = (Graticule)p;
// Calendar, well, we can't parcelize that, but we CAN serialize it,
// which is almost as good!
Serializable s = intent.getSerializableExtra(EXTRA_DATE);
if(s == null || !(s instanceof Calendar)) {
Log.e(DEBUG_TAG, "BAILING OUT: s is null or not a Calendar!");
return;
}
Calendar cal = (Calendar)s;
// First, ask the stock cache if we've got an Info we can throw back.
Info info = HashBuilder.getStoredInfo(this, cal, graticule);
// If we got something, great! Broadcast it right on out!
if(info != null) {
respFlags |= FLAG_CACHED;
Info[] nearby = null;
if((flags & FLAG_INCLUDE_NEARBY_POINTS) != 0)
nearby = getNearbyPoints(cal, graticule);
dispatchIntent(RESPONSE_OKAY, requestId, flags, respFlags, cal, graticule, info, nearby);
} else {
// Otherwise, we need to go to the web.
if(!AndroidUtil.isConnected(this)) {
// ...if we CAN go to the web, that is.
Log.i(DEBUG_TAG, "We're not connected, stopping now.");
dispatchIntent(RESPONSE_NO_CONNECTION, requestId, flags, respFlags, cal, graticule, null, null);
} else {
StockRunner runner = HashBuilder.requestStockRunner(this, cal, graticule);
runner.runStock();
// And the results are in!
int result = runner.getStatus();
switch(result) {
case HashBuilder.StockRunner.ALL_OKAY:
// Hooray! We win! Dispatch an intent with the info.
Log.d(DEBUG_TAG, "Stock's good! Away it goes!");
Info[] nearby = null;
if((flags & FLAG_INCLUDE_NEARBY_POINTS) != 0)
nearby = getNearbyPoints(cal, graticule);
dispatchIntent(RESPONSE_OKAY, requestId, flags, respFlags, cal, graticule, runner.getLastResultObject(), nearby);
break;
case HashBuilder.StockRunner.ERROR_NOT_POSTED:
// Aw. It's not posted yet.
Log.d(DEBUG_TAG, "Stock isn't posted yet.");
dispatchIntent(RESPONSE_NOT_POSTED_YET, requestId, flags, respFlags, cal, graticule, null, null);
break;
default:
// In all other cases, just assume it's a network error.
// We either got ERROR_NETWORK, which is just that, or
// we got IDLE, BUSY, or ABORTED, none of which make any
// sense in this context, which means something went
// horribly, horribly wrong.
Log.e(DEBUG_TAG, "Network error!");
dispatchIntent(RESPONSE_NETWORK_ERROR, requestId, flags, respFlags, cal, graticule, null, null);
}
}
}
}
private void dispatchIntent(int responseCode, long requestId, int flags, int respFlags, Calendar date, Graticule graticule, Info info, Info[] nearby) {
// Welcome to central Intent dispatch. How may I help you?
Intent intent = new Intent(ACTION_STOCK_RESULT);
// Stuff all the extras into a Bundle. There's ClassLoader issues on
// some devices that require us to do it this way (see comments on
// EXTRA_STUFF).
Bundle bun = new Bundle();
bun.putInt(EXTRA_RESPONSE_CODE, responseCode);
bun.putLong(EXTRA_REQUEST_ID, requestId);
bun.putInt(EXTRA_REQUEST_FLAGS, flags);
bun.putInt(EXTRA_RESPONSE_FLAGS, respFlags);
bun.putSerializable(EXTRA_DATE, date);
bun.putParcelable(EXTRA_GRATICULE, graticule);
bun.putParcelable(EXTRA_INFO, info);
if(nearby != null && nearby.length != 0) {
bun.putParcelableArray(EXTRA_NEARBY_POINTS, nearby);
}
intent.putExtra(EXTRA_STUFF, bun);
// And away it goes!
Log.d(DEBUG_TAG, "Dispatching intent...");
sendBroadcast(intent);
}
private Info[] getNearbyPoints(Calendar cal, Graticule g) {
if(g == null) return new Info[0];
List<Info> infos = new LinkedList<>();
// Hopefully, each nearby point is available. In addition to cases
// involving the poles, I *think* there's cases where a 30W point IS
// available, but a neighboring non-30W point ISN'T. We'll just ignore
// those cases.
for(int i = -1; i <= 1; i++) {
for(int j = -1; j <= 1; j++) {
// Zero and zero isn't a nearby point, that's the very point
// we're at right now!
if(i == 0 && j == 0) continue;
// If the user's truly adventurous enough to go to the 90N/S
// graticules, there aren't any nearby points north/south of
// where they are. Also, the nearby points aren't going to
// be drawn anyway due to the projection, but hey, that's
// nitpicking.
if(Math.abs((g.isSouth() ? -1 : 1) * g.getLatitude() + i) > 90)
continue;
// Make a new Graticule, properly offset...
Graticule offset = Graticule.createOffsetFrom(g, i, j);
// ...then do the request. Check the cache first!
Info info = HashBuilder.getStoredInfo(this, cal, offset);
if(info == null) {
// It's not in the cache. Try to make it be in the cache.
StockRunner runner = HashBuilder.requestStockRunner(this, cal, offset);
runner.runStock();
if(runner.getStatus() == HashBuilder.StockRunner.ALL_OKAY) {
// We've got a winner!
info = runner.getLastResultObject();
}
// We'll just ignore it if not. The user doesn't need to be
// bugged about cache failures or whatnot, they already got
// what they were looking for.
}
// Now, add that to the array, if it's not null...
if(info != null)
infos.add(info);
// And continue on!
}
}
Info[] toReturn = new Info[8];
return infos.toArray(toReturn);
}
}