package com.zachklipp.captivate.service;
import com.zachklipp.captivate.BuildConfig;
import com.zachklipp.captivate.Preferences;
import com.zachklipp.captivate.app.ConnectedNotification;
import com.zachklipp.captivate.captive_portal.HttpResponseContentsDetector;
import com.zachklipp.captivate.captive_portal.PortalDetector;
import com.zachklipp.captivate.captive_portal.PortalDetector.OverrideMode;
import com.zachklipp.captivate.captive_portal.PortalInfo;
import com.zachklipp.captivate.state_machine.PortalStateMachine;
import com.zachklipp.captivate.state_machine.PortalStateMachine.State;
import com.zachklipp.captivate.state_machine.PortalStateMachine.StorageBackendFactory;
import com.zachklipp.captivate.state_machine.PortalStateMachineStorageBackend;
import com.zachklipp.captivate.state_machine.StateMachineStorage;
import com.zachklipp.captivate.state_machine.StateMachineStorage.StorageBackend;
import com.zachklipp.captivate.state_machine.TransitionEvent;
import com.zachklipp.captivate.util.Log;
import com.zachklipp.captivate.util.Observable;
import com.zachklipp.captivate.util.Observer;
import com.zachklipp.captivate.util.WifiHelper;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
public class PortalDetectorService extends StickyIntentService
implements Observer<TransitionEvent>
{
private static final String STRING_NAMESPACE = "com.zachklipp.captivate.";
private static final String INTENT_NAMESPACE = STRING_NAMESPACE + "intent.";
/**
* If set to true when starting service, the actual state of any wifi connection isn't checked.
*/
static final String EXTRA_ASSUME_WIFI_CONNECTED = INTENT_NAMESPACE + "EXTRA_ASSUME_WIFI_CONNECTED";
/**
* Permission required to receive broadcast intents when the portal state changes.
*/
public static final String PERMISSION_ACCESS_PORTAL_STATE = STRING_NAMESPACE + "permission.ACCESS_PORTAL_STATE";
/**
* Action for intents broadcast when portal state changes.
*/
public static final String ACTION_PORTAL_STATE_CHANGED = INTENT_NAMESPACE + "ACTION_PORTAL_STATE_CHANGED";
public static final String EXTRA_PORTAL_STATE = INTENT_NAMESPACE + "EXTRA_PORTAL_STATE";
public static final String EXTRA_PORTAL_URL = INTENT_NAMESPACE + "EXTRA_PORTAL_URL";
public static final String EXTRA_FAVICON_URL = INTENT_NAMESPACE + "EXTRA_FAVICON_URL";
//private static PortalDetector sSeedPortalDetector = HttpResponseContentsDetector.createDetector();
private static PortalDetector.Factory sPortalDetectorFactory
= new PortalDetector.Factory()
{
@Override
public PortalDetector create()
{
return HttpResponseContentsDetector.createDetector();
}
};
private static StorageBackendFactory sStorageBackendFactory
= new StorageBackendFactory()
{
@Override
public StorageBackend create(Context context, PortalDetector detector)
{
return new PortalStateMachineStorageBackend(context, detector);
}
};
/*
* Set the detector to be used when the service is next started.
*/
public static void setPortalDetectorFactory(PortalDetector.Factory factory)
{
sPortalDetectorFactory = factory;
}
/*
* Set the storage backend to be used when the service is next started.
*/
public static void setStorageBackendFactory(StorageBackendFactory factory)
{
sStorageBackendFactory = factory;
}
public static ComponentName startService(Context context)
{
return context.startService(createStartServiceIntent(context));
}
/*
* Helper to send the necessary intent to start this service.
*/
public static ComponentName startService(Context context, boolean assumeWifiConnected)
{
Intent intent = createStartServiceIntent(context);
intent.putExtra(EXTRA_ASSUME_WIFI_CONNECTED, assumeWifiConnected);
return context.startService(intent);
}
private Preferences mPreferences;
private PortalDetector mPortalDetector;
private PortalStateMachine mStateMachine;
private AlarmManager mAlarmManager;
private boolean mSessionTimeoutCheckEnabled = false;
private PendingIntent mSessionTimeoutCheckPendingIntent;
private final BroadcastReceiver mScreenOnReceiver = new BroadcastReceiver()
{
@Override
public void onReceive(Context context, Intent intent)
{
context.startService(createStartServiceIntent(context));
}
};
/**
* @see Preferences.SIGNIN_CHECK_SECONDS_DEFAULT
*/
private final Runnable mSigninCheckRunnable = new Runnable()
{
@Override
public void run()
{
// Called from a handler on this service's worker thread, so we don't
// need to call startService().
onHandleIntent(createStartServiceIntent(PortalDetectorService.this));
}
};
public PortalDetectorService()
{
super("PortalMonitorThread");
}
@Override
public void onCreate()
{
mPreferences = Preferences.getPreferences(this);
mPortalDetector = sPortalDetectorFactory.create();
assert(mPortalDetector != null);
Log.d("Using portal detector " + mPortalDetector.getClass().getName());
mStateMachine = (PortalStateMachine) new StateMachineStorage(
sStorageBackendFactory.create(getApplicationContext(), mPortalDetector))
.loadOrCreate();
mStateMachine.addObserver(this);
mAlarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
mSessionTimeoutCheckPendingIntent = PendingIntent.getService(
this, 0, createStartServiceIntent(this), PendingIntent.FLAG_CANCEL_CURRENT);
// Called at end so thread doesn't get started before we're initialized
super.onCreate();
}
@Override
protected void onHandleIntent(Intent intent)
{
boolean assumeWifiConnected = BuildConfig.DEBUG;
if (mPreferences.isEnabled())
{
if (null != intent && intent.hasExtra(EXTRA_ASSUME_WIFI_CONNECTED))
{
assumeWifiConnected = intent.getBooleanExtra(EXTRA_ASSUME_WIFI_CONNECTED, assumeWifiConnected);
}
checkForPortal(assumeWifiConnected);
}
else
{
Log.d("Service started, but disabled, so doing nothing.");
mStateMachine.onDisabled();
}
}
private void checkForPortal(boolean assumeWifiConnected)
{
updateDetectorOverrideFromPrefs();
Log.d("Service updating portal status...");
if (assumeWifiConnected || WifiHelper.isWifiConnectedFromContext(this))
{
if (assumeWifiConnected)
{
Log.d("Intent suggests wifi is connected, going with that");
}
mPortalDetector.checkForPortal();
scheduleSigninCheckIfBlocked();
}
else
{
Log.d("Wifi not connected, reporting no portal");
mStateMachine.onNoWifi();
}
// If there's no portal, we aren't checking anything periodically so
// we can shutdown.
if (!mStateMachine.getCurrentPortalState().isBehindPortal())
{
Log.d("Not behind a portal, stopping detector service");
stopSelf();
}
}
@Override
public void update(Observable<TransitionEvent> observable, TransitionEvent event)
{
scheduleSessionTimeoutCheckIfNecessary();
updateNotification();
sendStateChangedBroadcast();
}
private void updateDetectorOverrideFromPrefs()
{
mPortalDetector.setPortalOverride(
mPreferences.isDebugOverrideEnabled() ? OverrideMode.ALWAYS_DETECT : OverrideMode.NONE);
}
private void scheduleSessionTimeoutCheckIfNecessary()
{
if (mStateMachine.getCurrentPortalState().isBehindPortal())
{
registerSessionTimeoutCheckReceiver();
}
else
{
unregisterSessionTimeoutCheckReceiver();
}
}
private void scheduleSigninCheckIfBlocked()
{
int refreshInterval = mPreferences.getSigninCheckSeconds();
if (mStateMachine.getCurrentPortalState().isBlocked())
{
Log.i("Scheduling state refresh in %d seconds", refreshInterval);
getHandler().postDelayed(mSigninCheckRunnable, refreshInterval * 1000);
}
}
@Override
public void onDestroy()
{
super.onDestroy();
unregisterSessionTimeoutCheckReceiver();
Log.d("PortalDetectorService destroyed.");
}
private static Intent createStartServiceIntent(Context context)
{
return new Intent(context, PortalDetectorService.class);
}
private void updateNotification()
{
if (State.SIGNIN_REQUIRED == mStateMachine.getCurrentState())
{
ConnectedNotification.showNotification(this, mPortalDetector.getPortal());
}
else
{
ConnectedNotification.hideNotification(this);
}
}
private void sendStateChangedBroadcast()
{
State state = (State) mStateMachine.getCurrentState();
PortalInfo portal = mPortalDetector.getPortal();
Intent intent = createStateChangedBroadcastIntent(state, portal);
Log.i("Broadcasting portal state change. new state=%s, portal=%s", state, portal);
sendBroadcast(intent, PERMISSION_ACCESS_PORTAL_STATE);
}
private static Intent createStateChangedBroadcastIntent(State state, PortalInfo portal)
{
Intent intent = new Intent(PortalDetectorService.ACTION_PORTAL_STATE_CHANGED);
intent.putExtra(PortalDetectorService.EXTRA_PORTAL_STATE, state.getName());
if (portal != null)
{
portal.saveToIntent(intent);
}
return intent;
}
private void registerSessionTimeoutCheckReceiver()
{
if (!mSessionTimeoutCheckEnabled)
{
mSessionTimeoutCheckEnabled = true;
// Check for timeout whenever screen is turned on.
IntentFilter filter = new IntentFilter(Intent.ACTION_SCREEN_ON);
registerReceiver(mScreenOnReceiver, filter);
// Check for timeout periodically.
int intervalSeconds = mPreferences.getSessionTimeoutCheckMinutes();
long intervalMillis = intervalSeconds * 1000;
mAlarmManager.setRepeating(
AlarmManager.ELAPSED_REALTIME, intervalMillis, intervalMillis,
mSessionTimeoutCheckPendingIntent);
Log.d("Checking for session timeout whenever screen is turned on and every %d seconds",
intervalSeconds);
}
}
private void unregisterSessionTimeoutCheckReceiver()
{
if (mSessionTimeoutCheckEnabled)
{
try
{
unregisterReceiver(mScreenOnReceiver);
}
catch (Exception e)
{
Log.w("Error unregistering for screen on broadcast", e);
}
try
{
mAlarmManager.cancel(mSessionTimeoutCheckPendingIntent);
}
catch (Exception e)
{
Log.w("Error cancelling session timeout check alarm", e);
}
mSessionTimeoutCheckEnabled = false;
Log.d("No longer periodically checking for session timeout");
}
}
}