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