/*
* This source is part of the
* _____ ___ ____
* __ / / _ \/ _ | / __/___ _______ _
* / // / , _/ __ |/ _/_/ _ \/ __/ _ `/
* \___/_/|_/_/ |_/_/ (_)___/_/ \_, /
* /___/
* repository.
*
* Copyright (C) 2013 Benoit 'BoD' Lubek (BoD@JRAF.org)
* Copyright (C) 2014-2015 Carmen Alvarez (c@rmen.ca)
*
* 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 ca.rmen.android.networkmonitor.app.service.datasources;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.URL;
import java.net.URLConnection;
import android.content.ContentValues;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
import android.preference.PreferenceManager;
import ca.rmen.android.networkmonitor.Constants;
import ca.rmen.android.networkmonitor.app.prefs.NetMonPreferences;
import ca.rmen.android.networkmonitor.app.service.NetMonNotification;
import ca.rmen.android.networkmonitor.provider.NetMonColumns;
import ca.rmen.android.networkmonitor.util.Log;
import ca.rmen.android.networkmonitor.util.TelephonyUtil;
/**
* Performs network connection tests and provides the results of each test.
*/
public class ConnectionTesterDataSource implements NetMonDataSource {
private static final String TAG = Constants.TAG + ConnectionTesterDataSource.class.getSimpleName();
private Context mContext;
private enum NetworkTestResult {
PASS, FAIL, SLOW
}
private static final int PORT = 80;
private static final int DURATION_SLOW = 5000;
// The maximum connection and read timeout for a connection test, in ms. We may actually set a lower timeout if the user has set the app to test very frequently (ex: every 10 seconds).
private static final int MAX_TIMEOUT_PER_TEST = 15000;
private static final String HTTP_GET = "GET / HTTP/1.1\r\n\r\n";
// The timeout for each connection test, in ms.
private volatile int mTimeout;
public ConnectionTesterDataSource() {
Log.v(TAG, "Constructor");
}
@Override
public void onCreate(Context context) {
Log.v(TAG, "onCreate");
mContext = context;
PreferenceManager.getDefaultSharedPreferences(context).registerOnSharedPreferenceChangeListener(mPrefListener);
int updateInterval = NetMonPreferences.getInstance(context).getUpdateInterval();
if (updateInterval == NetMonPreferences.PREF_UPDATE_ON_NETWORK_CHANGE) updateInterval = MAX_TIMEOUT_PER_TEST;
setTimeout(updateInterval);
}
@Override
public void onDestroy() {
PreferenceManager.getDefaultSharedPreferences(mContext).unregisterOnSharedPreferenceChangeListener(mPrefListener);
}
/**
* @param timeout the maximum total time it should take to perform all connection tests.
*/
private void setTimeout(int timeout) {
Log.v(TAG, "setTimeout " + timeout);
// Divide the total timeout by the total number of connection tests, to get the maximum timeout for each test.
mTimeout = Math.min(timeout / 2, MAX_TIMEOUT_PER_TEST);
Log.v(TAG, "setTimeout: set timeout to " + mTimeout);
}
/**
* @return Run the different connection tests and return their results. The keys are db column names and values the results of the tests as strings.
*/
@Override
public ContentValues getContentValues() {
Log.v(TAG, "getContentValues");
ContentValues values = new ContentValues(2);
if (!NetMonPreferences.getInstance(mContext).isConnectionTestEnabled()) {
Log.v(TAG, "Not doing data test");
return values;
}
NetworkTestResult socketTestResult = getSocketTestResult();
NetworkTestResult httpTestResult = getHttpTestResult();
values.put(NetMonColumns.SOCKET_CONNECTION_TEST, socketTestResult.name());
values.put(NetMonColumns.HTTP_CONNECTION_TEST, httpTestResult.name());
if ((socketTestResult == NetworkTestResult.FAIL || httpTestResult == NetworkTestResult.FAIL) && shouldHaveDataConnection()) {
Log.v(TAG, "A connection test failed even though we expect to have a data connection");
NetMonNotification.showFailedTestNotification(mContext);
} else {
NetMonNotification.dismissFailedTestNotification(mContext);
}
return values;
}
/**
* Try to open a connection to an HTTP server, and execute a simple GET request. If we can read a response to the GET request, we consider that the network
* is up. This test uses a basic socket connection.
*
* @return {@link NetworkTestResult#PASS} if we were able to read a response to a GET request quickly, {@link NetworkTestResult#FAIL} if any error occurred
* trying to execute the GET, or {@link NetworkTestResult#SLOW} if we were able to read a response, but it took too long.
*/
private NetworkTestResult getSocketTestResult() {
Log.v(TAG, "getSocketTestResult BEGIN");
Socket socket = null;
try {
// Prevent the system from closing the connection after 30 minutes of screen off.
long before = System.currentTimeMillis();
socket = new Socket();
socket.setSoTimeout(mTimeout);
String host = NetMonPreferences.getInstance(mContext).getTestServer().trim();
Log.d(TAG, "getSocketTestResult Resolving " + host);
InetSocketAddress remoteAddress = new InetSocketAddress(host, PORT);
InetAddress address = remoteAddress.getAddress();
if (address == null) {
Log.d(TAG, "getSocketTestResult Could not resolve");
return NetworkTestResult.FAIL;
}
Log.d(TAG, "getSocketTestResult Resolved " + address.getHostAddress());
Log.d(TAG, "getSocketTestResult Connecting...");
socket.connect(remoteAddress, mTimeout);
Log.d(TAG, "getSocketTestResult Connected");
Log.d(TAG, "getSocketTestResult Sending GET...");
OutputStream outputStream = socket.getOutputStream();
outputStream.write(HTTP_GET.getBytes("utf-8"));
outputStream.flush();
Log.d(TAG, "getSocketTestResult Sent GET");
InputStream inputStream = socket.getInputStream();
Log.d(TAG, "getSocketTestResult Reading...");
int read = inputStream.read();
Log.d(TAG, "getSocketTestResult Read read=" + read);
long after = System.currentTimeMillis();
if (read != -1) {
if (after - before > DURATION_SLOW) return NetworkTestResult.SLOW;
else
return NetworkTestResult.PASS;
}
return NetworkTestResult.FAIL;
} catch (Throwable t) {
Log.d(TAG, "getSocketTestResult Caught an exception", t);
return NetworkTestResult.FAIL;
} finally {
if (socket != null) {
try {
socket.close();
} catch (IOException e) {
Log.w(TAG, "getSocketTestResult Could not close socket", e);
}
}
Log.v(TAG, "getSocketTestResult END");
}
}
/**
* Try to open a connection to an HTTP server, and execute a simple GET request. If we can read a response to the GET request, we consider that the network
* is up. This test uses an HttpURLConnection.
*
* @return {@link NetworkTestResult#PASS} if we were able to read a response to a GET request quickly, {@link NetworkTestResult#FAIL} if any error occurred
* trying to execute the GET, or {@link NetworkTestResult#SLOW} if we were able to read a response, but it took too long.
*/
private NetworkTestResult getHttpTestResult() {
Log.v(TAG, "getHttpTestResult BEGIN");
InputStream inputStream = null;
try {
long before = System.currentTimeMillis();
String host = NetMonPreferences.getInstance(mContext).getTestServer().trim();
URL url = new URL("http", host, PORT, "/");
URLConnection connection = url.openConnection();
Log.v(TAG, "Opened connection");
connection.setConnectTimeout(mTimeout);
connection.setReadTimeout(mTimeout);
connection.addRequestProperty("Cache-Control", "no-cache");
connection.setUseCaches(false);
if (connection instanceof HttpURLConnection) ((HttpURLConnection) connection).setInstanceFollowRedirects(false);
Log.v(TAG, "Will open input stream");
inputStream = connection.getInputStream();
long after = System.currentTimeMillis();
if (inputStream.read() > 0) {
if (after - before > DURATION_SLOW) return NetworkTestResult.SLOW;
else
return NetworkTestResult.PASS;
} else {
return NetworkTestResult.FAIL;
}
} catch (Throwable t) {
Log.d(TAG, "getHttpTestResult Caught an exception", t);
return NetworkTestResult.FAIL;
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
Log.w(TAG, "getHttpTestResult Could not close stream", e);
}
}
Log.v(TAG, "getHttpTestResult END");
}
}
private final OnSharedPreferenceChangeListener mPrefListener = new OnSharedPreferenceChangeListener() {
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
// Issue #20: We should respect the testing interval. We shouldn't wait for more than this interval for
// the connection tests to timeout.
if (NetMonPreferences.PREF_UPDATE_INTERVAL.equals(key)) {
int updateInterval = NetMonPreferences.getInstance(mContext).getUpdateInterval();
if (updateInterval > 0) {
Log.v(TAG, "updateInterval changed to " + updateInterval);
setTimeout(updateInterval);
}
}
}
};
/**
* @return true if we should have an internet connection
*/
private boolean shouldHaveDataConnection() {
// If we're connected to a WiFi access point, we should have an internet connection.
WifiManager wifiMgr = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
WifiInfo connectionInfo = wifiMgr.getConnectionInfo();
if (connectionInfo != null && connectionInfo.getNetworkId() > -1) return true;
// If we're in airplane mode, we can't have an Internet connection.
if (TelephonyUtil.isAirplaneModeOn(mContext)) return false;
// We're not on WiFi, and we're not in airplane mode.
// Assume we should have Internet access if mobile data is enabled.
return TelephonyUtil.isMobileDataEnabled(mContext);
}
}