cmparts: Anonymous stats

 * Import anonymous user-counting from previous releases.

 * Original work by:
   - Chris Soyars
   - Danesh Mondegarian
   - Roman Birg
   - Steve Kondik
   - Ricardo Cerqueira
   - Adnan Begovic

Change-Id: Ib068c15b1ad238c9fe8818cf4eb9df46ba80d783
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 078135e..cdaee89 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -141,5 +141,30 @@
                 android:resource="@drawable/ic_settings_statusbar" />
         </activity-alias>
 
+        <!-- Anonymous Statistics -->
+        <receiver android:name=".cmstats.ReportingServiceManager"
+            android:enabled="true"
+            android:exported="false"
+            android:label="ReportingServiceManager">
+            <intent-filter>
+                <action android:name="android.intent.action.BOOT_COMPLETED" />
+                <action android:name="org.cyanogenmod.cmparts.action.TRIGGER_REPORT_METRICS" />
+            </intent-filter>
+        </receiver>
+
+        <service android:label="ReportingService"
+            android:enabled="true"
+            android:exported="false"
+            android:name=".cmstats.ReportingService">
+        </service>
+
+        <service android:name=".cmstats.StatsUploadJobService"
+                 android:permission="android.permission.BIND_JOB_SERVICE" />
+
+        <service android:name=".cmstats.ReportingService"
+                 android:label="ReportingService"
+                 android:enabled="true"
+                 android:exported="false" />
+
     </application>
 </manifest>
diff --git a/proguard.flags b/proguard.flags
index 653ffff..dc38ff8 100644
--- a/proguard.flags
+++ b/proguard.flags
@@ -3,6 +3,7 @@
 -keep class org.cyanogenmod.cmparts.*Picker
 -keep class org.cyanogenmod.cmparts.*Settings
 
+-keep class org.cyanogenmod.cmparts.cmstats.*
 -keep class org.cyanogenmod.cmparts.contributors.*
 -keep class org.cyanogenmod.cmparts.input.*
 -keep class org.cyanogenmod.cmparts.livedisplay.*
diff --git a/res/values/config.xml b/res/values/config.xml
new file mode 100644
index 0000000..82eea80
--- /dev/null
+++ b/res/values/config.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2016 The CyanogenMod 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.
+-->
+
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+
+    <!-- Metrics server endpoints -->
+    <string name="stats_cm_url">https://stats.cyanogenmod.org/submit</string>
+    <string name="stats_cyanogen_url">https://shopvac.cyngn.com/community/heartbeat</string>
+    <string name="stats_cyanogen_token_url">https://account.cyngn.com/api/v1/community/heartbeat_token</string>
+
+</resources>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index a69e663..83c8032 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -469,4 +469,26 @@
                  <b>Total commits:</b> <xliff:g id="total_commits">%2$s</xliff:g><br/><br/>
                  <b>Last update:</b> <xliff:g id="date">%3$s</xliff:g>]]></string>
 
+    <!-- Anonymous Statistics #CM -->
+    <!-- About device screen, list item title. Takes the user to the screen about opting in or out of anonymous statistics. -->
+    <string name="anonymous_statistics_title">CyanogenMod statistics</string>
+    <string name="anonymous_statistics_summary">Help make CyanogenMod better by opting into anonymous statistics reporting</string>
+    <string name="anonymous_statistics_warning_title">About</string>
+    <string name="anonymous_statistics_warning">Opting into CyanogenMod Statistics will allow non-personal data to be submitted to the
+        developers of CyanogenMod to track unique installations across devices. The information submitted includes an unique identifier,
+        which does not compromise your privacy or personal data. The data is submitted during each boot.\n\nFor an example of the data that is submitted, tap on Preview Data.</string>
+    <string name="enable_reporting_title">Enable reporting</string>
+    <string name="preview_data_title">Preview data</string>
+    <string name="view_stats_title">View stats</string>
+    <string name="anonymous_learn_more">Learn more</string>
+
+    <!-- Anonymous Statistics - Preview -->
+    <string name="preview_id_title">Unique ID</string>
+    <string name="preview_device_title">Device</string>
+    <string name="preview_version_title">Version</string>
+    <string name="preview_country_title">Country</string>
+    <string name="preview_carrier_title">Carrier</string>
+    <string name="stats_collection_title">Stats collection</string>
+    <string name="stats_collection_summary">When enabled, allows metrics collection</string>
+
 </resources>
diff --git a/res/xml/anonymous_stats.xml b/res/xml/anonymous_stats.xml
new file mode 100644
index 0000000..30a65d5
--- /dev/null
+++ b/res/xml/anonymous_stats.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2012 The CyanogenMod 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.
+-->
+
+<PreferenceScreen
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:title="@string/anonymous_statistics_title">
+
+    <cyanogenmod.preference.CMSecureSettingSwitchPreference
+            android:key="stats_collection"
+            android:title="@string/stats_collection_title"
+            android:summary="@string/stats_collection_summary"
+            android:defaultValue="true" />
+
+    <PreferenceScreen
+        android:title="@string/preview_data_title"
+        android:fragment="org.cyanogenmod.cmparts.cmstats.PreviewData" />
+
+</PreferenceScreen>
diff --git a/res/xml/preview_data.xml b/res/xml/preview_data.xml
new file mode 100644
index 0000000..025524c
--- /dev/null
+++ b/res/xml/preview_data.xml
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2012 The CyanogenMod 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.
+-->
+
+<PreferenceScreen
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:title="@string/preview_data_title">
+
+    <Preference
+        android:key="preview_id"
+        style="?android:preferenceInformationStyle"
+        android:title="@string/preview_id_title"
+        android:summary="%s" />
+
+    <Preference
+        android:key="preview_device"
+        style="?android:preferenceInformationStyle"
+        android:title="@string/preview_device_title"
+        android:summary="%s" />
+
+    <Preference
+        android:key="preview_version"
+        style="?android:preferenceInformationStyle"
+        android:title="@string/preview_version_title"
+        android:summary="%s" />
+
+    <Preference
+        android:key="preview_country"
+        style="?android:preferenceInformationStyle"
+        android:title="@string/preview_country_title"
+        android:summary="%s" />
+
+    <Preference
+        android:key="preview_carrier"
+        style="?android:preferenceInformationStyle"
+        android:title="@string/preview_carrier_title"
+        android:summary="%s" />
+
+</PreferenceScreen>
+
diff --git a/res/xml/privacy_settings.xml b/res/xml/privacy_settings.xml
index 02abaf4..5ae923a 100644
--- a/res/xml/privacy_settings.xml
+++ b/res/xml/privacy_settings.xml
@@ -19,12 +19,14 @@
         android:title="@string/privacy_settings_title"
         android:key="privacy_settings">
 
+    <!-- Privacy Guard -->
     <PreferenceScreen
         android:key="privacy_guard_manager"
         android:title="@*cyanogenmod.platform:string/privacy_guard_manager_title"
         android:summary="@string/privacy_guard_manager_summary"
         android:fragment="org.cyanogenmod.cmparts.privacyguard.PrivacyGuardManager" />
 
+    <!-- Protected apps -->
     <Preference
         android:title="@string/protected_apps_manager_title"
         android:summary="@string/protected_apps_manager_summary">
@@ -34,4 +36,12 @@
             android:targetClass="com.android.settings.applications.ProtectedAppsActivity" />
     </Preference>
 
+    <!-- Anonymous statistics - (CMStats) -->
+    <PreferenceScreen
+        android:key="cmstats"
+        android:title="@string/anonymous_statistics_title"
+        android:summary="@string/anonymous_statistics_summary"
+        android:fragment="org.cyanogenmod.cmparts.cmstats.AnonymousStats" >
+    </PreferenceScreen>
+
 </PreferenceScreen>
diff --git a/src/org/cyanogenmod/cmparts/cmstats/AnonymousStats.java b/src/org/cyanogenmod/cmparts/cmstats/AnonymousStats.java
new file mode 100644
index 0000000..7c6294e
--- /dev/null
+++ b/src/org/cyanogenmod/cmparts/cmstats/AnonymousStats.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2015 The CyanogenMod 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.
+ */
+
+package org.cyanogenmod.cmparts.cmstats;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.os.Bundle;
+
+import android.os.UserHandle;
+import android.support.v7.preference.Preference;
+import android.support.v7.preference.PreferenceScreen;
+import android.support.v14.preference.SwitchPreference;
+
+import org.cyanogenmod.cmparts.R;
+import org.cyanogenmod.cmparts.SettingsPreferenceFragment;
+
+import cyanogenmod.providers.CMSettings;
+
+public class AnonymousStats extends SettingsPreferenceFragment {
+
+    private static final String PREF_FILE_NAME = "CMStats";
+    /* package */ static final String ANONYMOUS_OPT_IN = "pref_anonymous_opt_in";
+    /* package */ static final String ANONYMOUS_LAST_CHECKED = "pref_anonymous_checked_in";
+
+    /* package */ static final String KEY_LAST_JOB_ID = "last_job_id";
+    /* package */ static final int QUEUE_MAX_THRESHOLD = 1000;
+
+    public static final String KEY_STATS = "stats_collection";
+
+    SwitchPreference mStatsSwitch;
+
+    public static SharedPreferences getPreferences(Context context) {
+        return context.getSharedPreferences(PREF_FILE_NAME, 0);
+    }
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        addPreferencesFromResource(R.xml.anonymous_stats);
+        mStatsSwitch = (SwitchPreference) findPreference(KEY_STATS);
+    }
+
+    @Override
+    public boolean onPreferenceTreeClick(Preference preference) {
+        if (preference == mStatsSwitch) {
+            boolean checked = mStatsSwitch.isChecked();
+            if (checked) {
+                // clear opt out flags
+                CMSettings.Secure.putIntForUser(getContentResolver(),
+                        CMSettings.Secure.STATS_COLLECTION_REPORTED, 0, UserHandle.USER_OWNER);
+            }
+            // will initiate opt out sequence if necessary
+            ReportingServiceManager.setAlarm(getActivity());
+            return true;
+        }
+        return super.onPreferenceTreeClick(preference);
+    }
+
+    public static void updateLastSynced(Context context) {
+        getPreferences(context)
+                .edit()
+                .putLong(ANONYMOUS_LAST_CHECKED,System.currentTimeMillis())
+                .commit();
+    }
+
+    private static int getLastJobId(Context context) {
+        return getPreferences(context).getInt(KEY_LAST_JOB_ID, 0);
+    }
+
+    private static void setLastJobId(Context context, int id) {
+        getPreferences(context)
+                .edit()
+                .putInt(KEY_LAST_JOB_ID, id)
+                .commit();
+    }
+
+    public static int getNextJobId(Context context) {
+        int lastId = getLastJobId(context);
+        if (lastId >= QUEUE_MAX_THRESHOLD) {
+            lastId = 1;
+        } else {
+            lastId += 1;
+        }
+        setLastJobId(context, lastId);
+        return lastId;
+    }
+}
diff --git a/src/org/cyanogenmod/cmparts/cmstats/PreviewData.java b/src/org/cyanogenmod/cmparts/cmstats/PreviewData.java
new file mode 100644
index 0000000..a14f76b
--- /dev/null
+++ b/src/org/cyanogenmod/cmparts/cmstats/PreviewData.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2012 The CyanogenMod 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.
+ */
+
+package org.cyanogenmod.cmparts.cmstats;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.support.v7.preference.Preference;
+import android.support.v7.preference.PreferenceScreen;
+
+import org.cyanogenmod.cmparts.R;
+import org.cyanogenmod.cmparts.SettingsPreferenceFragment;
+
+public class PreviewData extends SettingsPreferenceFragment {
+    private static final String UNIQUE_ID = "preview_id";
+    private static final String DEVICE = "preview_device";
+    private static final String VERSION = "preview_version";
+    private static final String COUNTRY = "preview_country";
+    private static final String CARRIER = "preview_carrier";
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        addPreferencesFromResource(R.xml.preview_data);
+
+        final PreferenceScreen prefSet = getPreferenceScreen();
+        final Context context = getActivity();
+
+        prefSet.findPreference(UNIQUE_ID).setSummary(Utilities.getUniqueID(context));
+        prefSet.findPreference(DEVICE).setSummary(Utilities.getDevice());
+        prefSet.findPreference(VERSION).setSummary(Utilities.getModVersion());
+        prefSet.findPreference(COUNTRY).setSummary(Utilities.getCountryCode(context));
+        prefSet.findPreference(CARRIER).setSummary(Utilities.getCarrier(context));
+    }
+}
diff --git a/src/org/cyanogenmod/cmparts/cmstats/ReportingService.java b/src/org/cyanogenmod/cmparts/cmstats/ReportingService.java
new file mode 100644
index 0000000..0725676
--- /dev/null
+++ b/src/org/cyanogenmod/cmparts/cmstats/ReportingService.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2015 The CyanogenMod 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.
+ */
+
+package org.cyanogenmod.cmparts.cmstats;
+
+import android.app.IntentService;
+import android.app.job.JobInfo;
+import android.app.job.JobScheduler;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.os.PersistableBundle;
+import android.os.UserHandle;
+import android.util.Log;
+import cyanogenmod.providers.CMSettings;
+
+import java.util.List;
+
+public class ReportingService extends IntentService {
+    /* package */ static final String TAG = "CMStats";
+    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
+    public static final String EXTRA_OPTING_OUT = "cmstats::opt_out";
+
+    public ReportingService() {
+        super(ReportingService.class.getSimpleName());
+    }
+
+    @Override
+    protected void onHandleIntent(Intent intent) {
+        JobScheduler js = (JobScheduler) getSystemService(Context.JOB_SCHEDULER_SERVICE);
+
+        String deviceId = Utilities.getUniqueID(getApplicationContext());
+        String deviceName = Utilities.getDevice();
+        String deviceVersion = Utilities.getModVersion();
+        String deviceCountry = Utilities.getCountryCode(getApplicationContext());
+        String deviceCarrier = Utilities.getCarrier(getApplicationContext());
+        String deviceCarrierId = Utilities.getCarrierId(getApplicationContext());
+        boolean optOut = intent.getBooleanExtra(EXTRA_OPTING_OUT, false);
+
+        final int cyanogenJobId = AnonymousStats.getNextJobId(getApplicationContext());
+        final int cmOrgJobId = AnonymousStats.getNextJobId(getApplicationContext());
+
+        if (DEBUG) Log.d(TAG, "scheduling jobs id: " + cyanogenJobId + ", " + cmOrgJobId);
+
+        PersistableBundle cyanogenBundle = new PersistableBundle();
+        cyanogenBundle.putBoolean(StatsUploadJobService.KEY_OPT_OUT, optOut);
+        cyanogenBundle.putString(StatsUploadJobService.KEY_DEVICE_NAME, deviceName);
+        cyanogenBundle.putString(StatsUploadJobService.KEY_UNIQUE_ID, deviceId);
+        cyanogenBundle.putString(StatsUploadJobService.KEY_VERSION, deviceVersion);
+        cyanogenBundle.putString(StatsUploadJobService.KEY_COUNTRY, deviceCountry);
+        cyanogenBundle.putString(StatsUploadJobService.KEY_CARRIER, deviceCarrier);
+        cyanogenBundle.putString(StatsUploadJobService.KEY_CARRIER_ID, deviceCarrierId);
+        cyanogenBundle.putLong(StatsUploadJobService.KEY_TIMESTAMP, System.currentTimeMillis());
+
+        // get snapshot and persist it
+        PersistableBundle cmBundle = new PersistableBundle(cyanogenBundle);
+
+        // set job types
+        cyanogenBundle.putInt(StatsUploadJobService.KEY_JOB_TYPE,
+                StatsUploadJobService.JOB_TYPE_CYANOGEN);
+        cmBundle.putInt(StatsUploadJobService.KEY_JOB_TYPE,
+                StatsUploadJobService.JOB_TYPE_CMORG);
+
+        // schedule cyanogen stats upload
+        js.schedule(new JobInfo.Builder(cyanogenJobId, new ComponentName(getPackageName(),
+                StatsUploadJobService.class.getName()))
+                .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
+                .setMinimumLatency(1000)
+                .setExtras(cyanogenBundle)
+                .setPersisted(true)
+                .build());
+
+        // schedule cmorg stats upload
+        js.schedule(new JobInfo.Builder(cmOrgJobId, new ComponentName(getPackageName(),
+                StatsUploadJobService.class.getName()))
+                .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
+                .setMinimumLatency(1000)
+                .setExtras(cmBundle)
+                .setPersisted(true)
+                .build());
+
+        if (optOut) {
+            // we've successfully scheduled the opt out.
+            CMSettings.Secure.putIntForUser(getContentResolver(),
+                    CMSettings.Secure.STATS_COLLECTION_REPORTED, 1, UserHandle.USER_OWNER);
+        }
+
+        // reschedule
+        AnonymousStats.updateLastSynced(this);
+        ReportingServiceManager.setAlarm(this);
+    }
+}
diff --git a/src/org/cyanogenmod/cmparts/cmstats/ReportingServiceManager.java b/src/org/cyanogenmod/cmparts/cmstats/ReportingServiceManager.java
new file mode 100644
index 0000000..8264739
--- /dev/null
+++ b/src/org/cyanogenmod/cmparts/cmstats/ReportingServiceManager.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2012 The CyanogenMod 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.
+ */
+
+package org.cyanogenmod.cmparts.cmstats;
+
+import android.app.AlarmManager;
+import android.app.PendingIntent;
+import android.app.job.JobScheduler;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.os.UserHandle;
+import android.util.Log;
+import cyanogenmod.providers.CMSettings;
+
+public class ReportingServiceManager extends BroadcastReceiver {
+    private static final long MILLIS_PER_HOUR = 60L * 60L * 1000L;
+    private static final long MILLIS_PER_DAY = 24L * MILLIS_PER_HOUR;
+    private static final long UPDATE_INTERVAL = 1L * MILLIS_PER_DAY;
+
+    private static final String TAG = ReportingServiceManager.class.getSimpleName();
+
+    public static final String ACTION_LAUNCH_SERVICE =
+            "org.cyanogenmod.cmparts.action.TRIGGER_REPORT_METRICS";
+    public static final String EXTRA_FORCE = "force";
+
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        if (intent.getAction().equals(Intent.ACTION_BOOT_COMPLETED)) {
+            setAlarm(context);
+        } else if (intent.getAction().equals(ACTION_LAUNCH_SERVICE)){
+            launchService(context, intent.getBooleanExtra(EXTRA_FORCE, false));
+        }
+    }
+
+    /**
+     * opt out if we haven't yet
+     */
+    public static void initiateOptOut(Context context) {
+        final boolean optOutReported = CMSettings.Secure.getIntForUser(context.getContentResolver(),
+                CMSettings.Secure.STATS_COLLECTION_REPORTED, 0, UserHandle.USER_OWNER) == 1;
+        if (!optOutReported) {
+            Intent intent = new Intent();
+            intent.setClass(context, ReportingService.class);
+            intent.putExtra(ReportingService.EXTRA_OPTING_OUT, true);
+            context.startServiceAsUser(intent, UserHandle.OWNER);
+        }
+    }
+
+    public static void setAlarm(Context context) {
+        SharedPreferences prefs = AnonymousStats.getPreferences(context);
+        if (prefs.contains(AnonymousStats.ANONYMOUS_OPT_IN)) {
+            migrate(context, prefs);
+        }
+        if (!Utilities.isStatsCollectionEnabled(context)) {
+            initiateOptOut(context);
+            return;
+        }
+        long lastSynced = prefs.getLong(AnonymousStats.ANONYMOUS_LAST_CHECKED, 0);
+        if (lastSynced == 0) {
+            launchService(context, true); // service will reschedule the next alarm
+            return;
+        }
+        long millisFromNow = (lastSynced + UPDATE_INTERVAL) - System.currentTimeMillis();
+
+        Intent intent = new Intent(ACTION_LAUNCH_SERVICE);
+        intent.setClass(context, ReportingServiceManager.class);
+
+        AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
+        alarmManager.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + millisFromNow,
+                PendingIntent.getBroadcast(context, 0, intent, 0));
+        Log.d(TAG, "Next sync attempt in : "
+                + (millisFromNow / MILLIS_PER_HOUR) + " hours");
+    }
+
+    public static void launchService(Context context, boolean force) {
+        SharedPreferences prefs = AnonymousStats.getPreferences(context);
+
+        if (!Utilities.isStatsCollectionEnabled(context)) {
+            return;
+        }
+
+        if (!force) {
+            long lastSynced = prefs.getLong(AnonymousStats.ANONYMOUS_LAST_CHECKED, 0);
+            if (lastSynced == 0) {
+                setAlarm(context);
+                return;
+            }
+            long timeElapsed = System.currentTimeMillis() - lastSynced;
+            if (timeElapsed < UPDATE_INTERVAL) {
+                long timeLeft = UPDATE_INTERVAL - timeElapsed;
+                Log.d(TAG, "Waiting for next sync : "
+                        + timeLeft / MILLIS_PER_HOUR + " hours");
+                return;
+            }
+        }
+
+        Intent intent = new Intent();
+        intent.setClass(context, ReportingService.class);
+        context.startServiceAsUser(intent, UserHandle.OWNER);
+    }
+
+    private static void migrate(Context context, SharedPreferences prefs) {
+        Utilities.setStatsCollectionEnabled(context,
+                prefs.getBoolean(AnonymousStats.ANONYMOUS_OPT_IN, true));
+        prefs.edit().remove(AnonymousStats.ANONYMOUS_OPT_IN).commit();
+    }
+
+}
diff --git a/src/org/cyanogenmod/cmparts/cmstats/StatsUploadJobService.java b/src/org/cyanogenmod/cmparts/cmstats/StatsUploadJobService.java
new file mode 100644
index 0000000..09f4ea1
--- /dev/null
+++ b/src/org/cyanogenmod/cmparts/cmstats/StatsUploadJobService.java
@@ -0,0 +1,291 @@
+/*
+ * Copyright (C) 2015 The CyanogenMod 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.
+ */
+
+package org.cyanogenmod.cmparts.cmstats;
+
+import android.app.job.JobParameters;
+import android.app.job.JobService;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.PersistableBundle;
+import android.util.ArrayMap;
+import android.util.Log;
+import org.cyanogenmod.cmparts.R;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.util.Collections;
+import java.util.Map;
+
+public class StatsUploadJobService extends JobService {
+
+    private static final String TAG = StatsUploadJobService.class.getSimpleName();
+    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
+    public static final String KEY_JOB_TYPE = "job_type";
+    public static final int JOB_TYPE_CYANOGEN = 1;
+    public static final int JOB_TYPE_CMORG = 2;
+
+    public static final String KEY_UNIQUE_ID = "uniqueId";
+    public static final String KEY_DEVICE_NAME = "deviceName";
+    public static final String KEY_VERSION = "version";
+    public static final String KEY_COUNTRY = "country";
+    public static final String KEY_CARRIER = "carrier";
+    public static final String KEY_CARRIER_ID = "carrierId";
+    public static final String KEY_TIMESTAMP = "timeStamp";
+    public static final String KEY_OPT_OUT = "optOut";
+
+    private final Map<JobParameters, StatsUploadTask> mCurrentJobs
+            = Collections.synchronizedMap(new ArrayMap<JobParameters, StatsUploadTask>());
+
+    @Override
+    public boolean onStartJob(JobParameters jobParameters) {
+        if (DEBUG)
+            Log.d(TAG, "onStartJob() called with " + "jobParameters = [" + jobParameters + "]");
+        final StatsUploadTask uploadTask = new StatsUploadTask(jobParameters);
+        mCurrentJobs.put(jobParameters, uploadTask);
+        uploadTask.execute((Void) null);
+        return true;
+    }
+
+    @Override
+    public boolean onStopJob(JobParameters jobParameters) {
+        if (DEBUG)
+            Log.d(TAG, "onStopJob() called with " + "jobParameters = [" + jobParameters + "]");
+
+        final StatsUploadTask cancelledJob;
+        cancelledJob = mCurrentJobs.remove(jobParameters);
+
+        if (cancelledJob != null) {
+            // cancel the ongoing background task
+            cancelledJob.cancel(true);
+            return true; // reschedule
+        }
+
+        return false;
+    }
+
+    private class StatsUploadTask extends AsyncTask<Void, Void, Boolean> {
+
+        private JobParameters mJobParams;
+
+        public StatsUploadTask(JobParameters jobParams) {
+            this.mJobParams = jobParams;
+        }
+
+        @Override
+        protected Boolean doInBackground(Void... params) {
+
+            PersistableBundle extras = mJobParams.getExtras();
+
+            String deviceId = extras.getString(KEY_UNIQUE_ID);
+            String deviceName = extras.getString(KEY_DEVICE_NAME);
+            String deviceVersion = extras.getString(KEY_VERSION);
+            String deviceCountry = extras.getString(KEY_COUNTRY);
+            String deviceCarrier = extras.getString(KEY_CARRIER);
+            String deviceCarrierId = extras.getString(KEY_CARRIER_ID);
+            long timeStamp = extras.getLong(KEY_TIMESTAMP);
+            boolean optOut = extras.getBoolean(KEY_OPT_OUT);
+
+            boolean success = false;
+            int jobType = extras.getInt(KEY_JOB_TYPE, -1);
+            if (!isCancelled()) {
+                switch (jobType) {
+                    case JOB_TYPE_CYANOGEN:
+                        try {
+                            JSONObject json = new JSONObject();
+                            json.put("optOut", optOut);
+                            json.put("uniqueId", deviceId);
+                            json.put("deviceName", deviceName);
+                            json.put("version", deviceVersion);
+                            json.put("country", deviceCountry);
+                            json.put("carrier", deviceCarrier);
+                            json.put("carrierId", deviceCarrierId);
+                            json.put("timestamp", timeStamp);
+
+                            success = uploadToCyanogen(json);
+                        } catch (IOException | JSONException e) {
+                            Log.e(TAG, "Could not upload stats checkin to cyanogen server", e);
+                            success = false;
+                        }
+                        break;
+
+                    case JOB_TYPE_CMORG:
+                        try {
+                            success = uploadToCM(deviceId, deviceName, deviceVersion, deviceCountry,
+                                    deviceCarrier, deviceCarrierId, optOut);
+                        } catch (IOException e) {
+                            Log.e(TAG, "Could not upload stats checkin to commnity server", e);
+                            success = false;
+                        }
+                        break;
+                }
+            }
+            if (DEBUG)
+                Log.d(TAG, "job id " + mJobParams.getJobId() + ", has finished with success="
+                        + success);
+            return success;
+        }
+
+        @Override
+        protected void onPostExecute(Boolean success) {
+            mCurrentJobs.remove(mJobParams);
+            jobFinished(mJobParams, !success);
+        }
+    }
+
+
+    private boolean uploadToCM(String deviceId, String deviceName, String deviceVersion,
+                               String deviceCountry, String deviceCarrier, String deviceCarrierId,
+                               boolean optOut)
+            throws IOException {
+
+        final Uri uri = Uri.parse(getString(R.string.stats_cm_url)).buildUpon()
+                .appendQueryParameter("opt_out", optOut ? "1" : "0")
+                .appendQueryParameter("device_hash", deviceId)
+                .appendQueryParameter("device_name", deviceName)
+                .appendQueryParameter("device_version", deviceVersion)
+                .appendQueryParameter("device_country", deviceCountry)
+                .appendQueryParameter("device_carrier", deviceCarrier)
+                .appendQueryParameter("device_carrier_id", deviceCarrierId).build();
+        URL url = new URL(uri.toString());
+        HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
+        try {
+            urlConnection.setInstanceFollowRedirects(true);
+            urlConnection.setDoOutput(true);
+            urlConnection.connect();
+
+            final int responseCode = urlConnection.getResponseCode();
+            if (DEBUG) Log.d(TAG, "cm server response code=" + responseCode);
+            final boolean success = responseCode == HttpURLConnection.HTTP_OK;
+            if (!success) {
+                Log.w(TAG, "failed sending, server returned: " + getResponse(urlConnection,
+                        !success));
+            }
+            return success;
+        } finally {
+            urlConnection.disconnect();
+        }
+
+    }
+
+    private boolean uploadToCyanogen(JSONObject json)
+            throws IOException, JSONException {
+        String authToken = getAuthToken();
+
+        if (authToken.isEmpty()) {
+            Log.w(TAG, "no auth token!");
+        }
+
+        URL url = new URL(getString(R.string.stats_cyanogen_url));
+        HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
+        try {
+            urlConnection.setInstanceFollowRedirects(true);
+            urlConnection.setDoInput(true);
+            urlConnection.setDoOutput(true);
+
+            urlConnection.setRequestProperty("Accept-Encoding", "identity");
+            urlConnection.setRequestProperty("Authorization", authToken);
+            urlConnection.setRequestProperty("Content-Type", "application/json");
+
+            OutputStream os = urlConnection.getOutputStream();
+            BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(os, "UTF-8"));
+            writer.write(json.toString());
+            writer.flush();
+            writer.close();
+            os.close();
+
+            urlConnection.connect();
+
+            final int responseCode = urlConnection.getResponseCode();
+            final boolean success = responseCode == HttpURLConnection.HTTP_OK;
+
+            final String response = getResponse(urlConnection, !success);
+            if (DEBUG)
+                Log.d(TAG, "server responseCode: " + responseCode +", response=" + response);
+
+            if (!success) {
+                Log.w(TAG, "failed sending, server returned: " + response);
+            }
+            return success;
+        } finally {
+            urlConnection.disconnect();
+        }
+    }
+
+    private String getAuthToken() {
+        HttpURLConnection urlConnection = null;
+        try {
+            URL url = new URL(getString(R.string.stats_cyanogen_token_url));
+            urlConnection = (HttpURLConnection) url.openConnection();
+            urlConnection.setInstanceFollowRedirects(true);
+            urlConnection.setDoInput(true);
+
+            urlConnection.setRequestProperty("Accept-Encoding", "identity");
+            urlConnection.setRequestProperty("Content-Type", "text/plain");
+
+            urlConnection.connect();
+
+            final int responseCode = urlConnection.getResponseCode();
+            final boolean success = responseCode == HttpURLConnection.HTTP_OK;
+            if (DEBUG) Log.d(TAG, "server auth response code=" + responseCode);
+            final String response = getResponse(urlConnection, !success);
+            if (DEBUG)
+                Log.d(TAG, "server auth response=" + response);
+
+            if (success) {
+                return response;
+            }
+        } catch (IOException e) {
+            Log.e(TAG, "error getting auth token", e);
+        } finally {
+            if (urlConnection != null) {
+                urlConnection.disconnect();
+            }
+        }
+        return "";
+    }
+
+    private String getResponse(HttpURLConnection httpUrlConnection, boolean errorStream)
+            throws IOException {
+        InputStream responseStream = new BufferedInputStream(errorStream
+                ? httpUrlConnection.getErrorStream()
+                : httpUrlConnection.getInputStream());
+
+        BufferedReader responseStreamReader = new BufferedReader(
+                new InputStreamReader(responseStream));
+        String line = "";
+        StringBuilder stringBuilder = new StringBuilder();
+        while ((line = responseStreamReader.readLine()) != null) {
+            stringBuilder.append(line).append("\n");
+        }
+        responseStreamReader.close();
+        responseStream.close();
+
+        return stringBuilder.toString();
+    }
+
+}
diff --git a/src/org/cyanogenmod/cmparts/cmstats/Utilities.java b/src/org/cyanogenmod/cmparts/cmstats/Utilities.java
new file mode 100644
index 0000000..eefafba
--- /dev/null
+++ b/src/org/cyanogenmod/cmparts/cmstats/Utilities.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2012 The CyanogenMod 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.
+ */
+
+package org.cyanogenmod.cmparts.cmstats;
+
+import android.content.Context;
+import android.os.Build;
+import android.os.SystemProperties;
+import android.provider.Settings;
+import android.telephony.TelephonyManager;
+import android.text.TextUtils;
+
+import cyanogenmod.providers.CMSettings;
+
+import java.math.BigInteger;
+import java.net.NetworkInterface;
+import java.security.MessageDigest;
+
+public class Utilities {
+    public static String getUniqueID(Context context) {
+        final String id = Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID);
+        return digest(context.getPackageName() + id);
+    }
+
+    public static String getCarrier(Context context) {
+        TelephonyManager tm = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
+        String carrier = tm.getNetworkOperatorName();
+        if (TextUtils.isEmpty(carrier)) {
+            carrier = "Unknown";
+        }
+        return carrier;
+    }
+
+    public static String getCarrierId(Context context) {
+        TelephonyManager tm = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
+        String carrierId = tm.getNetworkOperator();
+        if (TextUtils.isEmpty(carrierId)) {
+            carrierId = "0";
+        }
+        return carrierId;
+    }
+
+    public static String getCountryCode(Context context) {
+        TelephonyManager tm = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
+        String countryCode = tm.getNetworkCountryIso();
+        if (TextUtils.isEmpty(countryCode)) {
+            countryCode = "Unknown";
+        }
+        return countryCode;
+    }
+
+    public static String getDevice() {
+        return SystemProperties.get("ro.cm.device", Build.PRODUCT);
+    }
+
+    public static String getModVersion() {
+        return SystemProperties.get("ro.cm.version", Build.DISPLAY);
+    }
+
+    public static String digest(String input) {
+        try {
+            MessageDigest md = MessageDigest.getInstance("MD5");
+            return new BigInteger(1, md.digest(input.getBytes())).toString(16).toUpperCase();
+        } catch (Exception e) {
+            return null;
+        }
+    }
+
+    /**
+     * Check to see if global stats are enabled.
+     * @param context
+     * @return Whether or not stats collection is enabled.
+     */
+    public static boolean isStatsCollectionEnabled(Context context) {
+        return CMSettings.Secure.getInt(context.getContentResolver(),
+                CMSettings.Secure.STATS_COLLECTION, 1) != 0;
+    }
+
+    /**
+     * Enabled or disable stats collection
+     * @param context
+     * @param enabled Boolean that sets collection being enabled.
+     */
+    public static void setStatsCollectionEnabled(Context context, boolean enabled) {
+        int enable = (enabled) ? 1 : 0;
+        CMSettings.Secure.putInt(context.getContentResolver(),
+                CMSettings.Secure.STATS_COLLECTION, enable);
+    }
+}