/*
* Copyright (C) 2013 Sasha Vasko <sasha at aftercode dot net>
*
* 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.wifiafterconnect;
import java.net.URISyntaxException;
import java.net.URL;
import com.wifiafterconnect.URLRedirectChecker.AuthorizationType;
import com.wifiafterconnect.handlers.CaptivePageHandler;
import com.wifiafterconnect.util.Worker;
import com.wifiafterconnect.util.WifiTools;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.PowerManager;
import android.support.v4.app.NotificationCompat;
import android.support.v4.app.TaskStackBuilder;
public class WifiAuthenticator extends Worker{
public static final String OPTION_URL = "PARAM_URL";
public static final String OPTION_PAGE = "PARAM_PAGE";
public static final String OPTION_AUTH_HOST = "PARAM_AUTH_HOST";
public static final String OPTION_SITE_ID = "PARAM_SITE_ID";
private static final String CAPTIVE_PORTAL_TRACKER_NOTIFICATION_ID = "CaptivePortal.Notification"; // Android 4.2 and later
private static final String WALLED_GARDEN_NOTIFICATION_ID = "WifiWatchdog.walledgarden"; // Android 4.1
private static final String WATCHDOG_NOTIFICATION_ID = "Android.System.WifiWatchdog"; // Android 4.0
public enum AuthAction {
DEFAULT, BROWSER, IGNORE;
public static AuthAction parse(final String string) {
if (string != null) {
for (AuthAction a : AuthAction.values())
if (string.equalsIgnoreCase (a.toString()))
return a;
}
return DEFAULT;
}
}
public enum AuthStatus {
INPROGRESS, SUCCESS, FAIL;
public static AuthStatus valueOf(boolean success) {
return success ? SUCCESS: FAIL;
}
public static AuthStatus valueOf(CaptivePageHandler.States captiveResult){
return captiveResult == CaptivePageHandler.States.Success ? SUCCESS : FAIL;
}
public boolean isSuccess() {
return this == SUCCESS;
}
}
private String authHost;
public static String parseWifiHost (URL hostURL, Context context) {
String authHost = hostURL.getHost();
if (authHost.matches("([0-9]{1,3}\\.){3}[0-9]{1,3}")) // raw IP address - supplement with WiFi SSID
{
String ssid = WifiTools.getSSID(context);
if (ssid != null) {
if (ssid.startsWith("\""))
ssid = ssid.substring(1, ssid.length()-1);
authHost = ssid + ":" + authHost;
}
}
return authHost;
}
public WifiAuthenticator (Worker creator, URL hostURL) {
super (creator);
authHost = parseWifiHost(hostURL, getContext());
}
private WifiAuthDatabase getDb() {
return WifiAuthDatabase.getInstance(getContext());
}
public WifiAuthParams getStoredAuthParams() {
WifiAuthDatabase wifiDb = getDb();
return wifiDb != null ? wifiDb.getAuthParams (authHost): null;
}
public void storeAuthAction (final AuthAction action) {
WifiAuthDatabase wifiDb = getDb();
if (wifiDb != null)
wifiDb.storeAuthAction (authHost, action);
}
public AuthAction getAuthAction () {
WifiAuthDatabase wifiDb = getDb();
return wifiDb != null? wifiDb.getAuthAction (authHost): null;
}
public void storeWifiAction (final WifiTools.Action action) {
WifiAuthDatabase wifiDb = getDb();
if (wifiDb != null)
wifiDb.storeWifiAction (authHost, action);
}
public WifiTools.Action getWifiAction () {
WifiAuthDatabase wifiDb = getDb();
return wifiDb != null? wifiDb.getWifiAction (authHost): null;
}
protected void notifyWifiDisabled() {
NotificationCompat.Builder builder = new NotificationCompat.Builder(getContext())
.setSmallIcon (R.drawable.wifiac_small)//(android.R.drawable.presence_offline)
.setContentTitle(getResourceString(R.string.notif_wifi_disabled_title))
.setContentText(authHost + " - " + getResourceString(R.string.notif_wifi_disabled_text));
Intent resultIntent = makeIntent(MainActivity.class);
if (prefs.getReenableWifiQuiet())
resultIntent.setAction(getResourceString(R.string.action_reenable_wifi));
// The stack builder object will contain an artificial back stack for the
// started Activity.
// This ensures that navigating backward from the Activity leads out of
// your application to the Home screen.
TaskStackBuilder stackBuilder = TaskStackBuilder.create(getContext());
// Adds the back stack for the Intent (but not the Intent itself)
stackBuilder.addParentStack(MainActivity.class);
// Adds the Intent that starts the Activity to the top of the stack
stackBuilder.addNextIntent(resultIntent);
PendingIntent resultPendingIntent =
stackBuilder.getPendingIntent(
0,
PendingIntent.FLAG_UPDATE_CURRENT
);
builder.setContentIntent(resultPendingIntent);
Notification n = builder.build();
debug("posting notification that wifi was disabled (" + n.toString() + ")");
NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
nm.notify(0, n);
}
protected void notifyAuthentication(AuthStatus status)
{
if (prefs.getNotifyAuthentication())
{
NotificationCompat.Builder builder = new NotificationCompat.Builder(getContext())
.setSmallIcon (R.drawable.wifiac_small)
.setContentTitle(getResourceString(R.string.notif_wifi_auth_title));
if (status == AuthStatus.INPROGRESS)
builder.setContentText(getResourceString(R.string.notif_wifi_auth_inprogress));
else if (status == AuthStatus.SUCCESS)
builder.setContentText(getResourceString(R.string.notif_wifi_auth_success) + " - " + authHost);
else if (status == AuthStatus.FAIL)
builder.setContentText(getResourceString(R.string.notif_wifi_auth_fail));
Notification n = builder.build();
NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
nm.notify(1, n);
}
}
/*
* Android starting with 4.0 has watchdog that checks for walled garden if it detects on it
* posts a notification which may mislead user as we will take care of it.
* There is no API/config setting to turn it off, so we'll try this little hack.
* Probably not going to work as I suspect notifications are per-package, but we can try:
*
* Alternative is to change settings in /data/data/com.android.providers.settings/databases/settings.db
*
* Either:
* In Android 4.0 : secure.wifi_watchdog_show_disabled_network_popup
* In Android 4.1 : secure.wifi_watchdog_walled_garden_test_enabled
* In Android 4.2 and 4.3 : global.captive_portal_detection_enabled
* ( can use command from su shell: settings put global captive_portal_detection_enabled 0 )
*
* the code responsible was android/net/wifi/WifiWatchdogStateMachine.java
* Android 4.1 and WifiWatchdogStateMachine duties has changed into monitoring connection for
* packet loss (setting global.wifi_watchdog_on). Still does some walled Garden check in
* WifiWatchdogStateMachine.WalledGardenCheckState.
*
* Android 4.2 and later : functionality moved into android.net.CaptivePortalTracker
*
*
*/
public void cancelWatchdogNotification() {
NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
if (nm != null) {
nm.cancel(WATCHDOG_NOTIFICATION_ID, 1);
nm.cancel(WALLED_GARDEN_NOTIFICATION_ID, 1);
nm.cancel(CAPTIVE_PORTAL_TRACKER_NOTIFICATION_ID, 1);
}
}
protected void requestUserParams (ParsedHttpInput parsedPage) {
debug("Need user input for authentication credentials.");
if (getContext() == null) {
error ("Context is not set - cannot start WifiAuthenticationActivity");
return;
}
// Need to check that screen is not off:
PowerManager pm = (PowerManager)getSystemService(Context.POWER_SERVICE);
if (!pm.isScreenOn()) {
/** Screen is Off
* if Disable Wifi is enabled :
* 1) Disable wifi (if configured).
* 2) Post notification with intent to re-enable wifi.
* otherwise :
* setup Broadcast receiver waiting for SCREEN_ON event,
* which will restart the service on wake-up if wifi is still connected.
* Don't just want to pop the Activity and let it sit there,
* as Wifi may get disconnected and device moved to another location meanwhile
**/
boolean disableWifiOnLock = prefs.getAutoDisableWifi();
debug("Screen is off and disableWifiOnLock is " + disableWifiOnLock);
if (disableWifiOnLock) {
WifiTools.disableWifi(getContext());
notifyWifiDisabled();
}else {
ScreenOnReceiver.register(getContext());
// don't want to receive repeat notifications - will re-enable when screen comes on
WifiBroadcastReceiver.setEnabled(getContext(), false);
}
}else {
debug("Screen is on - Starting new activity.");
/**
* Screen is On - so proceeding displaying activity asking for credentials.
*/
Intent intent = makeIntent(WifiAuthenticatorActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.putExtra (OPTION_URL, parsedPage.getURL().toString());
intent.putExtra (OPTION_PAGE, parsedPage.getHtml());
toIntent(intent);
debug("Starting activity for intent:"+intent.toString());
startActivity (intent);
}
}
/*
* Doing our best showing user the Terms And Conditions page if he/she has not seen it yet.
*/
public boolean checkTNCShown(ParsedHttpInput parsed) {
String ssid = WifiTools.getSSID (getContext());
WifiAuthDatabase wifiDb = getDb();
if (wifiDb == null || ssid == null || ssid.isEmpty())
return true;
if (wifiDb.isKnownSSID (ssid))
return true;
PowerManager pm = (PowerManager)getSystemService(Context.POWER_SERVICE);
Context context = getContext();
boolean urlOpened = false;
if (pm.isScreenOn() && context != null) {
try {
debug("TNC not shown previously. Redirecting to page in browser.");
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(parsed.getURL().toURI().toString()));
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
urlOpened = true;
} catch (URISyntaxException e) { // don't care
}
}else {
// what should we do when the screen is locked???
}
wifiDb.storeSSID (ssid);
return !urlOpened;
}
public boolean attemptAuthentication (ParsedHttpInput currentPage, WifiAuthParams authParams) {
notifyAuthentication(AuthStatus.INPROGRESS);
CaptivePageHandler.States captiveState = CaptivePageHandler.States.HandleRedirects;
/* Some portals supply as the first page ip, MAC etc
* inside of the form that has to be submitted onLoad.
* Wandering WiFi is the worst offender.
*/
while (captiveState != CaptivePageHandler.States.Success && captiveState != CaptivePageHandler.States.Failed) {
debug("Handling pre-auth redirects. parsedPage = " + currentPage);
// don't want to do meta http-equiv=refresh here as it is used to detect browsers with no JS support
// and display error requiring it
if ((currentPage = currentPage.handleAutoRedirects (Constants.MAX_AUTOMATED_REQUESTS, false)) == null) {
error ("Failed to follow the sequence of redirects...");
notifyAuthentication(AuthStatus.FAIL);
return false;
}
debug("Done handling pre-auth redirects. parsedPage = " + currentPage);
if (!currentPage.isKnownCaptivePortal()) {
error ("Unknown Captive portal. Aborting.");
notifyAuthentication(AuthStatus.FAIL);
return false;
}
if (!checkTNCShown(currentPage)) {
notifyAuthentication(AuthStatus.FAIL);
return false; // it is the first time that user connected to this SSID ,
// so we let them go through the proper web authentication.
}
debug("Checking for missing inputs at [" + currentPage.getURL() + "]");
if (authParams == null) {
authParams = getStoredAuthParams();
if (currentPage.checkParamsMissing(authParams)){
requestUserParams (currentPage);
// we will have to try authentication directly from user-facing activity
return false;
}
}
debug("Attempting authentication at [" + currentPage.getURL() + "]");
ParsedHttpInput nextPage = currentPage.authenticateCaptivePortal (authParams);
captiveState = (nextPage == null) ? CaptivePageHandler.States.Failed : currentPage.getCaptivePortalState();
currentPage = nextPage;
}
AuthStatus status = AuthStatus.valueOf(captiveState);
if (status.isSuccess()) {
debug("Re-checking connection ...");
URLRedirectChecker checker = new URLRedirectChecker (this);
status = AuthStatus.valueOf(checker.checkHttpConnection (AuthorizationType.None));
if (status.isSuccess())
{
WifiAuthDatabase wifiDb = getDb();
debug("Saving Auth Params. db = [" + wifiDb + "]");
if (wifiDb != null)
wifiDb.storeAuthParams(authHost, authParams);
if (getContext() != null) {
cancelWatchdogNotification();
try {
//Toast.makeText(context, context.getText(R.string.success_notification) + " " + authHost, Toast.LENGTH_SHORT).show();
}catch (Throwable e){
// don't care
}
// do we need this so that Toast would actually display ?
Thread.yield();
}
}
}
notifyAuthentication(status);
return status.isSuccess();
}
}