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:
Garvan Keeley 2014-08-28 17:19:00 -07:00
parent ababcac00f
commit f4adb22136
26 changed files with 3879 additions and 9 deletions

View File

@ -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>");
}
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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());
}
}
}

View File

@ -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 */);
}
}
}

View File

@ -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();
}
}

View File

@ -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
}
}

View File

@ -2,11 +2,10 @@
* 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;
package org.mozilla.mozstumbler.service.stumblerthread.blocklist;
/**
* Bug 1024708: this class is a place-holder for landing the build integration
* of the background stumbler into Fennec.
*/
public class PlaceHolder {
public interface WifiBlockListInterface {
String[] getSsidPrefixList();
String[] getSsidSuffixList();
String[] getBssidOuiList();
}

View File

@ -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() {
}
}

View File

@ -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);
}
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}
}

View File

@ -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();
}
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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);
}

View File

@ -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();
}
}

View File

@ -1,2 +1,15 @@
<!-- Bug 1024708: this fragment is a place-holder for landing the
build integration of the background stumbler into Fennec. -->
<service
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>

View File

@ -5,5 +5,28 @@
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
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',
]