Bug 1244295 - Create client ID if it doesn't already exist in GeckoProfile. r=mfinkle draft
authorMichael Comella <michael.l.comella@gmail.com>
Fri, 12 Feb 2016 16:34:43 -0800
changeset 332014 f5acdba8f276f5cbc04a8877a0589f4ce8155e49
parent 332009 6008843fb7ba5de8a2c7e39bf6e2e4a2e7f1532b
child 332015 81cff9a199365623b16efe21a12509b8bf7b3c6e
push id11138
push usermichael.l.comella@gmail.com
push dateFri, 19 Feb 2016 01:47:06 +0000
reviewersmfinkle
bugs1244295
milestone47.0a1
Bug 1244295 - Create client ID if it doesn't already exist in GeckoProfile. r=mfinkle Additionally, we'll try to migrate the client ID from FHR if it doesn't already exist. MozReview-Commit-ID: B9vfefeVi2i
mobile/android/base/java/org/mozilla/gecko/GeckoProfile.java
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoProfile.java
+++ b/mobile/android/base/java/org/mozilla/gecko/GeckoProfile.java
@@ -11,16 +11,17 @@ import java.io.FileOutputStream;
 import java.io.FileReader;
 import java.io.FileWriter;
 import java.io.IOException;
 import java.io.OutputStreamWriter;
 import java.nio.charset.Charset;
 import java.util.Enumeration;
 import java.util.HashMap;
 import java.util.Hashtable;
+import java.util.UUID;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
 import org.json.JSONException;
 import org.json.JSONArray;
 import org.json.JSONObject;
 import org.mozilla.gecko.annotation.RobocopTarget;
 import org.mozilla.gecko.GeckoProfileDirectories.NoMozillaDirectoryException;
@@ -43,16 +44,17 @@ import android.support.annotation.Worker
 import android.text.TextUtils;
 import android.util.Log;
 
 public final class GeckoProfile {
     private static final String LOGTAG = "GeckoProfile";
 
     // The path in the profile to the file containing the client ID.
     private static final String CLIENT_ID_FILE_PATH = "datareporting/state.json";
+    private static final String FHR_CLIENT_ID_FILE_PATH = "healthreport/state.json";
     // In the client ID file, the attribute title in the JSON object containing the client ID value.
     private static final String CLIENT_ID_JSON_ATTR = "clientID";
 
     private static final String TIMES_PATH = "times.json";
     private static final String PROFILE_CREATION_DATE_JSON_ATTR = "created";
 
     // Only tests should need to do this.
     // We can default this to AppConstants.RELEASE_BUILD once we fix Bug 1069687.
@@ -594,42 +596,93 @@ public final class GeckoProfile {
         File f = getDir();
         if (f == null)
             return null;
 
         return new File(f, aFile);
     }
 
     /**
-     * Retrieves the Gecko client ID from the filesystem.
+     * Retrieves the Gecko client ID from the filesystem. If the client ID does not exist, we attempt to migrate and
+     * persist it from FHR and, if that fails, we attempt to create a new one ourselves.
      *
      * This method assumes the client ID is located in a file at a hard-coded path within the profile. The format of
      * this file is a JSONObject which at the bottom level contains a String -> String mapping containing the client ID.
      *
      * WARNING: the platform provides a JSM to retrieve the client ID [1] and this would be a
      * robust way to access it. However, we don't want to rely on Gecko running in order to get
      * the client ID so instead we access the file this module accesses directly. However, it's
      * possible the format of this file (and the access calls in the jsm) will change, leaving
      * this code to fail.
      *
      * TODO: Write tests to prevent regressions. Mention them here. Test both file location and file format.
      *
      * [1]: https://mxr.mozilla.org/mozilla-central/source/toolkit/modules/ClientID.jsm
+     *
+     * @throws IOException if the client ID could not be retrieved.
      */
     @WorkerThread
     public String getClientId() throws IOException {
-        final JSONObject obj = readJSONObjectFromFile(CLIENT_ID_FILE_PATH);
+        final JSONObject obj = getClientIdJSONObject();
         try {
             return obj.getString(CLIENT_ID_JSON_ATTR);
         } catch (final JSONException e) {
             // Don't log to avoid leaking data in JSONObject.
             throw new IOException("Client ID does not exist in JSONObject");
         }
     }
 
+    // Mimics ClientID.jsm – _doLoadClientID. One exception is that we don't validate client IDs like it does.
+    @WorkerThread
+    private JSONObject getClientIdJSONObject() throws IOException {
+        try {
+            return readJSONObjectFromFile(CLIENT_ID_FILE_PATH);
+        } catch (final IOException e) { /* No contemporary client ID: fallthrough. */ }
+
+        Log.d(LOGTAG, "Could not get client ID – attempting to migrate ID from FHR");
+        String clientIdToWrite;
+        try {
+            final JSONObject fhrClientIdObj = readJSONObjectFromFile(FHR_CLIENT_ID_FILE_PATH);
+            clientIdToWrite = fhrClientIdObj.getString(CLIENT_ID_JSON_ATTR);
+        } catch (final IOException|JSONException e) {
+            Log.d(LOGTAG, "Could not migrate client ID from FHR – creating a new one");
+            clientIdToWrite = UUID.randomUUID().toString();
+        }
+
+        // There is a possibility Gecko is running and the Gecko telemetry implementation decided it's time to generate
+        // the client ID, writing client ID underneath us. Since it's highly unlikely (e.g. we run in onStart before
+        // Gecko is started), we don't handle that possibility besides writing the ID and then reading from the file
+        // again (rather than just returning the value we generated before writing).
+        //
+        // In the event it does happen, any discrepancy will be resolved after a restart. In the mean time, both this
+        // implementation and the Gecko implementation could upload documents with inconsistent IDs.
+        //
+        // In any case, if we get an exception, intentionally throw - there's nothing more to do here.
+        persistClientId(clientIdToWrite);
+        return readJSONObjectFromFile(CLIENT_ID_FILE_PATH);
+    }
+
+    @WorkerThread
+    private void persistClientId(final String clientId) throws IOException {
+        if (!ensureParentDirs(CLIENT_ID_FILE_PATH)) {
+            throw new IOException("Could not create client ID parent directories");
+        }
+
+        final JSONObject obj = new JSONObject();
+        try {
+            obj.put(CLIENT_ID_JSON_ATTR, clientId);
+        } catch (final JSONException e) {
+            throw new IOException("Could not create client ID JSON object", e);
+        }
+
+        // ClientID.jsm overwrites the file to store the client ID so it's okay if we do it too.
+        Log.d(LOGTAG, "Attempting to write new client ID");
+        writeFile(CLIENT_ID_FILE_PATH, obj.toString()); // Logs errors within function: ideally we'd throw.
+    }
+
     /**
      * @return the profile creation date in the format returned by {@link System#currentTimeMillis()} or -1 if the value
      *         was not found.
      */
     @WorkerThread
     public long getProfileCreationDate() {
         try {
             return getProfileCreationDateFromTimesFile();
@@ -697,16 +750,30 @@ public final class GeckoProfile {
                 return readFile(sessionFile);
             }
         } catch (IOException ioe) {
             Log.e(LOGTAG, "Unable to read session file", ioe);
         }
         return null;
     }
 
+    /**
+     * Ensures the parent director(y|ies) of the given filename exist by making them
+     * if they don't already exist..
+     *
+     * @param filename The path to the file whose parents should be made directories
+     * @return true if the parent directory exists, false otherwise
+     */
+    @WorkerThread
+    public boolean ensureParentDirs(final String filename) {
+        final File file = new File(getDir(), filename);
+        final File parentFile = file.getParentFile();
+        return parentFile.mkdirs() || parentFile.isDirectory();
+    }
+
     public void writeFile(final String filename, final String data) {
         File file = new File(getDir(), filename);
         BufferedWriter bufferedWriter = null;
         try {
             bufferedWriter = new BufferedWriter(new FileWriter(file, false));
             bufferedWriter.write(data);
         } catch (IOException e) {
             Log.e(LOGTAG, "Unable to write to file", e);