Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit d59de63a authored by Steve Kondik's avatar Steve Kondik Committed by Abhisek Devkota
Browse files

Settings: add opt out stats event

- Get rid of keeping track of queued jobs, job scheduler does this and
  all we really want is a new job id to not interfere with old ones
  (unless we hit the max jobs allowed, then start the count over).

- Persist whether we have reported metrics for the owner user (once per
  device)

Ref: CYNGNOS-1131
Change-Id: Ib5bac2944b5ca4259ea82a357b24708377c1dc4c
(cherry picked from commit 50fed8b6)
parent bda7a5b7
Loading
Loading
Loading
Loading
+38 −42
Original line number Diff line number Diff line
@@ -20,20 +20,27 @@ import android.content.Context;
import android.content.SharedPreferences;
import android.os.Bundle;

import android.util.ArraySet;
import android.os.UserHandle;
import android.preference.Preference;
import android.preference.PreferenceScreen;
import android.preference.SwitchPreference;
import com.android.settings.R;
import com.android.settings.SettingsPreferenceFragment;

import java.util.Set;
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_JOB_QUEUE = "pref_job_queue";
    /* 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);
    }
@@ -42,62 +49,51 @@ public class AnonymousStats extends SettingsPreferenceFragment {
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        addPreferencesFromResource(R.xml.anonymous_stats);
        mStatsSwitch = (SwitchPreference) findPreference(KEY_STATS);
    }

    public static Set<String> getJobQueue(Context context) {
        return getPreferences(context).getStringSet(KEY_JOB_QUEUE, new ArraySet<String>());
    @Override
    public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, 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);
            }

    public static void clearJobQueue(Context context) {
        getPreferences(context)
                .edit()
                .remove(KEY_JOB_QUEUE)
                .commit();
            // will initiate opt out sequence if necessary
            ReportingServiceManager.setAlarm(getActivity());
            return true;
        }
        return super.onPreferenceTreeClick(preferenceScreen, preference);
    }

    public static void addJob(Context context, int jobId) {
        Set<String> jobQueue = getJobQueue(context);
        jobQueue.add(String.valueOf(jobId));

    public static void updateLastSynced(Context context) {
        getPreferences(context)
                .edit()
                .putStringSet(KEY_JOB_QUEUE, jobQueue)
                .putLong(ANONYMOUS_LAST_CHECKED,System.currentTimeMillis())
                .commit();
    }

    public static void removeJob(Context context, int jobId) {
        Set<String> jobQueue = getJobQueue(context);
        jobQueue.remove(String.valueOf(jobId));
        getPreferences(context)
                .edit()
                .remove(KEY_JOB_QUEUE)
                .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()
                .putStringSet(KEY_JOB_QUEUE, jobQueue)
                .putInt(KEY_LAST_JOB_ID, id)
                .commit();
    }

    /**
     * @param context context to use to get prefs
     * @return Returns the next unused int in the job queue, up until {@link #QUEUE_MAX_THRESHOLD}
     * is reached, then it will return -1
     */
    public static int getNextJobId(Context context) {
        Set<String> currentQueue = getJobQueue(context);

        if (currentQueue == null) {
            return 1;
        } else if (currentQueue.size() >= QUEUE_MAX_THRESHOLD) {
            return -1;
        int lastId = getLastJobId(context);
        if (lastId >= QUEUE_MAX_THRESHOLD) {
            lastId = 1;
        } else {
            int i = 1;
            while (currentQueue.contains(String.valueOf(i))) {
                i++;
            }
            return i;

            lastId += 1;
        }
        setLastJobId(context, lastId);
        return lastId;
    }
}
+21 −35
Original line number Diff line number Diff line
@@ -22,15 +22,18 @@ import android.app.job.JobScheduler;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
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 = false;
    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());
@@ -40,42 +43,21 @@ public class ReportingService extends IntentService {
    protected void onHandleIntent(Intent intent) {
        JobScheduler js = (JobScheduler) getSystemService(Context.JOB_SCHEDULER_SERVICE);

        if (AnonymousStats.getNextJobId(this) == -1) {
            // if we've filled up to the threshold, we may have some stale job queue ids, purge them
            // then re-add what hasn't executed yet
            AnonymousStats.clearJobQueue(this);

            final List<JobInfo> allPendingJobs = js.getAllPendingJobs();

            // add two extra jobs to the size for what we will schedule below so we *always*
            // have room for both.
            if (js.getAllPendingJobs().size() + 2 >= AnonymousStats.QUEUE_MAX_THRESHOLD) {
                // there are still as many actual pending jobs as our threshold allows.
                // since we are past the threshold we will be losing data if we don't schedule
                // another job here, so just clear out all the old data and start fresh
                js.cancelAll();
            } else {
                for (JobInfo pendingJob : allPendingJobs) {
                    AnonymousStats.addJob(this, pendingJob.getId());
                }
            }
        }

        int cyanogenJobId, cmOrgJobId;
        AnonymousStats.addJob(this, cyanogenJobId = AnonymousStats.getNextJobId(this));
        AnonymousStats.addJob(this, cmOrgJobId = AnonymousStats.getNextJobId(this));

        if (DEBUG) Log.d(TAG, "scheduling jobs id: " + cyanogenJobId + ", " + cmOrgJobId);

        // get snapshot and persist it
        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);
@@ -84,6 +66,7 @@ public class ReportingService extends IntentService {
        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
@@ -110,11 +93,14 @@ public class ReportingService extends IntentService {
                .setPersisted(true)
                .build());

        // reschedule
        final SharedPreferences prefs = AnonymousStats.getPreferences(this);
        prefs.edit().putLong(AnonymousStats.ANONYMOUS_LAST_CHECKED,
                System.currentTimeMillis()).apply();
        ReportingServiceManager.setAlarm(this, 0);
        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);
    }
}
+32 −19
Original line number Diff line number Diff line
@@ -18,17 +18,22 @@ package com.android.settings.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 =
            "com.android.settings.action.TRIGGER_REPORT_METRICS";
    public static final String EXTRA_FORCE = "force";
@@ -36,33 +41,41 @@ public class ReportingServiceManager extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        if (intent.getAction().equals(Intent.ACTION_BOOT_COMPLETED)) {
            setAlarm(context, 0);
            setAlarm(context);
        } else if (intent.getAction().equals(ACTION_LAUNCH_SERVICE)){
            launchService(context, intent.getBooleanExtra(EXTRA_FORCE, false));
        }
    }

    public static void setAlarm(Context context, long millisFromNow) {
    /**
     * 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;
        }

        if (millisFromNow <= 0) {
        long lastSynced = prefs.getLong(AnonymousStats.ANONYMOUS_LAST_CHECKED, 0);
        if (lastSynced == 0) {
                // never synced, so let's fake out that the last sync was just now.
                // this will allow the user tFrame time to opt out before it will start
                // sending up anonymous stats.
                lastSynced = System.currentTimeMillis();
                prefs.edit().putLong(AnonymousStats.ANONYMOUS_LAST_CHECKED, lastSynced).apply();
                Log.d(ReportingService.TAG, "Set alarm for first sync.");
            }
            millisFromNow = (lastSynced + UPDATE_INTERVAL) - System.currentTimeMillis();
            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);
@@ -70,8 +83,8 @@ public class ReportingServiceManager extends BroadcastReceiver {
        AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
        alarmManager.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + millisFromNow,
                PendingIntent.getBroadcast(context, 0, intent, 0));
        Log.d(ReportingService.TAG, "Next sync attempt in : "
                + millisFromNow / MILLIS_PER_HOUR + " hours");
        Log.d(TAG, "Next sync attempt in : "
                + (millisFromNow / MILLIS_PER_HOUR) + " hours");
    }

    public static void launchService(Context context, boolean force) {
@@ -84,13 +97,13 @@ public class ReportingServiceManager extends BroadcastReceiver {
        if (!force) {
            long lastSynced = prefs.getLong(AnonymousStats.ANONYMOUS_LAST_CHECKED, 0);
            if (lastSynced == 0) {
                setAlarm(context, 0);
                setAlarm(context);
                return;
            }
            long timeElapsed = System.currentTimeMillis() - lastSynced;
            if (timeElapsed < UPDATE_INTERVAL) {
                long timeLeft = UPDATE_INTERVAL - timeElapsed;
                Log.d(ReportingService.TAG, "Waiting for next sync : "
                Log.d(TAG, "Waiting for next sync : "
                        + timeLeft / MILLIS_PER_HOUR + " hours");
                return;
            }
@@ -98,7 +111,7 @@ public class ReportingServiceManager extends BroadcastReceiver {

        Intent intent = new Intent();
        intent.setClass(context, ReportingService.class);
        context.startService(intent);
        context.startServiceAsUser(intent, UserHandle.OWNER);
    }

    private static void migrate(Context context, SharedPreferences prefs) {
+26 −22
Original line number Diff line number Diff line
@@ -43,7 +43,7 @@ import java.util.Map;
public class StatsUploadJobService extends JobService {

    private static final String TAG = StatsUploadJobService.class.getSimpleName();
    private static final boolean DEBUG = false;
    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;
@@ -56,6 +56,7 @@ public class StatsUploadJobService extends JobService {
    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>());
@@ -107,16 +108,25 @@ public class StatsUploadJobService extends JobService {
            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;
            if (!isCancelled()) {
            int jobType = extras.getInt(KEY_JOB_TYPE, -1);

            if (!isCancelled()) {
                switch (jobType) {
                    case JOB_TYPE_CYANOGEN:
                        try {
                            success = uploadToCyanogen(deviceId, deviceName, deviceVersion,
                                    deviceCountry, deviceCarrier, deviceCarrierId, timeStamp);
                            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;
@@ -126,7 +136,7 @@ public class StatsUploadJobService extends JobService {
                    case JOB_TYPE_CMORG:
                        try {
                            success = uploadToCM(deviceId, deviceName, deviceVersion, deviceCountry,
                                    deviceCarrier, deviceCarrierId);
                                    deviceCarrier, deviceCarrierId, optOut);
                        } catch (IOException e) {
                            Log.e(TAG, "Could not upload stats checkin to commnity server", e);
                            success = false;
@@ -138,7 +148,6 @@ public class StatsUploadJobService extends JobService {
            if (success) {
                // we hit the server, succeed either which way.
                mCurrentJobs.remove(mJobParams);
                AnonymousStats.removeJob(StatsUploadJobService.this, mJobParams.getJobId());
            }

            if (DEBUG)
@@ -151,10 +160,12 @@ public class StatsUploadJobService extends JobService {


    private boolean uploadToCM(String deviceId, String deviceName, String deviceVersion,
                               String deviceCountry, String deviceCarrier, String deviceCarrierId)
                               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)
@@ -182,9 +193,7 @@ public class StatsUploadJobService extends JobService {

    }

    private boolean uploadToCyanogen(String deviceId, String deviceName, String deviceVersion,
                                     String deviceCountry, String carrier, String carrierId,
                                     long timeStamp)
    private boolean uploadToCyanogen(JSONObject json)
            throws IOException, JSONException {
        String authToken = getAuthToken();

@@ -192,15 +201,6 @@ public class StatsUploadJobService extends JobService {
            Log.w(TAG, "no auth token!");
        }

        JSONObject json = new JSONObject();
        json.put("uniqueId", deviceId);
        json.put("deviceName", deviceName);
        json.put("version", deviceVersion);
        json.put("country", deviceCountry);
        json.put("carrier", carrier);
        json.put("carrierId", carrierId);
        json.put("timestamp", timeStamp);

        URL url = new URL(getString(R.string.stats_cyanogen_url));
        HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
        try {
@@ -223,9 +223,13 @@ public class StatsUploadJobService extends JobService {

            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: " + getResponse(urlConnection,
                        !success));
                Log.w(TAG, "failed sending, server returned: " + response);
            }
            return success;
        } finally {