moved RaceLogPoller into a service

This commit is contained in:
Peter Siegmund
2016-06-21 16:33:28 +02:00
parent 96061ce5ca
commit e7dbe303d7
6 changed files with 275 additions and 215 deletions
@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.sap.sailing.racecommittee.app"
android:versionCode="19"
android:versionName="1.11">
android:versionCode="19"
android:versionName="1.11">
<uses-sdk
android:minSdkVersion="14"
@@ -51,20 +51,20 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="*"
android:scheme="http"
android:pathPattern="/apps/com.sap.sailing.racecommittee.app.apk"/>
android:pathPattern="/apps/com.sap.sailing.racecommittee.app.apk"
android:scheme="http" />
<data
android:host="*"
android:scheme="https"
android:pathPattern="apps/com.sap.sailing.racecommittee.app.apk"/>
android:pathPattern="apps/com.sap.sailing.racecommittee.app.apk"
android:scheme="https" />
</intent-filter>
</activity>
<activity android:name=".ui.activities.RacingActivity" />
@@ -80,6 +80,14 @@
</activity>
<activity android:name=".ui.activities.SystemInformationActivity" />
<service android:name=".services.polling.RaceLogPollerService">
<intent-filter>
<action android:name="com.sap.sailing.racecommittee.app.action.stopPoller" />
<action android:name="com.sap.sailing.racecommittee.app.action.addRace" />
<action android:name="com.sap.sailing.racecommittee.app.action.removeRace" />
<action android:name="com.sap.sailing.racecommittee.app.action.poll" />
</intent-filter>
</service>
<service
android:name=".services.RaceStateService"
android:enabled="true"
@@ -33,6 +33,11 @@ public class AppConstants {
public final static String INTENT_ACTION_RELOAD_RACES = INTENT_ACTION + ".reloadRaces";
public final static String INTENT_ACTION_START_PROCEDURE_SPECIFIC_ACTION = INTENT_ACTION + ".startProcedureSpecificAction";
public final static String INTENT_ACTION_POLLER_STOP = ".stopPoller";
public final static String INTENT_ACTION_POLLER_RACE_ADD = ".addRace";
public final static String INTENT_ACTION_POLLER_RACE_REMOVE = ".removeRace";
public final static String INTENT_ACTION_POLLER_POLL = ".poll";
// Login activity
public final static String EventIdTag = "EventId";
@@ -1,6 +1,5 @@
package com.sap.sailing.racecommittee.app.services;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
@@ -40,7 +39,7 @@ import com.sap.sailing.racecommittee.app.data.DataManager;
import com.sap.sailing.racecommittee.app.data.DataStore;
import com.sap.sailing.racecommittee.app.data.ReadonlyDataManager;
import com.sap.sailing.racecommittee.app.domain.ManagedRace;
import com.sap.sailing.racecommittee.app.services.polling.RaceLogPoller;
import com.sap.sailing.racecommittee.app.services.polling.RaceLogPollerService;
import com.sap.sailing.racecommittee.app.services.sending.RaceEventSender;
import com.sap.sailing.racecommittee.app.ui.activities.LoginActivity;
import com.sap.sailing.server.gateway.serialization.JsonSerializer;
@@ -73,8 +72,6 @@ public class RaceStateService extends Service {
private ReadonlyDataManager dataManager;
private RaceLogPoller poller;
private Map<ManagedRace, RaceLogEventVisitor> registeredLogListeners;
private Map<ManagedRace, RaceStateEventScheduler> registeredStateEventSchedulers;
@@ -89,8 +86,6 @@ public class RaceStateService extends Service {
this.alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
this.dataManager = DataManager.create(this);
this.poller = new RaceLogPoller(this);
this.registeredLogListeners = new HashMap<>();
this.registeredStateEventSchedulers = new HashMap<>();
this.managedIntents = new HashMap<>();
@@ -145,7 +140,9 @@ public class RaceStateService extends Service {
}
private void unregisterAllRaces() {
poller.unregisterAllAndStop();
Intent intent = new Intent(this, RaceLogPollerService.class);
intent.setAction(AppConstants.INTENT_ACTION_POLLER_STOP);
startService(intent);
for (Entry<ManagedRace, RaceLogEventVisitor> entry : registeredLogListeners.entrySet()) {
entry.getKey().getState().getRaceLog().removeListener(entry.getValue());
@@ -168,7 +165,10 @@ public class RaceStateService extends Service {
}
private void unregisterRace(ManagedRace race) {
poller.unregister(race);
Intent intent = new Intent(this, RaceLogPollerService.class);
intent.setAction(AppConstants.INTENT_ACTION_POLLER_RACE_REMOVE);
intent.putExtra(AppConstants.INTENT_ACTION_EXTRA, race.getId());
startService(intent);
race.getState().getRaceLog().removeAllListeners();
registeredLogListeners.remove(race);
@@ -349,7 +349,10 @@ public class RaceStateService extends Service {
state.setStateEventScheduler(stateEventScheduler);
// ... and register for polling!
poller.register(race);
Intent intent = new Intent(this, RaceLogPollerService.class);
intent.setAction(AppConstants.INTENT_ACTION_POLLER_RACE_ADD);
intent.putExtra(AppConstants.INTENT_ACTION_EXTRA, race.getId());
startService(intent);
registeredLogListeners.put(race, logListener);
registeredStateEventSchedulers.put(race, stateEventScheduler);
@@ -365,7 +368,7 @@ public class RaceStateService extends Service {
private PendingIntent createAlarmPendingIntent(ManagedRace managedRace, RaceStateEvent event) {
Intent intent = new Intent(this, RaceStateService.class);
intent.setAction(AppConstants.INTENT_ACTION_ALARM_ACTION);
intent.putExtra(EXTRAS_SERVICE_ID, serviceId);
intent.putExtra(EXTRAS_SERVICE_ID, serviceId.toString());
intent.putExtra(AppConstants.RACE_ID_KEY, managedRace.getId());
intent.putExtra(AppConstants.EXTRAS_RACE_STATE_EVENT, event);
return PendingIntent.getService(this, alarmManagerRequestCode++, intent, PendingIntent.FLAG_UPDATE_CURRENT);
@@ -389,6 +392,7 @@ public class RaceStateService extends Service {
for (Pair<PendingIntent, RaceStateEvents> intentPair : intents) {
if (intentPair.second.equals(stateEventName)) {
toBeRemoved = intentPair;
break;
}
}
if (toBeRemoved != null) {
@@ -401,11 +405,10 @@ public class RaceStateService extends Service {
}
public void clearAllAlarms(ManagedRace race) {
Serializable raceId = race.getId();
List<Pair<PendingIntent, RaceStateEvents>> intents = managedIntents.get(raceId);
List<Pair<PendingIntent, RaceStateEvents>> intents = managedIntents.get(race.getId());
if (intents == null) {
ExLog.w(this, TAG, "There are no intents for race " + raceId);
ExLog.w(this, TAG, "There are no intents for race " + race.getId());
return;
}
@@ -414,6 +417,6 @@ public class RaceStateService extends Service {
}
intents.clear();
ExLog.w(this, TAG, "All intents cleared for race " + raceId);
ExLog.w(this, TAG, "All intents cleared for race " + race.getId());
}
}
@@ -1,175 +0,0 @@
package com.sap.sailing.racecommittee.app.services.polling;
import java.io.InputStream;
import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import com.sap.sailing.android.shared.logging.ExLog;
import com.sap.sailing.android.shared.services.sending.MessageSendingService;
import com.sap.sailing.racecommittee.app.AppPreferences;
import com.sap.sailing.racecommittee.app.AppPreferences.PollingActiveChangedListener;
import com.sap.sailing.racecommittee.app.domain.ManagedRace;
import com.sap.sailing.racecommittee.app.domain.racelog.impl.RaceLogEventsCallback;
import com.sap.sailing.racecommittee.app.services.polling.RaceLogPollerTask.PollingResultListener;
import com.sap.sse.common.Util;
/**
* <p>
* Polls for server-side race log changes
* </p>
*
* <p>
* There is no multi-threading involved, everything will be done on the UI thread.
* </p>
*/
public class RaceLogPoller implements PollingActiveChangedListener {
protected static final String TAG = RaceLogPoller.class.getName();
private final Context context;
private final Handler pollingHandler;
private final PollingWorker pollingWorker;
private final Map<ManagedRace, URL> races;
private final AppPreferences appPreferences;
private boolean hasRacesToPoll;
public RaceLogPoller(Context context) {
this.context = context;
// We want to use the main (UI) loop
this.pollingHandler = new Handler(Looper.getMainLooper());
this.pollingWorker = new PollingWorker(this, context);
this.races = new HashMap<>();
this.appPreferences = AppPreferences.on(context);
this.appPreferences.registerPollingActiveChangedListener(this);
this.hasRacesToPoll = false;
}
public void register(ManagedRace race) {
try {
races.put(race, createURL(race));
// remove pending pollingWorker, to ensure that we only have one at once
pollingHandler.removeCallbacks(pollingWorker);
long pollingInterval = getPollingIntervalInMs();
pollingHandler.postDelayed(pollingWorker, pollingInterval);
this.hasRacesToPoll = true;
ExLog.i(context, TAG, String.format("Registered race %s for polling, will start in %d milliseconds.", race.getId(),
pollingInterval));
} catch (MalformedURLException | UnsupportedEncodingException e) {
ExLog.e(context, TAG, String.format("Unable to create polling URL for race %s: %s", race.getId(), e.getMessage()));
}
}
public void unregister(ManagedRace race) {
races.remove(race);
}
private URL createURL(ManagedRace race) throws MalformedURLException, UnsupportedEncodingException {
return new URL(MessageSendingService.getRaceLogEventSendAndReceiveUrl(context, race.getRaceGroup().getName(),
race.getName(), race.getFleet().getName()));
}
public void unregisterAllAndStop() {
hasRacesToPoll = false;
pollingHandler.removeCallbacksAndMessages(null);
races.clear();
appPreferences.unregisterPollingActiveChangedListener(this);
ExLog.i(context, TAG, "Polling will be stopped.");
}
protected boolean isPollingActive() {
return hasRacesToPoll && appPreferences.isPollingActive();
}
protected long getPollingIntervalInMs() {
return appPreferences.getPollingInterval() * 1000;
}
@Override
public void onPollingActiveChanged(boolean isActive) {
if (isActive) {
long pollingInterval = getPollingIntervalInMs();
pollingHandler.postDelayed(pollingWorker, pollingInterval);
ExLog.i(context, TAG, String.format("Polling has been activated, will start in %d milliseconds.", pollingInterval));
} else {
ExLog.i(context, TAG, "Polling has been deactivated, next polling attempt will be aborted.");
}
}
/**
* Will be run on the main (UI) thread!
*/
private static class PollingWorker implements Runnable, PollingResultListener {
private final RaceLogPoller poller;
private final RaceLogEventsCallback processor;
private RaceLogPollerTask task;
private Context context;
public PollingWorker(RaceLogPoller poller, Context context) {
this.poller = poller;
this.processor = new RaceLogEventsCallback();
this.context = context;
}
@SuppressWarnings("unchecked")
@Override
public void run() {
ExLog.i(context, TAG, "Polling for server-side race log changes...");
if (!poller.isPollingActive()) {
ExLog.i(context, TAG, "Polling aborted.");
return;
}
List<Util.Pair<Serializable, URL>> queries = getPollingQueries();
task = new RaceLogPollerTask(this, context);
task.execute(queries.toArray(new Util.Pair[0]));
}
@Override
public void onPollingResult(PollingResult result) {
if (!poller.isPollingActive()) {
task.cancel(true);
ExLog.i(context, TAG, "Polling aborted.");
return;
}
if (result.isSuccess) {
Serializable raceId = result.resultStreamForRaceId.getA();
InputStream responseStream = result.resultStreamForRaceId.getB();
processor.processResponse(poller.context, responseStream, raceId.toString());
} else {
ExLog.i(context, TAG, "Polling attempt not successful.");
}
}
@Override
public void onPollingFinished() {
if (!poller.isPollingActive()) {
ExLog.i(context, TAG, "Polling aborted.");
return;
}
long pollingInterval = poller.getPollingIntervalInMs();
ExLog.i(context, TAG, String.format("Polling done. Will poll again in %d milliseconds.", pollingInterval));
poller.pollingHandler.postDelayed(this, pollingInterval);
}
private List<Util.Pair<Serializable, URL>> getPollingQueries() {
List<Util.Pair<Serializable, URL>> queries = new ArrayList<Util.Pair<Serializable,URL>>();
for (Entry<ManagedRace, URL> entry : poller.races.entrySet()) {
queries.add(new Util.Pair<Serializable, URL>(entry.getKey().getId(), entry.getValue()));
}
return queries;
}
}
}
@@ -0,0 +1,223 @@
package com.sap.sailing.racecommittee.app.services.polling;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Intent;
import android.os.AsyncTask;
import android.os.Build;
import android.os.IBinder;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import com.sap.sailing.android.shared.logging.ExLog;
import com.sap.sailing.android.shared.services.sending.MessageSendingService;
import com.sap.sailing.racecommittee.app.AppConstants;
import com.sap.sailing.racecommittee.app.AppPreferences;
import com.sap.sailing.racecommittee.app.data.DataManager;
import com.sap.sailing.racecommittee.app.data.DataStore;
import com.sap.sailing.racecommittee.app.domain.ManagedRace;
import com.sap.sse.common.Util;
import com.sap.sse.common.impl.MillisecondsTimePoint;
public class RaceLogPollerService extends Service implements AppPreferences.PollingActiveChangedListener, RaceLogPollerTask.PollingResultListener {
private static final String TAG = RaceLogPollerService.class.getName();
private AlarmManager mAlarm;
private PendingIntent mPendingIntent;
private AppPreferences mAppPreferences;
private DataStore mDataStore;
private Map<ManagedRace, URL> mRaces;
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public void onCreate() {
mAlarm = (AlarmManager) getSystemService(ALARM_SERVICE);
mAppPreferences = AppPreferences.on(this);
mAppPreferences.registerPollingActiveChangedListener(this);
mRaces = new HashMap<>();
mDataStore = DataManager.create(this).getDataStore();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (intent == null) {
ExLog.i(this, TAG, "Restarted");
} else {
String action = intent.getAction();
String extra = null;
if (intent.getExtras() != null) {
extra = intent.getExtras().getString(AppConstants.INTENT_ACTION_EXTRA);
}
switch (action) {
case AppConstants.INTENT_ACTION_POLLER_STOP:
stopSelf();
break;
case AppConstants.INTENT_ACTION_POLLER_RACE_ADD:
if (TextUtils.isEmpty(extra)) {
ExLog.i(this, TAG, "INTENT_ACTION_EXTRA was null for " + action);
} else {
registerRace(extra);
}
break;
case AppConstants.INTENT_ACTION_POLLER_RACE_REMOVE:
if (TextUtils.isEmpty(extra)) {
ExLog.i(this, TAG, "INTENT_ACTION_EXTRA was null for " + action);
} else {
unregisterRace(extra);
}
break;
case AppConstants.INTENT_ACTION_POLLER_POLL:
poll();
break;
}
}
return super.onStartCommand(intent, flags, startId);
}
@Override
public void onDestroy() {
ExLog.i(this, TAG, "onDestroy");
mRaces.clear();
if (mAppPreferences != null) {
mAppPreferences.unregisterPollingActiveChangedListener(this);
}
if (mAlarm != null && mPendingIntent != null) {
mAlarm.cancel(mPendingIntent);
}
}
@Override
public void onPollingActiveChanged(boolean isActive) {
if (isActive) {
scheduleNextPoll();
ExLog.i(this, TAG, "Polling has been activated, will start in " + mAppPreferences.getPollingInterval() + " seconds.");
} else {
ExLog.i(this, TAG, "Polling has been deactivated, next polling attempt will be aborted.");
}
}
@Override
public void onPollingFinished() {
if (mRaces.size() > 0) {
scheduleNextPoll();
} else {
stopSelf();
}
}
/**
* calculates for the given race id the poll url and add it to the map
*
* @param raceId race id
*/
private void registerRace(String raceId) {
ExLog.i(this, TAG, "registerRace: " + raceId);
ManagedRace race = getManagedRace(raceId);
if (race != null) {
try {
mRaces.put(race, createURL(race));
} catch (MalformedURLException | UnsupportedEncodingException e) {
ExLog.e(this, TAG, String.format("Unable to create polling URL for race %s: %s", race.getId(), e.getMessage()));
}
scheduleNextPoll();
}
}
/**
* Remove the {@link ManagedRace}, so it will no longer be part of the polling
*
* @param raceId race id
*/
private void unregisterRace(String raceId) {
ExLog.i(this, TAG, "unregisterRace: " + raceId);
ManagedRace race = getManagedRace(raceId);
if (race != null) {
if (mRaces.containsKey(race)) {
mRaces.remove(race);
scheduleNextPoll();
}
}
}
/**
* Find the {@link ManagedRace} for the given race id in the {@link DataStore}
*
* @param raceId race id
* @return {@link ManagedRace} if found, else null
*/
private ManagedRace getManagedRace(String raceId) {
ManagedRace result = null;
for (ManagedRace race : mDataStore.getRaces()) {
if (race.getId().equals(raceId)) {
result = race;
break;
}
}
return result;
}
private URL createURL(ManagedRace race) throws MalformedURLException, UnsupportedEncodingException {
return new URL(MessageSendingService.getRaceLogEventSendAndReceiveUrl(this, race.getRaceGroup().getName(),
race.getName(), race.getFleet().getName()));
}
private void poll() {
ExLog.i(this, TAG, "Polling for server-side race log changes...");
if (mAppPreferences.isPollingActive()) {
List<Util.Pair<String, URL>> queries = getPollingQueries();
RaceLogPollerTask task = new RaceLogPollerTask(this, this);
@SuppressWarnings("unchecked")
Util.Pair<String, URL>[] param = queries.toArray(new Util.Pair[queries.size()]);
task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, param);
}
}
private List<Util.Pair<String, URL>> getPollingQueries() {
List<Util.Pair<String, URL>> queries = new ArrayList<>();
for (Map.Entry<ManagedRace, URL> entry : mRaces.entrySet()) {
queries.add(new Util.Pair<>(entry.getKey().getId(), entry.getValue()));
}
return queries;
}
/**
* calculate the new alarm for polling with current polling interval from preferences
*/
private void scheduleNextPoll() {
if (mPendingIntent != null) {
mAlarm.cancel(mPendingIntent);
}
if (mAppPreferences.isPollingActive() && mRaces.size() >= 0) {
long time = MillisecondsTimePoint.now().asMillis() + (1000 * mAppPreferences.getPollingInterval());
Intent intent = new Intent(this, this.getClass());
intent.setAction(AppConstants.INTENT_ACTION_POLLER_POLL);
mPendingIntent = PendingIntent.getService(this, 0, intent, 0);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
mAlarm.setExact(AlarmManager.RTC_WAKEUP, time, mPendingIntent);
} else {
mAlarm.set(AlarmManager.RTC_WAKEUP, time, mPendingIntent);
}
}
}
}
@@ -9,21 +9,21 @@ import android.os.AsyncTask;
import com.sap.sailing.android.shared.data.http.HttpJsonPostRequest;
import com.sap.sailing.android.shared.data.http.HttpRequest;
import com.sap.sailing.racecommittee.app.domain.racelog.impl.RaceLogEventsCallback;
import com.sap.sse.common.Util;
public class RaceLogPollerTask extends AsyncTask<Util.Pair<String, URL>, PollingResult, Void> {
public interface PollingResultListener {
public void onPollingResult(PollingResult result);
public void onPollingFinished();
void onPollingFinished();
}
private final PollingResultListener listener;
private final Context context;
private final PollingResultListener mListener;
private final Context mContext;
public RaceLogPollerTask(PollingResultListener listener, Context context) {
this.listener = listener;
this.context = context;
mListener = listener;
mContext = context.getApplicationContext();
}
@SuppressWarnings("unchecked")
@@ -34,30 +34,26 @@ public class RaceLogPollerTask extends AsyncTask<Util.Pair<String, URL>, Polling
return null;
}
HttpRequest request = new HttpJsonPostRequest(context, query.getB());
InputStream responseStream = null;
HttpRequest request = new HttpJsonPostRequest(mContext, query.getB());
InputStream responseStream;
try {
responseStream = request.execute();
publishProgress(new PollingResult(true,
new Util.Pair<String, InputStream>(query.getA(), responseStream)));
String raceId = query.getA();
new RaceLogEventsCallback().processResponse(mContext, responseStream, raceId);
} catch (IOException e) {
// don't need to close responseStream as it still must
// be null because the only call that may throw an
// IOException is the execute() call that would have
// initialized responseStream
publishProgress(new PollingResult(false, null));
}
}
return null;
}
@Override
protected void onProgressUpdate(PollingResult... values) {
listener.onPollingResult(values[0]);
}
@Override
protected void onPostExecute(Void result) {
listener.onPollingFinished();
if (mListener != null) {
mListener.onPollingFinished();
}
}
}