author | Richard Newman <rnewman@mozilla.com> |
Tue, 04 Jun 2013 17:16:57 -0700 | |
changeset 134131 | 530fbc32771b6c5420fa5adfcbdada6c322cc67f |
parent 134130 | 79088e422daf4d1af974ba219fc01e13ffa245d2 |
child 134132 | 168173d49c8768a3272b32aa75353badf25a25fc |
push id | 29067 |
push user | ryanvm@gmail.com |
push date | Wed, 05 Jun 2013 20:37:20 +0000 |
treeherder | mozilla-inbound@72fbfb2f8e51 [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
reviewers | nalexander |
bugs | 868445 |
milestone | 24.0a1 |
first release with | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
last release without | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
--- a/mobile/android/base/GeckoApp.java +++ b/mobile/android/base/GeckoApp.java @@ -11,16 +11,17 @@ import org.mozilla.gecko.db.BrowserDB; import org.mozilla.gecko.gfx.BitmapUtils; import org.mozilla.gecko.gfx.Layer; import org.mozilla.gecko.gfx.LayerView; import org.mozilla.gecko.gfx.PluginLayer; import org.mozilla.gecko.menu.GeckoMenu; import org.mozilla.gecko.menu.GeckoMenuInflater; import org.mozilla.gecko.menu.MenuPanel; import org.mozilla.gecko.health.BrowserHealthRecorder; +import org.mozilla.gecko.health.BrowserHealthRecorder.SessionInformation; import org.mozilla.gecko.updater.UpdateService; import org.mozilla.gecko.updater.UpdateServiceHelper; import org.mozilla.gecko.util.EventDispatcher; import org.mozilla.gecko.util.GeckoEventListener; import org.mozilla.gecko.util.GeckoEventResponder; import org.mozilla.gecko.util.HardwareUtils; import org.mozilla.gecko.util.ThreadUtils; import org.mozilla.gecko.util.UiAsyncTask; @@ -241,16 +242,20 @@ abstract public class GeckoApp } return this; } public SensorEventListener getSensorEventListener() { return this; } + public static SharedPreferences getAppSharedPreferences() { + return GeckoApp.sAppContext.getSharedPreferences(PREFS_NAME, 0); + } + public SurfaceView getCameraView() { return cameraView; } public FormAssistPopup getFormAssistPopup() { return mFormAssistPopup; } @@ -529,16 +534,21 @@ abstract public class GeckoApp } else if (event.equals("Reader:FaviconRequest")) { final String url = message.getString("url"); handleFaviconRequest(url); } else if (event.equals("Reader:GoToReadingList")) { showReadingList(); } else if (event.equals("Gecko:Ready")) { mGeckoReadyStartupTimer.stop(); geckoConnected(); + + // This method is already running on the background thread, so we + // know that mHealthRecorder will exist. This method is cheap, so + // don't spawn a new runnable. + mHealthRecorder.recordGeckoStartupTime(mGeckoReadyStartupTimer.getElapsed()); } else if (event.equals("ToggleChrome:Hide")) { toggleChrome(false); } else if (event.equals("ToggleChrome:Show")) { toggleChrome(true); } else if (event.equals("ToggleChrome:Focus")) { focusChrome(); } else if (event.equals("DOMFullScreen:Start")) { // Local ref to layerView for thread safety @@ -1225,39 +1235,40 @@ abstract public class GeckoApp mPrivateBrowsingSession = savedInstanceState.getString(SAVED_STATE_PRIVATE_SESSION); } // Perform background initialization. ThreadUtils.postToBackgroundThread(new Runnable() { @Override public void run() { - SharedPreferences prefs = - GeckoApp.sAppContext.getSharedPreferences(PREFS_NAME, 0); + final SharedPreferences prefs = GeckoApp.getAppSharedPreferences(); - boolean wasOOM = prefs.getBoolean(PREFS_OOM_EXCEPTION, false); - boolean wasStopped = prefs.getBoolean(PREFS_WAS_STOPPED, true); - if (wasOOM || !wasStopped) { + SessionInformation previousSession = SessionInformation.fromSharedPrefs(prefs); + if (previousSession.wasKilled()) { Telemetry.HistogramAdd("FENNEC_WAS_KILLED", 1); } + SharedPreferences.Editor editor = prefs.edit(); editor.putBoolean(GeckoApp.PREFS_OOM_EXCEPTION, false); - // Put a flag to check if we got a normal onSaveInstanceState - // on exit, or if we were suddenly killed (crash or native OOM) + // Put a flag to check if we got a normal `onSaveInstanceState` + // on exit, or if we were suddenly killed (crash or native OOM). editor.putBoolean(GeckoApp.PREFS_WAS_STOPPED, false); + editor.commit(); // The lifecycle of mHealthRecorder is "shortly after onCreate" // through "onDestroy" -- essentially the same as the lifecycle // of the activity itself. final String profilePath = getProfile().getDir().getAbsolutePath(); final EventDispatcher dispatcher = GeckoAppShell.getEventDispatcher(); Log.i(LOGTAG, "Creating BrowserHealthRecorder."); - mHealthRecorder = new BrowserHealthRecorder(sAppContext, profilePath, dispatcher); + mHealthRecorder = new BrowserHealthRecorder(sAppContext, profilePath, dispatcher, + previousSession); } }); GeckoAppShell.setNotificationClient(makeNotificationClient()); } protected void initializeChrome() { mDoorHangerPopup = new DoorHangerPopup(this, null); @@ -1455,22 +1466,26 @@ abstract public class GeckoApp @Override public void prefValue(String pref, int value) { if (value == 1) mShouldReportGeoData = true; else mShouldReportGeoData = false; } }); - // End of the startup of our Java App + // Trigger the completion of the telemetry timer that wraps activity startup, + // then grab the duration to give to FHR. mJavaUiStartupTimer.stop(); + final long javaDuration = mJavaUiStartupTimer.getElapsed(); ThreadUtils.getBackgroundHandler().postDelayed(new Runnable() { @Override public void run() { + mHealthRecorder.recordJavaStartupTime(javaDuration); + // Sync settings need Gecko to be loaded, so // no hurry in starting this. checkMigrateSync(); // Record our launch time for the announcements service // to use in assessing inactivity. final Context context = GeckoApp.sAppContext; AnnouncementsBroadcastService.recordLastLaunch(context); @@ -1575,17 +1590,17 @@ abstract public class GeckoApp return mProfile; } protected int getSessionRestoreState(Bundle savedInstanceState) { if (savedInstanceState != null) { return RESTORE_OOM; } - final SharedPreferences prefs = GeckoApp.sAppContext.getSharedPreferences(PREFS_NAME, 0); + final SharedPreferences prefs = GeckoApp.getAppSharedPreferences(); // We record crashes in the crash reporter. If sessionstore.js // exists, but we didn't flag a crash in the crash reporter, we // were probably just force killed by the user, so we shouldn't do // a restore. if (prefs.getBoolean(PREFS_CRASHED, false)) { ThreadUtils.postToBackgroundThread(new Runnable() { @Override @@ -1773,24 +1788,39 @@ abstract public class GeckoApp refreshChrome(); } GeckoScreenOrientationListener.getInstance().start(); // User may have enabled/disabled accessibility. GeckoAccessibility.updateAccessibilitySettings(this); + // We use two times: a pseudo-unique wall-clock time to identify the + // current session across power cycles, and the elapsed realtime to + // track the duration of the session. + final long now = System.currentTimeMillis(); + final long realTime = android.os.SystemClock.elapsedRealtime(); + final BrowserHealthRecorder rec = mHealthRecorder; + ThreadUtils.postToBackgroundThread(new Runnable() { @Override public void run() { - SharedPreferences prefs = - GeckoApp.sAppContext.getSharedPreferences(GeckoApp.PREFS_NAME, 0); + // Now construct the new session on BrowserHealthRecorder's behalf. We do this here + // so it can benefit from a single near-startup prefs commit. + SessionInformation currentSession = new SessionInformation(now, realTime); + + SharedPreferences prefs = GeckoApp.getAppSharedPreferences(); SharedPreferences.Editor editor = prefs.edit(); editor.putBoolean(GeckoApp.PREFS_WAS_STOPPED, false); + currentSession.recordBegin(editor); editor.commit(); + + if (rec != null) { + rec.setCurrentSession(currentSession); + } } }); } @Override public void onWindowFocusChanged(boolean hasFocus) { super.onWindowFocusChanged(hasFocus); @@ -1798,43 +1828,46 @@ abstract public class GeckoApp initialize(); getWindow().setBackgroundDrawable(null); } } @Override public void onPause() { + final BrowserHealthRecorder rec = mHealthRecorder; + // In some way it's sad that Android will trigger StrictMode warnings // here as the whole point is to save to disk while the activity is not // interacting with the user. ThreadUtils.postToBackgroundThread(new Runnable() { @Override public void run() { - SharedPreferences prefs = - GeckoApp.sAppContext.getSharedPreferences(GeckoApp.PREFS_NAME, 0); + SharedPreferences prefs = GeckoApp.getAppSharedPreferences(); SharedPreferences.Editor editor = prefs.edit(); editor.putBoolean(GeckoApp.PREFS_WAS_STOPPED, true); + if (rec != null) { + rec.recordSessionEnd("P", editor); + } editor.commit(); } }); GeckoScreenOrientationListener.getInstance().stop(); super.onPause(); } @Override public void onRestart() { ThreadUtils.postToBackgroundThread(new Runnable() { @Override public void run() { - SharedPreferences prefs = - GeckoApp.sAppContext.getSharedPreferences(GeckoApp.PREFS_NAME, 0); + SharedPreferences prefs = GeckoApp.getAppSharedPreferences(); SharedPreferences.Editor editor = prefs.edit(); editor.putBoolean(GeckoApp.PREFS_WAS_STOPPED, false); editor.commit(); } }); super.onRestart(); } @@ -1897,19 +1930,26 @@ abstract public class GeckoApp Tabs.getInstance().detachFromActivity(this); if (SmsManager.getInstance() != null) { SmsManager.getInstance().stop(); if (isFinishing()) SmsManager.getInstance().shutdown(); } - if (mHealthRecorder != null) { - mHealthRecorder.close(); - mHealthRecorder = null; + final BrowserHealthRecorder rec = mHealthRecorder; + mHealthRecorder = null; + if (rec != null) { + // Closing a BrowserHealthRecorder could incur a write. + ThreadUtils.postToBackgroundThread(new Runnable() { + @Override + public void run() { + rec.close(); + } + }); } super.onDestroy(); Tabs.unregisterOnTabsChangedListener(this); } protected void registerEventListener(String event) {
--- a/mobile/android/base/Telemetry.java +++ b/mobile/android/base/Telemetry.java @@ -31,36 +31,42 @@ public class Telemetry { Log.e(LOGTAG, "JSON exception: ", e); } } public static class Timer { private long mStartTime; private String mName; private boolean mHasFinished; + private volatile long mElapsed = -1; public Timer(String name) { mName = name; mStartTime = SystemClock.uptimeMillis(); mHasFinished = false; } public void cancel() { mHasFinished = true; } + public long getElapsed() { + return mElapsed; + } + public void stop() { // Only the first stop counts. if (mHasFinished) { return; } else { mHasFinished = true; } - long elapsed = SystemClock.uptimeMillis() - mStartTime; + final long elapsed = SystemClock.uptimeMillis() - mStartTime; + mElapsed = elapsed; if (elapsed < Integer.MAX_VALUE) { HistogramAdd(mName, (int)(elapsed)); } else { - Log.e(LOGTAG, "Duration of " + elapsed + " ms is too long."); + Log.e(LOGTAG, "Duration of " + elapsed + " ms is too long to add to histogram."); } } } }
--- a/mobile/android/base/health/BrowserHealthRecorder.java +++ b/mobile/android/base/health/BrowserHealthRecorder.java @@ -4,35 +4,38 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ package org.mozilla.gecko.health; import java.util.ArrayList; import android.content.Context; import android.content.ContentProviderClient; +import android.content.SharedPreferences; import android.util.Log; import org.mozilla.gecko.AppConstants; +import org.mozilla.gecko.GeckoApp; import org.mozilla.gecko.GeckoAppShell; import org.mozilla.gecko.GeckoEvent; import org.mozilla.gecko.PrefsHelper; import org.mozilla.gecko.PrefsHelper.PrefHandler; import org.mozilla.gecko.background.healthreport.EnvironmentBuilder; import org.mozilla.gecko.background.healthreport.HealthReportDatabaseStorage; import org.mozilla.gecko.background.healthreport.HealthReportStorage.Field; import org.mozilla.gecko.background.healthreport.HealthReportStorage.MeasurementFields; import org.mozilla.gecko.background.healthreport.HealthReportStorage.MeasurementFields.FieldSpec; import org.mozilla.gecko.background.healthreport.ProfileInformationCache; import org.mozilla.gecko.util.EventDispatcher; import org.mozilla.gecko.util.GeckoEventListener; import org.mozilla.gecko.util.ThreadUtils; +import org.json.JSONException; import org.json.JSONObject; import java.io.File; import java.io.FileOutputStream; import java.io.OutputStreamWriter; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Scanner; @@ -47,17 +50,17 @@ import java.util.Scanner; * * Tell it when an environment attribute has changed: call {@link * #onBlocklistPrefChanged(boolean)} or {@link * #onTelemetryPrefChanged(boolean)}, followed by {@link * #onEnvironmentChanged()}. * * Use it to record events: {@link #recordSearch(String, String)}. * - * Shut it down when you're done being a browser: {@link #close(EventDispatcher)}. + * Shut it down when you're done being a browser: {@link #close()}. */ public class BrowserHealthRecorder implements GeckoEventListener { private static final String LOG_TAG = "GeckoHealthRec"; private static final String PREF_BLOCKLIST_ENABLED = "extensions.blocklist.enabled"; private static final String EVENT_ADDONS_ALL = "Addons:All"; private static final String EVENT_ADDONS_CHANGE = "Addons:Change"; private static final String EVENT_PREF_CHANGE = "Pref:Change"; @@ -73,54 +76,181 @@ public class BrowserHealthRecorder imple INITIALIZING, INITIALIZED, INITIALIZATION_FAILED, CLOSED } protected volatile State state = State.NOT_INITIALIZED; + private final AtomicBoolean orphanChecked = new AtomicBoolean(false); private volatile int env = -1; private volatile HealthReportDatabaseStorage storage; private final ProfileInformationCache profileCache; private ContentProviderClient client; private final EventDispatcher dispatcher; + public static class SessionInformation { + private static final String LOG_TAG = "GeckoSessInfo"; + + public static final String PREFS_SESSION_START = "sessionStart"; + + public final long wallStartTime; // System wall clock. + public final long realStartTime; // Realtime clock. + + private final boolean wasOOM; + private final boolean wasStopped; + + private volatile long timedGeckoStartup = -1; + private volatile long timedJavaStartup = -1; + + // Current sessions don't (right now) care about wasOOM/wasStopped. + // Eventually we might want to lift that logic out of GeckoApp. + public SessionInformation(long wallTime, long realTime) { + this(wallTime, realTime, false, false); + } + + // Previous sessions do... + public SessionInformation(long wallTime, long realTime, boolean wasOOM, boolean wasStopped) { + this.wallStartTime = wallTime; + this.realStartTime = realTime; + this.wasOOM = wasOOM; + this.wasStopped = wasStopped; + } + + /** + * Initialize a new SessionInformation instance from the supplied prefs object. + * + * This includes retrieving OOM/crash data, as well as timings. + * + * If no wallStartTime was found, that implies that the previous + * session was correctly recorded, and an object with a zero + * wallStartTime is returned. + */ + public static SessionInformation fromSharedPrefs(SharedPreferences prefs) { + boolean wasOOM = prefs.getBoolean(GeckoApp.PREFS_OOM_EXCEPTION, false); + boolean wasStopped = prefs.getBoolean(GeckoApp.PREFS_WAS_STOPPED, true); + long wallStartTime = prefs.getLong(PREFS_SESSION_START, 0L); + long realStartTime = 0L; + Log.d(LOG_TAG, "Building SessionInformation from prefs: " + + wallStartTime + ", " + realStartTime + ", " + + wasStopped + ", " + wasOOM); + return new SessionInformation(wallStartTime, realStartTime, wasOOM, wasStopped); + } + + public boolean wasKilled() { + return wasOOM || !wasStopped; + } + + /** + * Record the beginning of this session to SharedPreferences by + * recording our start time. If a session was already recorded, it is + * overwritten (there can only be one running session at a time). Does + * not commit the editor. + */ + public void recordBegin(SharedPreferences.Editor editor) { + Log.d(LOG_TAG, "Recording start of session: " + this.wallStartTime); + editor.putLong(PREFS_SESSION_START, this.wallStartTime); + } + + /** + * Record the completion of this session to SharedPreferences by + * deleting our start time. Does not commit the editor. + */ + public void recordCompletion(SharedPreferences.Editor editor) { + Log.d(LOG_TAG, "Recording session done: " + this.wallStartTime); + editor.remove(PREFS_SESSION_START); + } + + /** + * Return the JSON that we'll put in the DB for this session. + */ + public JSONObject getCompletionJSON(String reason, long realEndTime) throws JSONException { + long durationSecs = (realEndTime - this.realStartTime) / 1000; + JSONObject out = new JSONObject(); + out.put("r", reason); + out.put("d", durationSecs); + if (this.timedGeckoStartup > 0) { + out.put("sg", this.timedGeckoStartup); + } + if (this.timedJavaStartup > 0) { + out.put("sj", this.timedJavaStartup); + } + return out; + } + + public JSONObject getCrashedJSON() throws JSONException { + JSONObject out = new JSONObject(); + // We use ints here instead of booleans, because we're packing + // stuff into JSON, and saving bytes in the DB is a worthwhile + // goal. + out.put("oom", this.wasOOM ? 1 : 0); + out.put("stopped", this.wasStopped ? 1 : 0); + out.put("r", "A"); + return out; + } + } + + // We track previousSession to avoid order-of-initialization confusion. We + // accept it in the constructor, and process it after init. + private final SessionInformation previousSession; + private volatile SessionInformation session = null; + public SessionInformation getCurrentSession() { + return this.session; + } + + public void setCurrentSession(SessionInformation session) { + this.session = session; + } + + public void recordGeckoStartupTime(long duration) { + if (this.session == null) { + return; + } + this.session.timedGeckoStartup = duration; + } + public void recordJavaStartupTime(long duration) { + if (this.session == null) { + return; + } + this.session.timedJavaStartup = duration; + } + /** * Persist the opaque identifier for the current Firefox Health Report environment. * This changes in certain circumstances; be sure to use the current value when recording data. */ private void setHealthEnvironment(final int env) { this.env = env; } /** * This constructor does IO. Run it on a background thread. */ - public BrowserHealthRecorder(final Context context, final String profilePath, final EventDispatcher dispatcher) { + public BrowserHealthRecorder(final Context context, final String profilePath, final EventDispatcher dispatcher, SessionInformation previousSession) { Log.d(LOG_TAG, "Initializing. Dispatcher is " + dispatcher); this.dispatcher = dispatcher; + this.previousSession = previousSession; + this.client = EnvironmentBuilder.getContentProviderClient(context); if (this.client == null) { throw new IllegalStateException("Could not fetch Health Report content provider."); } this.storage = EnvironmentBuilder.getStorage(this.client, profilePath); if (this.storage == null) { throw new IllegalStateException("No storage in health recorder!"); } this.profileCache = new ProfileInformationCache(profilePath); try { this.initialize(context, profilePath); } catch (Exception e) { Log.e(LOG_TAG, "Exception initializing.", e); } - - // TODO: record session start and end? } /** * Shut down database connections, unregister event listeners, and perform * provider-specific uninitialization. */ public synchronized void close() { switch (this.state) { @@ -348,29 +478,34 @@ public class BrowserHealthRecorder imple } try { // Listen for add-ons and prefs changes. dispatcher.registerEventListener(EVENT_ADDONS_CHANGE, self); dispatcher.registerEventListener(EVENT_PREF_CHANGE, self); // Initialize each provider here. + initializeSessionsProvider(); initializeSearchProvider(); Log.d(LOG_TAG, "Ensuring environment."); ensureEnvironment(); Log.d(LOG_TAG, "Finishing init."); storage.finishInitialization(); state = State.INITIALIZED; } catch (Exception e) { state = State.INITIALIZATION_FAILED; storage.abortInitialization(); Log.e(LOG_TAG, "Initialization failed.", e); + return; } + + // Now do whatever we do after we start up. + checkForOrphanSessions(); } } }); } /** * Add provider-specific initialization in this method. */ @@ -378,17 +513,17 @@ public class BrowserHealthRecorder imple final String profilePath) throws java.io.IOException { Log.d(LOG_TAG, "Initializing profile cache."); this.state = State.INITIALIZING; // If we can restore state from last time, great. if (this.profileCache.restoreUnlessInitialized()) { - Log.i(LOG_TAG, "Successfully restored state. Initializing storage."); + Log.d(LOG_TAG, "Successfully restored state. Initializing storage."); initializeStorage(); return; } // Otherwise, let's initialize it from scratch. this.profileCache.beginInitialization(); this.profileCache.setProfileCreationTime(getAndPersistProfileInitTime(context, profilePath)); @@ -636,10 +771,160 @@ public class BrowserHealthRecorder imple final int searchField = storage.getField(MEASUREMENT_NAME_SEARCH_COUNTS, MEASUREMENT_VERSION_SEARCH_COUNTS, location) .getID(); storage.recordDailyDiscrete(env, day, searchField, key); } }); } + + /* + * Sessions. + * + * We record session beginnings in SharedPreferences, because it's cheaper + * to do that than to either write to then update the DB (which requires + * keeping a row identifier to update, as well as two writes) or to record + * two events (which doubles storage space and requires rollup logic). + * + * The pattern is: + * + * 1. On startup, determine whether an orphan session exists by looking for + * a saved timestamp in prefs. If it does, then record the orphan in FHR + * storage. + * 2. Record in prefs that a new session has begun. Track the timestamp (so + * we know to which day the session belongs). + * 3. As startup timings become available, accumulate them in memory. + * 4. On clean shutdown, read the values from here, write them to the DB, and + * delete the sentinel time from SharedPreferences. + * 5. On a dirty shutdown, the in-memory session will not be written to the + * DB, and the current session will be orphaned. + * + * Sessions are begun in onResume (and thus implicitly onStart) and ended + * in onPause. + * + * Session objects are stored as discrete JSON. + * + * "org.mozilla.appSessions": { + * _v: 4, + * "normal": [ + * {"r":"P", "d": 123}, + * ], + * "abnormal": [ + * {"r":"A", "oom": true, "stopped": false} + * ] + * } + * + * "r": reason. Values are "P" (activity paused), "A" (abnormal termination) + * "d": duration. Value in seconds. + * "sg": Gecko startup time. Present if this is a clean launch. + * "sj": Java startup time. Present if this is a clean launch. + * + * Abnormal terminations will be missing a duration and will feature these keys: + * + * "oom": was the session killed by an OOM exception? + * "stopped": was the session stopped gently? + */ + + public static final String MEASUREMENT_NAME_SESSIONS = "org.mozilla.appSessions"; + public static final int MEASUREMENT_VERSION_SESSIONS = 4; + + private void initializeSessionsProvider() { + this.storage.ensureMeasurementInitialized( + MEASUREMENT_NAME_SESSIONS, + MEASUREMENT_VERSION_SESSIONS, + new MeasurementFields() { + @Override + public Iterable<FieldSpec> getFields() { + ArrayList<FieldSpec> out = new ArrayList<FieldSpec>(2); + out.add(new FieldSpec("normal", Field.TYPE_JSON_DISCRETE)); + out.add(new FieldSpec("abnormal", Field.TYPE_JSON_DISCRETE)); + return out; + } + }); + } + + /** + * Logic shared between crashed and normal sessions. + */ + private void recordSessionEntry(String field, SessionInformation session, JSONObject value) { + try { + final int sessionField = storage.getField(MEASUREMENT_NAME_SESSIONS, + MEASUREMENT_VERSION_SESSIONS, + field) + .getID(); + final int day = storage.getDay(session.wallStartTime); + storage.recordDailyDiscrete(env, day, sessionField, value); + } catch (Exception e) { + Log.w(LOG_TAG, "Unable to record session completion.", e); + } + } + + public void checkForOrphanSessions() { + if (!this.orphanChecked.compareAndSet(false, true)) { + Log.w(LOG_TAG, "Attempting to check for orphan sessions more than once."); + return; + } + + Log.d(LOG_TAG, "Checking for orphan session."); + if (this.previousSession == null) { + return; + } + if (this.previousSession.wallStartTime == 0) { + return; + } + + if (state != State.INITIALIZED) { + // Something has gone awry. + Log.e(LOG_TAG, "Attempted to record bad session end without initialized recorder."); + return; + } + + try { + recordSessionEntry("abnormal", this.previousSession, this.previousSession.getCrashedJSON()); + } catch (Exception e) { + Log.w(LOG_TAG, "Unable to generate session JSON.", e); + + // Future: record this exception in FHR's own error submitter. + } + } + + /** + * Record that the current session ended. Does not commit the provided editor. + */ + public void recordSessionEnd(String reason, SharedPreferences.Editor editor) { + Log.d(LOG_TAG, "Recording session end: " + reason); + if (state != State.INITIALIZED) { + // Something has gone awry. + Log.e(LOG_TAG, "Attempted to record session end without initialized recorder."); + return; + } + + final SessionInformation session = this.session; + this.session = null; // So it can't be double-recorded. + + if (session == null) { + Log.w(LOG_TAG, "Unable to record session end: no session. Already ended?"); + return; + } + + if (session.wallStartTime <= 0) { + Log.e(LOG_TAG, "Session start " + session.wallStartTime + " isn't valid! Can't record end."); + return; + } + + long realEndTime = android.os.SystemClock.elapsedRealtime(); + try { + JSONObject json = session.getCompletionJSON(reason, realEndTime); + recordSessionEntry("normal", session, json); + } catch (JSONException e) { + Log.w(LOG_TAG, "Unable to generate session JSON.", e); + + // Continue so we don't hit it next time. + // Future: record this exception in FHR's own error submitter. + } + + // Track the end of this session in shared prefs, so it doesn't get + // double-counted on next run. + session.recordCompletion(editor); + } }