summaryrefslogtreecommitdiffstats
path: root/mobile/android/stumbler
diff options
context:
space:
mode:
Diffstat (limited to 'mobile/android/stumbler')
-rw-r--r--mobile/android/stumbler/Makefile.in9
-rw-r--r--mobile/android/stumbler/java/org/mozilla/mozstumbler/service/AppGlobals.java82
-rw-r--r--mobile/android/stumbler/java/org/mozilla/mozstumbler/service/Prefs.java205
-rw-r--r--mobile/android/stumbler/java/org/mozilla/mozstumbler/service/mainthread/LocalPreferenceReceiver.java70
-rw-r--r--mobile/android/stumbler/java/org/mozilla/mozstumbler/service/mainthread/SafeReceiver.java43
-rw-r--r--mobile/android/stumbler/java/org/mozilla/mozstumbler/service/mainthread/SystemReceiver.java41
-rw-r--r--mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/Reporter.java219
-rw-r--r--mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/StumblerService.java254
-rw-r--r--mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/blocklist/BSSIDBlockList.java65
-rw-r--r--mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/blocklist/SSIDBlockList.java41
-rw-r--r--mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/blocklist/WifiBlockListInterface.java11
-rw-r--r--mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/datahandling/DataStorageContract.java38
-rw-r--r--mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/datahandling/DataStorageManager.java473
-rw-r--r--mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/datahandling/PersistedStats.java99
-rw-r--r--mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/datahandling/StumblerBundle.java187
-rw-r--r--mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/scanners/GPSScanner.java293
-rw-r--r--mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/scanners/LocationBlockList.java105
-rw-r--r--mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/scanners/ScanManager.java191
-rw-r--r--mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/scanners/WifiScanner.java228
-rw-r--r--mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/scanners/cellscanner/CellInfo.java391
-rw-r--r--mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/scanners/cellscanner/CellScanner.java178
-rw-r--r--mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/scanners/cellscanner/CellScannerImplementation.java299
-rw-r--r--mobile/android/stumbler/java/org/mozilla/mozstumbler/service/uploadthread/AsyncUploader.java214
-rw-r--r--mobile/android/stumbler/java/org/mozilla/mozstumbler/service/uploadthread/UploadAlarmReceiver.java138
-rw-r--r--mobile/android/stumbler/java/org/mozilla/mozstumbler/service/utils/AbstractCommunicator.java158
-rw-r--r--mobile/android/stumbler/java/org/mozilla/mozstumbler/service/utils/NetworkUtils.java32
-rw-r--r--mobile/android/stumbler/java/org/mozilla/mozstumbler/service/utils/PersistentIntentService.java85
-rw-r--r--mobile/android/stumbler/java/org/mozilla/mozstumbler/service/utils/TelemetryWrapper.java35
-rw-r--r--mobile/android/stumbler/java/org/mozilla/mozstumbler/service/utils/Zipper.java48
-rw-r--r--mobile/android/stumbler/manifests/StumblerManifest_services.xml.in32
-rw-r--r--mobile/android/stumbler/moz.build12
-rw-r--r--mobile/android/stumbler/stumbler_sources.mozbuild36
32 files changed, 4312 insertions, 0 deletions
diff --git a/mobile/android/stumbler/Makefile.in b/mobile/android/stumbler/Makefile.in
new file mode 100644
index 000000000..958d50362
--- /dev/null
+++ b/mobile/android/stumbler/Makefile.in
@@ -0,0 +1,9 @@
+# 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/.
+
+include $(topsrcdir)/config/rules.mk
+
+include $(topsrcdir)/config/android-common.mk
+
+libs:: stumbler.jar
diff --git a/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/AppGlobals.java b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/AppGlobals.java
new file mode 100644
index 000000000..11a3bf4e0
--- /dev/null
+++ b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/AppGlobals.java
@@ -0,0 +1,82 @@
+/* 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>");
+ }
+ }
+
+ public static String makeLogTag(String name) {
+ final int maxLen = 23 - LOG_PREFIX.length();
+ if (name.length() > maxLen) {
+ name = name.substring(name.length() - maxLen, name.length());
+ }
+ return LOG_PREFIX + name;
+ }
+
+ public static final String ACTION_TEST_SETTING_ENABLED = "stumbler-test-setting-enabled";
+ public static final String ACTION_TEST_SETTING_DISABLED = "stumbler-test-setting-disabled";
+
+ // Histogram values
+ public static final String TELEMETRY_TIME_BETWEEN_UPLOADS_SEC = "STUMBLER_TIME_BETWEEN_UPLOADS_SEC";
+ public static final String TELEMETRY_BYTES_UPLOADED_PER_SEC = "STUMBLER_VOLUME_BYTES_UPLOADED_PER_SEC";
+ public static final String TELEMETRY_TIME_BETWEEN_STARTS_SEC = "STUMBLER_TIME_BETWEEN_START_SEC";
+ public static final String TELEMETRY_BYTES_PER_UPLOAD = "STUMBLER_UPLOAD_BYTES";
+ public static final String TELEMETRY_OBSERVATIONS_PER_UPLOAD = "STUMBLER_UPLOAD_OBSERVATION_COUNT";
+ public static final String TELEMETRY_CELLS_PER_UPLOAD = "STUMBLER_UPLOAD_CELL_COUNT";
+ public static final String TELEMETRY_WIFIS_PER_UPLOAD = "STUMBLER_UPLOAD_WIFI_AP_COUNT";
+ public static final String TELEMETRY_OBSERVATIONS_PER_DAY = "STUMBLER_OBSERVATIONS_PER_DAY";
+ public static final String TELEMETRY_TIME_BETWEEN_RECEIVED_LOCATIONS_SEC = "STUMBLER_TIME_BETWEEN_RECEIVED_LOCATIONS_SEC";
+}
+
diff --git a/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/Prefs.java b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/Prefs.java
new file mode 100644
index 000000000..fa00f29e9
--- /dev/null
+++ b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/Prefs.java
@@ -0,0 +1,205 @@
+/* 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.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 = AppGlobals.makeLogTag(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_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();
+ }
+ }
+
+ public static Prefs getInstance(Context c) {
+ if (sInstance == null) {
+ sInstance = new Prefs(c);
+ }
+ return sInstance;
+ }
+
+ // Allows code without a context handle to grab the prefs. The caller must null check the return value.
+ public static Prefs getInstanceWithoutContext() {
+ 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?!"));
+ }
+ }
+
+ private SharedPreferences getPrefs() {
+ return mSharedPrefs;
+ }
+}
diff --git a/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/mainthread/LocalPreferenceReceiver.java b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/mainthread/LocalPreferenceReceiver.java
new file mode 100644
index 000000000..388abba15
--- /dev/null
+++ b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/mainthread/LocalPreferenceReceiver.java
@@ -0,0 +1,70 @@
+/* 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 local broadcast receiver in SafeReceiver.
+ * Starts the StumblerService in passive listening mode.
+ *
+ * The received intent contains enabled state, upload API key and user agent,
+ * and is used to initialize the StumblerService.
+ */
+public class LocalPreferenceReceiver extends BroadcastReceiver {
+ // This allows global debugging logs to be enabled by doing
+ // |adb shell setprop log.tag.PassiveStumbler DEBUG|
+ static final String LOG_TAG = "PassiveStumbler";
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (intent == null) {
+ return;
+ }
+
+ // This value is cached, so if |setprop| is performed (as described on the LOG_TAG above),
+ // then the start/stop intent must be resent by toggling the setting or stopping/starting Fennec.
+ // This does not guard against dumping PII (PII in stumbler is location, wifi BSSID, cell tower details).
+ AppGlobals.isDebug = Log.isLoggable(LOG_TAG, Log.DEBUG);
+
+ StumblerService.sFirefoxStumblingEnabled.set(intent.getBooleanExtra("enabled", false));
+
+ if (!StumblerService.sFirefoxStumblingEnabled.get()) {
+ Log.d(LOG_TAG, "Stopping StumblerService | isDebug:" + AppGlobals.isDebug);
+ // This calls the service's onDestroy(), and the service's onHandleIntent(...) is not called
+ context.stopService(new Intent(context, StumblerService.class));
+ // For testing service messages were received
+ context.sendBroadcast(new Intent(AppGlobals.ACTION_TEST_SETTING_DISABLED));
+ return;
+ }
+
+ // For testing service messages were received
+ context.sendBroadcast(new Intent(AppGlobals.ACTION_TEST_SETTING_ENABLED));
+
+ Log.d(LOG_TAG, "Sending passive start message | isDebug:" + AppGlobals.isDebug);
+
+ final Intent startServiceIntent = new Intent(context, StumblerService.class);
+
+ startServiceIntent.putExtra(StumblerService.ACTION_START_PASSIVE, true);
+ startServiceIntent.putExtra(
+ StumblerService.ACTION_EXTRA_MOZ_API_KEY,
+ intent.getStringExtra("moz_mozilla_api_key")
+ );
+ startServiceIntent.putExtra(
+ StumblerService.ACTION_EXTRA_USER_AGENT,
+ intent.getStringExtra("user_agent")
+ );
+
+ context.startService(startServiceIntent);
+ }
+}
+
diff --git a/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/mainthread/SafeReceiver.java b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/mainthread/SafeReceiver.java
new file mode 100644
index 000000000..e145dbb0f
--- /dev/null
+++ b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/mainthread/SafeReceiver.java
@@ -0,0 +1,43 @@
+/* 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.content.IntentFilter;
+import android.support.v4.content.LocalBroadcastManager;
+import android.util.Log;
+
+/**
+ * Responsible for registering LocalPreferenceReceiver as a receiver with LocalBroadcastManager.
+ * This receiver is registered in the AndroidManifest.xml
+ */
+public class SafeReceiver extends BroadcastReceiver {
+ static final String LOG_TAG = "StumblerSafeReceiver";
+ static final String PREFERENCE_INTENT_FILTER = "STUMBLER_PREF";
+
+ private boolean registeredLocalReceiver = false;
+
+ @Override
+ public synchronized void onReceive(Context context, Intent intent) {
+ if (intent == null) {
+ return;
+ }
+
+ if (registeredLocalReceiver) {
+ return;
+ }
+
+ LocalBroadcastManager.getInstance(context).registerReceiver(
+ new LocalPreferenceReceiver(),
+ new IntentFilter(PREFERENCE_INTENT_FILTER)
+ );
+
+ Log.d(LOG_TAG, "Registered local preference listener");
+
+ registeredLocalReceiver = true;
+ }
+}
diff --git a/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/mainthread/SystemReceiver.java b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/mainthread/SystemReceiver.java
new file mode 100644
index 000000000..eaaab3423
--- /dev/null
+++ b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/mainthread/SystemReceiver.java
@@ -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.mainthread;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.text.TextUtils;
+import android.util.Log;
+
+import org.mozilla.mozstumbler.service.stumblerthread.StumblerService;
+
+/**
+ * Responsible for starting StumblerService in response to
+ * BOOT_COMPLETE and EXTERNAL_APPLICATIONS_AVAILABLE system intents.
+ */
+public class SystemReceiver extends BroadcastReceiver {
+ static final String LOG_TAG = "StumblerSystemReceiver";
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (intent == null) {
+ return;
+ }
+
+ final String action = intent.getAction();
+
+ if (!TextUtils.equals(action, Intent.ACTION_BOOT_COMPLETED) && !TextUtils.equals(action, Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE)) {
+ // This is not the broadcast you are looking for.
+ return;
+ }
+
+ final Intent startServiceIntent = new Intent(context, StumblerService.class);
+ startServiceIntent.putExtra(StumblerService.ACTION_NOT_FROM_HOST_APP, true);
+ context.startService(startServiceIntent);
+
+ Log.d(LOG_TAG, "Responded to a system intent");
+ }
+}
diff --git a/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/Reporter.java b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/Reporter.java
new file mode 100644
index 000000000..8f7f19c8d
--- /dev/null
+++ b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/Reporter.java
@@ -0,0 +1,219 @@
+/* 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.makeLogTag(Reporter.class.getSimpleName());
+ public static final String ACTION_FLUSH_TO_BUNDLE = AppGlobals.ACTION_NAMESPACE + ".FLUSH";
+ public static final String ACTION_NEW_BUNDLE = AppGlobals.ACTION_NAMESPACE + ".NEW_BUNDLE";
+ 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);
+ if (tm != null) {
+ mPhoneType = tm.getPhoneType();
+ } else {
+ Log.d(LOG_TAG, "No telephony manager.");
+ mPhoneType = TelephonyManager.PHONE_TYPE_NONE;
+ }
+
+ 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 (GPSScanner.SUBJECT_NEW_LOCATION.equals(subject)) {
+ 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();
+
+ switch (action) {
+ case ACTION_FLUSH_TO_BUNDLE:
+ flush();
+ return;
+ case WifiScanner.ACTION_WIFIS_SCANNED:
+ receivedWifiMessage(intent);
+ break;
+ case CellScanner.ACTION_CELLS_SCANNED:
+ receivedCellMessage(intent);
+ break;
+ case GPSScanner.ACTION_GPS_UPDATED:
+ // Calls reportCollectedLocation, this is the ideal case
+ receivedGpsMessage(intent);
+ break;
+ }
+
+ 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) {
+ if (currentWifiData.size() > MAX_WIFIS_PER_LOCATION) {
+ return;
+ }
+
+ 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) {
+ if (currentCellData.size() > MAX_CELLS_PER_LOCATION) {
+ return;
+ }
+ 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) {
+ // PII: do not log the bundle without obfuscating it
+ Log.d(LOG_TAG, "Received bundle");
+ }
+
+ if (wifiCount + cellCount < 1) {
+ return;
+ }
+
+ try {
+ DataStorageManager.getInstance().insert(mlsObj.toString(), wifiCount, cellCount);
+ } catch (IOException e) {
+ Log.w(LOG_TAG, e.toString());
+ }
+ }
+}
+
diff --git a/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/StumblerService.java b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/StumblerService.java
new file mode 100644
index 000000000..5d1a278b9
--- /dev/null
+++ b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/StumblerService.java
@@ -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;
+
+import android.Manifest;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.location.Location;
+import android.os.AsyncTask;
+import android.support.v4.content.ContextCompat;
+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.uploadthread.UploadAlarmReceiver;
+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.makeLogTag(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(Context c) {
+ return Prefs.getInstance(c);
+ }
+
+ 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 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() {
+ // Ensure Prefs is created, so internal utility code can use getInstanceWithoutContext
+ Prefs.getInstance(this);
+ DataStorageManager.createGlobalInstance(this, 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(StumblerService.this).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 (!hasLocationPermission()) {
+ Log.d(LOG_TAG, "Location permission not granted. Aborting.");
+ return;
+ }
+
+ if (intent == null) {
+ return;
+ }
+
+ final boolean isScanEnabledInPrefs = Prefs.getInstance(this).getFirefoxScanEnabled();
+
+ if (!isScanEnabledInPrefs && intent.getBooleanExtra(ACTION_NOT_FROM_HOST_APP, false)) {
+ stopSelf();
+ return;
+ }
+
+ boolean hasFilesWaiting = !DataStorageManager.getInstance().isDirEmpty();
+ if (AppGlobals.isDebug) {
+ Log.d(LOG_TAG, "Files waiting:" + hasFilesWaiting);
+ }
+ if (hasFilesWaiting) {
+ // 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(this).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(this).setLastAttemptedUploadTime(timeNow);
+ UploadAlarmReceiver.scheduleAlarm(this, DELAY_IN_SEC_BEFORE_STARTING_UPLOAD_IN_PASSIVE_MODE, false /* no repeat*/);
+ }
+ }
+
+ if (!isScanEnabledInPrefs) {
+ Prefs.getInstance(this).setFirefoxScanEnabled(true);
+ }
+
+ String apiKey = intent.getStringExtra(ACTION_EXTRA_MOZ_API_KEY);
+ if (apiKey != null && !apiKey.equals(Prefs.getInstance(this).getMozApiKey())) {
+ Prefs.getInstance(this).setMozApiKey(apiKey);
+ }
+
+ String userAgent = intent.getStringExtra(ACTION_EXTRA_USER_AGENT);
+ if (userAgent != null && !userAgent.equals(Prefs.getInstance(this).getUserAgent())) {
+ Prefs.getInstance(this).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
+ @Override
+ 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 */);
+ }
+ }
+
+ private boolean hasLocationPermission() {
+ return ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED;
+ }
+}
diff --git a/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/blocklist/BSSIDBlockList.java b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/blocklist/BSSIDBlockList.java
new file mode 100644
index 000000000..6354cb0cc
--- /dev/null
+++ b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/blocklist/BSSIDBlockList.java
@@ -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.makeLogTag(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();
+ }
+}
diff --git a/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/blocklist/SSIDBlockList.java b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/blocklist/SSIDBlockList.java
new file mode 100644
index 000000000..f5086ab34
--- /dev/null
+++ b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/blocklist/SSIDBlockList.java
@@ -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
+ }
+}
diff --git a/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/blocklist/WifiBlockListInterface.java b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/blocklist/WifiBlockListInterface.java
new file mode 100644
index 000000000..0e940cdc9
--- /dev/null
+++ b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/blocklist/WifiBlockListInterface.java
@@ -0,0 +1,11 @@
+/* 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;
+
+public interface WifiBlockListInterface {
+ String[] getSsidPrefixList();
+ String[] getSsidSuffixList();
+ String[] getBssidOuiList();
+}
diff --git a/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/datahandling/DataStorageContract.java b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/datahandling/DataStorageContract.java
new file mode 100644
index 000000000..2aaeb05ff
--- /dev/null
+++ b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/datahandling/DataStorageContract.java
@@ -0,0 +1,38 @@
+/* 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 final String HEADING = "heading";
+ public static final String SPEED = "speed";
+ public static final String PRESSURE = "pressure";
+ }
+
+ public static class Stats {
+ public static final String KEY_VERSION = "version_code";
+ public static final int VERSION_CODE = 2;
+ 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";
+ public static final String KEY_OBSERVATIONS_PER_DAY = "obs_per_day";
+ }
+
+ private DataStorageContract() {
+ }
+}
diff --git a/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/datahandling/DataStorageManager.java b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/datahandling/DataStorageManager.java
new file mode 100644
index 000000000..adaaea4dc
--- /dev/null
+++ b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/datahandling/DataStorageManager.java
@@ -0,0 +1,473 @@
+/* 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.makeLogTag(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 final ReportFileList mFileList;
+ private Timer mFlushMemoryBuffersToDiskTimer;
+ private final PersistedStats mPersistedOnDiskUploadStats;
+
+ 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 = 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);
+ mPersistedOnDiskUploadStats = new PersistedStats(baseDir);
+ }
+
+ 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.equals(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();
+ 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 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);
+ }
+ }
+
+ public synchronized void incrementSyncStats(long bytesSent, long reports, long cells, long wifis) throws IOException {
+ mPersistedOnDiskUploadStats.incrementSyncStats(bytesSent, reports, cells, wifis);
+ }
+}
diff --git a/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/datahandling/PersistedStats.java b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/datahandling/PersistedStats.java
new file mode 100644
index 000000000..79c8e59d1
--- /dev/null
+++ b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/datahandling/PersistedStats.java
@@ -0,0 +1,99 @@
+/* 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 org.mozilla.mozstumbler.service.AppGlobals;
+import org.mozilla.mozstumbler.service.utils.TelemetryWrapper;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.Properties;
+import java.util.concurrent.TimeUnit;
+
+class PersistedStats {
+ private final File mStatsFile;
+
+ public PersistedStats(String baseDir) {
+ mStatsFile = new File(baseDir, "upload_stats.ini");
+ }
+
+ 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();
+ final long lastUploadTime = Long.parseLong(properties.getProperty(DataStorageContract.Stats.KEY_LAST_UPLOAD_TIME, "0"));
+ final long storedObsPerDay = Long.parseLong(properties.getProperty(DataStorageContract.Stats.KEY_OBSERVATIONS_PER_DAY, "0"));
+ long observationsToday = reports;
+ if (lastUploadTime > 0) {
+ long dayLastUploaded = TimeUnit.MILLISECONDS.toDays(lastUploadTime);
+ long dayDiff = TimeUnit.MILLISECONDS.toDays(time) - dayLastUploaded;
+ if (dayDiff > 0) {
+ // send value of store obs per day
+ TelemetryWrapper.addToHistogram(AppGlobals.TELEMETRY_OBSERVATIONS_PER_DAY,
+ Long.valueOf(storedObsPerDay / dayDiff).intValue());
+ } else {
+ observationsToday += storedObsPerDay;
+ }
+ }
+
+ 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,
+ observationsToday);
+
+
+ final long lastUploadMs = Long.parseLong(properties.getProperty(DataStorageContract.Stats.KEY_LAST_UPLOAD_TIME, "0"));
+ final int timeDiffSec = Long.valueOf((time - lastUploadMs) / 1000).intValue();
+ if (lastUploadMs > 0 && timeDiffSec > 0) {
+ TelemetryWrapper.addToHistogram(AppGlobals.TELEMETRY_TIME_BETWEEN_UPLOADS_SEC, timeDiffSec);
+ TelemetryWrapper.addToHistogram(AppGlobals.TELEMETRY_BYTES_UPLOADED_PER_SEC, Long.valueOf(bytesSent).intValue() / timeDiffSec);
+ }
+ TelemetryWrapper.addToHistogram(AppGlobals.TELEMETRY_BYTES_PER_UPLOAD, Long.valueOf(bytesSent).intValue());
+ TelemetryWrapper.addToHistogram(AppGlobals.TELEMETRY_OBSERVATIONS_PER_UPLOAD, Long.valueOf(reports).intValue());
+ TelemetryWrapper.addToHistogram(AppGlobals.TELEMETRY_WIFIS_PER_UPLOAD, Long.valueOf(wifis).intValue());
+ TelemetryWrapper.addToHistogram(AppGlobals.TELEMETRY_CELLS_PER_UPLOAD, Long.valueOf(cells).intValue());
+ }
+
+ public synchronized void writeSyncStats(long time, long bytesSent, long totalObs,
+ long totalCells, long totalWifis, long obsPerDay) 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.setProperty(DataStorageContract.Stats.KEY_OBSERVATIONS_PER_DAY, String.valueOf(obsPerDay));
+ props.store(out, null);
+ } finally {
+ out.close();
+ }
+ }
+
+}
diff --git a/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/datahandling/StumblerBundle.java b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/datahandling/StumblerBundle.java
new file mode 100644
index 000000000..4f47e3302
--- /dev/null
+++ b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/datahandling/StumblerBundle.java
@@ -0,0 +1,187 @@
+/* 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.Build;
+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;
+ private float mPressureHPA;
+
+
+ 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);
+
+ item.put(DataStorageContract.ReportsColumns.HEADING, mGpsPosition.getBearing());
+ item.put(DataStorageContract.ReportsColumns.SPEED, mGpsPosition.getSpeed());
+ if (mPressureHPA != 0.0) {
+ item.put(DataStorageContract.ReportsColumns.PRESSURE, mPressureHPA);
+ }
+
+
+ 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();
+
+ long gpsTimeSinceBootInMS = 0;
+
+ if (Build.VERSION.SDK_INT >= 17) {
+ gpsTimeSinceBootInMS = mGpsPosition.getElapsedRealtimeNanos() / 1000000;
+ }
+
+ for (ScanResult s : mWifiData.values()) {
+ JSONObject wifiEntry = new JSONObject();
+ wifiEntry.put("key", s.BSSID);
+ wifiEntry.put("frequency", s.frequency);
+ wifiEntry.put("signal", s.level);
+
+ if (Build.VERSION.SDK_INT >= 17) {
+ long wifiTimeSinceBootInMS = (s.timestamp / 1000);
+ long ageMS = wifiTimeSinceBootInMS - gpsTimeSinceBootInMS;
+ wifiEntry.put("age", ageMS);
+ }
+
+ wifis.put(wifiEntry);
+ }
+ item.put(DataStorageContract.ReportsColumns.WIFI, wifis);
+ item.put(DataStorageContract.ReportsColumns.WIFI_COUNT, wifis.length());
+
+ return item;
+ }
+
+
+ public void addPressure(float hPa) {
+ mPressureHPA = hPa;
+ }
+
+}
diff --git a/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/scanners/GPSScanner.java b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/scanners/GPSScanner.java
new file mode 100644
index 000000000..218b97af4
--- /dev/null
+++ b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/scanners/GPSScanner.java
@@ -0,0 +1,293 @@
+/* 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.annotation.SuppressLint;
+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 org.mozilla.mozstumbler.service.utils.TelemetryWrapper;
+
+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.makeLogTag(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 long mTelemetry_lastStartedMs;
+ 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 boolean isGpsAvailable(LocationManager locationManager) {
+ if (locationManager == null ||
+ locationManager.getProvider(LocationManager.GPS_PROVIDER) == null) {
+ String msg = "No GPS available, scanning not started.";
+ Log.d(LOG_TAG, msg);
+ AppGlobals.guiLogError(msg);
+ return false;
+ }
+ return true;
+ }
+
+ @SuppressLint("MissingPermission") // Permissions are explicitly checked for in StumblerService.onHandleIntent()
+ private void startPassiveMode() {
+ LocationManager locationManager = (LocationManager) mContext.getSystemService(Context.LOCATION_SERVICE);
+ if (!isGpsAvailable(locationManager)) {
+ return;
+ }
+
+ locationManager.requestLocationUpdates(LocationManager.PASSIVE_PROVIDER, 0, 0, this);
+
+ final int timeDiffSec = Long.valueOf((System.currentTimeMillis() - mTelemetry_lastStartedMs) / 1000).intValue();
+ if (mTelemetry_lastStartedMs > 0 && timeDiffSec > 0) {
+ TelemetryWrapper.addToHistogram(AppGlobals.TELEMETRY_TIME_BETWEEN_STARTS_SEC, timeDiffSec);
+ }
+ mTelemetry_lastStartedMs = System.currentTimeMillis();
+ }
+
+ @SuppressLint("MissingPermission") // Permissions are explicitly checked for in StumblerService.onHandleIntent()
+ private void startActiveMode() {
+ LocationManager lm = getLocationManager();
+ if (!isGpsAvailable(lm)) {
+ return;
+ }
+
+ 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() {
+ @Override
+ 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);
+ }
+
+ @SuppressLint("MissingPermission") // Permissions are explicitly checked for in StumblerService.onHandleIntent()
+ 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();
+ }
+
+ Prefs prefs = Prefs.getInstanceWithoutContext();
+ if (prefs == null) {
+ return;
+ }
+ mAutoGeofencing = prefs.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")) {
+ Log.d(LOG_TAG, "Discard fused/network location.");
+ // only interested in GPS locations
+ return;
+ }
+
+ final long timeDeltaMs = location.getTime() - mLocation.getTime();
+
+ // 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 boolean hasMoved = location.distanceTo(mLocation) > PASSIVE_GPS_MOVEMENT_MIN_DELTA_M;
+
+ if (timeDeltaMs < 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)) {
+ reportLocationLost();
+ return;
+ }
+
+ mLocation = location;
+
+ if (!mAutoGeofencing) {
+ reportNewLocationReceived(location);
+ }
+ mLocationCount++;
+
+ if (mIsPassiveMode) {
+ mScanManager.newPassiveGpsLocation();
+ }
+
+ if (timeDeltaMs > 0) {
+ TelemetryWrapper.addToHistogram(AppGlobals.TELEMETRY_TIME_BETWEEN_RECEIVED_LOCATIONS_SEC,
+ Long.valueOf(timeDeltaMs).intValue() / 1000);
+ }
+ }
+
+ @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);
+ }
+}
diff --git a/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/scanners/LocationBlockList.java b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/scanners/LocationBlockList.java
new file mode 100644
index 000000000..c3cba7b45
--- /dev/null
+++ b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/scanners/LocationBlockList.java
@@ -0,0 +1,105 @@
+/* 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.makeLogTag(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() {
+ Prefs prefs = Prefs.getInstanceWithoutContext();
+ if (prefs == null) {
+ return;
+ }
+ mBlockedLocation = prefs.getGeofenceLocation();
+ mGeofencingEnabled = prefs.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;
+ }
+}
diff --git a/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/scanners/ScanManager.java b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/scanners/ScanManager.java
new file mode 100644
index 000000000..60d7c8f1c
--- /dev/null
+++ b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/scanners/ScanManager.java
@@ -0,0 +1,191 @@
+/* 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.makeLogTag(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 (mContext == null) {
+ Log.w(LOG_TAG, "No app context available.");
+ return;
+ }
+
+ 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 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) && mGPSScanner.isGeofenced();
+ }
+}
diff --git a/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/scanners/WifiScanner.java b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/scanners/WifiScanner.java
new file mode 100644
index 000000000..eed61d8bb
--- /dev/null
+++ b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/scanners/WifiScanner.java
@@ -0,0 +1,228 @@
+/* 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.makeLogTag(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 final 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() {
+ WifiManager manager = getWifiManager();
+ if (manager == null) {
+ return null;
+ }
+ return getWifiManager().getScanResults();
+ }
+
+
+ public synchronized void start(final ActiveOrPassiveStumbling stumblingMode) {
+ Prefs prefs = Prefs.getInstanceWithoutContext();
+ if (mStarted || prefs == null) {
+ return;
+ }
+ mStarted = true;
+
+ boolean scanAlways = prefs.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;
+ }
+
+ @Override
+ 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)) {
+ final List<ScanResult> scanResultList = getScanResults();
+ if (scanResultList == null) {
+ return;
+ }
+ final ArrayList<ScanResult> scanResults = new ArrayList<ScanResult>();
+ for (ScanResult scanResult : scanResultList) {
+ 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)) {
+ return false;
+ }
+ if (SSIDBlockList.contains(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);
+ }
+}
diff --git a/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/scanners/cellscanner/CellInfo.java b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/scanners/cellscanner/CellInfo.java
new file mode 100644
index 000000000..f435dcf11
--- /dev/null
+++ b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/scanners/cellscanner/CellInfo.java
@@ -0,0 +1,391 @@
+/* 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.makeLogTag(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>() {
+ @Override
+ public CellInfo createFromParcel(Parcel in) {
+ return new CellInfo(in);
+ }
+
+ @Override
+ 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;
+ }
+}
diff --git a/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/scanners/cellscanner/CellScanner.java b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/scanners/cellscanner/CellScanner.java
new file mode 100644
index 000000000..193de9923
--- /dev/null
+++ b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/scanners/cellscanner/CellScanner.java
@@ -0,0 +1,178 @@
+/* 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.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Handler;
+import android.os.Message;
+import android.support.v4.content.LocalBroadcastManager;
+import android.telephony.TelephonyManager;
+import android.util.Log;
+
+import org.mozilla.mozstumbler.service.AppGlobals;
+import org.mozilla.mozstumbler.service.AppGlobals.ActiveOrPassiveStumbling;
+
+import java.lang.ref.WeakReference;
+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 java.util.concurrent.atomic.AtomicBoolean;
+import org.mozilla.mozstumbler.service.AppGlobals.ActiveOrPassiveStumbling;
+import org.mozilla.mozstumbler.service.stumblerthread.Reporter;
+
+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.makeLogTag(CellScanner.class.getSimpleName());
+ private static final long CELL_MIN_UPDATE_TIME = 1000; // milliseconds
+
+ private final Context mContext;
+ private Timer mCellScanTimer;
+ private final Set<String> mCells = new HashSet<String>();
+ private final ReportFlushedReceiver mReportFlushedReceiver = new ReportFlushedReceiver();
+ private final AtomicBoolean mReportWasFlushed = new AtomicBoolean();
+ private Handler mBroadcastScannedHandler;
+ private final CellScannerImpl mCellScannerImplementation;
+
+ public ArrayList<CellInfo> sTestingModeCellInfoArray;
+
+ public interface CellScannerImpl {
+ void start();
+ boolean isStarted();
+ boolean isSupportedOnThisDevice();
+ void stop();
+ List<CellInfo> getCellInfo();
+ }
+
+ public CellScanner(Context context) {
+ mContext = context;
+ mCellScannerImplementation = new CellScannerImplementation(context);
+ }
+
+ public void start(final ActiveOrPassiveStumbling stumblingMode) {
+ if (!mCellScannerImplementation.isSupportedOnThisDevice()) {
+ return;
+ }
+
+ if (mCellScanTimer != null) {
+ return;
+ }
+
+ LocalBroadcastManager.getInstance(mContext).registerReceiver(mReportFlushedReceiver,
+ new IntentFilter(Reporter.ACTION_NEW_BUNDLE));
+
+ // This is to ensure the broadcast happens from the same thread the CellScanner start() is on
+ mBroadcastScannedHandler = new BroadcastScannedHandler(this);
+
+ mCellScannerImplementation.start();
+
+ mCellScanTimer = new Timer();
+
+ mCellScanTimer.schedule(new TimerTask() {
+ int mPassiveScanCount;
+ @Override
+ public void run() {
+ if (!mCellScannerImplementation.isStarted()) {
+ return;
+ }
+
+ if (stumblingMode == ActiveOrPassiveStumbling.PASSIVE_STUMBLING &&
+ mPassiveScanCount++ > AppGlobals.PASSIVE_MODE_MAX_SCANS_PER_GPS)
+ {
+ mPassiveScanCount = 0;
+ stop();
+ return;
+ }
+
+ final long curTime = System.currentTimeMillis();
+
+ ArrayList<CellInfo> cells = (sTestingModeCellInfoArray != null)? sTestingModeCellInfoArray :
+ new ArrayList<CellInfo>(mCellScannerImplementation.getCellInfo());
+
+ if (mReportWasFlushed.getAndSet(false)) {
+ clearCells();
+ }
+
+ if (cells.isEmpty()) {
+ return;
+ }
+
+ for (CellInfo cell : cells) {
+ addToCells(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);
+ // send to handler, so broadcast is not from timer thread
+ Message message = new Message();
+ message.obj = intent;
+ mBroadcastScannedHandler.sendMessage(message);
+
+ }
+ }, 0, CELL_MIN_UPDATE_TIME);
+ }
+
+ private synchronized void clearCells() {
+ mCells.clear();
+ }
+
+ private synchronized void addToCells(String cell) {
+ mCells.add(cell);
+ }
+
+ public synchronized void stop() {
+ mReportWasFlushed.set(false);
+ clearCells();
+ LocalBroadcastManager.getInstance(mContext).unregisterReceiver(mReportFlushedReceiver);
+
+ if (mCellScanTimer != null) {
+ mCellScanTimer.cancel();
+ mCellScanTimer = null;
+ }
+ mCellScannerImplementation.stop();
+ }
+
+ public synchronized int getCellInfoCount() {
+ return mCells.size();
+ }
+
+ private class ReportFlushedReceiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context c, Intent i) {
+ mReportWasFlushed.set(true);
+ }
+ }
+
+ // Note: this reimplements org.mozilla.gecko.util.WeakReferenceHandler because it's not available here.
+ private static class BroadcastScannedHandler extends Handler {
+ private WeakReference<CellScanner> mTarget;
+
+ public BroadcastScannedHandler(final CellScanner that) {
+ super();
+ mTarget = new WeakReference<>(that);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ final CellScanner that = mTarget.get();
+ if (that == null) {
+ return;
+ }
+
+ final Intent intent = (Intent) msg.obj;
+ LocalBroadcastManager.getInstance(that.mContext).sendBroadcastSync(intent);
+ }
+ };
+}
diff --git a/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/scanners/cellscanner/CellScannerImplementation.java b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/scanners/cellscanner/CellScannerImplementation.java
new file mode 100644
index 000000000..c6674a3c4
--- /dev/null
+++ b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/scanners/cellscanner/CellScannerImplementation.java
@@ -0,0 +1,299 @@
+/* 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.CellIdentityWcdma;
+import android.telephony.CellInfoCdma;
+import android.telephony.CellInfoGsm;
+import android.telephony.CellInfoLte;
+import android.telephony.CellInfoWcdma;
+import android.telephony.CellLocation;
+import android.telephony.CellSignalStrengthCdma;
+import android.telephony.CellSignalStrengthGsm;
+import android.telephony.CellSignalStrengthLte;
+import android.telephony.CellSignalStrengthWcdma;
+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;
+
+public class CellScannerImplementation implements CellScanner.CellScannerImpl {
+
+ protected static String LOG_TAG = AppGlobals.makeLogTag(CellScannerImplementation.class.getSimpleName());
+ protected GetAllCellInfoScannerImpl mGetAllInfoCellScanner;
+ protected TelephonyManager mTelephonyManager;
+ protected boolean mIsStarted;
+ protected int mPhoneType;
+ protected final Context mContext;
+
+ protected volatile int mSignalStrength = CellInfo.UNKNOWN_SIGNAL;
+ protected volatile int mCdmaDbm = CellInfo.UNKNOWN_SIGNAL;
+
+ 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 CellScannerImplementation(Context context) {
+ mContext = context;
+ }
+
+ public boolean isSupportedOnThisDevice() {
+ TelephonyManager telephonyManager = mTelephonyManager;
+ if (telephonyManager == null) {
+ telephonyManager = (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE);
+ }
+ return telephonyManager != null &&
+ (telephonyManager.getPhoneType() == TelephonyManager.PHONE_TYPE_CDMA ||
+ telephonyManager.getPhoneType() == TelephonyManager.PHONE_TYPE_GSM);
+ }
+
+ @Override
+ public synchronized boolean isStarted() {
+ return mIsStarted;
+ }
+
+ @Override
+ public synchronized void start() {
+ if (mIsStarted || !isSupportedOnThisDevice()) {
+ return;
+ }
+ mIsStarted = true;
+
+ if (mTelephonyManager == null) {
+ if (Build.VERSION.SDK_INT >= 18 /*Build.VERSION_CODES.JELLY_BEAN_MR2 */) {
+ mGetAllInfoCellScanner = new GetAllCellInfoScannerMr2();
+ } else {
+ mGetAllInfoCellScanner = new GetAllCellInfoScannerDummy();
+ }
+
+ mTelephonyManager = (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE);
+ }
+
+ 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 synchronized void stop() {
+ mIsStarted = false;
+ if (mTelephonyManager != null && mPhoneStateListener != null) {
+ mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_NONE);
+ }
+ mSignalStrength = CellInfo.UNKNOWN_SIGNAL;
+ mCdmaDbm = CellInfo.UNKNOWN_SIGNAL;
+ }
+
+ @Override
+ public synchronized 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() {
+ // For max fennec compatibility, avoid VERSION_CODES
+ if (Build.VERSION.SDK_INT >= 22 /* Build.VERSION_CODES.LOLLIPOP_MR1 */) {
+ return Collections.emptyList();
+ }
+
+ @SuppressWarnings("deprecation")
+ 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 addWCDMACellToList(List<CellInfo> cells,
+ android.telephony.CellInfo observedCell,
+ TelephonyManager tm) {
+ boolean added = false;
+ if (Build.VERSION.SDK_INT >= 18 &&
+ observedCell instanceof CellInfoWcdma) {
+ CellIdentityWcdma ident = ((CellInfoWcdma) observedCell).getCellIdentity();
+ if (ident.getMnc() != Integer.MAX_VALUE && ident.getMcc() != Integer.MAX_VALUE) {
+ CellInfo cell = new CellInfo(tm.getPhoneType());
+ CellSignalStrengthWcdma strength = ((CellInfoWcdma) observedCell).getCellSignalStrength();
+ cell.setWcmdaCellInfo(ident.getMcc(),
+ ident.getMnc(),
+ ident.getLac(),
+ ident.getCid(),
+ ident.getPsc(),
+ strength.getAsuLevel());
+ cells.add(cell);
+ added = true;
+ }
+ }
+ return added;
+ }
+
+ @TargetApi(18)
+ protected boolean addCellToList(List<CellInfo> cells,
+ android.telephony.CellInfo observedCell,
+ TelephonyManager tm) {
+ if (tm.getPhoneType() == 0) {
+ return false;
+ }
+
+ 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;
+ }
+ }
+
+ if (!added && Build.VERSION.SDK_INT >= 18) {
+ added = addWCDMACellToList(cells, observedCell, tm);
+ }
+
+ 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;
+ }
+ }
+}
diff --git a/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/uploadthread/AsyncUploader.java b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/uploadthread/AsyncUploader.java
new file mode 100644
index 000000000..308e35678
--- /dev/null
+++ b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/uploadthread/AsyncUploader.java
@@ -0,0 +1,214 @@
+/* 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.makeLogTag(AsyncUploader.class.getSimpleName());
+ private final AsyncUploadArgs mUploadArgs;
+ private final Object mListenerLock = new Object();
+ private AsyncUploaderListener mListener;
+ private static final AtomicBoolean sIsUploading = new AtomicBoolean();
+ private String mNickname;
+
+ public interface AsyncUploaderListener {
+ public void onUploadComplete(SyncSummary result);
+ public void onUploadProgress();
+ }
+
+ public static class AsyncUploadArgs {
+ public final NetworkUtils mNetworkUtils;
+ public final boolean mShouldIgnoreWifiStatus;
+ public final boolean mUseWifiOnly;
+ public AsyncUploadArgs(NetworkUtils networkUtils,
+ boolean shouldIgnoreWifiStatus,
+ boolean useWifiOnly) {
+ mNetworkUtils = networkUtils;
+ mShouldIgnoreWifiStatus = shouldIgnoreWifiStatus;
+ mUseWifiOnly = useWifiOnly;
+ }
+ }
+
+ public AsyncUploader(AsyncUploadArgs args, AsyncUploaderListener listener) {
+ mListener = listener;
+ mUploadArgs = args;
+ }
+
+ 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";
+
+ @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 (!mUploadArgs.mShouldIgnoreWifiStatus && mUploadArgs.mUseWifiOnly &&
+ !mUploadArgs.mNetworkUtils.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();
+ }
+ }
+}
diff --git a/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/uploadthread/UploadAlarmReceiver.java b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/uploadthread/UploadAlarmReceiver.java
new file mode 100644
index 000000000..d6680a161
--- /dev/null
+++ b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/uploadthread/UploadAlarmReceiver.java
@@ -0,0 +1,138 @@
+/* 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.makeLogTag(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);
+ // makes the service START_NOT_STICKY, that is, the service is not auto-restarted
+ setIntentRedelivery(false);
+ }
+
+ public UploadAlarmService() {
+ this(LOG_TAG);
+ }
+
+ @Override
+ protected void onHandleIntent(Intent intent) {
+ if (intent == null) {
+ return;
+ }
+ 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;
+ }
+ }
+
+ NetworkUtils networkUtils = new NetworkUtils(this);
+ if (networkUtils.isWifiAvailable() &&
+ !AsyncUploader.isUploading()) {
+ Log.d(LOG_TAG, "Alarm upload(), call AsyncUploader");
+ AsyncUploader.AsyncUploadArgs settings =
+ new AsyncUploader.AsyncUploadArgs(networkUtils,
+ Prefs.getInstance(this).getWifiScanAlways(),
+ Prefs.getInstance(this).getUseWifiOnly());
+ AsyncUploader uploader = new AsyncUploader(settings, null);
+ uploader.setNickname(Prefs.getInstance(this).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);
+ }
+}
diff --git a/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/utils/AbstractCommunicator.java b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/utils/AbstractCommunicator.java
new file mode 100644
index 000000000..70816371a
--- /dev/null
+++ b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/utils/AbstractCommunicator.java
@@ -0,0 +1,158 @@
+/* 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.makeLogTag(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() {
+ Prefs prefs = Prefs.getInstanceWithoutContext();
+ mUserAgent = (prefs != null)? prefs.getUserAgent() : "fennec-stumbler-unset-user-agent";
+ }
+
+ private void openConnectionAndSetHeaders() {
+ try {
+ Prefs prefs = Prefs.getInstanceWithoutContext();
+ if (sMozApiKey == null || prefs != null) {
+ sMozApiKey = prefs.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;
+ }
+}
diff --git a/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/utils/NetworkUtils.java b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/utils/NetworkUtils.java
new file mode 100644
index 000000000..b3b33b02a
--- /dev/null
+++ b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/utils/NetworkUtils.java
@@ -0,0 +1,32 @@
+/* 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.makeLogTag(NetworkUtils.class.getSimpleName());
+
+ ConnectivityManager mConnectivityManager;
+
+ public NetworkUtils(Context context) {
+ mConnectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+ }
+
+ 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);
+ }
+
+}
diff --git a/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/utils/PersistentIntentService.java b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/utils/PersistentIntentService.java
new file mode 100644
index 000000000..8387c7edd
--- /dev/null
+++ b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/utils/PersistentIntentService.java
@@ -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 final 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);
+}
+
diff --git a/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/utils/TelemetryWrapper.java b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/utils/TelemetryWrapper.java
new file mode 100644
index 000000000..91cde26f2
--- /dev/null
+++ b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/utils/TelemetryWrapper.java
@@ -0,0 +1,35 @@
+package org.mozilla.mozstumbler.service.utils;
+
+import android.util.Log;
+import org.mozilla.mozstumbler.service.AppGlobals;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+public class TelemetryWrapper {
+ private static final String LOG_TAG = AppGlobals.makeLogTag(TelemetryWrapper.class.getSimpleName());
+ private static Method mAddToHistogram;
+
+ public static void addToHistogram(String key, int value) {
+ if (mAddToHistogram == null) {
+ try {
+ Class<?> telemetry = Class.forName("org.mozilla.gecko.Telemetry");
+ mAddToHistogram = telemetry.getMethod("addToHistogram", String.class, int.class);
+ } catch (ClassNotFoundException e) {
+ Log.d(LOG_TAG, "Class not found!");
+ return;
+ } catch (NoSuchMethodException e) {
+ Log.d(LOG_TAG, "Method not found!");
+ return;
+ }
+ }
+
+ if (mAddToHistogram != null) {
+ try {
+ mAddToHistogram.invoke(null, key, value);
+ }
+ catch (IllegalArgumentException | InvocationTargetException | IllegalAccessException e) {
+ Log.d(LOG_TAG, "Got exception invoking.");
+ }
+ }
+ }
+}
diff --git a/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/utils/Zipper.java b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/utils/Zipper.java
new file mode 100644
index 000000000..90e0ee7f5
--- /dev/null
+++ b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/utils/Zipper.java
@@ -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();
+ }
+}
diff --git a/mobile/android/stumbler/manifests/StumblerManifest_services.xml.in b/mobile/android/stumbler/manifests/StumblerManifest_services.xml.in
new file mode 100644
index 000000000..400863187
--- /dev/null
+++ b/mobile/android/stumbler/manifests/StumblerManifest_services.xml.in
@@ -0,0 +1,32 @@
+<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" />
+
+<!-- How Fennec and Stumbler interact:
+- On start, Fennec broadcasts an empty STUMBLER_REGISTER_LOCAL_LISTENER intent, indicating that Stumbler should
+ start listening for a locally-broadcast Stumbler preferences.
+- In response, Stumbler's SafeReceiver registers LocalPreferenceReceiver to listen for broadcasts
+ sent over LocalBroadcastManager which contain sensitive information.
+- This registration happens only once, and SafeReceiver can't unregister the listener.
+- LocalPreferenceReceiver responds to internal broadcasts with sensitive information,
+ and is able to start/stop StumblerService.
+- Fennec startup (if stumbling is enabled) or Fennec stumbling preference adjustment will trigger
+ a local preference intent, and Stumbler's internal state will be adjusted via LocalPreferenceReceiver.
+-->
+<receiver android:exported="false" android:name="org.mozilla.mozstumbler.service.mainthread.SafeReceiver">
+ <intent-filter>
+ <action android:name="org.mozilla.gecko.STUMBLER_REGISTER_LOCAL_LISTENER" />
+ </intent-filter>
+</receiver>
+
+<receiver android:exported="true" android:name="org.mozilla.mozstumbler.service.mainthread.SystemReceiver">
+ <intent-filter>
+ <action android:name="android.intent.action.BOOT_COMPLETED" />
+ <action android:name="android.intent.action.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE" />
+ </intent-filter>
+</receiver>
+
diff --git a/mobile/android/stumbler/moz.build b/mobile/android/stumbler/moz.build
new file mode 100644
index 000000000..651cd18dd
--- /dev/null
+++ b/mobile/android/stumbler/moz.build
@@ -0,0 +1,12 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+include('stumbler_sources.mozbuild')
+
+stumbler_jar = add_java_jar('stumbler')
+stumbler_jar.sources += stumbler_sources
+stumbler_jar.extra_jars += [CONFIG['ANDROID_SUPPORT_V4_AAR_LIB']]
+stumbler_jar.javac_flags += ['-Xlint:all']
diff --git a/mobile/android/stumbler/stumbler_sources.mozbuild b/mobile/android/stumbler/stumbler_sources.mozbuild
new file mode 100644
index 000000000..63bc559f6
--- /dev/null
+++ b/mobile/android/stumbler/stumbler_sources.mozbuild
@@ -0,0 +1,36 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+stumbler_sources = [
+ 'java/org/mozilla/mozstumbler/service/AppGlobals.java',
+ 'java/org/mozilla/mozstumbler/service/mainthread/LocalPreferenceReceiver.java',
+ 'java/org/mozilla/mozstumbler/service/mainthread/SafeReceiver.java',
+ 'java/org/mozilla/mozstumbler/service/mainthread/SystemReceiver.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/PersistedStats.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/CellScannerImplementation.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/TelemetryWrapper.java',
+ 'java/org/mozilla/mozstumbler/service/utils/Zipper.java',
+]