зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1038843 - Part 1: Land initial stumbler Java code and manifest fragments. r=nalexander,rnewman
The stumbler is a geolocation data collecting and uploading service. This code is a partial export of the MozStumbler repository hosted at https://github.com/mozilla/MozStumbler.
This commit is contained in:
Родитель
294fcbd543
Коммит
abbc2d7245
|
@ -0,0 +1,60 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package org.mozilla.mozstumbler.service;
|
||||||
|
|
||||||
|
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||||
|
|
||||||
|
public class AppGlobals {
|
||||||
|
public static final String LOG_PREFIX = "Stumbler:";
|
||||||
|
|
||||||
|
/* All intent actions start with this string. Only locally broadcasted. */
|
||||||
|
public static final String ACTION_NAMESPACE = "org.mozilla.mozstumbler.intent.action";
|
||||||
|
|
||||||
|
/* Handle this for logging reporter info. */
|
||||||
|
public static final String ACTION_GUI_LOG_MESSAGE = AppGlobals.ACTION_NAMESPACE + ".LOG_MESSAGE";
|
||||||
|
public static final String ACTION_GUI_LOG_MESSAGE_EXTRA = ACTION_GUI_LOG_MESSAGE + ".MESSAGE";
|
||||||
|
|
||||||
|
/* Defined here so that the Reporter class can access the time of an Intent in a generic fashion.
|
||||||
|
* Classes should have their own constant that is assigned to this, for example,
|
||||||
|
* WifiScanner has ACTION_WIFIS_SCANNED_ARG_TIME = ACTION_ARG_TIME.
|
||||||
|
* This member definition in the broadcaster makes it clear what the extra Intent args are for that class. */
|
||||||
|
public static final String ACTION_ARG_TIME = "time";
|
||||||
|
|
||||||
|
/* Location constructor requires a named origin, these are created in the app. */
|
||||||
|
public static final String LOCATION_ORIGIN_INTERNAL = "internal";
|
||||||
|
|
||||||
|
public enum ActiveOrPassiveStumbling { ACTIVE_STUMBLING, PASSIVE_STUMBLING }
|
||||||
|
|
||||||
|
/* In passive mode, only scan this many times for each gps. */
|
||||||
|
public static final int PASSIVE_MODE_MAX_SCANS_PER_GPS = 3;
|
||||||
|
|
||||||
|
/* These are set on startup. The appVersionName and code are not used in the service-only case. */
|
||||||
|
public static String appVersionName = "0.0.0";
|
||||||
|
public static int appVersionCode = 0;
|
||||||
|
public static String appName = "StumblerService";
|
||||||
|
public static boolean isDebug;
|
||||||
|
|
||||||
|
/* The log activity will clear this periodically, and display the messages.
|
||||||
|
* Always null when the stumbler service is used stand-alone. */
|
||||||
|
public static volatile ConcurrentLinkedQueue<String> guiLogMessageBuffer;
|
||||||
|
|
||||||
|
public static void guiLogError(String msg) {
|
||||||
|
guiLogInfo(msg, "red", true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void guiLogInfo(String msg) {
|
||||||
|
guiLogInfo(msg, "white", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void guiLogInfo(String msg, String color, boolean isBold) {
|
||||||
|
if (guiLogMessageBuffer != null) {
|
||||||
|
if (isBold) {
|
||||||
|
msg = "<b>" + msg + "</b>";
|
||||||
|
}
|
||||||
|
guiLogMessageBuffer.add("<font color='" + color +"'>" + msg + "</font>");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,210 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package org.mozilla.mozstumbler.service;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
|
import android.annotation.TargetApi;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
import android.location.Location;
|
||||||
|
import android.os.Build.VERSION;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
public final class Prefs {
|
||||||
|
private static final String LOG_TAG = Prefs.class.getSimpleName();
|
||||||
|
private static final String NICKNAME_PREF = "nickname";
|
||||||
|
private static final String USER_AGENT_PREF = "user-agent";
|
||||||
|
private static final String VALUES_VERSION_PREF = "values_version";
|
||||||
|
private static final String WIFI_ONLY = "wifi_only";
|
||||||
|
private static final String LAT_PREF = "lat_pref";
|
||||||
|
private static final String LON_PREF = "lon_pref";
|
||||||
|
private static final String GEOFENCE_HERE = "geofence_here";
|
||||||
|
private static final String GEOFENCE_SWITCH = "geofence_switch";
|
||||||
|
private static final String FIREFOX_SCAN_ENABLED = "firefox_scan_on";
|
||||||
|
private static final String MOZ_API_KEY = "moz_api_key";
|
||||||
|
private static final String WIFI_SCAN_ALWAYS = "wifi_scan_always";
|
||||||
|
private static final String LAST_ATTEMPTED_UPLOAD_TIME = "last_attempted_upload_time";
|
||||||
|
// Public for MozStumbler to use for manual upgrade of old prefs.
|
||||||
|
public static final String PREFS_FILE = Prefs.class.getSimpleName();
|
||||||
|
|
||||||
|
private final SharedPreferences mSharedPrefs;
|
||||||
|
static private Prefs sInstance;
|
||||||
|
|
||||||
|
private Prefs(Context context) {
|
||||||
|
mSharedPrefs = context.getSharedPreferences(PREFS_FILE, Context.MODE_MULTI_PROCESS | Context.MODE_PRIVATE);
|
||||||
|
if (getPrefs().getInt(VALUES_VERSION_PREF, -1) != AppGlobals.appVersionCode) {
|
||||||
|
Log.i(LOG_TAG, "Version of the application has changed. Updating default values.");
|
||||||
|
// Remove old keys
|
||||||
|
getPrefs().edit()
|
||||||
|
.remove("reports")
|
||||||
|
.remove("power_saving_mode")
|
||||||
|
.commit();
|
||||||
|
|
||||||
|
getPrefs().edit().putInt(VALUES_VERSION_PREF, AppGlobals.appVersionCode).commit();
|
||||||
|
getPrefs().edit().commit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Prefs must be created on application startup or service startup.
|
||||||
|
* TODO: turn into regular singleton if Context dependency can be removed. */
|
||||||
|
public static void createGlobalInstance(Context c) {
|
||||||
|
if (sInstance != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sInstance = new Prefs(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Only access after CreatePrefsInstance(Context) has been called at startup. */
|
||||||
|
public static Prefs getInstance() {
|
||||||
|
assert(sInstance != null);
|
||||||
|
return sInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Setters
|
||||||
|
///
|
||||||
|
public synchronized void setUserAgent(String userAgent) {
|
||||||
|
setStringPref(USER_AGENT_PREF, userAgent);
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void setUseWifiOnly(boolean state) {
|
||||||
|
setBoolPref(WIFI_ONLY, state);
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void setGeofenceEnabled(boolean state) {
|
||||||
|
setBoolPref(GEOFENCE_SWITCH, state);
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void setGeofenceHere(boolean flag) {
|
||||||
|
setBoolPref(GEOFENCE_HERE, flag);
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void setGeofenceLocation(Location location) {
|
||||||
|
SharedPreferences.Editor editor = getPrefs().edit();
|
||||||
|
editor.putFloat(LAT_PREF, (float) location.getLatitude());
|
||||||
|
editor.putFloat(LON_PREF, (float) location.getLongitude());
|
||||||
|
apply(editor);
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void setMozApiKey(String s) {
|
||||||
|
setStringPref(MOZ_API_KEY, s);
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Getters
|
||||||
|
///
|
||||||
|
public synchronized String getUserAgent() {
|
||||||
|
String s = getStringPref(USER_AGENT_PREF);
|
||||||
|
return (s == null)? AppGlobals.appName + "/" + AppGlobals.appVersionName : s;
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized boolean getFirefoxScanEnabled() {
|
||||||
|
return getBoolPrefWithDefault(FIREFOX_SCAN_ENABLED, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized String getMozApiKey() {
|
||||||
|
String s = getStringPref(MOZ_API_KEY);
|
||||||
|
return (s == null)? "no-mozilla-api-key" : s;
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized boolean getGeofenceEnabled() {
|
||||||
|
return getBoolPrefWithDefault(GEOFENCE_SWITCH, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized boolean getGeofenceHere() {
|
||||||
|
return getBoolPrefWithDefault(GEOFENCE_HERE, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized Location getGeofenceLocation() {
|
||||||
|
Location loc = new Location(AppGlobals.LOCATION_ORIGIN_INTERNAL);
|
||||||
|
loc.setLatitude(getPrefs().getFloat(LAT_PREF, 0));
|
||||||
|
loc.setLongitude(getPrefs().getFloat(LON_PREF,0));
|
||||||
|
return loc;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is the time an upload was last attempted, not necessarily successful.
|
||||||
|
// Used to ensure upload attempts aren't happening too frequently.
|
||||||
|
public synchronized long getLastAttemptedUploadTime() {
|
||||||
|
return getPrefs().getLong(LAST_ATTEMPTED_UPLOAD_TIME, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized String getNickname() {
|
||||||
|
String nickname = getStringPref(NICKNAME_PREF);
|
||||||
|
if (nickname != null) {
|
||||||
|
nickname = nickname.trim();
|
||||||
|
}
|
||||||
|
return TextUtils.isEmpty(nickname) ? null : nickname;
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void setFirefoxScanEnabled(boolean on) {
|
||||||
|
setBoolPref(FIREFOX_SCAN_ENABLED, on);
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void setLastAttemptedUploadTime(long time) {
|
||||||
|
SharedPreferences.Editor editor = getPrefs().edit();
|
||||||
|
editor.putLong(LAST_ATTEMPTED_UPLOAD_TIME, time);
|
||||||
|
apply(editor);
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void setNickname(String nick) {
|
||||||
|
if (nick != null) {
|
||||||
|
nick = nick.trim();
|
||||||
|
if (nick.length() > 0) {
|
||||||
|
setStringPref(NICKNAME_PREF, nick);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized boolean getUseWifiOnly() {
|
||||||
|
return getBoolPrefWithDefault(WIFI_ONLY, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized boolean getWifiScanAlways() {
|
||||||
|
return getBoolPrefWithDefault(WIFI_SCAN_ALWAYS, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void setWifiScanAlways(boolean b) {
|
||||||
|
setBoolPref(WIFI_SCAN_ALWAYS, b);
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Privates
|
||||||
|
///
|
||||||
|
|
||||||
|
private String getStringPref(String key) {
|
||||||
|
return getPrefs().getString(key, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean getBoolPrefWithDefault(String key, boolean def) {
|
||||||
|
return getPrefs().getBoolean(key, def);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setBoolPref(String key, Boolean state) {
|
||||||
|
SharedPreferences.Editor editor = getPrefs().edit();
|
||||||
|
editor.putBoolean(key,state);
|
||||||
|
apply(editor);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setStringPref(String key, String value) {
|
||||||
|
SharedPreferences.Editor editor = getPrefs().edit();
|
||||||
|
editor.putString(key, value);
|
||||||
|
apply(editor);
|
||||||
|
}
|
||||||
|
|
||||||
|
@TargetApi(9)
|
||||||
|
private static void apply(SharedPreferences.Editor editor) {
|
||||||
|
if (VERSION.SDK_INT >= 9) {
|
||||||
|
editor.apply();
|
||||||
|
} else if (!editor.commit()) {
|
||||||
|
Log.e(LOG_TAG, "", new IllegalStateException("commit() failed?!"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("InlinedApi")
|
||||||
|
private SharedPreferences getPrefs() {
|
||||||
|
return mSharedPrefs;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,73 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package org.mozilla.mozstumbler.service.mainthread;
|
||||||
|
|
||||||
|
import android.content.BroadcastReceiver;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import org.mozilla.mozstumbler.service.AppGlobals;
|
||||||
|
import org.mozilla.mozstumbler.service.stumblerthread.StumblerService;
|
||||||
|
|
||||||
|
/* Starts the StumblerService, an Intent service, which by definition runs on its own thread.
|
||||||
|
* Registered as a receiver in the AndroidManifest.xml.
|
||||||
|
* Starts the StumblerService in passive listening mode.
|
||||||
|
*
|
||||||
|
* The received intent serves these purposes:
|
||||||
|
* 1) The host application enables (or disables) the StumblerService.
|
||||||
|
* Enabling requires passing in the upload API key. Both the enabled state, and the API key are stored in prefs.
|
||||||
|
*
|
||||||
|
* 2) If previously enabled by (1), notify the service to start (such as with a BOOT Intent).
|
||||||
|
* The StumblerService is where the enabled state is checked, and if not enabled, the
|
||||||
|
* service stops immediately.
|
||||||
|
*
|
||||||
|
* 3) Upload notification: onReceive intents are used to tell the StumblerService to check for upload.
|
||||||
|
* In the Fennec host app use, startup and pause are used as indicators to the StumblerService that now
|
||||||
|
* is a good time to try upload, as it is likely that the network is in use.
|
||||||
|
*/
|
||||||
|
public class PassiveServiceReceiver extends BroadcastReceiver {
|
||||||
|
static final String LOG_TAG = AppGlobals.LOG_PREFIX + PassiveServiceReceiver.class.getSimpleName();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onReceive(Context context, Intent intent) {
|
||||||
|
if (intent == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final String action = intent.getAction();
|
||||||
|
final boolean isIntentFromHostApp = (action != null) && action.contains(".STUMBLER_PREF");
|
||||||
|
if (!isIntentFromHostApp) {
|
||||||
|
Log.d(LOG_TAG, "Stumbler: received intent external to host app");
|
||||||
|
Intent startServiceIntent = new Intent(context, StumblerService.class);
|
||||||
|
startServiceIntent.putExtra(StumblerService.ACTION_NOT_FROM_HOST_APP, true);
|
||||||
|
context.startService(startServiceIntent);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (intent.hasExtra("is_debug")) {
|
||||||
|
AppGlobals.isDebug = intent.getBooleanExtra("is_debug", false);
|
||||||
|
}
|
||||||
|
StumblerService.sFirefoxStumblingEnabled.set(intent.getBooleanExtra("enabled", false));
|
||||||
|
|
||||||
|
if (!StumblerService.sFirefoxStumblingEnabled.get()) {
|
||||||
|
// This calls the service's onDestroy(), and the service's onHandleIntent(...) is not called
|
||||||
|
context.stopService(new Intent(context, StumblerService.class));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.d(LOG_TAG, "Stumbler: Sending passive start message | isDebug:" + AppGlobals.isDebug);
|
||||||
|
|
||||||
|
|
||||||
|
final Intent startServiceIntent = new Intent(context, StumblerService.class);
|
||||||
|
startServiceIntent.putExtra(StumblerService.ACTION_START_PASSIVE, true);
|
||||||
|
final String mozApiKey = intent.getStringExtra("moz_mozilla_api_key");
|
||||||
|
startServiceIntent.putExtra(StumblerService.ACTION_EXTRA_MOZ_API_KEY, mozApiKey);
|
||||||
|
final String userAgent = intent.getStringExtra("user_agent");
|
||||||
|
startServiceIntent.putExtra(StumblerService.ACTION_EXTRA_USER_AGENT, userAgent);
|
||||||
|
context.startService(startServiceIntent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,199 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package org.mozilla.mozstumbler.service.stumblerthread;
|
||||||
|
|
||||||
|
import android.content.BroadcastReceiver;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.IntentFilter;
|
||||||
|
import android.location.Location;
|
||||||
|
import android.net.wifi.ScanResult;
|
||||||
|
import android.support.v4.content.LocalBroadcastManager;
|
||||||
|
import android.telephony.TelephonyManager;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.json.JSONException;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
import org.mozilla.mozstumbler.service.AppGlobals;
|
||||||
|
import org.mozilla.mozstumbler.service.stumblerthread.datahandling.DataStorageContract;
|
||||||
|
import org.mozilla.mozstumbler.service.stumblerthread.datahandling.DataStorageManager;
|
||||||
|
import org.mozilla.mozstumbler.service.stumblerthread.datahandling.StumblerBundle;
|
||||||
|
import org.mozilla.mozstumbler.service.stumblerthread.scanners.cellscanner.CellInfo;
|
||||||
|
import org.mozilla.mozstumbler.service.stumblerthread.scanners.cellscanner.CellScanner;
|
||||||
|
import org.mozilla.mozstumbler.service.stumblerthread.scanners.GPSScanner;
|
||||||
|
import org.mozilla.mozstumbler.service.stumblerthread.scanners.WifiScanner;
|
||||||
|
|
||||||
|
public final class Reporter extends BroadcastReceiver {
|
||||||
|
private static final String LOG_TAG = AppGlobals.LOG_PREFIX + Reporter.class.getSimpleName();
|
||||||
|
public static final String ACTION_FLUSH_TO_BUNDLE = AppGlobals.ACTION_NAMESPACE + ".FLUSH";
|
||||||
|
private boolean mIsStarted;
|
||||||
|
|
||||||
|
/* The maximum number of Wi-Fi access points in a single observation. */
|
||||||
|
private static final int MAX_WIFIS_PER_LOCATION = 200;
|
||||||
|
|
||||||
|
/* The maximum number of cells in a single observation */
|
||||||
|
private static final int MAX_CELLS_PER_LOCATION = 50;
|
||||||
|
|
||||||
|
private Context mContext;
|
||||||
|
private int mPhoneType;
|
||||||
|
|
||||||
|
private StumblerBundle mBundle;
|
||||||
|
|
||||||
|
Reporter() {}
|
||||||
|
|
||||||
|
private void resetData() {
|
||||||
|
mBundle = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void flush() {
|
||||||
|
reportCollectedLocation();
|
||||||
|
}
|
||||||
|
|
||||||
|
void startup(Context context) {
|
||||||
|
if (mIsStarted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mContext = context.getApplicationContext();
|
||||||
|
TelephonyManager tm = (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE);
|
||||||
|
mPhoneType = tm.getPhoneType();
|
||||||
|
|
||||||
|
mIsStarted = true;
|
||||||
|
|
||||||
|
resetData();
|
||||||
|
IntentFilter intentFilter = new IntentFilter();
|
||||||
|
intentFilter.addAction(WifiScanner.ACTION_WIFIS_SCANNED);
|
||||||
|
intentFilter.addAction(CellScanner.ACTION_CELLS_SCANNED);
|
||||||
|
intentFilter.addAction(GPSScanner.ACTION_GPS_UPDATED);
|
||||||
|
intentFilter.addAction(ACTION_FLUSH_TO_BUNDLE);
|
||||||
|
LocalBroadcastManager.getInstance(mContext).registerReceiver(this,
|
||||||
|
intentFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
void shutdown() {
|
||||||
|
if (mContext == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mIsStarted = false;
|
||||||
|
|
||||||
|
Log.d(LOG_TAG, "shutdown");
|
||||||
|
flush();
|
||||||
|
LocalBroadcastManager.getInstance(mContext).unregisterReceiver(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void receivedWifiMessage(Intent intent) {
|
||||||
|
List<ScanResult> results = intent.getParcelableArrayListExtra(WifiScanner.ACTION_WIFIS_SCANNED_ARG_RESULTS);
|
||||||
|
putWifiResults(results);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void receivedCellMessage(Intent intent) {
|
||||||
|
List<CellInfo> results = intent.getParcelableArrayListExtra(CellScanner.ACTION_CELLS_SCANNED_ARG_CELLS);
|
||||||
|
putCellResults(results);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void receivedGpsMessage(Intent intent) {
|
||||||
|
String subject = intent.getStringExtra(Intent.EXTRA_SUBJECT);
|
||||||
|
if (subject.equals(GPSScanner.SUBJECT_NEW_LOCATION)) {
|
||||||
|
reportCollectedLocation();
|
||||||
|
Location newPosition = intent.getParcelableExtra(GPSScanner.NEW_LOCATION_ARG_LOCATION);
|
||||||
|
mBundle = (newPosition != null) ? new StumblerBundle(newPosition, mPhoneType) : mBundle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onReceive(Context context, Intent intent) {
|
||||||
|
String action = intent.getAction();
|
||||||
|
|
||||||
|
if (action.equals(ACTION_FLUSH_TO_BUNDLE)) {
|
||||||
|
flush();
|
||||||
|
return;
|
||||||
|
} else if (action.equals(WifiScanner.ACTION_WIFIS_SCANNED)) {
|
||||||
|
receivedWifiMessage(intent);
|
||||||
|
} else if (action.equals(CellScanner.ACTION_CELLS_SCANNED)) {
|
||||||
|
receivedCellMessage(intent);
|
||||||
|
} else if (action.equals(GPSScanner.ACTION_GPS_UPDATED)) {
|
||||||
|
// Calls reportCollectedLocation, this is the ideal case
|
||||||
|
receivedGpsMessage(intent);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mBundle != null &&
|
||||||
|
(mBundle.getWifiData().size() > MAX_WIFIS_PER_LOCATION ||
|
||||||
|
mBundle.getCellData().size() > MAX_CELLS_PER_LOCATION)) {
|
||||||
|
// no gps for a while, have too much data, just bundle it
|
||||||
|
reportCollectedLocation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void putWifiResults(List<ScanResult> results) {
|
||||||
|
if (mBundle == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, ScanResult> currentWifiData = mBundle.getWifiData();
|
||||||
|
for (ScanResult result : results) {
|
||||||
|
String key = result.BSSID;
|
||||||
|
if (!currentWifiData.containsKey(key)) {
|
||||||
|
currentWifiData.put(key, result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void putCellResults(List<CellInfo> cells) {
|
||||||
|
if (mBundle == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, CellInfo> currentCellData = mBundle.getCellData();
|
||||||
|
for (CellInfo result : cells) {
|
||||||
|
String key = result.getCellIdentity();
|
||||||
|
if (!currentCellData.containsKey(key)) {
|
||||||
|
currentCellData.put(key, result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void reportCollectedLocation() {
|
||||||
|
if (mBundle == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
storeBundleAsJSON(mBundle);
|
||||||
|
|
||||||
|
mBundle.wasSent();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void storeBundleAsJSON(StumblerBundle bundle) {
|
||||||
|
JSONObject mlsObj;
|
||||||
|
int wifiCount = 0;
|
||||||
|
int cellCount = 0;
|
||||||
|
try {
|
||||||
|
mlsObj = bundle.toMLSJSON();
|
||||||
|
wifiCount = mlsObj.getInt(DataStorageContract.ReportsColumns.WIFI_COUNT);
|
||||||
|
cellCount = mlsObj.getInt(DataStorageContract.ReportsColumns.CELL_COUNT);
|
||||||
|
|
||||||
|
} catch (JSONException e) {
|
||||||
|
Log.w(LOG_TAG, "Failed to convert bundle to JSON: " + e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (AppGlobals.isDebug) {
|
||||||
|
Log.d(LOG_TAG, "Received bundle: " + mlsObj.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
AppGlobals.guiLogInfo(mlsObj.toString());
|
||||||
|
|
||||||
|
try {
|
||||||
|
DataStorageManager.getInstance().insert(mlsObj.toString(), wifiCount, cellCount);
|
||||||
|
} catch (IOException e) {
|
||||||
|
Log.w(LOG_TAG, e.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,247 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package org.mozilla.mozstumbler.service.stumblerthread;
|
||||||
|
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.location.Location;
|
||||||
|
import android.os.AsyncTask;
|
||||||
|
import android.util.Log;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
|
import org.mozilla.mozstumbler.service.AppGlobals;
|
||||||
|
import org.mozilla.mozstumbler.service.Prefs;
|
||||||
|
import org.mozilla.mozstumbler.service.stumblerthread.blocklist.WifiBlockListInterface;
|
||||||
|
import org.mozilla.mozstumbler.service.stumblerthread.datahandling.DataStorageManager;
|
||||||
|
import org.mozilla.mozstumbler.service.stumblerthread.scanners.ScanManager;
|
||||||
|
import org.mozilla.mozstumbler.service.stumblerthread.scanners.cellscanner.CellScanner;
|
||||||
|
import org.mozilla.mozstumbler.service.stumblerthread.scanners.cellscanner.CellScannerNoWCDMA;
|
||||||
|
import org.mozilla.mozstumbler.service.uploadthread.UploadAlarmReceiver;
|
||||||
|
import org.mozilla.mozstumbler.service.utils.NetworkUtils;
|
||||||
|
import org.mozilla.mozstumbler.service.utils.PersistentIntentService;
|
||||||
|
|
||||||
|
// In stand-alone service mode (a.k.a passive scanning mode), this is created from PassiveServiceReceiver (by calling startService).
|
||||||
|
// The StumblerService is a sticky unbound service in this usage.
|
||||||
|
//
|
||||||
|
public class StumblerService extends PersistentIntentService
|
||||||
|
implements DataStorageManager.StorageIsEmptyTracker {
|
||||||
|
private static final String LOG_TAG = AppGlobals.LOG_PREFIX + StumblerService.class.getSimpleName();
|
||||||
|
public static final String ACTION_BASE = AppGlobals.ACTION_NAMESPACE;
|
||||||
|
public static final String ACTION_START_PASSIVE = ACTION_BASE + ".START_PASSIVE";
|
||||||
|
public static final String ACTION_EXTRA_MOZ_API_KEY = ACTION_BASE + ".MOZKEY";
|
||||||
|
public static final String ACTION_EXTRA_USER_AGENT = ACTION_BASE + ".USER_AGENT";
|
||||||
|
public static final String ACTION_NOT_FROM_HOST_APP = ACTION_BASE + ".NOT_FROM_HOST";
|
||||||
|
public static final AtomicBoolean sFirefoxStumblingEnabled = new AtomicBoolean();
|
||||||
|
protected final ScanManager mScanManager = new ScanManager();
|
||||||
|
protected final Reporter mReporter = new Reporter();
|
||||||
|
|
||||||
|
// This is a delay before the single-shot upload is attempted. The number is arbitrary
|
||||||
|
// and used to avoid startup tasks bunching up.
|
||||||
|
private static final int DELAY_IN_SEC_BEFORE_STARTING_UPLOAD_IN_PASSIVE_MODE = 2;
|
||||||
|
|
||||||
|
// This is the frequency of the repeating upload alarm in active scanning mode.
|
||||||
|
private static final int FREQUENCY_IN_SEC_OF_UPLOAD_IN_ACTIVE_MODE = 5 * 60;
|
||||||
|
|
||||||
|
// Used to guard against attempting to upload too frequently in passive mode.
|
||||||
|
private static final long PASSIVE_UPLOAD_FREQ_GUARD_MSEC = 5 * 60 * 1000;
|
||||||
|
|
||||||
|
public StumblerService() {
|
||||||
|
this("StumblerService");
|
||||||
|
}
|
||||||
|
|
||||||
|
public StumblerService(String name) {
|
||||||
|
super(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isScanning() {
|
||||||
|
return mScanManager.isScanning();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void startScanning() {
|
||||||
|
mScanManager.startScanning(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is optional, not used in Fennec, and is for clients to specify a (potentially long) list
|
||||||
|
// of blocklisted SSIDs/BSSIDs
|
||||||
|
public void setWifiBlockList(WifiBlockListInterface list) {
|
||||||
|
mScanManager.setWifiBlockList(list);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Prefs getPrefs() {
|
||||||
|
return Prefs.getInstance();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void checkPrefs() {
|
||||||
|
mScanManager.checkPrefs();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getLocationCount() {
|
||||||
|
return mScanManager.getLocationCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getLatitude() {
|
||||||
|
return mScanManager.getLatitude();
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getLongitude() {
|
||||||
|
return mScanManager.getLongitude();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Location getLocation() {
|
||||||
|
return mScanManager.getLocation();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getWifiStatus() {
|
||||||
|
return mScanManager.getWifiStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getAPCount() {
|
||||||
|
return mScanManager.getAPCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getVisibleAPCount() {
|
||||||
|
return mScanManager.getVisibleAPCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getCellInfoCount() {
|
||||||
|
return mScanManager.getCellInfoCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getCurrentCellInfoCount() {
|
||||||
|
return mScanManager.getCurrentCellInfoCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isGeofenced () {
|
||||||
|
return mScanManager.isGeofenced();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Previously this was done in onCreate(). Moved out of that so that in the passive standalone service
|
||||||
|
// use (i.e. Fennec), init() can be called from this class's dedicated thread.
|
||||||
|
// Safe to call more than once, ensure added code complies with that intent.
|
||||||
|
protected void init() {
|
||||||
|
Prefs.createGlobalInstance(this);
|
||||||
|
NetworkUtils.createGlobalInstance(this);
|
||||||
|
DataStorageManager.createGlobalInstance(this, this);
|
||||||
|
|
||||||
|
if (!CellScanner.isCellScannerImplSet()) {
|
||||||
|
CellScanner.setCellScannerImpl(new CellScannerNoWCDMA(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
mReporter.startup(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called from the main thread.
|
||||||
|
@Override
|
||||||
|
public void onCreate() {
|
||||||
|
super.onCreate();
|
||||||
|
setIntentRedelivery(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called from the main thread
|
||||||
|
@Override
|
||||||
|
public void onDestroy() {
|
||||||
|
super.onDestroy();
|
||||||
|
|
||||||
|
if (!mScanManager.isScanning()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Used to move these disk I/O ops off the calling thread. The current operations here are synchronized,
|
||||||
|
// however instead of creating another thread (if onDestroy grew to have concurrency complications)
|
||||||
|
// we could be messaging the stumbler thread to perform a shutdown function.
|
||||||
|
new AsyncTask<Void, Void, Void>() {
|
||||||
|
@Override
|
||||||
|
protected Void doInBackground(Void... params) {
|
||||||
|
if (AppGlobals.isDebug) {
|
||||||
|
Log.d(LOG_TAG, "onDestroy");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!sFirefoxStumblingEnabled.get()) {
|
||||||
|
Prefs.getInstance().setFirefoxScanEnabled(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (DataStorageManager.getInstance() != null) {
|
||||||
|
try {
|
||||||
|
DataStorageManager.getInstance().saveCurrentReportsToDisk();
|
||||||
|
} catch (IOException ex) {
|
||||||
|
AppGlobals.guiLogInfo(ex.toString());
|
||||||
|
Log.e(LOG_TAG, "Exception in onDestroy saving reports" + ex.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}.execute();
|
||||||
|
|
||||||
|
mReporter.shutdown();
|
||||||
|
mScanManager.stopScanning();
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is the entry point for the stumbler thread.
|
||||||
|
@Override
|
||||||
|
protected void onHandleIntent(Intent intent) {
|
||||||
|
// Do init() in all cases, there is no cost, whereas it is easy to add code that depends on this.
|
||||||
|
init();
|
||||||
|
|
||||||
|
// Post-init(), set the mode to passive.
|
||||||
|
mScanManager.setPassiveMode(true);
|
||||||
|
|
||||||
|
if (intent == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final boolean isScanEnabledInPrefs = Prefs.getInstance().getFirefoxScanEnabled();
|
||||||
|
|
||||||
|
if (!isScanEnabledInPrefs && intent.getBooleanExtra(ACTION_NOT_FROM_HOST_APP, false)) {
|
||||||
|
stopSelf();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!DataStorageManager.getInstance().isDirEmpty()) {
|
||||||
|
// non-empty on startup, schedule an upload
|
||||||
|
// This is the only upload trigger in Firefox mode
|
||||||
|
// Firefox triggers this ~4 seconds after startup (after Gecko is loaded), add a small delay to avoid
|
||||||
|
// clustering with other operations that are triggered at this time.
|
||||||
|
final long lastAttemptedTime = Prefs.getInstance().getLastAttemptedUploadTime();
|
||||||
|
final long timeNow = System.currentTimeMillis();
|
||||||
|
|
||||||
|
if (timeNow - lastAttemptedTime < PASSIVE_UPLOAD_FREQ_GUARD_MSEC) {
|
||||||
|
// TODO Consider telemetry to track this.
|
||||||
|
if (AppGlobals.isDebug) {
|
||||||
|
Log.d(LOG_TAG, "Upload attempt too frequent.");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Prefs.getInstance().setLastAttemptedUploadTime(timeNow);
|
||||||
|
UploadAlarmReceiver.scheduleAlarm(this, DELAY_IN_SEC_BEFORE_STARTING_UPLOAD_IN_PASSIVE_MODE, false /* no repeat*/);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isScanEnabledInPrefs) {
|
||||||
|
Prefs.getInstance().setFirefoxScanEnabled(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
String apiKey = intent.getStringExtra(ACTION_EXTRA_MOZ_API_KEY);
|
||||||
|
if (apiKey != null && !apiKey.equals(Prefs.getInstance().getMozApiKey())) {
|
||||||
|
Prefs.getInstance().setMozApiKey(apiKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
String userAgent = intent.getStringExtra(ACTION_EXTRA_USER_AGENT);
|
||||||
|
if (userAgent != null && !userAgent.equals(Prefs.getInstance().getUserAgent())) {
|
||||||
|
Prefs.getInstance().setUserAgent(userAgent);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mScanManager.isScanning()) {
|
||||||
|
startScanning();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note that in passive mode, having data isn't an upload trigger, it is triggered by the start intent
|
||||||
|
public void notifyStorageStateEmpty(boolean isEmpty) {
|
||||||
|
if (isEmpty) {
|
||||||
|
UploadAlarmReceiver.cancelAlarm(this, !mScanManager.isPassiveMode());
|
||||||
|
} else if (!mScanManager.isPassiveMode()) {
|
||||||
|
UploadAlarmReceiver.scheduleAlarm(this, FREQUENCY_IN_SEC_OF_UPLOAD_IN_ACTIVE_MODE, true /* repeating */);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,65 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package org.mozilla.mozstumbler.service.stumblerthread.blocklist;
|
||||||
|
|
||||||
|
import android.net.wifi.ScanResult;
|
||||||
|
import android.util.Log;
|
||||||
|
import org.mozilla.mozstumbler.service.AppGlobals;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
public final class BSSIDBlockList {
|
||||||
|
private static final String LOG_TAG = AppGlobals.LOG_PREFIX + BSSIDBlockList.class.getSimpleName();
|
||||||
|
private static final String NULL_BSSID = "000000000000";
|
||||||
|
private static final String WILDCARD_BSSID = "ffffffffffff";
|
||||||
|
private static final Pattern BSSID_PATTERN = Pattern.compile("([0-9a-f]{12})");
|
||||||
|
private static String[] sOuiList = new String[]{};
|
||||||
|
|
||||||
|
private BSSIDBlockList() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void setFilterList(String[] list) {
|
||||||
|
sOuiList = list;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean contains(ScanResult scanResult) {
|
||||||
|
String BSSID = scanResult.BSSID;
|
||||||
|
if (BSSID == null || NULL_BSSID.equals(BSSID) || WILDCARD_BSSID.equals(BSSID)) {
|
||||||
|
return true; // blocked!
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isCanonicalBSSID(BSSID)) {
|
||||||
|
Log.w(LOG_TAG, "", new IllegalArgumentException("Unexpected BSSID format: " + BSSID));
|
||||||
|
return true; // blocked!
|
||||||
|
}
|
||||||
|
|
||||||
|
for (String oui : sOuiList) {
|
||||||
|
if (BSSID.startsWith(oui)) {
|
||||||
|
return true; // blocked!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false; // OK
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String canonicalizeBSSID(String BSSID) {
|
||||||
|
if (BSSID == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isCanonicalBSSID(BSSID)) {
|
||||||
|
return BSSID;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Some devices may return BSSIDs with ':', '-' or '.' delimiters.
|
||||||
|
BSSID = BSSID.toLowerCase(Locale.US).replaceAll("[\\-\\.:]", "");
|
||||||
|
|
||||||
|
return isCanonicalBSSID(BSSID) ? BSSID : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isCanonicalBSSID(String BSSID) {
|
||||||
|
return BSSID_PATTERN.matcher(BSSID).matches();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package org.mozilla.mozstumbler.service.stumblerthread.blocklist;
|
||||||
|
|
||||||
|
import android.net.wifi.ScanResult;
|
||||||
|
|
||||||
|
public final class SSIDBlockList {
|
||||||
|
private static String[] sPrefixList = new String[]{};
|
||||||
|
private static String[] sSuffixList = new String[]{"_nomap"};
|
||||||
|
|
||||||
|
private SSIDBlockList() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void setFilterLists(String[] prefix, String[] suffix) {
|
||||||
|
sPrefixList = prefix;
|
||||||
|
sSuffixList = suffix;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean contains(ScanResult scanResult) {
|
||||||
|
String SSID = scanResult.SSID;
|
||||||
|
if (SSID == null) {
|
||||||
|
return true; // no SSID?
|
||||||
|
}
|
||||||
|
|
||||||
|
for (String prefix : sPrefixList) {
|
||||||
|
if (SSID.startsWith(prefix)) {
|
||||||
|
return true; // blocked!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (String suffix : sSuffixList) {
|
||||||
|
if (SSID.endsWith(suffix)) {
|
||||||
|
return true; // blocked!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false; // OK
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,11 +2,10 @@
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
package org.mozilla.mozstumbler;
|
package org.mozilla.mozstumbler.service.stumblerthread.blocklist;
|
||||||
|
|
||||||
/**
|
public interface WifiBlockListInterface {
|
||||||
* Bug 1024708: this class is a place-holder for landing the build integration
|
String[] getSsidPrefixList();
|
||||||
* of the background stumbler into Fennec.
|
String[] getSsidSuffixList();
|
||||||
*/
|
String[] getBssidOuiList();
|
||||||
public class PlaceHolder {
|
|
||||||
}
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package org.mozilla.mozstumbler.service.stumblerthread.datahandling;
|
||||||
|
|
||||||
|
public final class DataStorageContract {
|
||||||
|
|
||||||
|
public static class ReportsColumns {
|
||||||
|
public static final String LAT = "lat";
|
||||||
|
public static final String LON = "lon";
|
||||||
|
public static final String TIME = "timestamp";
|
||||||
|
public static final String ACCURACY = "accuracy";
|
||||||
|
public static final String ALTITUDE = "altitude";
|
||||||
|
public static final String RADIO = "radio";
|
||||||
|
public static final String CELL = "cell";
|
||||||
|
public static final String WIFI = "wifi";
|
||||||
|
public static final String CELL_COUNT = "cell_count";
|
||||||
|
public static final String WIFI_COUNT = "wifi_count";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Stats {
|
||||||
|
public static final String KEY_VERSION = "version_code";
|
||||||
|
public static final int VERSION_CODE = 1;
|
||||||
|
public static final String KEY_BYTES_SENT = "bytes_sent";
|
||||||
|
public static final String KEY_LAST_UPLOAD_TIME = "last_upload_time";
|
||||||
|
public static final String KEY_OBSERVATIONS_SENT = "observations_sent";
|
||||||
|
public static final String KEY_WIFIS_SENT = "wifis_sent";
|
||||||
|
public static final String KEY_CELLS_SENT = "cells_sent";
|
||||||
|
}
|
||||||
|
|
||||||
|
private DataStorageContract() {
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,527 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package org.mozilla.mozstumbler.service.stumblerthread.datahandling;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.util.Log;
|
||||||
|
import org.mozilla.mozstumbler.service.AppGlobals;
|
||||||
|
import org.mozilla.mozstumbler.service.utils.Zipper;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.RandomAccessFile;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Properties;
|
||||||
|
import java.util.Timer;
|
||||||
|
import java.util.TimerTask;
|
||||||
|
|
||||||
|
/* Stores reports in memory (mCurrentReports) until MAX_REPORTS_IN_MEMORY,
|
||||||
|
* then writes them to disk as a .gz file. The name of the file has
|
||||||
|
* the time written, the # of reports, and the # of cells and wifis.
|
||||||
|
*
|
||||||
|
* Each .gz file is typically 1-5KB. File name example: reports-t1406863343313-r4-w25-c7.gz
|
||||||
|
*
|
||||||
|
* The sync stats are written as a key-value pair file (not zipped).
|
||||||
|
*
|
||||||
|
* The tricky bit is the mCurrentReportsSendBuffer. When the uploader code begins accessing the
|
||||||
|
* report batches, mCurrentReports gets pushed to mCurrentReportsSendBuffer.
|
||||||
|
* The mCurrentReports is then cleared, and can continue receiving new reports.
|
||||||
|
* From the uploader perspective, mCurrentReportsSendBuffer looks and acts exactly like a batch file on disk.
|
||||||
|
*
|
||||||
|
* If the network is reasonably active, and reporting is slow enough, there is no disk I/O, it all happens
|
||||||
|
* in-memory.
|
||||||
|
*
|
||||||
|
* Also of note: the in-memory buffers (both mCurrentReports and mCurrentReportsSendBuffer) are saved
|
||||||
|
* when the service is destroyed.
|
||||||
|
*/
|
||||||
|
public class DataStorageManager {
|
||||||
|
private static final String LOG_TAG = AppGlobals.LOG_PREFIX + DataStorageManager.class.getSimpleName();
|
||||||
|
|
||||||
|
// The max number of reports stored in the mCurrentReports. Each report is a GPS location plus wifi and cell scan.
|
||||||
|
// After this size is reached, data is persisted to disk, mCurrentReports is cleared.
|
||||||
|
private static final int MAX_REPORTS_IN_MEMORY = 50;
|
||||||
|
|
||||||
|
// Used to cap the amount of data stored. When this limit is hit, no more data is saved to disk
|
||||||
|
// until the data is uploaded, or and data exceeds DEFAULT_MAX_WEEKS_DATA_ON_DISK.
|
||||||
|
private static final long DEFAULT_MAX_BYTES_STORED_ON_DISK = 1024 * 250; // 250 KiB max by default
|
||||||
|
|
||||||
|
// Used as a safeguard to ensure stumbling data is not persisted. The intended use case of the stumbler lib is not
|
||||||
|
// for long-term storage, and so if ANY data on disk is this old, ALL data is wiped as a privacy mechanism.
|
||||||
|
private static final int DEFAULT_MAX_WEEKS_DATA_ON_DISK = 2;
|
||||||
|
|
||||||
|
// Set to the default value specified above.
|
||||||
|
private final long mMaxBytesDiskStorage;
|
||||||
|
|
||||||
|
// Set to the default value specified above.
|
||||||
|
private final int mMaxWeeksStored;
|
||||||
|
|
||||||
|
private final ReportBatchBuilder mCurrentReports = new ReportBatchBuilder();
|
||||||
|
private final File mReportsDir;
|
||||||
|
private final File mStatsFile;
|
||||||
|
private final StorageIsEmptyTracker mTracker;
|
||||||
|
|
||||||
|
private static DataStorageManager sInstance;
|
||||||
|
|
||||||
|
private ReportBatch mCurrentReportsSendBuffer;
|
||||||
|
private ReportBatchIterator mReportBatchIterator;
|
||||||
|
private ReportFileList mFileList;
|
||||||
|
private Timer mFlushMemoryBuffersToDiskTimer;
|
||||||
|
|
||||||
|
static final String SEP_REPORT_COUNT = "-r";
|
||||||
|
static final String SEP_WIFI_COUNT = "-w";
|
||||||
|
static final String SEP_CELL_COUNT = "-c";
|
||||||
|
static final String SEP_TIME_MS = "-t";
|
||||||
|
static final String FILENAME_PREFIX = "reports";
|
||||||
|
static final String MEMORY_BUFFER_NAME = "in memory send buffer";
|
||||||
|
|
||||||
|
public static class QueuedCounts {
|
||||||
|
public final int mReportCount;
|
||||||
|
public final int mWifiCount;
|
||||||
|
public final int mCellCount;
|
||||||
|
public final long mBytes;
|
||||||
|
|
||||||
|
QueuedCounts(int reportCount, int wifiCount, int cellCount, long bytes) {
|
||||||
|
this.mReportCount = reportCount;
|
||||||
|
this.mWifiCount = wifiCount;
|
||||||
|
this.mCellCount = cellCount;
|
||||||
|
this.mBytes = bytes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Some data is calculated on-demand, don't abuse this function */
|
||||||
|
public QueuedCounts getQueuedCounts() {
|
||||||
|
int reportCount = mFileList.mReportCount + mCurrentReports.reports.size();
|
||||||
|
int wifiCount = mFileList.mWifiCount + mCurrentReports.wifiCount;
|
||||||
|
int cellCount = mFileList.mCellCount + mCurrentReports.cellCount;
|
||||||
|
long bytes = 0;
|
||||||
|
|
||||||
|
if (mCurrentReports.reports.size() > 0) {
|
||||||
|
try {
|
||||||
|
bytes = Zipper.zipData(finalizeReports(mCurrentReports.reports).getBytes()).length;
|
||||||
|
} catch (IOException ex) {
|
||||||
|
Log.e(LOG_TAG, "Zip error in getQueuedCounts()", ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mFileList.mReportCount > 0) {
|
||||||
|
bytes += mFileList.mFilesOnDiskBytes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mCurrentReportsSendBuffer != null) {
|
||||||
|
reportCount += mCurrentReportsSendBuffer.reportCount;
|
||||||
|
wifiCount += mCurrentReportsSendBuffer.wifiCount;
|
||||||
|
cellCount += mCurrentReportsSendBuffer.cellCount;
|
||||||
|
bytes += mCurrentReportsSendBuffer.data.length;
|
||||||
|
}
|
||||||
|
return new QueuedCounts(reportCount, wifiCount, cellCount, bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class ReportFileList {
|
||||||
|
File[] mFiles;
|
||||||
|
int mReportCount;
|
||||||
|
int mWifiCount;
|
||||||
|
int mCellCount;
|
||||||
|
long mFilesOnDiskBytes;
|
||||||
|
|
||||||
|
public ReportFileList() {}
|
||||||
|
public ReportFileList(ReportFileList other) {
|
||||||
|
if (other == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (other.mFiles != null) {
|
||||||
|
mFiles = other.mFiles.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
mReportCount = other.mReportCount;
|
||||||
|
mWifiCount = other.mWifiCount;
|
||||||
|
mCellCount = other.mCellCount;
|
||||||
|
mFilesOnDiskBytes = other.mFilesOnDiskBytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
void update(File directory) {
|
||||||
|
mFiles = directory.listFiles();
|
||||||
|
if (mFiles == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (AppGlobals.isDebug) {
|
||||||
|
for (File f : mFiles) {
|
||||||
|
Log.d("StumblerFiles", f.getName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mFilesOnDiskBytes = mReportCount = mWifiCount = mCellCount = 0;
|
||||||
|
for (File f : mFiles) {
|
||||||
|
mReportCount += (int) getLongFromFilename(f.getName(), SEP_REPORT_COUNT);
|
||||||
|
mWifiCount += (int) getLongFromFilename(f.getName(), SEP_WIFI_COUNT);
|
||||||
|
mCellCount += (int) getLongFromFilename(f.getName(), SEP_CELL_COUNT);
|
||||||
|
mFilesOnDiskBytes += f.length();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class ReportBatch {
|
||||||
|
public final String filename;
|
||||||
|
public final byte[] data;
|
||||||
|
public final int reportCount;
|
||||||
|
public final int wifiCount;
|
||||||
|
public final int cellCount;
|
||||||
|
|
||||||
|
public ReportBatch(String filename, byte[] data, int reportCount, int wifiCount, int cellCount) {
|
||||||
|
this.filename = filename;
|
||||||
|
this.data = data;
|
||||||
|
this.reportCount = reportCount;
|
||||||
|
this.wifiCount = wifiCount;
|
||||||
|
this.cellCount = cellCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class ReportBatchBuilder {
|
||||||
|
public final ArrayList<String> reports = new ArrayList<String>();
|
||||||
|
public int wifiCount;
|
||||||
|
public int cellCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class ReportBatchIterator {
|
||||||
|
public ReportBatchIterator(ReportFileList list) {
|
||||||
|
fileList = new ReportFileList(list);
|
||||||
|
}
|
||||||
|
|
||||||
|
static final int BATCH_INDEX_FOR_MEM_BUFFER = -1;
|
||||||
|
public int currentIndex = BATCH_INDEX_FOR_MEM_BUFFER;
|
||||||
|
public final ReportFileList fileList;
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface StorageIsEmptyTracker {
|
||||||
|
public void notifyStorageStateEmpty(boolean isEmpty);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getStorageDir(Context c) {
|
||||||
|
File dir = null;
|
||||||
|
if (AppGlobals.isDebug) {
|
||||||
|
// in debug, put files in public location
|
||||||
|
dir = c.getExternalFilesDir(null);
|
||||||
|
if (dir != null) {
|
||||||
|
dir = new File(dir.getAbsolutePath() + "/mozstumbler");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dir == null) {
|
||||||
|
dir = c.getFilesDir();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!dir.exists()) {
|
||||||
|
boolean ok = dir.mkdirs();
|
||||||
|
if (!ok) {
|
||||||
|
Log.d(LOG_TAG, "getStorageDir: error in mkdirs()");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return dir.getPath();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static synchronized void createGlobalInstance(Context context, StorageIsEmptyTracker tracker) {
|
||||||
|
DataStorageManager.createGlobalInstance(context, tracker,
|
||||||
|
DEFAULT_MAX_BYTES_STORED_ON_DISK, DEFAULT_MAX_WEEKS_DATA_ON_DISK);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static synchronized void createGlobalInstance(Context context, StorageIsEmptyTracker tracker,
|
||||||
|
long maxBytesStoredOnDisk, int maxWeeksDataStored) {
|
||||||
|
if (sInstance != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sInstance = new DataStorageManager(context, tracker, maxBytesStoredOnDisk, maxWeeksDataStored);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static synchronized DataStorageManager getInstance() {
|
||||||
|
return sInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
private DataStorageManager(Context c, StorageIsEmptyTracker tracker,
|
||||||
|
long maxBytesStoredOnDisk, int maxWeeksDataStored) {
|
||||||
|
mMaxBytesDiskStorage = maxBytesStoredOnDisk;
|
||||||
|
mMaxWeeksStored = maxWeeksDataStored;
|
||||||
|
mTracker = tracker;
|
||||||
|
final String baseDir = getStorageDir(c);
|
||||||
|
mStatsFile = new File(baseDir, "upload_stats.ini");
|
||||||
|
mReportsDir = new File(baseDir + "/reports");
|
||||||
|
if (!mReportsDir.exists()) {
|
||||||
|
mReportsDir.mkdirs();
|
||||||
|
}
|
||||||
|
mFileList = new ReportFileList();
|
||||||
|
mFileList.update(mReportsDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized int getMaxWeeksStored() {
|
||||||
|
return mMaxWeeksStored;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] readFile(File file) throws IOException {
|
||||||
|
final RandomAccessFile f = new RandomAccessFile(file, "r");
|
||||||
|
try {
|
||||||
|
final byte[] data = new byte[(int) f.length()];
|
||||||
|
f.readFully(data);
|
||||||
|
return data;
|
||||||
|
} finally {
|
||||||
|
f.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized boolean isDirEmpty() {
|
||||||
|
return (mFileList.mFiles == null || mFileList.mFiles.length < 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pass filename returned from dataToSend() */
|
||||||
|
public synchronized boolean delete(String filename) {
|
||||||
|
if (filename == MEMORY_BUFFER_NAME) {
|
||||||
|
mCurrentReportsSendBuffer = null;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
final File file = new File(mReportsDir, filename);
|
||||||
|
final boolean ok = file.delete();
|
||||||
|
mFileList.update(mReportsDir);
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long getLongFromFilename(String name, String separator) {
|
||||||
|
final int s = name.indexOf(separator) + separator.length();
|
||||||
|
int e = name.indexOf('-', s);
|
||||||
|
if (e < 0) {
|
||||||
|
e = name.indexOf('.', s);
|
||||||
|
}
|
||||||
|
return Long.parseLong(name.substring(s, e));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* return name of file used, or memory buffer sentinel value.
|
||||||
|
* The return value is used to delete the file/buffer later. */
|
||||||
|
public synchronized ReportBatch getFirstBatch() throws IOException {
|
||||||
|
final boolean dirEmpty = isDirEmpty();
|
||||||
|
final int currentReportsCount = mCurrentReports.reports.size();
|
||||||
|
|
||||||
|
if (dirEmpty && currentReportsCount < 1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
mReportBatchIterator = new ReportBatchIterator(mFileList);
|
||||||
|
|
||||||
|
if (currentReportsCount > 0) {
|
||||||
|
final String filename = MEMORY_BUFFER_NAME;
|
||||||
|
final byte[] data = Zipper.zipData(finalizeReports(mCurrentReports.reports).getBytes());
|
||||||
|
final int wifiCount = mCurrentReports.wifiCount;
|
||||||
|
final int cellCount = mCurrentReports.cellCount;
|
||||||
|
clearCurrentReports();
|
||||||
|
final ReportBatch result = new ReportBatch(filename, data, currentReportsCount, wifiCount, cellCount);
|
||||||
|
mCurrentReportsSendBuffer = result;
|
||||||
|
return result;
|
||||||
|
} else {
|
||||||
|
return getNextBatch();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void clearCurrentReports() {
|
||||||
|
mCurrentReports.reports.clear();
|
||||||
|
mCurrentReports.wifiCount = mCurrentReports.cellCount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized ReportBatch getNextBatch() throws IOException {
|
||||||
|
if (mReportBatchIterator == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
mReportBatchIterator.currentIndex++;
|
||||||
|
if (mReportBatchIterator.currentIndex < 0 ||
|
||||||
|
mReportBatchIterator.currentIndex > mReportBatchIterator.fileList.mFiles.length - 1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final File f = mReportBatchIterator.fileList.mFiles[mReportBatchIterator.currentIndex];
|
||||||
|
final String filename = f.getName();
|
||||||
|
final int reportCount = (int) getLongFromFilename(f.getName(), SEP_REPORT_COUNT);
|
||||||
|
final int wifiCount = (int) getLongFromFilename(f.getName(), SEP_WIFI_COUNT);
|
||||||
|
final int cellCount = (int) getLongFromFilename(f.getName(), SEP_CELL_COUNT);
|
||||||
|
final byte[] data = readFile(f);
|
||||||
|
return new ReportBatch(filename, data, reportCount, wifiCount, cellCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
private File createFile(int reportCount, int wifiCount, int cellCount) {
|
||||||
|
final long time = System.currentTimeMillis();
|
||||||
|
final String name = FILENAME_PREFIX +
|
||||||
|
SEP_TIME_MS + time +
|
||||||
|
SEP_REPORT_COUNT + reportCount +
|
||||||
|
SEP_WIFI_COUNT + wifiCount +
|
||||||
|
SEP_CELL_COUNT + cellCount + ".gz";
|
||||||
|
return new File(mReportsDir, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized long getOldestBatchTimeMs() {
|
||||||
|
if (isDirEmpty()) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
long oldest = Long.MAX_VALUE;
|
||||||
|
for (File f : mFileList.mFiles) {
|
||||||
|
final long t = getLongFromFilename(f.getName(), SEP_TIME_MS);
|
||||||
|
if (t < oldest) {
|
||||||
|
oldest = t;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return oldest;
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void saveCurrentReportsSendBufferToDisk() throws IOException {
|
||||||
|
if (mCurrentReportsSendBuffer == null || mCurrentReportsSendBuffer.reportCount < 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
saveToDisk(mCurrentReportsSendBuffer.data,
|
||||||
|
mCurrentReportsSendBuffer.reportCount,
|
||||||
|
mCurrentReportsSendBuffer.wifiCount,
|
||||||
|
mCurrentReportsSendBuffer.cellCount);
|
||||||
|
mCurrentReportsSendBuffer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void saveToDisk(byte[] bytes, int reportCount, int wifiCount, int cellCount)
|
||||||
|
throws IOException {
|
||||||
|
if (mFileList.mFilesOnDiskBytes > mMaxBytesDiskStorage) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final FileOutputStream fos = new FileOutputStream(createFile(reportCount, wifiCount, cellCount));
|
||||||
|
try {
|
||||||
|
fos.write(bytes);
|
||||||
|
} finally {
|
||||||
|
fos.close();
|
||||||
|
}
|
||||||
|
mFileList.update(mReportsDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String finalizeReports(ArrayList<String> reports) {
|
||||||
|
final String kPrefix = "{\"items\":[";
|
||||||
|
final String kSuffix = "]}";
|
||||||
|
final StringBuilder sb = new StringBuilder(kPrefix);
|
||||||
|
String sep = "";
|
||||||
|
final String separator = ",";
|
||||||
|
if (reports != null) {
|
||||||
|
for(String s: reports) {
|
||||||
|
sb.append(sep).append(s);
|
||||||
|
sep = separator;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final String result = sb.append(kSuffix).toString();
|
||||||
|
if (AppGlobals.isDebug) {
|
||||||
|
Log.d(LOG_TAG, result);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void saveCurrentReportsToDisk() throws IOException {
|
||||||
|
saveCurrentReportsSendBufferToDisk();
|
||||||
|
if (mCurrentReports.reports.size() < 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final byte[] bytes = Zipper.zipData(finalizeReports(mCurrentReports.reports).getBytes());
|
||||||
|
saveToDisk(bytes, mCurrentReports.reports.size(), mCurrentReports.wifiCount, mCurrentReports.cellCount);
|
||||||
|
clearCurrentReports();
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void insert(String report, int wifiCount, int cellCount) throws IOException {
|
||||||
|
notifyStorageIsEmpty(false);
|
||||||
|
|
||||||
|
if (mFlushMemoryBuffersToDiskTimer != null) {
|
||||||
|
mFlushMemoryBuffersToDiskTimer.cancel();
|
||||||
|
mFlushMemoryBuffersToDiskTimer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
mCurrentReports.reports.add(report);
|
||||||
|
mCurrentReports.wifiCount = wifiCount;
|
||||||
|
mCurrentReports.cellCount = cellCount;
|
||||||
|
|
||||||
|
if (mCurrentReports.reports.size() >= MAX_REPORTS_IN_MEMORY) {
|
||||||
|
// save to disk
|
||||||
|
saveCurrentReportsToDisk();
|
||||||
|
} else {
|
||||||
|
// Schedule a timer to flush to disk after a few mins.
|
||||||
|
// If collection stops and wifi not available for uploading, the memory buffer is flushed to disk.
|
||||||
|
final int kMillis = 1000 * 60 * 3;
|
||||||
|
mFlushMemoryBuffersToDiskTimer = new Timer();
|
||||||
|
mFlushMemoryBuffersToDiskTimer.schedule(new TimerTask() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
try {
|
||||||
|
saveCurrentReportsToDisk();
|
||||||
|
} catch (IOException ex) {
|
||||||
|
Log.e(LOG_TAG, "mFlushMemoryBuffersToDiskTimer exception" + ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, kMillis);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized Properties readSyncStats() throws IOException {
|
||||||
|
if (!mStatsFile.exists()) {
|
||||||
|
return new Properties();
|
||||||
|
}
|
||||||
|
|
||||||
|
final FileInputStream input = new FileInputStream(mStatsFile);
|
||||||
|
try {
|
||||||
|
final Properties props = new Properties();
|
||||||
|
props.load(input);
|
||||||
|
return props;
|
||||||
|
} finally {
|
||||||
|
input.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void incrementSyncStats(long bytesSent, long reports, long cells, long wifis) throws IOException {
|
||||||
|
if (reports + cells + wifis < 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final Properties properties = readSyncStats();
|
||||||
|
final long time = System.currentTimeMillis();
|
||||||
|
writeSyncStats(time,
|
||||||
|
Long.parseLong(properties.getProperty(DataStorageContract.Stats.KEY_BYTES_SENT, "0")) + bytesSent,
|
||||||
|
Long.parseLong(properties.getProperty(DataStorageContract.Stats.KEY_OBSERVATIONS_SENT, "0")) + reports,
|
||||||
|
Long.parseLong(properties.getProperty(DataStorageContract.Stats.KEY_CELLS_SENT, "0")) + cells,
|
||||||
|
Long.parseLong(properties.getProperty(DataStorageContract.Stats.KEY_WIFIS_SENT, "0")) + wifis);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void writeSyncStats(long time, long bytesSent, long totalObs, long totalCells, long totalWifis) throws IOException {
|
||||||
|
final FileOutputStream out = new FileOutputStream(mStatsFile);
|
||||||
|
try {
|
||||||
|
final Properties props = new Properties();
|
||||||
|
props.setProperty(DataStorageContract.Stats.KEY_LAST_UPLOAD_TIME, String.valueOf(time));
|
||||||
|
props.setProperty(DataStorageContract.Stats.KEY_BYTES_SENT, String.valueOf(bytesSent));
|
||||||
|
props.setProperty(DataStorageContract.Stats.KEY_OBSERVATIONS_SENT, String.valueOf(totalObs));
|
||||||
|
props.setProperty(DataStorageContract.Stats.KEY_CELLS_SENT, String.valueOf(totalCells));
|
||||||
|
props.setProperty(DataStorageContract.Stats.KEY_WIFIS_SENT, String.valueOf(totalWifis));
|
||||||
|
props.setProperty(DataStorageContract.Stats.KEY_VERSION, String.valueOf(DataStorageContract.Stats.VERSION_CODE));
|
||||||
|
props.store(out, null);
|
||||||
|
} finally {
|
||||||
|
out.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void deleteAll() {
|
||||||
|
if (mFileList.mFiles == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (File f : mFileList.mFiles) {
|
||||||
|
f.delete();
|
||||||
|
}
|
||||||
|
mFileList.update(mReportsDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void notifyStorageIsEmpty(boolean isEmpty) {
|
||||||
|
if (mTracker != null) {
|
||||||
|
mTracker.notifyStorageStateEmpty(isEmpty);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,157 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package org.mozilla.mozstumbler.service.stumblerthread.datahandling;
|
||||||
|
|
||||||
|
import android.location.Location;
|
||||||
|
import android.net.wifi.ScanResult;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.os.Parcel;
|
||||||
|
import android.os.Parcelable;
|
||||||
|
import android.telephony.TelephonyManager;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import org.json.JSONArray;
|
||||||
|
import org.json.JSONException;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
import org.mozilla.mozstumbler.service.stumblerthread.scanners.cellscanner.CellInfo;
|
||||||
|
|
||||||
|
public final class StumblerBundle implements Parcelable {
|
||||||
|
private final int mPhoneType;
|
||||||
|
private final Location mGpsPosition;
|
||||||
|
private final Map<String, ScanResult> mWifiData;
|
||||||
|
private final Map<String, CellInfo> mCellData;
|
||||||
|
|
||||||
|
public void wasSent() {
|
||||||
|
mGpsPosition.setTime(System.currentTimeMillis());
|
||||||
|
mWifiData.clear();
|
||||||
|
mCellData.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int describeContents() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void writeToParcel(Parcel out, int flags) {
|
||||||
|
Bundle wifiBundle = new Bundle(ScanResult.class.getClassLoader());
|
||||||
|
Collection<String> scans = mWifiData.keySet();
|
||||||
|
for (String s : scans) {
|
||||||
|
wifiBundle.putParcelable(s, mWifiData.get(s));
|
||||||
|
}
|
||||||
|
|
||||||
|
Bundle cellBundle = new Bundle(CellInfo.class.getClassLoader());
|
||||||
|
Collection<String> cells = mCellData.keySet();
|
||||||
|
for (String c : cells) {
|
||||||
|
cellBundle.putParcelable(c, mCellData.get(c));
|
||||||
|
}
|
||||||
|
|
||||||
|
out.writeBundle(wifiBundle);
|
||||||
|
out.writeBundle(cellBundle);
|
||||||
|
out.writeParcelable(mGpsPosition, 0);
|
||||||
|
out.writeInt(mPhoneType);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final Parcelable.Creator<StumblerBundle> CREATOR
|
||||||
|
= new Parcelable.Creator<StumblerBundle>() {
|
||||||
|
@Override
|
||||||
|
public StumblerBundle createFromParcel(Parcel in) {
|
||||||
|
return new StumblerBundle(in);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public StumblerBundle[] newArray(int size) {
|
||||||
|
return new StumblerBundle[size];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private StumblerBundle(Parcel in) {
|
||||||
|
mWifiData = new HashMap<String, ScanResult>();
|
||||||
|
mCellData = new HashMap<String, CellInfo>();
|
||||||
|
|
||||||
|
Bundle wifiBundle = in.readBundle(ScanResult.class.getClassLoader());
|
||||||
|
Bundle cellBundle = in.readBundle(CellInfo.class.getClassLoader());
|
||||||
|
|
||||||
|
Collection<String> scans = wifiBundle.keySet();
|
||||||
|
for (String s : scans) {
|
||||||
|
mWifiData.put(s, (ScanResult) wifiBundle.get(s));
|
||||||
|
}
|
||||||
|
|
||||||
|
Collection<String> cells = cellBundle.keySet();
|
||||||
|
for (String c : cells) {
|
||||||
|
mCellData.put(c, (CellInfo) cellBundle.get(c));
|
||||||
|
}
|
||||||
|
|
||||||
|
mGpsPosition = in.readParcelable(Location.class.getClassLoader());
|
||||||
|
mPhoneType = in.readInt();
|
||||||
|
}
|
||||||
|
|
||||||
|
public StumblerBundle(Location position, int phoneType) {
|
||||||
|
mGpsPosition = position;
|
||||||
|
mPhoneType = phoneType;
|
||||||
|
mWifiData = new HashMap<String, ScanResult>();
|
||||||
|
mCellData = new HashMap<String, CellInfo>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Location getGpsPosition() {
|
||||||
|
return mGpsPosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, ScanResult> getWifiData() {
|
||||||
|
return mWifiData;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, CellInfo> getCellData() {
|
||||||
|
return mCellData;
|
||||||
|
}
|
||||||
|
|
||||||
|
public JSONObject toMLSJSON() throws JSONException {
|
||||||
|
JSONObject item = new JSONObject();
|
||||||
|
|
||||||
|
item.put(DataStorageContract.ReportsColumns.TIME, mGpsPosition.getTime());
|
||||||
|
item.put(DataStorageContract.ReportsColumns.LAT, Math.floor(mGpsPosition.getLatitude() * 1.0E6) / 1.0E6);
|
||||||
|
item.put(DataStorageContract.ReportsColumns.LON, Math.floor(mGpsPosition.getLongitude() * 1.0E6) / 1.0E6);
|
||||||
|
|
||||||
|
if (mGpsPosition.hasAccuracy()) {
|
||||||
|
item.put(DataStorageContract.ReportsColumns.ACCURACY, (int) Math.ceil(mGpsPosition.getAccuracy()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mGpsPosition.hasAltitude()) {
|
||||||
|
item.put(DataStorageContract.ReportsColumns.ALTITUDE, Math.round(mGpsPosition.getAltitude()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mPhoneType == TelephonyManager.PHONE_TYPE_GSM) {
|
||||||
|
item.put(DataStorageContract.ReportsColumns.RADIO, "gsm");
|
||||||
|
} else if (mPhoneType == TelephonyManager.PHONE_TYPE_CDMA) {
|
||||||
|
item.put(DataStorageContract.ReportsColumns.RADIO, "cdma");
|
||||||
|
} else {
|
||||||
|
// issue #598. investigate this case further in future
|
||||||
|
item.put(DataStorageContract.ReportsColumns.RADIO, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
JSONArray cellJSON = new JSONArray();
|
||||||
|
for (CellInfo c : mCellData.values()) {
|
||||||
|
JSONObject obj = c.toJSONObject();
|
||||||
|
cellJSON.put(obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
item.put(DataStorageContract.ReportsColumns.CELL, cellJSON);
|
||||||
|
item.put(DataStorageContract.ReportsColumns.CELL_COUNT, cellJSON.length());
|
||||||
|
|
||||||
|
JSONArray wifis = new JSONArray();
|
||||||
|
for (ScanResult s : mWifiData.values()) {
|
||||||
|
JSONObject wifiEntry = new JSONObject();
|
||||||
|
wifiEntry.put("key", s.BSSID);
|
||||||
|
wifiEntry.put("frequency", s.frequency);
|
||||||
|
wifiEntry.put("signal", s.level);
|
||||||
|
wifis.put(wifiEntry);
|
||||||
|
}
|
||||||
|
item.put(DataStorageContract.ReportsColumns.WIFI, wifis);
|
||||||
|
item.put(DataStorageContract.ReportsColumns.WIFI_COUNT, wifis.length());
|
||||||
|
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,260 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package org.mozilla.mozstumbler.service.stumblerthread.scanners;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.location.GpsSatellite;
|
||||||
|
import android.location.GpsStatus;
|
||||||
|
import android.location.Location;
|
||||||
|
import android.location.LocationListener;
|
||||||
|
import android.location.LocationManager;
|
||||||
|
import android.location.LocationProvider;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.support.v4.content.LocalBroadcastManager;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import org.mozilla.mozstumbler.service.AppGlobals;
|
||||||
|
import org.mozilla.mozstumbler.service.AppGlobals.ActiveOrPassiveStumbling;
|
||||||
|
import org.mozilla.mozstumbler.service.Prefs;
|
||||||
|
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
public class GPSScanner implements LocationListener {
|
||||||
|
public static final String ACTION_BASE = AppGlobals.ACTION_NAMESPACE + ".GPSScanner.";
|
||||||
|
public static final String ACTION_GPS_UPDATED = ACTION_BASE + "GPS_UPDATED";
|
||||||
|
public static final String ACTION_ARG_TIME = AppGlobals.ACTION_ARG_TIME;
|
||||||
|
public static final String SUBJECT_NEW_STATUS = "new_status";
|
||||||
|
public static final String SUBJECT_LOCATION_LOST = "location_lost";
|
||||||
|
public static final String SUBJECT_NEW_LOCATION = "new_location";
|
||||||
|
public static final String NEW_STATUS_ARG_FIXES = "fixes";
|
||||||
|
public static final String NEW_STATUS_ARG_SATS = "sats";
|
||||||
|
public static final String NEW_LOCATION_ARG_LOCATION = "location";
|
||||||
|
|
||||||
|
private static final String LOG_TAG = AppGlobals.LOG_PREFIX + GPSScanner.class.getSimpleName();
|
||||||
|
private static final int MIN_SAT_USED_IN_FIX = 3;
|
||||||
|
private static final long ACTIVE_MODE_GPS_MIN_UPDATE_TIME_MS = 1000;
|
||||||
|
private static final float ACTIVE_MODE_GPS_MIN_UPDATE_DISTANCE_M = 10;
|
||||||
|
private static final long PASSIVE_GPS_MIN_UPDATE_FREQ_MS = 3000;
|
||||||
|
private static final float PASSIVE_GPS_MOVEMENT_MIN_DELTA_M = 30;
|
||||||
|
|
||||||
|
private final LocationBlockList mBlockList = new LocationBlockList();
|
||||||
|
private final Context mContext;
|
||||||
|
private GpsStatus.Listener mGPSListener;
|
||||||
|
private int mLocationCount;
|
||||||
|
private Location mLocation = new Location("internal");
|
||||||
|
private boolean mAutoGeofencing;
|
||||||
|
private boolean mIsPassiveMode;
|
||||||
|
|
||||||
|
private final ScanManager mScanManager;
|
||||||
|
|
||||||
|
public GPSScanner(Context context, ScanManager scanManager) {
|
||||||
|
mContext = context;
|
||||||
|
mScanManager = scanManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void start(final ActiveOrPassiveStumbling stumblingMode) {
|
||||||
|
mIsPassiveMode = (stumblingMode == ActiveOrPassiveStumbling.PASSIVE_STUMBLING);
|
||||||
|
if (mIsPassiveMode ) {
|
||||||
|
startPassiveMode();
|
||||||
|
} else {
|
||||||
|
startActiveMode();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startPassiveMode() {
|
||||||
|
LocationManager locationManager = (LocationManager) mContext.getSystemService(Context.LOCATION_SERVICE);
|
||||||
|
locationManager.requestLocationUpdates(LocationManager.PASSIVE_PROVIDER,
|
||||||
|
0,
|
||||||
|
0, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startActiveMode() {
|
||||||
|
LocationManager lm = getLocationManager();
|
||||||
|
lm.requestLocationUpdates(LocationManager.GPS_PROVIDER,
|
||||||
|
ACTIVE_MODE_GPS_MIN_UPDATE_TIME_MS,
|
||||||
|
ACTIVE_MODE_GPS_MIN_UPDATE_DISTANCE_M,
|
||||||
|
this);
|
||||||
|
|
||||||
|
reportLocationLost();
|
||||||
|
mGPSListener = new GpsStatus.Listener() {
|
||||||
|
public void onGpsStatusChanged(int event) {
|
||||||
|
if (event == GpsStatus.GPS_EVENT_SATELLITE_STATUS) {
|
||||||
|
GpsStatus status = getLocationManager().getGpsStatus(null);
|
||||||
|
Iterable<GpsSatellite> sats = status.getSatellites();
|
||||||
|
|
||||||
|
int satellites = 0;
|
||||||
|
int fixes = 0;
|
||||||
|
|
||||||
|
for (GpsSatellite sat : sats) {
|
||||||
|
satellites++;
|
||||||
|
if (sat.usedInFix()) {
|
||||||
|
fixes++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reportNewGpsStatus(fixes, satellites);
|
||||||
|
if (fixes < MIN_SAT_USED_IN_FIX) {
|
||||||
|
reportLocationLost();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (AppGlobals.isDebug) {
|
||||||
|
Log.v(LOG_TAG, "onGpsStatusChange - satellites: " + satellites + " fixes: " + fixes);
|
||||||
|
}
|
||||||
|
} else if (event == GpsStatus.GPS_EVENT_STOPPED) {
|
||||||
|
reportLocationLost();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
lm.addGpsStatusListener(mGPSListener);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void stop() {
|
||||||
|
LocationManager lm = getLocationManager();
|
||||||
|
lm.removeUpdates(this);
|
||||||
|
reportLocationLost();
|
||||||
|
|
||||||
|
if (mGPSListener != null) {
|
||||||
|
lm.removeGpsStatusListener(mGPSListener);
|
||||||
|
mGPSListener = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getLocationCount() {
|
||||||
|
return mLocationCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getLatitude() {
|
||||||
|
return mLocation.getLatitude();
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getLongitude() {
|
||||||
|
return mLocation.getLongitude();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Location getLocation() {
|
||||||
|
return mLocation;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void checkPrefs() {
|
||||||
|
if (mBlockList != null) {
|
||||||
|
mBlockList.updateBlocks();
|
||||||
|
}
|
||||||
|
|
||||||
|
mAutoGeofencing = Prefs.getInstance().getGeofenceHere();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isGeofenced() {
|
||||||
|
return (mBlockList != null) && mBlockList.isGeofenced();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendToLogActivity(String msg) {
|
||||||
|
AppGlobals.guiLogInfo(msg, "#33ccff", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onLocationChanged(Location location) {
|
||||||
|
if (location == null) { // TODO: is this even possible??
|
||||||
|
reportLocationLost();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String logMsg = (mIsPassiveMode)? "[Passive] " : "[Active] ";
|
||||||
|
|
||||||
|
String provider = location.getProvider();
|
||||||
|
if (!provider.toLowerCase().contains("gps")) {
|
||||||
|
sendToLogActivity(logMsg + "Discard fused/network location.");
|
||||||
|
// only interested in GPS locations
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Seem to get greater likelihood of non-fused location with higher update freq.
|
||||||
|
// Check dist and time threshold here, not set on the listener.
|
||||||
|
if (mIsPassiveMode) {
|
||||||
|
final long timeDelta = location.getTime() - mLocation.getTime();
|
||||||
|
final boolean hasMoved = location.distanceTo(mLocation) > PASSIVE_GPS_MOVEMENT_MIN_DELTA_M;
|
||||||
|
|
||||||
|
if (timeDelta < PASSIVE_GPS_MIN_UPDATE_FREQ_MS || !hasMoved) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Date date = new Date(location.getTime());
|
||||||
|
SimpleDateFormat formatter = new SimpleDateFormat("HH:mm:ss");
|
||||||
|
String time = formatter.format(date);
|
||||||
|
logMsg += String.format("%s Coord: %.4f,%.4f, Acc: %.0f, Speed: %.0f, Alt: %.0f, Bearing: %.1f", time, location.getLatitude(),
|
||||||
|
location.getLongitude(), location.getAccuracy(), location.getSpeed(), location.getAltitude(), location.getBearing());
|
||||||
|
sendToLogActivity(logMsg);
|
||||||
|
|
||||||
|
if (mBlockList.contains(location)) {
|
||||||
|
Log.w(LOG_TAG, "Blocked location: " + location);
|
||||||
|
reportLocationLost();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (AppGlobals.isDebug) {
|
||||||
|
Log.d(LOG_TAG, "New location: " + location);
|
||||||
|
}
|
||||||
|
|
||||||
|
mLocation = location;
|
||||||
|
|
||||||
|
if (!mAutoGeofencing) {
|
||||||
|
reportNewLocationReceived(location);
|
||||||
|
}
|
||||||
|
mLocationCount++;
|
||||||
|
|
||||||
|
if (mIsPassiveMode) {
|
||||||
|
mScanManager.newPassiveGpsLocation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onProviderDisabled(String provider) {
|
||||||
|
if (LocationManager.GPS_PROVIDER.equals(provider)) {
|
||||||
|
reportLocationLost();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onProviderEnabled(String provider) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onStatusChanged(String provider, int status, Bundle extras) {
|
||||||
|
if ((status != LocationProvider.AVAILABLE) &&
|
||||||
|
(LocationManager.GPS_PROVIDER.equals(provider))) {
|
||||||
|
reportLocationLost();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private LocationManager getLocationManager() {
|
||||||
|
return (LocationManager) mContext.getSystemService(Context.LOCATION_SERVICE);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void reportNewLocationReceived(Location location) {
|
||||||
|
Intent i = new Intent(ACTION_GPS_UPDATED);
|
||||||
|
i.putExtra(Intent.EXTRA_SUBJECT, SUBJECT_NEW_LOCATION);
|
||||||
|
i.putExtra(NEW_LOCATION_ARG_LOCATION, location);
|
||||||
|
i.putExtra(ACTION_ARG_TIME, System.currentTimeMillis());
|
||||||
|
LocalBroadcastManager.getInstance(mContext).sendBroadcastSync(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void reportLocationLost() {
|
||||||
|
Intent i = new Intent(ACTION_GPS_UPDATED);
|
||||||
|
i.putExtra(Intent.EXTRA_SUBJECT, SUBJECT_LOCATION_LOST);
|
||||||
|
i.putExtra(ACTION_ARG_TIME, System.currentTimeMillis());
|
||||||
|
LocalBroadcastManager.getInstance(mContext).sendBroadcastSync(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void reportNewGpsStatus(int fixes, int sats) {
|
||||||
|
Intent i = new Intent(ACTION_GPS_UPDATED);
|
||||||
|
i.putExtra(Intent.EXTRA_SUBJECT, SUBJECT_NEW_STATUS);
|
||||||
|
i.putExtra(NEW_STATUS_ARG_FIXES, fixes);
|
||||||
|
i.putExtra(NEW_STATUS_ARG_SATS, sats);
|
||||||
|
i.putExtra(ACTION_ARG_TIME, System.currentTimeMillis());
|
||||||
|
LocalBroadcastManager.getInstance(mContext).sendBroadcastSync(i);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,101 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package org.mozilla.mozstumbler.service.stumblerthread.scanners;
|
||||||
|
|
||||||
|
import android.location.Location;
|
||||||
|
import android.util.Log;
|
||||||
|
import org.mozilla.mozstumbler.service.AppGlobals;
|
||||||
|
import org.mozilla.mozstumbler.service.Prefs;
|
||||||
|
|
||||||
|
public final class LocationBlockList {
|
||||||
|
private static final String LOG_TAG = AppGlobals.LOG_PREFIX + LocationBlockList.class.getSimpleName();
|
||||||
|
private static final double MAX_ALTITUDE = 8848; // Mount Everest's altitude in meters
|
||||||
|
private static final double MIN_ALTITUDE = -418; // Dead Sea's altitude in meters
|
||||||
|
private static final float MAX_SPEED = 340.29f; // Mach 1 in meters/second
|
||||||
|
private static final float MIN_ACCURACY = 500; // meter radius
|
||||||
|
private static final long MIN_TIMESTAMP = 946684801; // 2000-01-01 00:00:01
|
||||||
|
private static final double GEOFENCE_RADIUS = 0.01; // .01 degrees is approximately 1km
|
||||||
|
private static final long MILLISECONDS_PER_DAY = 86400000;
|
||||||
|
|
||||||
|
private Location mBlockedLocation;
|
||||||
|
private boolean mGeofencingEnabled;
|
||||||
|
private boolean mIsGeofenced = false;
|
||||||
|
|
||||||
|
public LocationBlockList() {
|
||||||
|
updateBlocks();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateBlocks() {
|
||||||
|
mBlockedLocation = Prefs.getInstance().getGeofenceLocation();
|
||||||
|
mGeofencingEnabled = Prefs.getInstance().getGeofenceEnabled();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean contains(Location location) {
|
||||||
|
final float inaccuracy = location.getAccuracy();
|
||||||
|
final double altitude = location.getAltitude();
|
||||||
|
final float bearing = location.getBearing();
|
||||||
|
final double latitude = location.getLatitude();
|
||||||
|
final double longitude = location.getLongitude();
|
||||||
|
final float speed = location.getSpeed();
|
||||||
|
final long timestamp = location.getTime();
|
||||||
|
final long tomorrow = System.currentTimeMillis() + MILLISECONDS_PER_DAY;
|
||||||
|
|
||||||
|
boolean block = false;
|
||||||
|
mIsGeofenced = false;
|
||||||
|
|
||||||
|
if (latitude == 0 && longitude == 0) {
|
||||||
|
block = true;
|
||||||
|
Log.w(LOG_TAG, "Bogus latitude,longitude: 0,0");
|
||||||
|
} else {
|
||||||
|
if (latitude < -90 || latitude > 90) {
|
||||||
|
block = true;
|
||||||
|
Log.w(LOG_TAG, "Bogus latitude: " + latitude);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (longitude < -180 || longitude > 180) {
|
||||||
|
block = true;
|
||||||
|
Log.w(LOG_TAG, "Bogus longitude: " + longitude);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (location.hasAccuracy() && (inaccuracy < 0 || inaccuracy > MIN_ACCURACY)) {
|
||||||
|
block = true;
|
||||||
|
Log.w(LOG_TAG, "Insufficient accuracy: " + inaccuracy + " meters");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (location.hasAltitude() && (altitude < MIN_ALTITUDE || altitude > MAX_ALTITUDE)) {
|
||||||
|
block = true;
|
||||||
|
Log.w(LOG_TAG, "Bogus altitude: " + altitude + " meters");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (location.hasBearing() && (bearing < 0 || bearing > 360)) {
|
||||||
|
block = true;
|
||||||
|
Log.w(LOG_TAG, "Bogus bearing: " + bearing + " degrees");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (location.hasSpeed() && (speed < 0 || speed > MAX_SPEED)) {
|
||||||
|
block = true;
|
||||||
|
Log.w(LOG_TAG, "Bogus speed: " + speed + " meters/second");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timestamp < MIN_TIMESTAMP || timestamp > tomorrow) {
|
||||||
|
block = true;
|
||||||
|
Log.w(LOG_TAG, "Bogus timestamp: " + timestamp);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mGeofencingEnabled &&
|
||||||
|
Math.abs(location.getLatitude() - mBlockedLocation.getLatitude()) < GEOFENCE_RADIUS &&
|
||||||
|
Math.abs(location.getLongitude() - mBlockedLocation.getLongitude()) < GEOFENCE_RADIUS) {
|
||||||
|
block = true;
|
||||||
|
mIsGeofenced = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return block;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isGeofenced() {
|
||||||
|
return mIsGeofenced;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,190 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package org.mozilla.mozstumbler.service.stumblerthread.scanners;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.IntentFilter;
|
||||||
|
import android.location.Location;
|
||||||
|
import android.os.BatteryManager;
|
||||||
|
import android.support.v4.content.LocalBroadcastManager;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import org.mozilla.mozstumbler.service.AppGlobals;
|
||||||
|
import org.mozilla.mozstumbler.service.stumblerthread.Reporter;
|
||||||
|
import org.mozilla.mozstumbler.service.stumblerthread.blocklist.WifiBlockListInterface;
|
||||||
|
import org.mozilla.mozstumbler.service.stumblerthread.scanners.cellscanner.CellScanner;
|
||||||
|
import org.mozilla.mozstumbler.service.AppGlobals.ActiveOrPassiveStumbling;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.Timer;
|
||||||
|
import java.util.TimerTask;
|
||||||
|
|
||||||
|
public class ScanManager {
|
||||||
|
private static final String LOG_TAG = AppGlobals.LOG_PREFIX + ScanManager.class.getSimpleName();
|
||||||
|
private Timer mPassiveModeFlushTimer;
|
||||||
|
private Context mContext;
|
||||||
|
private boolean mIsScanning;
|
||||||
|
private GPSScanner mGPSScanner;
|
||||||
|
private WifiScanner mWifiScanner;
|
||||||
|
private CellScanner mCellScanner;
|
||||||
|
private ActiveOrPassiveStumbling mStumblingMode = ActiveOrPassiveStumbling.ACTIVE_STUMBLING;
|
||||||
|
|
||||||
|
public ScanManager() {
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isBatteryLow() {
|
||||||
|
Intent intent = mContext.registerReceiver(null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
|
||||||
|
if (intent == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int rawLevel = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1);
|
||||||
|
int scale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1);
|
||||||
|
int status = intent.getIntExtra(BatteryManager.EXTRA_STATUS, -1);
|
||||||
|
boolean isCharging = (status == BatteryManager.BATTERY_STATUS_CHARGING);
|
||||||
|
int level = Math.round(rawLevel * scale/100.0f);
|
||||||
|
|
||||||
|
final int kMinBatteryPct = 15;
|
||||||
|
return !isCharging && level < kMinBatteryPct;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void newPassiveGpsLocation() {
|
||||||
|
if (isBatteryLow()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (AppGlobals.isDebug) {
|
||||||
|
Log.d(LOG_TAG, "New passive location");
|
||||||
|
}
|
||||||
|
|
||||||
|
mWifiScanner.start(ActiveOrPassiveStumbling.PASSIVE_STUMBLING);
|
||||||
|
mCellScanner.start(ActiveOrPassiveStumbling.PASSIVE_STUMBLING);
|
||||||
|
|
||||||
|
// how often to flush a leftover bundle to the reports table
|
||||||
|
// If there is a bundle, and nothing happens for 10sec, then flush it
|
||||||
|
final int flushRate_ms = 10000;
|
||||||
|
|
||||||
|
if (mPassiveModeFlushTimer != null) {
|
||||||
|
mPassiveModeFlushTimer.cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
Date when = new Date();
|
||||||
|
when.setTime(when.getTime() + flushRate_ms);
|
||||||
|
mPassiveModeFlushTimer = new Timer();
|
||||||
|
mPassiveModeFlushTimer.schedule(new TimerTask() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
Intent flush = new Intent(Reporter.ACTION_FLUSH_TO_BUNDLE);
|
||||||
|
LocalBroadcastManager.getInstance(mContext).sendBroadcastSync(flush);
|
||||||
|
}
|
||||||
|
}, when);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPassiveMode(boolean on) {
|
||||||
|
mStumblingMode = (on)? ActiveOrPassiveStumbling.PASSIVE_STUMBLING :
|
||||||
|
ActiveOrPassiveStumbling.ACTIVE_STUMBLING;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isPassiveMode() {
|
||||||
|
return ActiveOrPassiveStumbling.PASSIVE_STUMBLING == mStumblingMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void startScanning(Context context) {
|
||||||
|
if (mIsScanning) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mContext = context.getApplicationContext();
|
||||||
|
if (mGPSScanner == null) {
|
||||||
|
mGPSScanner = new GPSScanner(context, this);
|
||||||
|
mWifiScanner = new WifiScanner(context);
|
||||||
|
mCellScanner = new CellScanner(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (AppGlobals.isDebug) {
|
||||||
|
Log.d(LOG_TAG, "Scanning started...");
|
||||||
|
}
|
||||||
|
|
||||||
|
mGPSScanner.start(mStumblingMode);
|
||||||
|
if (mStumblingMode == ActiveOrPassiveStumbling.ACTIVE_STUMBLING) {
|
||||||
|
mWifiScanner.start(mStumblingMode);
|
||||||
|
mCellScanner.start(mStumblingMode);
|
||||||
|
// in passive mode, these scans are started by passive gps notifications
|
||||||
|
}
|
||||||
|
mIsScanning = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean stopScanning() {
|
||||||
|
if (!mIsScanning) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (AppGlobals.isDebug) {
|
||||||
|
Log.d(LOG_TAG, "Scanning stopped");
|
||||||
|
}
|
||||||
|
|
||||||
|
mGPSScanner.stop();
|
||||||
|
mWifiScanner.stop();
|
||||||
|
mCellScanner.stop();
|
||||||
|
|
||||||
|
mIsScanning = false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setWifiBlockList(WifiBlockListInterface list) {
|
||||||
|
WifiScanner.setWifiBlockList(list);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isScanning() {
|
||||||
|
return mIsScanning;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getAPCount() {
|
||||||
|
return (mWifiScanner == null)? 0 : mWifiScanner.getAPCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getVisibleAPCount() {
|
||||||
|
return (mWifiScanner == null)? 0 :mWifiScanner.getVisibleAPCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getWifiStatus() {
|
||||||
|
return (mWifiScanner == null)? 0 : mWifiScanner.getStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getCellInfoCount() {
|
||||||
|
return (mCellScanner == null)? 0 :mCellScanner.getCellInfoCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getCurrentCellInfoCount() {
|
||||||
|
return (mCellScanner == null)? 0 :mCellScanner.getCurrentCellInfoCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getLocationCount() {
|
||||||
|
return (mGPSScanner == null)? 0 : mGPSScanner.getLocationCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getLatitude() {
|
||||||
|
return (mGPSScanner == null)? 0.0 : mGPSScanner.getLatitude();
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getLongitude() {
|
||||||
|
return (mGPSScanner == null)? 0.0 : mGPSScanner.getLongitude();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Location getLocation() {
|
||||||
|
return (mGPSScanner == null)? new Location("null") : mGPSScanner.getLocation();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void checkPrefs() {
|
||||||
|
if (mGPSScanner != null) {
|
||||||
|
mGPSScanner.checkPrefs();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isGeofenced() {
|
||||||
|
return (mGPSScanner == null)? false : mGPSScanner.isGeofenced();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,220 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package org.mozilla.mozstumbler.service.stumblerthread.scanners;
|
||||||
|
|
||||||
|
import android.content.BroadcastReceiver;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.IntentFilter;
|
||||||
|
import android.net.wifi.ScanResult;
|
||||||
|
import android.net.wifi.WifiManager;
|
||||||
|
import android.net.wifi.WifiManager.WifiLock;
|
||||||
|
import android.support.v4.content.LocalBroadcastManager;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.Timer;
|
||||||
|
import java.util.TimerTask;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
|
import org.mozilla.mozstumbler.service.AppGlobals;
|
||||||
|
import org.mozilla.mozstumbler.service.stumblerthread.blocklist.BSSIDBlockList;
|
||||||
|
import org.mozilla.mozstumbler.service.stumblerthread.blocklist.SSIDBlockList;
|
||||||
|
import org.mozilla.mozstumbler.service.AppGlobals.ActiveOrPassiveStumbling;
|
||||||
|
import org.mozilla.mozstumbler.service.Prefs;
|
||||||
|
import org.mozilla.mozstumbler.service.stumblerthread.blocklist.WifiBlockListInterface;
|
||||||
|
|
||||||
|
public class WifiScanner extends BroadcastReceiver {
|
||||||
|
public static final String ACTION_BASE = AppGlobals.ACTION_NAMESPACE + ".WifiScanner.";
|
||||||
|
public static final String ACTION_WIFIS_SCANNED = ACTION_BASE + "WIFIS_SCANNED";
|
||||||
|
public static final String ACTION_WIFIS_SCANNED_ARG_RESULTS = "scan_results";
|
||||||
|
public static final String ACTION_WIFIS_SCANNED_ARG_TIME = AppGlobals.ACTION_ARG_TIME;
|
||||||
|
|
||||||
|
public static final int STATUS_IDLE = 0;
|
||||||
|
public static final int STATUS_ACTIVE = 1;
|
||||||
|
public static final int STATUS_WIFI_DISABLED = -1;
|
||||||
|
|
||||||
|
private static final String LOG_TAG = AppGlobals.LOG_PREFIX + WifiScanner.class.getSimpleName();
|
||||||
|
private static final long WIFI_MIN_UPDATE_TIME = 5000; // milliseconds
|
||||||
|
|
||||||
|
private boolean mStarted;
|
||||||
|
private final Context mContext;
|
||||||
|
private WifiLock mWifiLock;
|
||||||
|
private Timer mWifiScanTimer;
|
||||||
|
private final Set<String> mAPs = Collections.synchronizedSet(new HashSet<String>());
|
||||||
|
private AtomicInteger mVisibleAPs = new AtomicInteger();
|
||||||
|
|
||||||
|
/* Testing */
|
||||||
|
public static boolean sIsTestMode;
|
||||||
|
public List<ScanResult> mTestModeFakeScanResults = new ArrayList<ScanResult>();
|
||||||
|
public Set<String> getAccessPoints(android.test.AndroidTestCase restrictedAccessor) { return mAPs; }
|
||||||
|
/* ------- */
|
||||||
|
|
||||||
|
public WifiScanner(Context c) {
|
||||||
|
mContext = c;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isWifiEnabled() {
|
||||||
|
return (sIsTestMode) || getWifiManager().isWifiEnabled();
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<ScanResult> getScanResults() {
|
||||||
|
return (sIsTestMode)? mTestModeFakeScanResults : getWifiManager().getScanResults();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public synchronized void start(final ActiveOrPassiveStumbling stumblingMode) {
|
||||||
|
if (mStarted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
mStarted = true;
|
||||||
|
|
||||||
|
boolean scanAlways = Prefs.getInstance().getWifiScanAlways();
|
||||||
|
|
||||||
|
if (scanAlways || isWifiEnabled()) {
|
||||||
|
activatePeriodicScan(stumblingMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
IntentFilter i = new IntentFilter(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION);
|
||||||
|
if (!scanAlways) i.addAction(WifiManager.WIFI_STATE_CHANGED_ACTION);
|
||||||
|
mContext.registerReceiver(this, i);
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void stop() {
|
||||||
|
if (mStarted) {
|
||||||
|
mContext.unregisterReceiver(this);
|
||||||
|
}
|
||||||
|
deactivatePeriodicScan();
|
||||||
|
mStarted = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onReceive(Context c, Intent intent) {
|
||||||
|
String action = intent.getAction();
|
||||||
|
|
||||||
|
if (WifiManager.WIFI_STATE_CHANGED_ACTION.equals(action)) {
|
||||||
|
if (isWifiEnabled()) {
|
||||||
|
activatePeriodicScan(ActiveOrPassiveStumbling.ACTIVE_STUMBLING);
|
||||||
|
} else {
|
||||||
|
deactivatePeriodicScan();
|
||||||
|
}
|
||||||
|
} else if (WifiManager.SCAN_RESULTS_AVAILABLE_ACTION.equals(action)) {
|
||||||
|
ArrayList<ScanResult> scanResults = new ArrayList<ScanResult>();
|
||||||
|
for (ScanResult scanResult : getScanResults()) {
|
||||||
|
scanResult.BSSID = BSSIDBlockList.canonicalizeBSSID(scanResult.BSSID);
|
||||||
|
if (shouldLog(scanResult)) {
|
||||||
|
scanResults.add(scanResult);
|
||||||
|
mAPs.add(scanResult.BSSID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mVisibleAPs.set(scanResults.size());
|
||||||
|
reportScanResults(scanResults);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void setWifiBlockList(WifiBlockListInterface blockList) {
|
||||||
|
BSSIDBlockList.setFilterList(blockList.getBssidOuiList());
|
||||||
|
SSIDBlockList.setFilterLists(blockList.getSsidPrefixList(), blockList.getSsidSuffixList());
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getAPCount() {
|
||||||
|
return mAPs.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getVisibleAPCount() {
|
||||||
|
return mVisibleAPs.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized int getStatus() {
|
||||||
|
if (!mStarted) {
|
||||||
|
return STATUS_IDLE;
|
||||||
|
}
|
||||||
|
if (mWifiScanTimer == null) {
|
||||||
|
return STATUS_WIFI_DISABLED;
|
||||||
|
}
|
||||||
|
return STATUS_ACTIVE;
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized void activatePeriodicScan(final ActiveOrPassiveStumbling stumblingMode) {
|
||||||
|
if (mWifiScanTimer != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (AppGlobals.isDebug) {
|
||||||
|
Log.v(LOG_TAG, "Activate Periodic Scan");
|
||||||
|
}
|
||||||
|
|
||||||
|
mWifiLock = getWifiManager().createWifiLock(WifiManager.WIFI_MODE_SCAN_ONLY, "MozStumbler");
|
||||||
|
mWifiLock.acquire();
|
||||||
|
|
||||||
|
// Ensure that we are constantly scanning for new access points.
|
||||||
|
mWifiScanTimer = new Timer();
|
||||||
|
mWifiScanTimer.schedule(new TimerTask() {
|
||||||
|
int mPassiveScanCount;
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
if (stumblingMode == ActiveOrPassiveStumbling.PASSIVE_STUMBLING &&
|
||||||
|
mPassiveScanCount++ > AppGlobals.PASSIVE_MODE_MAX_SCANS_PER_GPS)
|
||||||
|
{
|
||||||
|
mPassiveScanCount = 0;
|
||||||
|
stop(); // set mWifiScanTimer to null
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (AppGlobals.isDebug) {
|
||||||
|
Log.v(LOG_TAG, "WiFi Scanning Timer fired");
|
||||||
|
}
|
||||||
|
getWifiManager().startScan();
|
||||||
|
}
|
||||||
|
}, 0, WIFI_MIN_UPDATE_TIME);
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized void deactivatePeriodicScan() {
|
||||||
|
if (mWifiScanTimer == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (AppGlobals.isDebug) {
|
||||||
|
Log.v(LOG_TAG, "Deactivate periodic scan");
|
||||||
|
}
|
||||||
|
|
||||||
|
mWifiLock.release();
|
||||||
|
mWifiLock = null;
|
||||||
|
|
||||||
|
mWifiScanTimer.cancel();
|
||||||
|
mWifiScanTimer = null;
|
||||||
|
|
||||||
|
mVisibleAPs.set(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean shouldLog(ScanResult scanResult) {
|
||||||
|
if (BSSIDBlockList.contains(scanResult)) {
|
||||||
|
Log.w(LOG_TAG, "Blocked BSSID: " + scanResult);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (SSIDBlockList.contains(scanResult)) {
|
||||||
|
Log.w(LOG_TAG, "Blocked SSID: " + scanResult);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private WifiManager getWifiManager() {
|
||||||
|
return (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void reportScanResults(ArrayList<ScanResult> scanResults) {
|
||||||
|
if (scanResults.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Intent i = new Intent(ACTION_WIFIS_SCANNED);
|
||||||
|
i.putParcelableArrayListExtra(ACTION_WIFIS_SCANNED_ARG_RESULTS, scanResults);
|
||||||
|
i.putExtra(ACTION_WIFIS_SCANNED_ARG_TIME, System.currentTimeMillis());
|
||||||
|
LocalBroadcastManager.getInstance(mContext).sendBroadcastSync(i);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,389 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package org.mozilla.mozstumbler.service.stumblerthread.scanners.cellscanner;
|
||||||
|
|
||||||
|
import android.os.Build;
|
||||||
|
import android.os.Parcel;
|
||||||
|
import android.os.Parcelable;
|
||||||
|
import android.telephony.CellLocation;
|
||||||
|
import android.telephony.NeighboringCellInfo;
|
||||||
|
import android.telephony.TelephonyManager;
|
||||||
|
import android.telephony.cdma.CdmaCellLocation;
|
||||||
|
import android.telephony.gsm.GsmCellLocation;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import org.json.JSONException;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
import org.mozilla.mozstumbler.service.AppGlobals;
|
||||||
|
|
||||||
|
public class CellInfo implements Parcelable {
|
||||||
|
private static final String LOG_TAG = AppGlobals.LOG_PREFIX + CellInfo.class.getSimpleName();
|
||||||
|
|
||||||
|
public static final String RADIO_GSM = "gsm";
|
||||||
|
public static final String RADIO_CDMA = "cdma";
|
||||||
|
public static final String RADIO_WCDMA = "wcdma";
|
||||||
|
|
||||||
|
public static final String CELL_RADIO_GSM = "gsm";
|
||||||
|
public static final String CELL_RADIO_UMTS = "umts";
|
||||||
|
public static final String CELL_RADIO_CDMA = "cdma";
|
||||||
|
public static final String CELL_RADIO_LTE = "lte";
|
||||||
|
|
||||||
|
public static final int UNKNOWN_CID = -1;
|
||||||
|
public static final int UNKNOWN_SIGNAL = -1000;
|
||||||
|
|
||||||
|
public static final Parcelable.Creator<CellInfo> CREATOR
|
||||||
|
= new Parcelable.Creator<CellInfo>() {
|
||||||
|
public CellInfo createFromParcel(Parcel in) {
|
||||||
|
return new CellInfo(in);
|
||||||
|
}
|
||||||
|
|
||||||
|
public CellInfo[] newArray(int size) {
|
||||||
|
return new CellInfo[size];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private String mRadio;
|
||||||
|
private String mCellRadio;
|
||||||
|
|
||||||
|
private int mMcc;
|
||||||
|
private int mMnc;
|
||||||
|
private int mCid;
|
||||||
|
private int mLac;
|
||||||
|
private int mSignal;
|
||||||
|
private int mAsu;
|
||||||
|
private int mTa;
|
||||||
|
private int mPsc;
|
||||||
|
|
||||||
|
public CellInfo(int phoneType) {
|
||||||
|
reset();
|
||||||
|
setRadio(phoneType);
|
||||||
|
}
|
||||||
|
|
||||||
|
private CellInfo(Parcel in) {
|
||||||
|
mRadio = in.readString();
|
||||||
|
mCellRadio = in.readString();
|
||||||
|
mMcc = in.readInt();
|
||||||
|
mMnc = in.readInt();
|
||||||
|
mCid = in.readInt();
|
||||||
|
mLac = in.readInt();
|
||||||
|
mSignal = in.readInt();
|
||||||
|
mAsu = in.readInt();
|
||||||
|
mTa = in.readInt();
|
||||||
|
mPsc = in.readInt();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isCellRadioValid() {
|
||||||
|
return mCellRadio != null && (mCellRadio.length() > 0) && !mCellRadio.equals("0");
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getRadio() {
|
||||||
|
return mRadio;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCellRadio() {
|
||||||
|
return mCellRadio;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getMcc() {
|
||||||
|
return mMcc;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getMnc() {
|
||||||
|
return mMnc;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getCid() {
|
||||||
|
return mCid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getLac() {
|
||||||
|
return mLac;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getPsc() {
|
||||||
|
return mPsc;
|
||||||
|
}
|
||||||
|
|
||||||
|
public JSONObject toJSONObject() {
|
||||||
|
final JSONObject obj = new JSONObject();
|
||||||
|
|
||||||
|
try {
|
||||||
|
obj.put("radio", getCellRadio());
|
||||||
|
obj.put("mcc", mMcc);
|
||||||
|
obj.put("mnc", mMnc);
|
||||||
|
if (mLac != UNKNOWN_CID) obj.put("lac", mLac);
|
||||||
|
if (mCid != UNKNOWN_CID) obj.put("cid", mCid);
|
||||||
|
if (mSignal != UNKNOWN_SIGNAL) obj.put("signal", mSignal);
|
||||||
|
if (mAsu != UNKNOWN_SIGNAL) obj.put("asu", mAsu);
|
||||||
|
if (mTa != UNKNOWN_CID) obj.put("ta", mTa);
|
||||||
|
if (mPsc != UNKNOWN_CID) obj.put("psc", mPsc);
|
||||||
|
} catch (JSONException jsonE) {
|
||||||
|
throw new IllegalStateException(jsonE);
|
||||||
|
}
|
||||||
|
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCellIdentity() {
|
||||||
|
return getRadio()
|
||||||
|
+ " " + getCellRadio()
|
||||||
|
+ " " + getMcc()
|
||||||
|
+ " " + getMnc()
|
||||||
|
+ " " + getLac()
|
||||||
|
+ " " + getCid()
|
||||||
|
+ " " + getPsc();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int describeContents() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void writeToParcel(Parcel dest, int flags) {
|
||||||
|
dest.writeString(mRadio);
|
||||||
|
dest.writeString(mCellRadio);
|
||||||
|
dest.writeInt(mMcc);
|
||||||
|
dest.writeInt(mMnc);
|
||||||
|
dest.writeInt(mCid);
|
||||||
|
dest.writeInt(mLac);
|
||||||
|
dest.writeInt(mSignal);
|
||||||
|
dest.writeInt(mAsu);
|
||||||
|
dest.writeInt(mTa);
|
||||||
|
dest.writeInt(mPsc);
|
||||||
|
}
|
||||||
|
|
||||||
|
void reset() {
|
||||||
|
mRadio = RADIO_GSM;
|
||||||
|
mCellRadio = CELL_RADIO_GSM;
|
||||||
|
mMcc = UNKNOWN_CID;
|
||||||
|
mMnc = UNKNOWN_CID;
|
||||||
|
mLac = UNKNOWN_CID;
|
||||||
|
mCid = UNKNOWN_CID;
|
||||||
|
mSignal = UNKNOWN_SIGNAL;
|
||||||
|
mAsu = UNKNOWN_SIGNAL;
|
||||||
|
mTa = UNKNOWN_CID;
|
||||||
|
mPsc = UNKNOWN_CID;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setRadio(int phoneType) {
|
||||||
|
mRadio = getRadioTypeName(phoneType);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setCellLocation(CellLocation cl,
|
||||||
|
int networkType,
|
||||||
|
String networkOperator,
|
||||||
|
Integer gsmSignalStrength,
|
||||||
|
Integer cdmaRssi) {
|
||||||
|
if (cl instanceof GsmCellLocation) {
|
||||||
|
final int lac, cid;
|
||||||
|
final GsmCellLocation gcl = (GsmCellLocation) cl;
|
||||||
|
|
||||||
|
reset();
|
||||||
|
mCellRadio = getCellRadioTypeName(networkType);
|
||||||
|
setNetworkOperator(networkOperator);
|
||||||
|
|
||||||
|
lac = gcl.getLac();
|
||||||
|
cid = gcl.getCid();
|
||||||
|
if (lac >= 0) mLac = lac;
|
||||||
|
if (cid >= 0) mCid = cid;
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= 9) {
|
||||||
|
final int psc = gcl.getPsc();
|
||||||
|
if (psc >= 0) mPsc = psc;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (gsmSignalStrength != null) {
|
||||||
|
mAsu = gsmSignalStrength;
|
||||||
|
}
|
||||||
|
} else if (cl instanceof CdmaCellLocation) {
|
||||||
|
final CdmaCellLocation cdl = (CdmaCellLocation) cl;
|
||||||
|
|
||||||
|
reset();
|
||||||
|
mCellRadio = getCellRadioTypeName(networkType);
|
||||||
|
|
||||||
|
setNetworkOperator(networkOperator);
|
||||||
|
|
||||||
|
mMnc = cdl.getSystemId();
|
||||||
|
|
||||||
|
mLac = cdl.getNetworkId();
|
||||||
|
mCid = cdl.getBaseStationId();
|
||||||
|
|
||||||
|
if (cdmaRssi != null) {
|
||||||
|
mSignal = cdmaRssi;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new IllegalArgumentException("Unexpected CellLocation type: " + cl.getClass().getName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void setNeighboringCellInfo(NeighboringCellInfo nci, String networkOperator) {
|
||||||
|
final int lac, cid, psc, rssi;
|
||||||
|
|
||||||
|
reset();
|
||||||
|
mCellRadio = getCellRadioTypeName(nci.getNetworkType());
|
||||||
|
setNetworkOperator(networkOperator);
|
||||||
|
|
||||||
|
lac = nci.getLac();
|
||||||
|
cid = nci.getCid();
|
||||||
|
psc = nci.getPsc();
|
||||||
|
rssi = nci.getRssi();
|
||||||
|
|
||||||
|
if (lac >= 0) mLac = lac;
|
||||||
|
if (cid >= 0) mCid = cid;
|
||||||
|
if (psc >= 0) mPsc = psc;
|
||||||
|
if (rssi != NeighboringCellInfo.UNKNOWN_RSSI) mAsu = rssi;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setGsmCellInfo(int mcc, int mnc, int lac, int cid, int asu) {
|
||||||
|
mCellRadio = CELL_RADIO_GSM;
|
||||||
|
mMcc = mcc != Integer.MAX_VALUE ? mcc : UNKNOWN_CID;
|
||||||
|
mMnc = mnc != Integer.MAX_VALUE ? mnc : UNKNOWN_CID;
|
||||||
|
mLac = lac != Integer.MAX_VALUE ? lac : UNKNOWN_CID;
|
||||||
|
mCid = cid != Integer.MAX_VALUE ? cid : UNKNOWN_CID;
|
||||||
|
mAsu = asu;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setWcmdaCellInfo(int mcc, int mnc, int lac, int cid, int psc, int asu) {
|
||||||
|
mCellRadio = CELL_RADIO_UMTS;
|
||||||
|
mMcc = mcc != Integer.MAX_VALUE ? mcc : UNKNOWN_CID;
|
||||||
|
mMnc = mnc != Integer.MAX_VALUE ? mnc : UNKNOWN_CID;
|
||||||
|
mLac = lac != Integer.MAX_VALUE ? lac : UNKNOWN_CID;
|
||||||
|
mCid = cid != Integer.MAX_VALUE ? cid : UNKNOWN_CID;
|
||||||
|
mPsc = psc != Integer.MAX_VALUE ? psc : UNKNOWN_CID;
|
||||||
|
mAsu = asu;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param mcc Mobile Country Code, Integer.MAX_VALUE if unknown
|
||||||
|
* @param mnc Mobile Network Code, Integer.MAX_VALUE if unknown
|
||||||
|
* @param ci Cell Identity, Integer.MAX_VALUE if unknown
|
||||||
|
* @param pci Physical Cell Id, Integer.MAX_VALUE if unknown
|
||||||
|
* @param tac Tracking Area Code, Integer.MAX_VALUE if unknown
|
||||||
|
* @param asu Arbitrary strength unit
|
||||||
|
* @param ta Timing advance
|
||||||
|
*/
|
||||||
|
void setLteCellInfo(int mcc, int mnc, int ci, int pci, int tac, int asu, int ta) {
|
||||||
|
mCellRadio = CELL_RADIO_LTE;
|
||||||
|
mMcc = mcc != Integer.MAX_VALUE ? mcc : UNKNOWN_CID;
|
||||||
|
mMnc = mnc != Integer.MAX_VALUE ? mnc : UNKNOWN_CID;
|
||||||
|
mLac = tac != Integer.MAX_VALUE ? tac : UNKNOWN_CID;
|
||||||
|
mCid = ci != Integer.MAX_VALUE ? ci : UNKNOWN_CID;
|
||||||
|
mPsc = pci != Integer.MAX_VALUE ? pci : UNKNOWN_CID;
|
||||||
|
mAsu = asu;
|
||||||
|
mTa = ta;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setCdmaCellInfo(int baseStationId, int networkId, int systemId, int dbm) {
|
||||||
|
mCellRadio = CELL_RADIO_CDMA;
|
||||||
|
mMnc = systemId != Integer.MAX_VALUE ? systemId : UNKNOWN_CID;
|
||||||
|
mLac = networkId != Integer.MAX_VALUE ? networkId : UNKNOWN_CID;
|
||||||
|
mCid = baseStationId != Integer.MAX_VALUE ? baseStationId : UNKNOWN_CID;
|
||||||
|
mSignal = dbm;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setNetworkOperator(String mccMnc) {
|
||||||
|
if (mccMnc == null || mccMnc.length() < 5 || mccMnc.length() > 8) {
|
||||||
|
throw new IllegalArgumentException("Bad mccMnc: " + mccMnc);
|
||||||
|
}
|
||||||
|
mMcc = Integer.parseInt(mccMnc.substring(0, 3));
|
||||||
|
mMnc = Integer.parseInt(mccMnc.substring(3));
|
||||||
|
}
|
||||||
|
|
||||||
|
static String getCellRadioTypeName(int networkType) {
|
||||||
|
switch (networkType) {
|
||||||
|
// If the network is either GSM or any high-data-rate variant of it, the radio
|
||||||
|
// field should be specified as `gsm`. This includes `GSM`, `EDGE` and `GPRS`.
|
||||||
|
case TelephonyManager.NETWORK_TYPE_GPRS:
|
||||||
|
case TelephonyManager.NETWORK_TYPE_EDGE:
|
||||||
|
return CELL_RADIO_GSM;
|
||||||
|
|
||||||
|
// If the network is either UMTS or any high-data-rate variant of it, the radio
|
||||||
|
// field should be specified as `umts`. This includes `UMTS`, `HSPA`, `HSDPA`,
|
||||||
|
// `HSPA+` and `HSUPA`.
|
||||||
|
case TelephonyManager.NETWORK_TYPE_UMTS:
|
||||||
|
case TelephonyManager.NETWORK_TYPE_HSDPA:
|
||||||
|
case TelephonyManager.NETWORK_TYPE_HSUPA:
|
||||||
|
case TelephonyManager.NETWORK_TYPE_HSPA:
|
||||||
|
case TelephonyManager.NETWORK_TYPE_HSPAP:
|
||||||
|
return CELL_RADIO_UMTS;
|
||||||
|
|
||||||
|
case TelephonyManager.NETWORK_TYPE_LTE:
|
||||||
|
return CELL_RADIO_LTE;
|
||||||
|
|
||||||
|
// If the network is either CDMA or one of the EVDO variants, the radio
|
||||||
|
// field should be specified as `cdma`. This includes `1xRTT`, `CDMA`, `eHRPD`,
|
||||||
|
// `EVDO_0`, `EVDO_A`, `EVDO_B`, `IS95A` and `IS95B`.
|
||||||
|
case TelephonyManager.NETWORK_TYPE_EVDO_0:
|
||||||
|
case TelephonyManager.NETWORK_TYPE_EVDO_A:
|
||||||
|
case TelephonyManager.NETWORK_TYPE_EVDO_B:
|
||||||
|
case TelephonyManager.NETWORK_TYPE_1xRTT:
|
||||||
|
case TelephonyManager.NETWORK_TYPE_EHRPD:
|
||||||
|
case TelephonyManager.NETWORK_TYPE_IDEN:
|
||||||
|
return CELL_RADIO_CDMA;
|
||||||
|
|
||||||
|
default:
|
||||||
|
Log.e(LOG_TAG, "", new IllegalArgumentException("Unexpected network type: " + networkType));
|
||||||
|
return String.valueOf(networkType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("fallthrough")
|
||||||
|
private static String getRadioTypeName(int phoneType) {
|
||||||
|
switch (phoneType) {
|
||||||
|
case TelephonyManager.PHONE_TYPE_CDMA:
|
||||||
|
return RADIO_CDMA;
|
||||||
|
|
||||||
|
case TelephonyManager.PHONE_TYPE_GSM:
|
||||||
|
return RADIO_GSM;
|
||||||
|
|
||||||
|
default:
|
||||||
|
Log.e(LOG_TAG, "", new IllegalArgumentException("Unexpected phone type: " + phoneType));
|
||||||
|
// fallthrough
|
||||||
|
|
||||||
|
case TelephonyManager.PHONE_TYPE_NONE:
|
||||||
|
case TelephonyManager.PHONE_TYPE_SIP:
|
||||||
|
// These devices have no radio.
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (o == this) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!(o instanceof CellInfo)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
CellInfo ci = (CellInfo) o;
|
||||||
|
return mRadio.equals(ci.mRadio)
|
||||||
|
&& mCellRadio.equals(ci.mCellRadio)
|
||||||
|
&& mMcc == ci.mMcc
|
||||||
|
&& mMnc == ci.mMnc
|
||||||
|
&& mCid == ci.mCid
|
||||||
|
&& mLac == ci.mLac
|
||||||
|
&& mSignal == ci.mSignal
|
||||||
|
&& mAsu == ci.mAsu
|
||||||
|
&& mTa == ci.mTa
|
||||||
|
&& mPsc == ci.mPsc;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
int result = 17;
|
||||||
|
result = 31 * result + mRadio.hashCode();
|
||||||
|
result = 31 * result + mCellRadio.hashCode();
|
||||||
|
result = 31 * result + mMcc;
|
||||||
|
result = 31 * result + mMnc;
|
||||||
|
result = 31 * result + mCid;
|
||||||
|
result = 31 * result + mLac;
|
||||||
|
result = 31 * result + mSignal;
|
||||||
|
result = 31 * result + mAsu;
|
||||||
|
result = 31 * result + mTa;
|
||||||
|
result = 31 * result + mPsc;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,135 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package org.mozilla.mozstumbler.service.stumblerthread.scanners.cellscanner;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.support.v4.content.LocalBroadcastManager;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.Timer;
|
||||||
|
import java.util.TimerTask;
|
||||||
|
|
||||||
|
import org.mozilla.mozstumbler.service.AppGlobals;
|
||||||
|
import org.mozilla.mozstumbler.service.AppGlobals.ActiveOrPassiveStumbling;
|
||||||
|
|
||||||
|
|
||||||
|
public class CellScanner {
|
||||||
|
public static final String ACTION_BASE = AppGlobals.ACTION_NAMESPACE + ".CellScanner.";
|
||||||
|
public static final String ACTION_CELLS_SCANNED = ACTION_BASE + "CELLS_SCANNED";
|
||||||
|
public static final String ACTION_CELLS_SCANNED_ARG_CELLS = "cells";
|
||||||
|
public static final String ACTION_CELLS_SCANNED_ARG_TIME = AppGlobals.ACTION_ARG_TIME;
|
||||||
|
|
||||||
|
private static final String LOG_TAG = AppGlobals.LOG_PREFIX + CellScanner.class.getSimpleName();
|
||||||
|
private static final long CELL_MIN_UPDATE_TIME = 1000; // milliseconds
|
||||||
|
|
||||||
|
private final Context mContext;
|
||||||
|
private static CellScannerImpl sImpl;
|
||||||
|
private Timer mCellScanTimer;
|
||||||
|
private final Set<String> mCells = new HashSet<String>();
|
||||||
|
private int mCurrentCellInfoCount;
|
||||||
|
|
||||||
|
public ArrayList<CellInfo> sTestingModeCellInfoArray;
|
||||||
|
|
||||||
|
public interface CellScannerImpl {
|
||||||
|
public void start();
|
||||||
|
|
||||||
|
public void stop();
|
||||||
|
|
||||||
|
public List<CellInfo> getCellInfo();
|
||||||
|
}
|
||||||
|
|
||||||
|
public CellScanner(Context context) {
|
||||||
|
mContext = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static synchronized CellScannerImpl getImplementation() {
|
||||||
|
return sImpl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static synchronized boolean isCellScannerImplSet() {
|
||||||
|
return sImpl != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fennec doesn't support the apis needed for full scanning, we have different implementations.*/
|
||||||
|
public static synchronized void setCellScannerImpl(CellScannerImpl cellScanner) {
|
||||||
|
sImpl = cellScanner;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void start(final ActiveOrPassiveStumbling stumblingMode) {
|
||||||
|
if (getImplementation() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
getImplementation().start();
|
||||||
|
} catch (UnsupportedOperationException uoe) {
|
||||||
|
Log.e(LOG_TAG, "Cell scanner probe failed", uoe);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mCellScanTimer != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mCellScanTimer = new Timer();
|
||||||
|
|
||||||
|
mCellScanTimer.schedule(new TimerTask() {
|
||||||
|
int mPassiveScanCount;
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
if (getImplementation() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stumblingMode == ActiveOrPassiveStumbling.PASSIVE_STUMBLING &&
|
||||||
|
mPassiveScanCount++ > AppGlobals.PASSIVE_MODE_MAX_SCANS_PER_GPS)
|
||||||
|
{
|
||||||
|
mPassiveScanCount = 0;
|
||||||
|
stop();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
//if (SharedConstants.isDebug) Log.d(LOG_TAG, "Cell Scanning Timer fired");
|
||||||
|
final long curTime = System.currentTimeMillis();
|
||||||
|
|
||||||
|
ArrayList<CellInfo> cells = (sTestingModeCellInfoArray != null)? sTestingModeCellInfoArray :
|
||||||
|
new ArrayList<CellInfo>(getImplementation().getCellInfo());
|
||||||
|
|
||||||
|
mCurrentCellInfoCount = cells.size();
|
||||||
|
if (cells.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (CellInfo cell: cells) mCells.add(cell.getCellIdentity());
|
||||||
|
|
||||||
|
Intent intent = new Intent(ACTION_CELLS_SCANNED);
|
||||||
|
intent.putParcelableArrayListExtra(ACTION_CELLS_SCANNED_ARG_CELLS, cells);
|
||||||
|
intent.putExtra(ACTION_CELLS_SCANNED_ARG_TIME, curTime);
|
||||||
|
LocalBroadcastManager.getInstance(mContext).sendBroadcastSync(intent);
|
||||||
|
}
|
||||||
|
}, 0, CELL_MIN_UPDATE_TIME);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void stop() {
|
||||||
|
if (mCellScanTimer != null) {
|
||||||
|
mCellScanTimer.cancel();
|
||||||
|
mCellScanTimer = null;
|
||||||
|
}
|
||||||
|
if (getImplementation() != null) {
|
||||||
|
getImplementation().stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getCellInfoCount() {
|
||||||
|
return mCells.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getCurrentCellInfoCount() {
|
||||||
|
return mCurrentCellInfoCount;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,254 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package org.mozilla.mozstumbler.service.stumblerthread.scanners.cellscanner;
|
||||||
|
|
||||||
|
import android.annotation.TargetApi;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.telephony.CellIdentityCdma;
|
||||||
|
import android.telephony.CellIdentityGsm;
|
||||||
|
import android.telephony.CellIdentityLte;
|
||||||
|
import android.telephony.CellInfoCdma;
|
||||||
|
import android.telephony.CellInfoGsm;
|
||||||
|
import android.telephony.CellInfoLte;
|
||||||
|
import android.telephony.CellLocation;
|
||||||
|
import android.telephony.CellSignalStrengthCdma;
|
||||||
|
import android.telephony.CellSignalStrengthGsm;
|
||||||
|
import android.telephony.CellSignalStrengthLte;
|
||||||
|
import android.telephony.NeighboringCellInfo;
|
||||||
|
import android.telephony.PhoneStateListener;
|
||||||
|
import android.telephony.SignalStrength;
|
||||||
|
import android.telephony.TelephonyManager;
|
||||||
|
import android.util.Log;
|
||||||
|
import org.mozilla.mozstumbler.service.AppGlobals;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/* Fennec does not yet support the api level for WCDMA import */
|
||||||
|
public class CellScannerNoWCDMA implements CellScanner.CellScannerImpl {
|
||||||
|
|
||||||
|
protected static String LOG_TAG = AppGlobals.LOG_PREFIX + CellScannerNoWCDMA.class.getSimpleName();
|
||||||
|
protected GetAllCellInfoScannerImpl mGetAllInfoCellScanner;
|
||||||
|
protected TelephonyManager mTelephonyManager;
|
||||||
|
protected boolean mIsStarted;
|
||||||
|
protected int mPhoneType;
|
||||||
|
protected final Context mContext;
|
||||||
|
protected volatile int mSignalStrength;
|
||||||
|
protected volatile int mCdmaDbm;
|
||||||
|
|
||||||
|
private PhoneStateListener mPhoneStateListener;
|
||||||
|
|
||||||
|
private static class GetAllCellInfoScannerDummy implements GetAllCellInfoScannerImpl {
|
||||||
|
@Override
|
||||||
|
public List<CellInfo> getAllCellInfo(TelephonyManager tm) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GetAllCellInfoScannerImpl {
|
||||||
|
List<CellInfo> getAllCellInfo(TelephonyManager tm);
|
||||||
|
}
|
||||||
|
|
||||||
|
public CellScannerNoWCDMA(Context context) {
|
||||||
|
mContext = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void start() {
|
||||||
|
if (mIsStarted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
mIsStarted = true;
|
||||||
|
|
||||||
|
if (mTelephonyManager == null) {
|
||||||
|
if (Build.VERSION.SDK_INT >= 18 /*Build.VERSION_CODES.JELLY_BEAN_MR2 */) { // Fennec: no Build.VERSION_CODES
|
||||||
|
mGetAllInfoCellScanner = new GetAllCellInfoScannerMr2();
|
||||||
|
} else {
|
||||||
|
mGetAllInfoCellScanner = new GetAllCellInfoScannerDummy();
|
||||||
|
}
|
||||||
|
|
||||||
|
mTelephonyManager = (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE);
|
||||||
|
if (mTelephonyManager == null) {
|
||||||
|
throw new UnsupportedOperationException("TelephonyManager service is not available");
|
||||||
|
}
|
||||||
|
|
||||||
|
mPhoneType = mTelephonyManager.getPhoneType();
|
||||||
|
|
||||||
|
if (mPhoneType != TelephonyManager.PHONE_TYPE_GSM
|
||||||
|
&& mPhoneType != TelephonyManager.PHONE_TYPE_CDMA) {
|
||||||
|
throw new UnsupportedOperationException("Unexpected Phone Type: " + mPhoneType);
|
||||||
|
}
|
||||||
|
mSignalStrength = CellInfo.UNKNOWN_SIGNAL;
|
||||||
|
mCdmaDbm = CellInfo.UNKNOWN_SIGNAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
mSignalStrength = CellInfo.UNKNOWN_SIGNAL;
|
||||||
|
mCdmaDbm = CellInfo.UNKNOWN_SIGNAL;
|
||||||
|
|
||||||
|
mPhoneStateListener = new PhoneStateListener() {
|
||||||
|
@Override
|
||||||
|
public void onSignalStrengthsChanged(SignalStrength ss) {
|
||||||
|
if (ss.isGsm()) {
|
||||||
|
mSignalStrength = ss.getGsmSignalStrength();
|
||||||
|
} else {
|
||||||
|
mCdmaDbm = ss.getCdmaDbm();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_SIGNAL_STRENGTHS);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void stop() {
|
||||||
|
mIsStarted = false;
|
||||||
|
if (mTelephonyManager != null) {
|
||||||
|
mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_NONE);
|
||||||
|
}
|
||||||
|
mSignalStrength = CellInfo.UNKNOWN_SIGNAL;
|
||||||
|
mCdmaDbm = CellInfo.UNKNOWN_SIGNAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<CellInfo> getCellInfo() {
|
||||||
|
List<CellInfo> records = new ArrayList<CellInfo>();
|
||||||
|
|
||||||
|
List<CellInfo> allCells = mGetAllInfoCellScanner.getAllCellInfo(mTelephonyManager);
|
||||||
|
if (allCells.isEmpty()) {
|
||||||
|
CellInfo currentCell = getCurrentCellInfo();
|
||||||
|
if (currentCell == null) {
|
||||||
|
return records;
|
||||||
|
}
|
||||||
|
records.add(currentCell);
|
||||||
|
}else {
|
||||||
|
records.addAll(allCells);
|
||||||
|
}
|
||||||
|
|
||||||
|
// getNeighboringCells() sometimes contains more information than that is already
|
||||||
|
// in getAllCellInfo(). Use the results of both of them.
|
||||||
|
records.addAll(getNeighboringCells());
|
||||||
|
return records;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getNetworkOperator() {
|
||||||
|
String networkOperator = mTelephonyManager.getNetworkOperator();
|
||||||
|
// getNetworkOperator() may be unreliable on CDMA networks
|
||||||
|
if (networkOperator == null || networkOperator.length() <= 3) {
|
||||||
|
networkOperator = mTelephonyManager.getSimOperator();
|
||||||
|
}
|
||||||
|
return networkOperator;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected CellInfo getCurrentCellInfo() {
|
||||||
|
final CellLocation currentCell = mTelephonyManager.getCellLocation();
|
||||||
|
if (currentCell == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final CellInfo info = new CellInfo(mPhoneType);
|
||||||
|
final int signalStrength = mSignalStrength;
|
||||||
|
final int cdmaDbm = mCdmaDbm;
|
||||||
|
info.setCellLocation(currentCell,
|
||||||
|
mTelephonyManager.getNetworkType(),
|
||||||
|
getNetworkOperator(),
|
||||||
|
signalStrength == CellInfo.UNKNOWN_SIGNAL ? null : signalStrength,
|
||||||
|
cdmaDbm == CellInfo.UNKNOWN_SIGNAL ? null : cdmaDbm);
|
||||||
|
return info;
|
||||||
|
} catch (IllegalArgumentException iae) {
|
||||||
|
Log.e(LOG_TAG, "Skip invalid or incomplete CellLocation: " + currentCell, iae);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<CellInfo> getNeighboringCells() {
|
||||||
|
Collection<NeighboringCellInfo> cells = mTelephonyManager.getNeighboringCellInfo();
|
||||||
|
if (cells == null || cells.isEmpty()) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
String networkOperator = getNetworkOperator();
|
||||||
|
List<CellInfo> records = new ArrayList<CellInfo>(cells.size());
|
||||||
|
for (NeighboringCellInfo nci : cells) {
|
||||||
|
try {
|
||||||
|
final CellInfo record = new CellInfo(mPhoneType);
|
||||||
|
record.setNeighboringCellInfo(nci, networkOperator);
|
||||||
|
if (record.isCellRadioValid()) {
|
||||||
|
records.add(record);
|
||||||
|
}
|
||||||
|
} catch (IllegalArgumentException iae) {
|
||||||
|
Log.e(LOG_TAG, "Skip invalid or incomplete NeighboringCellInfo: " + nci, iae);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return records;
|
||||||
|
}
|
||||||
|
|
||||||
|
@TargetApi(18)
|
||||||
|
protected boolean addCellToList(List<CellInfo> cells,
|
||||||
|
android.telephony.CellInfo observedCell,
|
||||||
|
TelephonyManager tm) {
|
||||||
|
boolean added = false;
|
||||||
|
if (observedCell instanceof CellInfoGsm) {
|
||||||
|
CellIdentityGsm ident = ((CellInfoGsm) observedCell).getCellIdentity();
|
||||||
|
if (ident.getMcc() != Integer.MAX_VALUE && ident.getMnc() != Integer.MAX_VALUE) {
|
||||||
|
CellSignalStrengthGsm strength = ((CellInfoGsm) observedCell).getCellSignalStrength();
|
||||||
|
CellInfo cell = new CellInfo(tm.getPhoneType());
|
||||||
|
cell.setGsmCellInfo(ident.getMcc(),
|
||||||
|
ident.getMnc(),
|
||||||
|
ident.getLac(),
|
||||||
|
ident.getCid(),
|
||||||
|
strength.getAsuLevel());
|
||||||
|
cells.add(cell);
|
||||||
|
added = true;
|
||||||
|
}
|
||||||
|
} else if (observedCell instanceof CellInfoCdma) {
|
||||||
|
CellInfo cell = new CellInfo(tm.getPhoneType());
|
||||||
|
CellIdentityCdma ident = ((CellInfoCdma) observedCell).getCellIdentity();
|
||||||
|
CellSignalStrengthCdma strength = ((CellInfoCdma) observedCell).getCellSignalStrength();
|
||||||
|
cell.setCdmaCellInfo(ident.getBasestationId(),
|
||||||
|
ident.getNetworkId(),
|
||||||
|
ident.getSystemId(),
|
||||||
|
strength.getDbm());
|
||||||
|
cells.add(cell);
|
||||||
|
added = true;
|
||||||
|
} else if (observedCell instanceof CellInfoLte) {
|
||||||
|
CellIdentityLte ident = ((CellInfoLte) observedCell).getCellIdentity();
|
||||||
|
if (ident.getMnc() != Integer.MAX_VALUE && ident.getMcc() != Integer.MAX_VALUE) {
|
||||||
|
CellInfo cell = new CellInfo(tm.getPhoneType());
|
||||||
|
CellSignalStrengthLte strength = ((CellInfoLte) observedCell).getCellSignalStrength();
|
||||||
|
cell.setLteCellInfo(ident.getMcc(),
|
||||||
|
ident.getMnc(),
|
||||||
|
ident.getCi(),
|
||||||
|
ident.getPci(),
|
||||||
|
ident.getTac(),
|
||||||
|
strength.getAsuLevel(),
|
||||||
|
strength.getTimingAdvance());
|
||||||
|
cells.add(cell);
|
||||||
|
added = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return added;
|
||||||
|
}
|
||||||
|
|
||||||
|
@TargetApi(18)
|
||||||
|
private class GetAllCellInfoScannerMr2 implements GetAllCellInfoScannerImpl {
|
||||||
|
@Override
|
||||||
|
public List<CellInfo> getAllCellInfo(TelephonyManager tm) {
|
||||||
|
final List<android.telephony.CellInfo> observed = tm.getAllCellInfo();
|
||||||
|
if (observed == null || observed.isEmpty()) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<CellInfo> cells = new ArrayList<CellInfo>(observed.size());
|
||||||
|
for (android.telephony.CellInfo observedCell : observed) {
|
||||||
|
if (!addCellToList(cells, observedCell, tm)) {
|
||||||
|
//Log.i(LOG_TAG, "Skipped CellInfo of unknown class: " + observedCell.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cells;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,213 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package org.mozilla.mozstumbler.service.uploadthread;
|
||||||
|
|
||||||
|
import android.os.AsyncTask;
|
||||||
|
import android.util.Log;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
|
import org.mozilla.mozstumbler.service.Prefs;
|
||||||
|
import org.mozilla.mozstumbler.service.utils.AbstractCommunicator;
|
||||||
|
import org.mozilla.mozstumbler.service.utils.AbstractCommunicator.SyncSummary;
|
||||||
|
import org.mozilla.mozstumbler.service.AppGlobals;
|
||||||
|
import org.mozilla.mozstumbler.service.stumblerthread.datahandling.DataStorageManager;
|
||||||
|
import org.mozilla.mozstumbler.service.utils.NetworkUtils;
|
||||||
|
|
||||||
|
/* Only one at a time may be uploading. If executed while another upload is in progress
|
||||||
|
* it will return immediately, and SyncResult is null.
|
||||||
|
*
|
||||||
|
* Threading:
|
||||||
|
* Uploads on a separate thread. ONLY DataStorageManager is thread-safe, do not call
|
||||||
|
* preferences, do not call any code that isn't thread-safe. You will cause suffering.
|
||||||
|
* An exception is made for AppGlobals.isDebug, a false reading is of no consequence. */
|
||||||
|
public class AsyncUploader extends AsyncTask<Void, Void, SyncSummary> {
|
||||||
|
private static final String LOG_TAG = AppGlobals.LOG_PREFIX + AsyncUploader.class.getSimpleName();
|
||||||
|
private final UploadSettings mSettings;
|
||||||
|
private final Object mListenerLock = new Object();
|
||||||
|
private AsyncUploaderListener mListener;
|
||||||
|
private static AtomicBoolean sIsUploading = new AtomicBoolean();
|
||||||
|
private String mNickname;
|
||||||
|
|
||||||
|
public interface AsyncUploaderListener {
|
||||||
|
public void onUploadComplete(SyncSummary result);
|
||||||
|
public void onUploadProgress();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class UploadSettings {
|
||||||
|
public final boolean mShouldIgnoreWifiStatus;
|
||||||
|
public final boolean mUseWifiOnly;
|
||||||
|
public UploadSettings(boolean shouldIgnoreWifiStatus, boolean useWifiOnly) {
|
||||||
|
mShouldIgnoreWifiStatus = shouldIgnoreWifiStatus;
|
||||||
|
mUseWifiOnly = useWifiOnly;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public AsyncUploader(UploadSettings settings, AsyncUploaderListener listener) {
|
||||||
|
mListener = listener;
|
||||||
|
mSettings = settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setNickname(String name) {
|
||||||
|
mNickname = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clearListener() {
|
||||||
|
synchronized (mListenerLock) {
|
||||||
|
mListener = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isUploading() {
|
||||||
|
return sIsUploading.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected SyncSummary doInBackground(Void... voids) {
|
||||||
|
if (sIsUploading.get()) {
|
||||||
|
// This if-block is not synchronized, don't care, this is an erroneous usage.
|
||||||
|
Log.d(LOG_TAG, "Usage error: check isUploading first, only one at a time task usage is permitted.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
sIsUploading.set(true);
|
||||||
|
SyncSummary result = new SyncSummary();
|
||||||
|
Runnable progressListener = null;
|
||||||
|
|
||||||
|
// no need to lock here, lock is checked again later
|
||||||
|
if (mListener != null) {
|
||||||
|
progressListener = new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
synchronized (mListenerLock) {
|
||||||
|
if (mListener != null) {
|
||||||
|
mListener.onUploadProgress();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadReports(result, progressListener);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
protected void onPostExecute(SyncSummary result) {
|
||||||
|
sIsUploading.set(false);
|
||||||
|
|
||||||
|
synchronized (mListenerLock) {
|
||||||
|
if (mListener != null) {
|
||||||
|
mListener.onUploadComplete(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
protected void onCancelled(SyncSummary result) {
|
||||||
|
sIsUploading.set(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private class Submitter extends AbstractCommunicator {
|
||||||
|
private static final String SUBMIT_URL = "https://location.services.mozilla.com/v1/submit";
|
||||||
|
|
||||||
|
public Submitter() {
|
||||||
|
super(Prefs.getInstance().getUserAgent());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getUrlString() {
|
||||||
|
return SUBMIT_URL;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getNickname(){
|
||||||
|
return mNickname;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public NetworkSendResult cleanSend(byte[] data) {
|
||||||
|
final NetworkSendResult result = new NetworkSendResult();
|
||||||
|
try {
|
||||||
|
result.bytesSent = this.send(data, ZippedState.eAlreadyZipped);
|
||||||
|
result.errorCode = 0;
|
||||||
|
} catch (IOException ex) {
|
||||||
|
String msg = "Error submitting: " + ex;
|
||||||
|
if (ex instanceof HttpErrorException) {
|
||||||
|
result.errorCode = ((HttpErrorException) ex).responseCode;
|
||||||
|
msg += " Code:" + result.errorCode;
|
||||||
|
}
|
||||||
|
Log.e(LOG_TAG, msg);
|
||||||
|
AppGlobals.guiLogError(msg);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void uploadReports(AbstractCommunicator.SyncSummary syncResult, Runnable progressListener) {
|
||||||
|
long uploadedObservations = 0;
|
||||||
|
long uploadedCells = 0;
|
||||||
|
long uploadedWifis = 0;
|
||||||
|
|
||||||
|
if (!mSettings.mShouldIgnoreWifiStatus && mSettings.mUseWifiOnly && !NetworkUtils.getInstance().isWifiAvailable()) {
|
||||||
|
if (AppGlobals.isDebug) {
|
||||||
|
Log.d(LOG_TAG, "not on WiFi, not sending");
|
||||||
|
}
|
||||||
|
syncResult.numIoExceptions += 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Submitter submitter = new Submitter();
|
||||||
|
DataStorageManager dm = DataStorageManager.getInstance();
|
||||||
|
|
||||||
|
String error = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
DataStorageManager.ReportBatch batch = dm.getFirstBatch();
|
||||||
|
while (batch != null) {
|
||||||
|
AbstractCommunicator.NetworkSendResult result = submitter.cleanSend(batch.data);
|
||||||
|
|
||||||
|
if (result.errorCode == 0) {
|
||||||
|
syncResult.totalBytesSent += result.bytesSent;
|
||||||
|
|
||||||
|
dm.delete(batch.filename);
|
||||||
|
|
||||||
|
uploadedObservations += batch.reportCount;
|
||||||
|
uploadedWifis += batch.wifiCount;
|
||||||
|
uploadedCells += batch.cellCount;
|
||||||
|
} else {
|
||||||
|
if (result.errorCode / 100 == 4) {
|
||||||
|
// delete on 4xx, no point in resending
|
||||||
|
dm.delete(batch.filename);
|
||||||
|
} else {
|
||||||
|
DataStorageManager.getInstance().saveCurrentReportsSendBufferToDisk();
|
||||||
|
}
|
||||||
|
syncResult.numIoExceptions += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (progressListener != null) {
|
||||||
|
progressListener.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
batch = dm.getNextBatch();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (IOException ex) {
|
||||||
|
error = ex.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
dm.incrementSyncStats(syncResult.totalBytesSent, uploadedObservations, uploadedCells, uploadedWifis);
|
||||||
|
} catch (IOException ex) {
|
||||||
|
error = ex.toString();
|
||||||
|
} finally {
|
||||||
|
if (error != null) {
|
||||||
|
syncResult.numIoExceptions += 1;
|
||||||
|
Log.d(LOG_TAG, error);
|
||||||
|
AppGlobals.guiLogError(error + " (uploadReports)");
|
||||||
|
}
|
||||||
|
submitter.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,130 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package org.mozilla.mozstumbler.service.uploadthread;
|
||||||
|
|
||||||
|
import android.app.AlarmManager;
|
||||||
|
import android.app.IntentService;
|
||||||
|
import android.app.PendingIntent;
|
||||||
|
import android.content.BroadcastReceiver;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import org.mozilla.mozstumbler.service.AppGlobals;
|
||||||
|
import org.mozilla.mozstumbler.service.Prefs;
|
||||||
|
import org.mozilla.mozstumbler.service.stumblerthread.datahandling.DataStorageManager;
|
||||||
|
import org.mozilla.mozstumbler.service.utils.NetworkUtils;
|
||||||
|
|
||||||
|
// Only if data is queued and device awake: check network availability and upload.
|
||||||
|
// MozStumbler use: this alarm is periodic and repeating.
|
||||||
|
// Fennec use: The alarm is single-shot and it is set to run -if there is data in the queue-
|
||||||
|
// under these conditions:
|
||||||
|
// 1) Fennec start/pause (actually gecko start which is ~4 sec after Fennec start).
|
||||||
|
// 2) Changing the pref in Fennec to stumble or not.
|
||||||
|
// 3) Boot intent (and SD card app available intent).
|
||||||
|
//
|
||||||
|
// Threading:
|
||||||
|
// - scheduled from the stumbler thread
|
||||||
|
// - triggered from the main thread
|
||||||
|
// - actual work is done the upload thread (AsyncUploader)
|
||||||
|
public class UploadAlarmReceiver extends BroadcastReceiver {
|
||||||
|
private static final String LOG_TAG = AppGlobals.LOG_PREFIX + UploadAlarmReceiver.class.getSimpleName();
|
||||||
|
private static final String EXTRA_IS_REPEATING = "is_repeating";
|
||||||
|
private static boolean sIsAlreadyScheduled;
|
||||||
|
|
||||||
|
public UploadAlarmReceiver() {}
|
||||||
|
|
||||||
|
public static class UploadAlarmService extends IntentService {
|
||||||
|
|
||||||
|
public UploadAlarmService(String name) {
|
||||||
|
super(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public UploadAlarmService() {
|
||||||
|
super(LOG_TAG);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onHandleIntent(Intent intent) {
|
||||||
|
boolean isRepeating = intent.getBooleanExtra(EXTRA_IS_REPEATING, true);
|
||||||
|
if (DataStorageManager.getInstance() == null) {
|
||||||
|
DataStorageManager.createGlobalInstance(this, null);
|
||||||
|
}
|
||||||
|
upload(isRepeating);
|
||||||
|
}
|
||||||
|
|
||||||
|
void upload(boolean isRepeating) {
|
||||||
|
if (!isRepeating) {
|
||||||
|
sIsAlreadyScheduled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Defensive approach: if it is too old, delete all data
|
||||||
|
long oldestMs = DataStorageManager.getInstance().getOldestBatchTimeMs();
|
||||||
|
int maxWeeks = DataStorageManager.getInstance().getMaxWeeksStored();
|
||||||
|
if (oldestMs > 0) {
|
||||||
|
long currentTime = System.currentTimeMillis();
|
||||||
|
long msPerWeek = 604800 * 1000;
|
||||||
|
if (currentTime - oldestMs > maxWeeks * msPerWeek) {
|
||||||
|
DataStorageManager.getInstance().deleteAll();
|
||||||
|
UploadAlarmReceiver.cancelAlarm(this, isRepeating);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (NetworkUtils.getInstance().isWifiAvailable() &&
|
||||||
|
!AsyncUploader.isUploading()) {
|
||||||
|
Log.d(LOG_TAG, "Alarm upload(), call AsyncUploader");
|
||||||
|
AsyncUploader.UploadSettings settings =
|
||||||
|
new AsyncUploader.UploadSettings(Prefs.getInstance().getWifiScanAlways(), Prefs.getInstance().getUseWifiOnly());
|
||||||
|
AsyncUploader uploader = new AsyncUploader(settings, null);
|
||||||
|
uploader.setNickname(Prefs.getInstance().getNickname());
|
||||||
|
uploader.execute();
|
||||||
|
// we could listen for completion and cancel, instead, cancel on next alarm when db empty
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static PendingIntent createIntent(Context c, boolean isRepeating) {
|
||||||
|
Intent intent = new Intent(c, UploadAlarmReceiver.class);
|
||||||
|
intent.putExtra(EXTRA_IS_REPEATING, isRepeating);
|
||||||
|
PendingIntent pi = PendingIntent.getBroadcast(c, 0, intent, 0);
|
||||||
|
return pi;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void cancelAlarm(Context c, boolean isRepeating) {
|
||||||
|
Log.d(LOG_TAG, "cancelAlarm");
|
||||||
|
// this is to stop scheduleAlarm from constantly rescheduling, not to guard cancellation.
|
||||||
|
sIsAlreadyScheduled = false;
|
||||||
|
AlarmManager alarmManager = (AlarmManager) c.getSystemService(Context.ALARM_SERVICE);
|
||||||
|
PendingIntent pi = createIntent(c, isRepeating);
|
||||||
|
alarmManager.cancel(pi);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void scheduleAlarm(Context c, long secondsToWait, boolean isRepeating) {
|
||||||
|
if (sIsAlreadyScheduled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
long intervalMsec = secondsToWait * 1000;
|
||||||
|
Log.d(LOG_TAG, "schedule alarm (ms):" + intervalMsec);
|
||||||
|
|
||||||
|
sIsAlreadyScheduled = true;
|
||||||
|
AlarmManager alarmManager = (AlarmManager) c.getSystemService(Context.ALARM_SERVICE);
|
||||||
|
PendingIntent pi = createIntent(c, isRepeating);
|
||||||
|
|
||||||
|
long triggerAtMs = System.currentTimeMillis() + intervalMsec;
|
||||||
|
if (isRepeating) {
|
||||||
|
alarmManager.setInexactRepeating(AlarmManager.RTC, triggerAtMs, intervalMsec, pi);
|
||||||
|
} else {
|
||||||
|
alarmManager.set(AlarmManager.RTC, triggerAtMs, pi);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onReceive(final Context context, Intent intent) {
|
||||||
|
Intent startServiceIntent = new Intent(context, UploadAlarmService.class);
|
||||||
|
context.startService(startServiceIntent);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,156 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package org.mozilla.mozstumbler.service.utils;
|
||||||
|
|
||||||
|
import android.os.Build;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import org.mozilla.mozstumbler.service.AppGlobals;
|
||||||
|
import org.mozilla.mozstumbler.service.Prefs;
|
||||||
|
|
||||||
|
import java.io.BufferedOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.net.HttpURLConnection;
|
||||||
|
import java.net.MalformedURLException;
|
||||||
|
import java.net.URL;
|
||||||
|
|
||||||
|
public abstract class AbstractCommunicator {
|
||||||
|
|
||||||
|
private static final String LOG_TAG = AppGlobals.LOG_PREFIX + AbstractCommunicator.class.getSimpleName();
|
||||||
|
private static final String NICKNAME_HEADER = "X-Nickname";
|
||||||
|
private static final String USER_AGENT_HEADER = "User-Agent";
|
||||||
|
private HttpURLConnection mHttpURLConnection;
|
||||||
|
private final String mUserAgent;
|
||||||
|
private static int sBytesSentTotal = 0;
|
||||||
|
private static String sMozApiKey;
|
||||||
|
|
||||||
|
public abstract String getUrlString();
|
||||||
|
|
||||||
|
public static class HttpErrorException extends IOException {
|
||||||
|
private static final long serialVersionUID = -5404095858043243126L;
|
||||||
|
public final int responseCode;
|
||||||
|
|
||||||
|
public HttpErrorException(int responseCode) {
|
||||||
|
super();
|
||||||
|
this.responseCode = responseCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isTemporary() {
|
||||||
|
return responseCode >= 500 && responseCode <= 599;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class SyncSummary {
|
||||||
|
public int numIoExceptions;
|
||||||
|
public int totalBytesSent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class NetworkSendResult {
|
||||||
|
public int bytesSent;
|
||||||
|
// Zero is no error, for HTTP error cases, set this code to the error
|
||||||
|
public int errorCode = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract NetworkSendResult cleanSend(byte[] data);
|
||||||
|
|
||||||
|
public String getNickname() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AbstractCommunicator(String userAgent) {
|
||||||
|
mUserAgent = userAgent;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void openConnectionAndSetHeaders() {
|
||||||
|
try {
|
||||||
|
if (sMozApiKey == null) {
|
||||||
|
sMozApiKey = Prefs.getInstance().getMozApiKey();
|
||||||
|
}
|
||||||
|
URL url = new URL(getUrlString() + "?key=" + sMozApiKey);
|
||||||
|
mHttpURLConnection = (HttpURLConnection) url.openConnection();
|
||||||
|
mHttpURLConnection.setRequestMethod("POST");
|
||||||
|
} catch (MalformedURLException e) {
|
||||||
|
throw new IllegalArgumentException(e);
|
||||||
|
} catch (IOException e) {
|
||||||
|
Log.e(LOG_TAG, "Couldn't open a connection: " + e);
|
||||||
|
}
|
||||||
|
mHttpURLConnection.setDoOutput(true);
|
||||||
|
mHttpURLConnection.setRequestProperty(USER_AGENT_HEADER, mUserAgent);
|
||||||
|
mHttpURLConnection.setRequestProperty("Content-Type", "application/json");
|
||||||
|
|
||||||
|
// Workaround for a bug in Android mHttpURLConnection. When the library
|
||||||
|
// reuses a stale connection, the connection may fail with an EOFException
|
||||||
|
if (Build.VERSION.SDK_INT > 13 && Build.VERSION.SDK_INT < 19) {
|
||||||
|
mHttpURLConnection.setRequestProperty("Connection", "Close");
|
||||||
|
}
|
||||||
|
String nickname = getNickname();
|
||||||
|
if (nickname != null) {
|
||||||
|
mHttpURLConnection.setRequestProperty(NICKNAME_HEADER, nickname);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] zipData(byte[] data) throws IOException {
|
||||||
|
byte[] output = Zipper.zipData(data);
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendData(byte[] data) throws IOException{
|
||||||
|
mHttpURLConnection.setFixedLengthStreamingMode(data.length);
|
||||||
|
OutputStream out = new BufferedOutputStream(mHttpURLConnection.getOutputStream());
|
||||||
|
out.write(data);
|
||||||
|
out.flush();
|
||||||
|
int code = mHttpURLConnection.getResponseCode();
|
||||||
|
final boolean isSuccessCode2XX = (code/100 == 2);
|
||||||
|
if (!isSuccessCode2XX) {
|
||||||
|
throw new HttpErrorException(code);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum ZippedState { eNotZipped, eAlreadyZipped };
|
||||||
|
/* Return the number of bytes sent. */
|
||||||
|
public int send(byte[] data, ZippedState isAlreadyZipped) throws IOException {
|
||||||
|
openConnectionAndSetHeaders();
|
||||||
|
String logMsg;
|
||||||
|
try {
|
||||||
|
if (isAlreadyZipped != ZippedState.eAlreadyZipped) {
|
||||||
|
data = zipData(data);
|
||||||
|
}
|
||||||
|
mHttpURLConnection.setRequestProperty("Content-Encoding","gzip");
|
||||||
|
} catch (IOException e) {
|
||||||
|
Log.e(LOG_TAG, "Couldn't compress and send data, falling back to plain-text: ", e);
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
sendData(data);
|
||||||
|
} finally {
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
sBytesSentTotal += data.length;
|
||||||
|
logMsg = "Send data: " + String.format("%.2f", data.length / 1024.0) + " kB";
|
||||||
|
logMsg += " Session Total:" + String.format("%.2f", sBytesSentTotal / 1024.0) + " kB";
|
||||||
|
AppGlobals.guiLogInfo(logMsg, "#FFFFCC", true);
|
||||||
|
Log.d(LOG_TAG, logMsg);
|
||||||
|
return data.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
public InputStream getInputStream() {
|
||||||
|
try {
|
||||||
|
return mHttpURLConnection.getInputStream();
|
||||||
|
} catch (IOException e) {
|
||||||
|
return mHttpURLConnection.getErrorStream();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void close() {
|
||||||
|
if (mHttpURLConnection == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
mHttpURLConnection.disconnect();
|
||||||
|
mHttpURLConnection = null;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package org.mozilla.mozstumbler.service.utils;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.net.ConnectivityManager;
|
||||||
|
import android.net.NetworkInfo;
|
||||||
|
import android.util.Log;
|
||||||
|
import org.mozilla.mozstumbler.service.AppGlobals;
|
||||||
|
|
||||||
|
public final class NetworkUtils {
|
||||||
|
private static final String LOG_TAG = AppGlobals.LOG_PREFIX + NetworkUtils.class.getSimpleName();
|
||||||
|
|
||||||
|
ConnectivityManager mConnectivityManager;
|
||||||
|
static NetworkUtils sInstance;
|
||||||
|
|
||||||
|
/* Created at startup by app, or service, using a context. */
|
||||||
|
static public void createGlobalInstance(Context context) {
|
||||||
|
sInstance = new NetworkUtils();
|
||||||
|
sInstance.mConnectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* If accessed before singleton instantiation will abort. */
|
||||||
|
public static NetworkUtils getInstance() {
|
||||||
|
assert(sInstance != null);
|
||||||
|
return sInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized boolean isWifiAvailable() {
|
||||||
|
if (mConnectivityManager == null) {
|
||||||
|
Log.e(LOG_TAG, "ConnectivityManager is null!");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
NetworkInfo aNet = mConnectivityManager.getActiveNetworkInfo();
|
||||||
|
return (aNet != null && aNet.getType() == ConnectivityManager.TYPE_WIFI);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,85 @@
|
||||||
|
package org.mozilla.mozstumbler.service.utils;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2008 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import android.app.Service;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.HandlerThread;
|
||||||
|
import android.os.IBinder;
|
||||||
|
import android.os.Looper;
|
||||||
|
import android.os.Message;
|
||||||
|
|
||||||
|
/* This code is copied from android IntentService, with stopSelf commented out. */
|
||||||
|
public abstract class PersistentIntentService extends Service {
|
||||||
|
private volatile Looper mServiceLooper;
|
||||||
|
private volatile ServiceHandler mServiceHandler;
|
||||||
|
private String mName;
|
||||||
|
private boolean mRedelivery;
|
||||||
|
|
||||||
|
private final class ServiceHandler extends Handler {
|
||||||
|
public ServiceHandler(Looper looper) {
|
||||||
|
super(looper);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handleMessage(Message msg) {
|
||||||
|
onHandleIntent((Intent) msg.obj);
|
||||||
|
// stopSelf(msg.arg1); <-- modified from original file
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public PersistentIntentService(String name) {
|
||||||
|
super();
|
||||||
|
mName = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setIntentRedelivery(boolean enabled) {
|
||||||
|
mRedelivery = enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate() {
|
||||||
|
super.onCreate();
|
||||||
|
HandlerThread thread = new HandlerThread("IntentService[" + mName + "]");
|
||||||
|
thread.start();
|
||||||
|
mServiceLooper = thread.getLooper();
|
||||||
|
mServiceHandler = new ServiceHandler(mServiceLooper);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||||
|
Message msg = mServiceHandler.obtainMessage();
|
||||||
|
msg.arg1 = startId;
|
||||||
|
msg.obj = intent;
|
||||||
|
mServiceHandler.sendMessage(msg);
|
||||||
|
return mRedelivery ? START_REDELIVER_INTENT : START_NOT_STICKY;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroy() {
|
||||||
|
mServiceLooper.quit();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public IBinder onBind(Intent intent) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract void onHandleIntent(Intent intent);
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,48 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package org.mozilla.mozstumbler.service.utils;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.util.zip.GZIPInputStream;
|
||||||
|
import java.util.zip.GZIPOutputStream;
|
||||||
|
|
||||||
|
public class Zipper {
|
||||||
|
public static byte[] zipData(byte[] data) throws IOException {
|
||||||
|
final ByteArrayOutputStream os = new ByteArrayOutputStream();
|
||||||
|
GZIPOutputStream gstream = new GZIPOutputStream(os);
|
||||||
|
byte[] output;
|
||||||
|
try {
|
||||||
|
gstream.write(data);
|
||||||
|
gstream.finish();
|
||||||
|
output = os.toByteArray();
|
||||||
|
} finally {
|
||||||
|
gstream.close();
|
||||||
|
os.close();
|
||||||
|
}
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String unzipData(byte[] data) throws IOException {
|
||||||
|
StringBuilder result = new StringBuilder();
|
||||||
|
final ByteArrayInputStream bs = new ByteArrayInputStream(data);
|
||||||
|
GZIPInputStream gstream = new GZIPInputStream(bs);
|
||||||
|
try {
|
||||||
|
InputStreamReader reader = new InputStreamReader(gstream);
|
||||||
|
BufferedReader in = new BufferedReader(reader);
|
||||||
|
String read;
|
||||||
|
while ((read = in.readLine()) != null) {
|
||||||
|
result.append(read);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
gstream.close();
|
||||||
|
bs.close();
|
||||||
|
}
|
||||||
|
return result.toString();
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,2 +1,15 @@
|
||||||
<!-- Bug 1024708: this fragment is a place-holder for landing the
|
<service
|
||||||
build integration of the background stumbler into Fennec. -->
|
android:name="org.mozilla.mozstumbler.service.stumblerthread.StumblerService"
|
||||||
|
android:label="stumbler">
|
||||||
|
</service>
|
||||||
|
|
||||||
|
<receiver android:name="org.mozilla.mozstumbler.service.uploadthread.UploadAlarmReceiver" />
|
||||||
|
<service android:name="org.mozilla.mozstumbler.service.uploadthread.UploadAlarmReceiver$UploadAlarmService" />
|
||||||
|
|
||||||
|
<receiver android:name="org.mozilla.mozstumbler.service.mainthread.PassiveServiceReceiver">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||||
|
<action android:name="android.intent.action.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE" />
|
||||||
|
<action android:name="@ANDROID_PACKAGE_NAME@.STUMBLER_PREF" />
|
||||||
|
</intent-filter>
|
||||||
|
</receiver>
|
||||||
|
|
|
@ -5,5 +5,28 @@
|
||||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
stumbler_sources = [
|
stumbler_sources = [
|
||||||
'java/org/mozilla/mozstumbler/PlaceHolder.java',
|
'java/org/mozilla/mozstumbler/service/AppGlobals.java',
|
||||||
|
'java/org/mozilla/mozstumbler/service/mainthread/PassiveServiceReceiver.java',
|
||||||
|
'java/org/mozilla/mozstumbler/service/Prefs.java',
|
||||||
|
'java/org/mozilla/mozstumbler/service/stumblerthread/blocklist/BSSIDBlockList.java',
|
||||||
|
'java/org/mozilla/mozstumbler/service/stumblerthread/blocklist/SSIDBlockList.java',
|
||||||
|
'java/org/mozilla/mozstumbler/service/stumblerthread/blocklist/WifiBlockListInterface.java',
|
||||||
|
'java/org/mozilla/mozstumbler/service/stumblerthread/datahandling/DataStorageContract.java',
|
||||||
|
'java/org/mozilla/mozstumbler/service/stumblerthread/datahandling/DataStorageManager.java',
|
||||||
|
'java/org/mozilla/mozstumbler/service/stumblerthread/datahandling/StumblerBundle.java',
|
||||||
|
'java/org/mozilla/mozstumbler/service/stumblerthread/Reporter.java',
|
||||||
|
'java/org/mozilla/mozstumbler/service/stumblerthread/scanners/cellscanner/CellInfo.java',
|
||||||
|
'java/org/mozilla/mozstumbler/service/stumblerthread/scanners/cellscanner/CellScanner.java',
|
||||||
|
'java/org/mozilla/mozstumbler/service/stumblerthread/scanners/cellscanner/CellScannerNoWCDMA.java',
|
||||||
|
'java/org/mozilla/mozstumbler/service/stumblerthread/scanners/GPSScanner.java',
|
||||||
|
'java/org/mozilla/mozstumbler/service/stumblerthread/scanners/LocationBlockList.java',
|
||||||
|
'java/org/mozilla/mozstumbler/service/stumblerthread/scanners/ScanManager.java',
|
||||||
|
'java/org/mozilla/mozstumbler/service/stumblerthread/scanners/WifiScanner.java',
|
||||||
|
'java/org/mozilla/mozstumbler/service/stumblerthread/StumblerService.java',
|
||||||
|
'java/org/mozilla/mozstumbler/service/uploadthread/AsyncUploader.java',
|
||||||
|
'java/org/mozilla/mozstumbler/service/uploadthread/UploadAlarmReceiver.java',
|
||||||
|
'java/org/mozilla/mozstumbler/service/utils/AbstractCommunicator.java',
|
||||||
|
'java/org/mozilla/mozstumbler/service/utils/NetworkUtils.java',
|
||||||
|
'java/org/mozilla/mozstumbler/service/utils/PersistentIntentService.java',
|
||||||
|
'java/org/mozilla/mozstumbler/service/utils/Zipper.java',
|
||||||
]
|
]
|
||||||
|
|
Загрузка…
Ссылка в новой задаче