Wed, 31 Dec 2014 06:09:35 +0100
Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.
michael@0 | 1 | /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- |
michael@0 | 2 | * This Source Code Form is subject to the terms of the Mozilla Public |
michael@0 | 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this |
michael@0 | 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
michael@0 | 5 | |
michael@0 | 6 | package org.mozilla.gecko.health; |
michael@0 | 7 | |
michael@0 | 8 | import java.io.File; |
michael@0 | 9 | import java.io.FileOutputStream; |
michael@0 | 10 | import java.io.OutputStreamWriter; |
michael@0 | 11 | import java.nio.charset.Charset; |
michael@0 | 12 | import java.util.ArrayList; |
michael@0 | 13 | import java.util.Arrays; |
michael@0 | 14 | import java.util.Collections; |
michael@0 | 15 | import java.util.HashSet; |
michael@0 | 16 | import java.util.Iterator; |
michael@0 | 17 | import java.util.List; |
michael@0 | 18 | import java.util.Scanner; |
michael@0 | 19 | import java.util.Set; |
michael@0 | 20 | import java.util.concurrent.atomic.AtomicBoolean; |
michael@0 | 21 | |
michael@0 | 22 | import org.json.JSONException; |
michael@0 | 23 | import org.json.JSONObject; |
michael@0 | 24 | import org.mozilla.gecko.AppConstants; |
michael@0 | 25 | import org.mozilla.gecko.Distribution; |
michael@0 | 26 | import org.mozilla.gecko.Distribution.DistributionDescriptor; |
michael@0 | 27 | import org.mozilla.gecko.EventDispatcher; |
michael@0 | 28 | import org.mozilla.gecko.GeckoAppShell; |
michael@0 | 29 | import org.mozilla.gecko.GeckoEvent; |
michael@0 | 30 | import org.mozilla.gecko.background.healthreport.EnvironmentBuilder; |
michael@0 | 31 | import org.mozilla.gecko.background.healthreport.HealthReportDatabaseStorage; |
michael@0 | 32 | import org.mozilla.gecko.background.healthreport.HealthReportStorage.Field; |
michael@0 | 33 | import org.mozilla.gecko.background.healthreport.HealthReportStorage.MeasurementFields; |
michael@0 | 34 | import org.mozilla.gecko.background.healthreport.ProfileInformationCache; |
michael@0 | 35 | import org.mozilla.gecko.util.GeckoEventListener; |
michael@0 | 36 | import org.mozilla.gecko.util.ThreadUtils; |
michael@0 | 37 | |
michael@0 | 38 | import android.content.ContentProviderClient; |
michael@0 | 39 | import android.content.Context; |
michael@0 | 40 | import android.content.SharedPreferences; |
michael@0 | 41 | import android.util.Log; |
michael@0 | 42 | |
michael@0 | 43 | /** |
michael@0 | 44 | * BrowserHealthRecorder is the browser's interface to the Firefox Health |
michael@0 | 45 | * Report storage system. It manages environments (a collection of attributes |
michael@0 | 46 | * that are tracked longitudinally) on the browser's behalf, exposing a simpler |
michael@0 | 47 | * interface for recording changes. |
michael@0 | 48 | * |
michael@0 | 49 | * Keep an instance of this class around. |
michael@0 | 50 | * |
michael@0 | 51 | * Tell it when an environment attribute has changed: call {@link |
michael@0 | 52 | * #onAppLocaleChanged(String)} followed by {@link |
michael@0 | 53 | * #onEnvironmentChanged()}. |
michael@0 | 54 | * |
michael@0 | 55 | * Use it to record events: {@link #recordSearch(String, String)}. |
michael@0 | 56 | * |
michael@0 | 57 | * Shut it down when you're done being a browser: {@link #close()}. |
michael@0 | 58 | */ |
michael@0 | 59 | public class BrowserHealthRecorder implements HealthRecorder, GeckoEventListener { |
michael@0 | 60 | private static final String LOG_TAG = "GeckoHealthRec"; |
michael@0 | 61 | private static final String PREF_ACCEPT_LANG = "intl.accept_languages"; |
michael@0 | 62 | private static final String PREF_BLOCKLIST_ENABLED = "extensions.blocklist.enabled"; |
michael@0 | 63 | private static final String EVENT_SNAPSHOT = "HealthReport:Snapshot"; |
michael@0 | 64 | private static final String EVENT_ADDONS_CHANGE = "Addons:Change"; |
michael@0 | 65 | private static final String EVENT_ADDONS_UNINSTALLING = "Addons:Uninstalling"; |
michael@0 | 66 | private static final String EVENT_PREF_CHANGE = "Pref:Change"; |
michael@0 | 67 | |
michael@0 | 68 | // This is raised from Gecko and signifies a search via the URL bar (not a bookmarks keyword |
michael@0 | 69 | // search). Using this event (rather than passing the invocation location as an arg) avoids |
michael@0 | 70 | // browser.js having to know about the invocation location. |
michael@0 | 71 | public static final String EVENT_KEYWORD_SEARCH = "Search:Keyword"; |
michael@0 | 72 | |
michael@0 | 73 | // This is raised from Java. We include the location in the message. |
michael@0 | 74 | public static final String EVENT_SEARCH = "Search:Event"; |
michael@0 | 75 | |
michael@0 | 76 | public enum State { |
michael@0 | 77 | NOT_INITIALIZED, |
michael@0 | 78 | INITIALIZING, |
michael@0 | 79 | INITIALIZED, |
michael@0 | 80 | INITIALIZATION_FAILED, |
michael@0 | 81 | CLOSED |
michael@0 | 82 | } |
michael@0 | 83 | |
michael@0 | 84 | protected volatile State state = State.NOT_INITIALIZED; |
michael@0 | 85 | |
michael@0 | 86 | private final AtomicBoolean orphanChecked = new AtomicBoolean(false); |
michael@0 | 87 | private volatile int env = -1; |
michael@0 | 88 | |
michael@0 | 89 | private ContentProviderClient client; |
michael@0 | 90 | private volatile HealthReportDatabaseStorage storage; |
michael@0 | 91 | private final ProfileInformationCache profileCache; |
michael@0 | 92 | private final EventDispatcher dispatcher; |
michael@0 | 93 | private final SharedPreferences prefs; |
michael@0 | 94 | |
michael@0 | 95 | // We track previousSession to avoid order-of-initialization confusion. We |
michael@0 | 96 | // accept it in the constructor, and process it after init. |
michael@0 | 97 | private final SessionInformation previousSession; |
michael@0 | 98 | private volatile SessionInformation session = null; |
michael@0 | 99 | |
michael@0 | 100 | public void setCurrentSession(SessionInformation session) { |
michael@0 | 101 | this.session = session; |
michael@0 | 102 | } |
michael@0 | 103 | |
michael@0 | 104 | public void recordGeckoStartupTime(long duration) { |
michael@0 | 105 | if (this.session == null) { |
michael@0 | 106 | return; |
michael@0 | 107 | } |
michael@0 | 108 | this.session.setTimedGeckoStartup(duration); |
michael@0 | 109 | } |
michael@0 | 110 | public void recordJavaStartupTime(long duration) { |
michael@0 | 111 | if (this.session == null) { |
michael@0 | 112 | return; |
michael@0 | 113 | } |
michael@0 | 114 | this.session.setTimedJavaStartup(duration); |
michael@0 | 115 | } |
michael@0 | 116 | |
michael@0 | 117 | /** |
michael@0 | 118 | * This constructor does IO. Run it on a background thread. |
michael@0 | 119 | * |
michael@0 | 120 | * appLocale can be null, which indicates that it will be provided later. |
michael@0 | 121 | */ |
michael@0 | 122 | public BrowserHealthRecorder(final Context context, |
michael@0 | 123 | final SharedPreferences appPrefs, |
michael@0 | 124 | final String profilePath, |
michael@0 | 125 | final EventDispatcher dispatcher, |
michael@0 | 126 | final String osLocale, |
michael@0 | 127 | final String appLocale, |
michael@0 | 128 | SessionInformation previousSession) { |
michael@0 | 129 | Log.d(LOG_TAG, "Initializing. Dispatcher is " + dispatcher); |
michael@0 | 130 | this.dispatcher = dispatcher; |
michael@0 | 131 | this.previousSession = previousSession; |
michael@0 | 132 | |
michael@0 | 133 | this.client = EnvironmentBuilder.getContentProviderClient(context); |
michael@0 | 134 | if (this.client == null) { |
michael@0 | 135 | throw new IllegalStateException("Could not fetch Health Report content provider."); |
michael@0 | 136 | } |
michael@0 | 137 | |
michael@0 | 138 | this.storage = EnvironmentBuilder.getStorage(this.client, profilePath); |
michael@0 | 139 | if (this.storage == null) { |
michael@0 | 140 | // Stick around even if we don't have storage: eventually we'll |
michael@0 | 141 | // want to report total failures of FHR storage itself, and this |
michael@0 | 142 | // way callers don't need to worry about whether their health |
michael@0 | 143 | // recorder didn't initialize. |
michael@0 | 144 | this.client.release(); |
michael@0 | 145 | this.client = null; |
michael@0 | 146 | } |
michael@0 | 147 | |
michael@0 | 148 | // Note that the PIC is not necessarily fully initialized at this point: |
michael@0 | 149 | // we haven't set the app locale. This must be done before an environment |
michael@0 | 150 | // is recorded. |
michael@0 | 151 | this.profileCache = new ProfileInformationCache(profilePath); |
michael@0 | 152 | try { |
michael@0 | 153 | this.initialize(context, profilePath, osLocale, appLocale); |
michael@0 | 154 | } catch (Exception e) { |
michael@0 | 155 | Log.e(LOG_TAG, "Exception initializing.", e); |
michael@0 | 156 | } |
michael@0 | 157 | |
michael@0 | 158 | this.prefs = appPrefs; |
michael@0 | 159 | } |
michael@0 | 160 | |
michael@0 | 161 | public boolean isEnabled() { |
michael@0 | 162 | return true; |
michael@0 | 163 | } |
michael@0 | 164 | |
michael@0 | 165 | /** |
michael@0 | 166 | * Shut down database connections, unregister event listeners, and perform |
michael@0 | 167 | * provider-specific uninitialization. |
michael@0 | 168 | */ |
michael@0 | 169 | public synchronized void close() { |
michael@0 | 170 | switch (this.state) { |
michael@0 | 171 | case CLOSED: |
michael@0 | 172 | Log.w(LOG_TAG, "Ignoring attempt to double-close closed BrowserHealthRecorder."); |
michael@0 | 173 | return; |
michael@0 | 174 | case INITIALIZED: |
michael@0 | 175 | Log.i(LOG_TAG, "Closing Health Report client."); |
michael@0 | 176 | break; |
michael@0 | 177 | default: |
michael@0 | 178 | Log.i(LOG_TAG, "Closing incompletely initialized BrowserHealthRecorder."); |
michael@0 | 179 | } |
michael@0 | 180 | |
michael@0 | 181 | this.state = State.CLOSED; |
michael@0 | 182 | this.unregisterEventListeners(); |
michael@0 | 183 | |
michael@0 | 184 | // Add any necessary provider uninitialization here. |
michael@0 | 185 | this.storage = null; |
michael@0 | 186 | if (this.client != null) { |
michael@0 | 187 | this.client.release(); |
michael@0 | 188 | this.client = null; |
michael@0 | 189 | } |
michael@0 | 190 | } |
michael@0 | 191 | |
michael@0 | 192 | private void unregisterEventListeners() { |
michael@0 | 193 | if (state != State.INITIALIZED) { |
michael@0 | 194 | return; |
michael@0 | 195 | } |
michael@0 | 196 | this.dispatcher.unregisterEventListener(EVENT_SNAPSHOT, this); |
michael@0 | 197 | this.dispatcher.unregisterEventListener(EVENT_ADDONS_CHANGE, this); |
michael@0 | 198 | this.dispatcher.unregisterEventListener(EVENT_ADDONS_UNINSTALLING, this); |
michael@0 | 199 | this.dispatcher.unregisterEventListener(EVENT_PREF_CHANGE, this); |
michael@0 | 200 | this.dispatcher.unregisterEventListener(EVENT_KEYWORD_SEARCH, this); |
michael@0 | 201 | this.dispatcher.unregisterEventListener(EVENT_SEARCH, this); |
michael@0 | 202 | } |
michael@0 | 203 | |
michael@0 | 204 | public void onAppLocaleChanged(String to) { |
michael@0 | 205 | Log.d(LOG_TAG, "Setting health recorder app locale to " + to); |
michael@0 | 206 | this.profileCache.beginInitialization(); |
michael@0 | 207 | this.profileCache.setAppLocale(to); |
michael@0 | 208 | } |
michael@0 | 209 | |
michael@0 | 210 | public void onAddonChanged(String id, JSONObject json) { |
michael@0 | 211 | this.profileCache.beginInitialization(); |
michael@0 | 212 | try { |
michael@0 | 213 | this.profileCache.updateJSONForAddon(id, json); |
michael@0 | 214 | } catch (IllegalStateException e) { |
michael@0 | 215 | Log.w(LOG_TAG, "Attempted to update add-on cache prior to full init.", e); |
michael@0 | 216 | } |
michael@0 | 217 | } |
michael@0 | 218 | |
michael@0 | 219 | public void onAddonUninstalling(String id) { |
michael@0 | 220 | this.profileCache.beginInitialization(); |
michael@0 | 221 | try { |
michael@0 | 222 | this.profileCache.removeAddon(id); |
michael@0 | 223 | } catch (IllegalStateException e) { |
michael@0 | 224 | Log.w(LOG_TAG, "Attempted to update add-on cache prior to full init.", e); |
michael@0 | 225 | } |
michael@0 | 226 | } |
michael@0 | 227 | |
michael@0 | 228 | /** |
michael@0 | 229 | * Call this when a material change might have occurred in the running |
michael@0 | 230 | * environment, such that a new environment should be computed and prepared |
michael@0 | 231 | * for use in future events. |
michael@0 | 232 | * |
michael@0 | 233 | * Invoke this method after calls that mutate the environment. |
michael@0 | 234 | * |
michael@0 | 235 | * If this change resulted in a transition between two environments, {@link |
michael@0 | 236 | * #onEnvironmentTransition(int, int, boolean, String)} will be invoked on the background |
michael@0 | 237 | * thread. |
michael@0 | 238 | */ |
michael@0 | 239 | public synchronized void onEnvironmentChanged() { |
michael@0 | 240 | onEnvironmentChanged(true, "E"); |
michael@0 | 241 | } |
michael@0 | 242 | |
michael@0 | 243 | /** |
michael@0 | 244 | * If `startNewSession` is false, it means no new session should begin |
michael@0 | 245 | * (e.g., because we're about to restart, and we don't want to create |
michael@0 | 246 | * an orphan). |
michael@0 | 247 | */ |
michael@0 | 248 | public synchronized void onEnvironmentChanged(final boolean startNewSession, final String sessionEndReason) { |
michael@0 | 249 | final int previousEnv = this.env; |
michael@0 | 250 | this.env = -1; |
michael@0 | 251 | try { |
michael@0 | 252 | profileCache.completeInitialization(); |
michael@0 | 253 | } catch (java.io.IOException e) { |
michael@0 | 254 | Log.e(LOG_TAG, "Error completing profile cache initialization.", e); |
michael@0 | 255 | this.state = State.INITIALIZATION_FAILED; |
michael@0 | 256 | return; |
michael@0 | 257 | } |
michael@0 | 258 | |
michael@0 | 259 | final int updatedEnv = ensureEnvironment(); |
michael@0 | 260 | |
michael@0 | 261 | if (updatedEnv == -1 || |
michael@0 | 262 | updatedEnv == previousEnv) { |
michael@0 | 263 | Log.v(LOG_TAG, "Environment didn't change."); |
michael@0 | 264 | return; |
michael@0 | 265 | } |
michael@0 | 266 | ThreadUtils.postToBackgroundThread(new Runnable() { |
michael@0 | 267 | @Override |
michael@0 | 268 | public void run() { |
michael@0 | 269 | try { |
michael@0 | 270 | onEnvironmentTransition(previousEnv, updatedEnv, startNewSession, sessionEndReason); |
michael@0 | 271 | } catch (Exception e) { |
michael@0 | 272 | Log.w(LOG_TAG, "Could not record environment transition.", e); |
michael@0 | 273 | } |
michael@0 | 274 | } |
michael@0 | 275 | }); |
michael@0 | 276 | } |
michael@0 | 277 | |
michael@0 | 278 | protected synchronized int ensureEnvironment() { |
michael@0 | 279 | if (!(state == State.INITIALIZING || |
michael@0 | 280 | state == State.INITIALIZED)) { |
michael@0 | 281 | throw new IllegalStateException("Not initialized."); |
michael@0 | 282 | } |
michael@0 | 283 | |
michael@0 | 284 | if (this.env != -1) { |
michael@0 | 285 | return this.env; |
michael@0 | 286 | } |
michael@0 | 287 | if (this.storage == null) { |
michael@0 | 288 | // Oh well. |
michael@0 | 289 | return -1; |
michael@0 | 290 | } |
michael@0 | 291 | return this.env = EnvironmentBuilder.registerCurrentEnvironment(this.storage, |
michael@0 | 292 | this.profileCache); |
michael@0 | 293 | } |
michael@0 | 294 | |
michael@0 | 295 | private static final String getTimesPath(final String profilePath) { |
michael@0 | 296 | return profilePath + File.separator + "times.json"; |
michael@0 | 297 | } |
michael@0 | 298 | |
michael@0 | 299 | /** |
michael@0 | 300 | * Retrieve the stored profile creation time from the profile directory. |
michael@0 | 301 | * |
michael@0 | 302 | * @return the <code>created</code> value from the times.json file, or -1 on failure. |
michael@0 | 303 | */ |
michael@0 | 304 | protected static long getProfileInitTimeFromFile(final String profilePath) { |
michael@0 | 305 | final File times = new File(getTimesPath(profilePath)); |
michael@0 | 306 | Log.d(LOG_TAG, "Looking for " + times.getAbsolutePath()); |
michael@0 | 307 | if (!times.exists()) { |
michael@0 | 308 | return -1; |
michael@0 | 309 | } |
michael@0 | 310 | |
michael@0 | 311 | Log.d(LOG_TAG, "Using times.json for profile creation time."); |
michael@0 | 312 | Scanner scanner = null; |
michael@0 | 313 | try { |
michael@0 | 314 | scanner = new Scanner(times, "UTF-8"); |
michael@0 | 315 | final String contents = scanner.useDelimiter("\\A").next(); |
michael@0 | 316 | return new JSONObject(contents).getLong("created"); |
michael@0 | 317 | } catch (Exception e) { |
michael@0 | 318 | // There are assorted reasons why this might occur, but we |
michael@0 | 319 | // don't care. Move on. |
michael@0 | 320 | Log.w(LOG_TAG, "Failed to read times.json.", e); |
michael@0 | 321 | } finally { |
michael@0 | 322 | if (scanner != null) { |
michael@0 | 323 | scanner.close(); |
michael@0 | 324 | } |
michael@0 | 325 | } |
michael@0 | 326 | return -1; |
michael@0 | 327 | } |
michael@0 | 328 | |
michael@0 | 329 | /** |
michael@0 | 330 | * Only works on API 9 and up. |
michael@0 | 331 | * |
michael@0 | 332 | * @return the package install time, or -1 if an error occurred. |
michael@0 | 333 | */ |
michael@0 | 334 | protected static long getPackageInstallTime(final Context context) { |
michael@0 | 335 | if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.GINGERBREAD) { |
michael@0 | 336 | return -1; |
michael@0 | 337 | } |
michael@0 | 338 | |
michael@0 | 339 | try { |
michael@0 | 340 | return context.getPackageManager().getPackageInfo(AppConstants.ANDROID_PACKAGE_NAME, 0).firstInstallTime; |
michael@0 | 341 | } catch (android.content.pm.PackageManager.NameNotFoundException e) { |
michael@0 | 342 | Log.e(LOG_TAG, "Unable to fetch our own package info. This should never occur.", e); |
michael@0 | 343 | } |
michael@0 | 344 | return -1; |
michael@0 | 345 | } |
michael@0 | 346 | |
michael@0 | 347 | private static long getProfileInitTimeHeuristic(final Context context, final String profilePath) { |
michael@0 | 348 | // As a pretty good shortcut, settle for installation time. |
michael@0 | 349 | // In all recent Firefox profiles, times.json should exist. |
michael@0 | 350 | final long time = getPackageInstallTime(context); |
michael@0 | 351 | if (time != -1) { |
michael@0 | 352 | return time; |
michael@0 | 353 | } |
michael@0 | 354 | |
michael@0 | 355 | // Otherwise, fall back to the filesystem. |
michael@0 | 356 | // We'll settle for the modification time of the profile directory. |
michael@0 | 357 | Log.d(LOG_TAG, "Using profile directory modified time as proxy for profile creation time."); |
michael@0 | 358 | return new File(profilePath).lastModified(); |
michael@0 | 359 | } |
michael@0 | 360 | |
michael@0 | 361 | private static long getAndPersistProfileInitTime(final Context context, final String profilePath) { |
michael@0 | 362 | // Let's look in the profile. |
michael@0 | 363 | long time = getProfileInitTimeFromFile(profilePath); |
michael@0 | 364 | if (time > 0) { |
michael@0 | 365 | Log.d(LOG_TAG, "Incorporating environment: times.json profile creation = " + time); |
michael@0 | 366 | return time; |
michael@0 | 367 | } |
michael@0 | 368 | |
michael@0 | 369 | // Otherwise, we need to compute a valid creation time and write it out. |
michael@0 | 370 | time = getProfileInitTimeHeuristic(context, profilePath); |
michael@0 | 371 | |
michael@0 | 372 | if (time > 0) { |
michael@0 | 373 | // Write out a stub times.json. |
michael@0 | 374 | try { |
michael@0 | 375 | FileOutputStream stream = new FileOutputStream(getTimesPath(profilePath)); |
michael@0 | 376 | OutputStreamWriter writer = new OutputStreamWriter(stream, Charset.forName("UTF-8")); |
michael@0 | 377 | try { |
michael@0 | 378 | writer.append("{\"created\": " + time + "}\n"); |
michael@0 | 379 | } finally { |
michael@0 | 380 | writer.close(); |
michael@0 | 381 | } |
michael@0 | 382 | } catch (Exception e) { |
michael@0 | 383 | // Best-effort. |
michael@0 | 384 | Log.w(LOG_TAG, "Couldn't write times.json.", e); |
michael@0 | 385 | } |
michael@0 | 386 | } |
michael@0 | 387 | |
michael@0 | 388 | Log.d(LOG_TAG, "Incorporating environment: profile creation = " + time); |
michael@0 | 389 | return time; |
michael@0 | 390 | } |
michael@0 | 391 | |
michael@0 | 392 | private void onPrefMessage(final String pref, final JSONObject message) { |
michael@0 | 393 | Log.d(LOG_TAG, "Incorporating environment: " + pref); |
michael@0 | 394 | if (PREF_ACCEPT_LANG.equals(pref)) { |
michael@0 | 395 | // We only record whether this is user-set. |
michael@0 | 396 | try { |
michael@0 | 397 | this.profileCache.beginInitialization(); |
michael@0 | 398 | this.profileCache.setAcceptLangUserSet(message.getBoolean("isUserSet")); |
michael@0 | 399 | } catch (JSONException ex) { |
michael@0 | 400 | Log.w(LOG_TAG, "Unexpected JSONException fetching isUserSet for " + pref); |
michael@0 | 401 | } |
michael@0 | 402 | return; |
michael@0 | 403 | } |
michael@0 | 404 | |
michael@0 | 405 | // (We only handle boolean prefs right now.) |
michael@0 | 406 | try { |
michael@0 | 407 | boolean value = message.getBoolean("value"); |
michael@0 | 408 | |
michael@0 | 409 | if (AppConstants.TELEMETRY_PREF_NAME.equals(pref)) { |
michael@0 | 410 | this.profileCache.beginInitialization(); |
michael@0 | 411 | this.profileCache.setTelemetryEnabled(value); |
michael@0 | 412 | return; |
michael@0 | 413 | } |
michael@0 | 414 | |
michael@0 | 415 | if (PREF_BLOCKLIST_ENABLED.equals(pref)) { |
michael@0 | 416 | this.profileCache.beginInitialization(); |
michael@0 | 417 | this.profileCache.setBlocklistEnabled(value); |
michael@0 | 418 | return; |
michael@0 | 419 | } |
michael@0 | 420 | } catch (JSONException ex) { |
michael@0 | 421 | Log.w(LOG_TAG, "Unexpected JSONException fetching boolean value for " + pref); |
michael@0 | 422 | return; |
michael@0 | 423 | } |
michael@0 | 424 | Log.w(LOG_TAG, "Unexpected pref: " + pref); |
michael@0 | 425 | } |
michael@0 | 426 | |
michael@0 | 427 | /** |
michael@0 | 428 | * Background init helper. |
michael@0 | 429 | */ |
michael@0 | 430 | private void initializeStorage() { |
michael@0 | 431 | Log.d(LOG_TAG, "Done initializing profile cache. Beginning storage init."); |
michael@0 | 432 | |
michael@0 | 433 | final BrowserHealthRecorder self = this; |
michael@0 | 434 | ThreadUtils.postToBackgroundThread(new Runnable() { |
michael@0 | 435 | @Override |
michael@0 | 436 | public void run() { |
michael@0 | 437 | synchronized (self) { |
michael@0 | 438 | if (state != State.INITIALIZING) { |
michael@0 | 439 | Log.w(LOG_TAG, "Unexpected state during init: " + state); |
michael@0 | 440 | return; |
michael@0 | 441 | } |
michael@0 | 442 | |
michael@0 | 443 | // Belt and braces. |
michael@0 | 444 | if (storage == null) { |
michael@0 | 445 | Log.w(LOG_TAG, "Storage is null during init; shutting down?"); |
michael@0 | 446 | if (state == State.INITIALIZING) { |
michael@0 | 447 | state = State.INITIALIZATION_FAILED; |
michael@0 | 448 | } |
michael@0 | 449 | return; |
michael@0 | 450 | } |
michael@0 | 451 | |
michael@0 | 452 | try { |
michael@0 | 453 | storage.beginInitialization(); |
michael@0 | 454 | } catch (Exception e) { |
michael@0 | 455 | Log.e(LOG_TAG, "Failed to init storage.", e); |
michael@0 | 456 | state = State.INITIALIZATION_FAILED; |
michael@0 | 457 | return; |
michael@0 | 458 | } |
michael@0 | 459 | |
michael@0 | 460 | try { |
michael@0 | 461 | // Listen for add-ons and prefs changes. |
michael@0 | 462 | dispatcher.registerEventListener(EVENT_ADDONS_UNINSTALLING, self); |
michael@0 | 463 | dispatcher.registerEventListener(EVENT_ADDONS_CHANGE, self); |
michael@0 | 464 | dispatcher.registerEventListener(EVENT_PREF_CHANGE, self); |
michael@0 | 465 | |
michael@0 | 466 | // Initialize each provider here. |
michael@0 | 467 | initializeSessionsProvider(); |
michael@0 | 468 | initializeSearchProvider(); |
michael@0 | 469 | |
michael@0 | 470 | Log.d(LOG_TAG, "Ensuring environment."); |
michael@0 | 471 | ensureEnvironment(); |
michael@0 | 472 | |
michael@0 | 473 | Log.d(LOG_TAG, "Finishing init."); |
michael@0 | 474 | storage.finishInitialization(); |
michael@0 | 475 | state = State.INITIALIZED; |
michael@0 | 476 | } catch (Exception e) { |
michael@0 | 477 | state = State.INITIALIZATION_FAILED; |
michael@0 | 478 | storage.abortInitialization(); |
michael@0 | 479 | Log.e(LOG_TAG, "Initialization failed.", e); |
michael@0 | 480 | return; |
michael@0 | 481 | } |
michael@0 | 482 | |
michael@0 | 483 | // Now do whatever we do after we start up. |
michael@0 | 484 | checkForOrphanSessions(); |
michael@0 | 485 | } |
michael@0 | 486 | } |
michael@0 | 487 | }); |
michael@0 | 488 | } |
michael@0 | 489 | |
michael@0 | 490 | /** |
michael@0 | 491 | * Add provider-specific initialization in this method. |
michael@0 | 492 | */ |
michael@0 | 493 | private synchronized void initialize(final Context context, |
michael@0 | 494 | final String profilePath, |
michael@0 | 495 | final String osLocale, |
michael@0 | 496 | final String appLocale) |
michael@0 | 497 | throws java.io.IOException { |
michael@0 | 498 | |
michael@0 | 499 | Log.d(LOG_TAG, "Initializing profile cache."); |
michael@0 | 500 | this.state = State.INITIALIZING; |
michael@0 | 501 | |
michael@0 | 502 | // If we can restore state from last time, great. |
michael@0 | 503 | if (this.profileCache.restoreUnlessInitialized()) { |
michael@0 | 504 | this.profileCache.updateLocales(osLocale, appLocale); |
michael@0 | 505 | this.profileCache.completeInitialization(); |
michael@0 | 506 | |
michael@0 | 507 | Log.d(LOG_TAG, "Successfully restored state. Initializing storage."); |
michael@0 | 508 | initializeStorage(); |
michael@0 | 509 | return; |
michael@0 | 510 | } |
michael@0 | 511 | |
michael@0 | 512 | // Otherwise, let's initialize it from scratch. |
michael@0 | 513 | this.profileCache.beginInitialization(); |
michael@0 | 514 | this.profileCache.setProfileCreationTime(getAndPersistProfileInitTime(context, profilePath)); |
michael@0 | 515 | this.profileCache.setOSLocale(osLocale); |
michael@0 | 516 | this.profileCache.setAppLocale(appLocale); |
michael@0 | 517 | |
michael@0 | 518 | // Because the distribution lookup can take some time, do it at the end of |
michael@0 | 519 | // our background startup work, along with the Gecko snapshot fetch. |
michael@0 | 520 | final GeckoEventListener self = this; |
michael@0 | 521 | ThreadUtils.postToBackgroundThread(new Runnable() { |
michael@0 | 522 | @Override |
michael@0 | 523 | public void run() { |
michael@0 | 524 | final DistributionDescriptor desc = new Distribution(context).getDescriptor(); |
michael@0 | 525 | if (desc != null && desc.valid) { |
michael@0 | 526 | profileCache.setDistributionString(desc.id, desc.version); |
michael@0 | 527 | } |
michael@0 | 528 | Log.d(LOG_TAG, "Requesting all add-ons and FHR prefs from Gecko."); |
michael@0 | 529 | dispatcher.registerEventListener(EVENT_SNAPSHOT, self); |
michael@0 | 530 | GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("HealthReport:RequestSnapshot", null)); |
michael@0 | 531 | } |
michael@0 | 532 | }); |
michael@0 | 533 | } |
michael@0 | 534 | |
michael@0 | 535 | /** |
michael@0 | 536 | * Invoked in the background whenever the environment transitions between |
michael@0 | 537 | * two valid values. |
michael@0 | 538 | */ |
michael@0 | 539 | protected void onEnvironmentTransition(int prev, int env, boolean startNewSession, String sessionEndReason) { |
michael@0 | 540 | if (this.state != State.INITIALIZED) { |
michael@0 | 541 | Log.d(LOG_TAG, "Not initialized: not recording env transition (" + prev + " => " + env + ")."); |
michael@0 | 542 | return; |
michael@0 | 543 | } |
michael@0 | 544 | |
michael@0 | 545 | final SharedPreferences.Editor editor = this.prefs.edit(); |
michael@0 | 546 | |
michael@0 | 547 | recordSessionEnd(sessionEndReason, editor, prev); |
michael@0 | 548 | |
michael@0 | 549 | if (!startNewSession) { |
michael@0 | 550 | editor.commit(); |
michael@0 | 551 | return; |
michael@0 | 552 | } |
michael@0 | 553 | |
michael@0 | 554 | final SessionInformation newSession = SessionInformation.forRuntimeTransition(); |
michael@0 | 555 | setCurrentSession(newSession); |
michael@0 | 556 | newSession.recordBegin(editor); |
michael@0 | 557 | editor.commit(); |
michael@0 | 558 | } |
michael@0 | 559 | |
michael@0 | 560 | @Override |
michael@0 | 561 | public void handleMessage(String event, JSONObject message) { |
michael@0 | 562 | try { |
michael@0 | 563 | if (EVENT_SNAPSHOT.equals(event)) { |
michael@0 | 564 | Log.d(LOG_TAG, "Got all add-ons and prefs."); |
michael@0 | 565 | try { |
michael@0 | 566 | JSONObject json = message.getJSONObject("json"); |
michael@0 | 567 | JSONObject addons = json.getJSONObject("addons"); |
michael@0 | 568 | Log.i(LOG_TAG, "Persisting " + addons.length() + " add-ons."); |
michael@0 | 569 | profileCache.setJSONForAddons(addons); |
michael@0 | 570 | |
michael@0 | 571 | JSONObject prefs = json.getJSONObject("prefs"); |
michael@0 | 572 | Log.i(LOG_TAG, "Persisting prefs."); |
michael@0 | 573 | Iterator<?> keys = prefs.keys(); |
michael@0 | 574 | while (keys.hasNext()) { |
michael@0 | 575 | String pref = (String) keys.next(); |
michael@0 | 576 | this.onPrefMessage(pref, prefs.getJSONObject(pref)); |
michael@0 | 577 | } |
michael@0 | 578 | |
michael@0 | 579 | profileCache.completeInitialization(); |
michael@0 | 580 | } catch (java.io.IOException e) { |
michael@0 | 581 | Log.e(LOG_TAG, "Error completing profile cache initialization.", e); |
michael@0 | 582 | state = State.INITIALIZATION_FAILED; |
michael@0 | 583 | return; |
michael@0 | 584 | } |
michael@0 | 585 | |
michael@0 | 586 | if (state == State.INITIALIZING) { |
michael@0 | 587 | initializeStorage(); |
michael@0 | 588 | } else { |
michael@0 | 589 | this.onEnvironmentChanged(); |
michael@0 | 590 | } |
michael@0 | 591 | |
michael@0 | 592 | return; |
michael@0 | 593 | } |
michael@0 | 594 | |
michael@0 | 595 | if (EVENT_ADDONS_UNINSTALLING.equals(event)) { |
michael@0 | 596 | this.onAddonUninstalling(message.getString("id")); |
michael@0 | 597 | this.onEnvironmentChanged(); |
michael@0 | 598 | return; |
michael@0 | 599 | } |
michael@0 | 600 | |
michael@0 | 601 | if (EVENT_ADDONS_CHANGE.equals(event)) { |
michael@0 | 602 | this.onAddonChanged(message.getString("id"), message.getJSONObject("json")); |
michael@0 | 603 | this.onEnvironmentChanged(); |
michael@0 | 604 | return; |
michael@0 | 605 | } |
michael@0 | 606 | |
michael@0 | 607 | if (EVENT_PREF_CHANGE.equals(event)) { |
michael@0 | 608 | final String pref = message.getString("pref"); |
michael@0 | 609 | Log.d(LOG_TAG, "Pref changed: " + pref); |
michael@0 | 610 | this.onPrefMessage(pref, message); |
michael@0 | 611 | this.onEnvironmentChanged(); |
michael@0 | 612 | return; |
michael@0 | 613 | } |
michael@0 | 614 | |
michael@0 | 615 | // Searches. |
michael@0 | 616 | if (EVENT_KEYWORD_SEARCH.equals(event)) { |
michael@0 | 617 | // A search via the URL bar. Since we eliminate all other search possibilities |
michael@0 | 618 | // (e.g. bookmarks keyword, search suggestion) when we initially process the |
michael@0 | 619 | // search URL, this is considered a default search. |
michael@0 | 620 | recordSearch(message.getString("identifier"), "bartext"); |
michael@0 | 621 | return; |
michael@0 | 622 | } |
michael@0 | 623 | if (EVENT_SEARCH.equals(event)) { |
michael@0 | 624 | if (!message.has("location")) { |
michael@0 | 625 | Log.d(LOG_TAG, "Ignoring search without location."); |
michael@0 | 626 | return; |
michael@0 | 627 | } |
michael@0 | 628 | recordSearch(message.optString("identifier", null), message.getString("location")); |
michael@0 | 629 | return; |
michael@0 | 630 | } |
michael@0 | 631 | } catch (Exception e) { |
michael@0 | 632 | Log.e(LOG_TAG, "Exception handling message \"" + event + "\":", e); |
michael@0 | 633 | } |
michael@0 | 634 | } |
michael@0 | 635 | |
michael@0 | 636 | /* |
michael@0 | 637 | * Searches. |
michael@0 | 638 | */ |
michael@0 | 639 | |
michael@0 | 640 | public static final String MEASUREMENT_NAME_SEARCH_COUNTS = "org.mozilla.searches.counts"; |
michael@0 | 641 | public static final int MEASUREMENT_VERSION_SEARCH_COUNTS = 5; |
michael@0 | 642 | |
michael@0 | 643 | public static final Set<String> SEARCH_LOCATIONS = Collections.unmodifiableSet(new HashSet<String>(Arrays.asList(new String[] { |
michael@0 | 644 | "barkeyword", |
michael@0 | 645 | "barsuggest", |
michael@0 | 646 | "bartext", |
michael@0 | 647 | }))); |
michael@0 | 648 | |
michael@0 | 649 | private void initializeSearchProvider() { |
michael@0 | 650 | this.storage.ensureMeasurementInitialized( |
michael@0 | 651 | MEASUREMENT_NAME_SEARCH_COUNTS, |
michael@0 | 652 | MEASUREMENT_VERSION_SEARCH_COUNTS, |
michael@0 | 653 | new MeasurementFields() { |
michael@0 | 654 | @Override |
michael@0 | 655 | public Iterable<FieldSpec> getFields() { |
michael@0 | 656 | ArrayList<FieldSpec> out = new ArrayList<FieldSpec>(SEARCH_LOCATIONS.size()); |
michael@0 | 657 | for (String location : SEARCH_LOCATIONS) { |
michael@0 | 658 | // We're not using a counter, because the set of engine |
michael@0 | 659 | // identifiers is potentially unbounded, and thus our |
michael@0 | 660 | // measurement version would have to keep growing as |
michael@0 | 661 | // fields changed. Instead we store discrete values, and |
michael@0 | 662 | // accumulate them into a counting map during processing. |
michael@0 | 663 | out.add(new FieldSpec(location, Field.TYPE_COUNTED_STRING_DISCRETE)); |
michael@0 | 664 | } |
michael@0 | 665 | return out; |
michael@0 | 666 | } |
michael@0 | 667 | }); |
michael@0 | 668 | |
michael@0 | 669 | // Do this here, rather than in a centralized registration spot, in |
michael@0 | 670 | // case the above throws and we wind up handling events that we can't |
michael@0 | 671 | // store. |
michael@0 | 672 | this.dispatcher.registerEventListener(EVENT_KEYWORD_SEARCH, this); |
michael@0 | 673 | this.dispatcher.registerEventListener(EVENT_SEARCH, this); |
michael@0 | 674 | } |
michael@0 | 675 | |
michael@0 | 676 | /** |
michael@0 | 677 | * Record a search. |
michael@0 | 678 | * |
michael@0 | 679 | * @param engineID the string identifier for the engine. Can be <code>null</code>. |
michael@0 | 680 | * @param location one of a fixed set of locations: see {@link #SEARCH_LOCATIONS}. |
michael@0 | 681 | */ |
michael@0 | 682 | public void recordSearch(final String engineID, final String location) { |
michael@0 | 683 | if (this.state != State.INITIALIZED) { |
michael@0 | 684 | Log.d(LOG_TAG, "Not initialized: not recording search. (" + this.state + ")"); |
michael@0 | 685 | return; |
michael@0 | 686 | } |
michael@0 | 687 | |
michael@0 | 688 | final int env = this.env; |
michael@0 | 689 | |
michael@0 | 690 | if (env == -1) { |
michael@0 | 691 | Log.d(LOG_TAG, "No environment: not recording search."); |
michael@0 | 692 | return; |
michael@0 | 693 | } |
michael@0 | 694 | |
michael@0 | 695 | if (location == null) { |
michael@0 | 696 | throw new IllegalArgumentException("location must be provided for search."); |
michael@0 | 697 | } |
michael@0 | 698 | |
michael@0 | 699 | // Ensure that we don't throw when trying to look up the field for an |
michael@0 | 700 | // unknown location. If you add a search location, you must extend the |
michael@0 | 701 | // list of search locations *and update the measurement version*. |
michael@0 | 702 | if (!SEARCH_LOCATIONS.contains(location)) { |
michael@0 | 703 | throw new IllegalArgumentException("Unexpected location: " + location); |
michael@0 | 704 | } |
michael@0 | 705 | |
michael@0 | 706 | final int day = storage.getDay(); |
michael@0 | 707 | final String key = (engineID == null) ? "other" : engineID; |
michael@0 | 708 | |
michael@0 | 709 | ThreadUtils.postToBackgroundThread(new Runnable() { |
michael@0 | 710 | @Override |
michael@0 | 711 | public void run() { |
michael@0 | 712 | final HealthReportDatabaseStorage storage = BrowserHealthRecorder.this.storage; |
michael@0 | 713 | if (storage == null) { |
michael@0 | 714 | Log.d(LOG_TAG, "No storage: not recording search. Shutting down?"); |
michael@0 | 715 | return; |
michael@0 | 716 | } |
michael@0 | 717 | |
michael@0 | 718 | Log.d(LOG_TAG, "Recording search: " + key + ", " + location + |
michael@0 | 719 | " (" + day + ", " + env + ")."); |
michael@0 | 720 | final int searchField = storage.getField(MEASUREMENT_NAME_SEARCH_COUNTS, |
michael@0 | 721 | MEASUREMENT_VERSION_SEARCH_COUNTS, |
michael@0 | 722 | location) |
michael@0 | 723 | .getID(); |
michael@0 | 724 | storage.recordDailyDiscrete(env, day, searchField, key); |
michael@0 | 725 | } |
michael@0 | 726 | }); |
michael@0 | 727 | } |
michael@0 | 728 | |
michael@0 | 729 | /* |
michael@0 | 730 | * Sessions. |
michael@0 | 731 | * |
michael@0 | 732 | * We record session beginnings in SharedPreferences, because it's cheaper |
michael@0 | 733 | * to do that than to either write to then update the DB (which requires |
michael@0 | 734 | * keeping a row identifier to update, as well as two writes) or to record |
michael@0 | 735 | * two events (which doubles storage space and requires rollup logic). |
michael@0 | 736 | * |
michael@0 | 737 | * The pattern is: |
michael@0 | 738 | * |
michael@0 | 739 | * 1. On startup, determine whether an orphan session exists by looking for |
michael@0 | 740 | * a saved timestamp in prefs. If it does, then record the orphan in FHR |
michael@0 | 741 | * storage. |
michael@0 | 742 | * 2. Record in prefs that a new session has begun. Track the timestamp (so |
michael@0 | 743 | * we know to which day the session belongs). |
michael@0 | 744 | * 3. As startup timings become available, accumulate them in memory. |
michael@0 | 745 | * 4. On clean shutdown, read the values from here, write them to the DB, and |
michael@0 | 746 | * delete the sentinel time from SharedPreferences. |
michael@0 | 747 | * 5. On a dirty shutdown, the in-memory session will not be written to the |
michael@0 | 748 | * DB, and the current session will be orphaned. |
michael@0 | 749 | * |
michael@0 | 750 | * Sessions are begun in onResume (and thus implicitly onStart) and ended |
michael@0 | 751 | * in onPause. |
michael@0 | 752 | * |
michael@0 | 753 | * Session objects are stored as discrete JSON. |
michael@0 | 754 | * |
michael@0 | 755 | * "org.mozilla.appSessions": { |
michael@0 | 756 | * _v: 4, |
michael@0 | 757 | * "normal": [ |
michael@0 | 758 | * {"r":"P", "d": 123}, |
michael@0 | 759 | * ], |
michael@0 | 760 | * "abnormal": [ |
michael@0 | 761 | * {"r":"A", "oom": true, "stopped": false} |
michael@0 | 762 | * ] |
michael@0 | 763 | * } |
michael@0 | 764 | * |
michael@0 | 765 | * "r": reason. Values are "P" (activity paused), "A" (abnormal termination) |
michael@0 | 766 | * "d": duration. Value in seconds. |
michael@0 | 767 | * "sg": Gecko startup time. Present if this is a clean launch. This |
michael@0 | 768 | * corresponds to the telemetry timer FENNEC_STARTUP_TIME_GECKOREADY. |
michael@0 | 769 | * "sj": Java activity init time. Present if this is a clean launch. This |
michael@0 | 770 | * corresponds to the telemetry timer FENNEC_STARTUP_TIME_JAVAUI, |
michael@0 | 771 | * and includes initialization tasks beyond initial |
michael@0 | 772 | * onWindowFocusChanged. |
michael@0 | 773 | * |
michael@0 | 774 | * Abnormal terminations will be missing a duration and will feature these keys: |
michael@0 | 775 | * |
michael@0 | 776 | * "oom": was the session killed by an OOM exception? |
michael@0 | 777 | * "stopped": was the session stopped gently? |
michael@0 | 778 | */ |
michael@0 | 779 | |
michael@0 | 780 | public static final String MEASUREMENT_NAME_SESSIONS = "org.mozilla.appSessions"; |
michael@0 | 781 | public static final int MEASUREMENT_VERSION_SESSIONS = 4; |
michael@0 | 782 | |
michael@0 | 783 | private void initializeSessionsProvider() { |
michael@0 | 784 | this.storage.ensureMeasurementInitialized( |
michael@0 | 785 | MEASUREMENT_NAME_SESSIONS, |
michael@0 | 786 | MEASUREMENT_VERSION_SESSIONS, |
michael@0 | 787 | new MeasurementFields() { |
michael@0 | 788 | @Override |
michael@0 | 789 | public Iterable<FieldSpec> getFields() { |
michael@0 | 790 | List<FieldSpec> out = Arrays.asList( |
michael@0 | 791 | new FieldSpec("normal", Field.TYPE_JSON_DISCRETE), |
michael@0 | 792 | new FieldSpec("abnormal", Field.TYPE_JSON_DISCRETE)); |
michael@0 | 793 | return out; |
michael@0 | 794 | } |
michael@0 | 795 | }); |
michael@0 | 796 | } |
michael@0 | 797 | |
michael@0 | 798 | /** |
michael@0 | 799 | * Logic shared between crashed and normal sessions. |
michael@0 | 800 | */ |
michael@0 | 801 | private void recordSessionEntry(String field, SessionInformation session, final int environment, JSONObject value) { |
michael@0 | 802 | final HealthReportDatabaseStorage storage = this.storage; |
michael@0 | 803 | if (storage == null) { |
michael@0 | 804 | Log.d(LOG_TAG, "No storage: not recording session entry. Shutting down?"); |
michael@0 | 805 | return; |
michael@0 | 806 | } |
michael@0 | 807 | |
michael@0 | 808 | try { |
michael@0 | 809 | final int sessionField = storage.getField(MEASUREMENT_NAME_SESSIONS, |
michael@0 | 810 | MEASUREMENT_VERSION_SESSIONS, |
michael@0 | 811 | field) |
michael@0 | 812 | .getID(); |
michael@0 | 813 | final int day = storage.getDay(session.wallStartTime); |
michael@0 | 814 | storage.recordDailyDiscrete(environment, day, sessionField, value); |
michael@0 | 815 | Log.v(LOG_TAG, "Recorded session entry for env " + environment + ", current is " + env); |
michael@0 | 816 | } catch (Exception e) { |
michael@0 | 817 | Log.w(LOG_TAG, "Unable to record session completion.", e); |
michael@0 | 818 | } |
michael@0 | 819 | } |
michael@0 | 820 | |
michael@0 | 821 | public void checkForOrphanSessions() { |
michael@0 | 822 | if (!this.orphanChecked.compareAndSet(false, true)) { |
michael@0 | 823 | Log.w(LOG_TAG, "Attempting to check for orphan sessions more than once."); |
michael@0 | 824 | return; |
michael@0 | 825 | } |
michael@0 | 826 | |
michael@0 | 827 | Log.d(LOG_TAG, "Checking for orphan session."); |
michael@0 | 828 | if (this.previousSession == null) { |
michael@0 | 829 | return; |
michael@0 | 830 | } |
michael@0 | 831 | if (this.previousSession.wallStartTime == 0) { |
michael@0 | 832 | return; |
michael@0 | 833 | } |
michael@0 | 834 | |
michael@0 | 835 | if (state != State.INITIALIZED) { |
michael@0 | 836 | // Something has gone awry. |
michael@0 | 837 | Log.e(LOG_TAG, "Attempted to record bad session end without initialized recorder."); |
michael@0 | 838 | return; |
michael@0 | 839 | } |
michael@0 | 840 | |
michael@0 | 841 | try { |
michael@0 | 842 | recordSessionEntry("abnormal", this.previousSession, this.env, |
michael@0 | 843 | this.previousSession.getCrashedJSON()); |
michael@0 | 844 | } catch (Exception e) { |
michael@0 | 845 | Log.w(LOG_TAG, "Unable to generate session JSON.", e); |
michael@0 | 846 | |
michael@0 | 847 | // Future: record this exception in FHR's own error submitter. |
michael@0 | 848 | } |
michael@0 | 849 | } |
michael@0 | 850 | |
michael@0 | 851 | public void recordSessionEnd(String reason, SharedPreferences.Editor editor) { |
michael@0 | 852 | recordSessionEnd(reason, editor, env); |
michael@0 | 853 | } |
michael@0 | 854 | |
michael@0 | 855 | /** |
michael@0 | 856 | * Record that the current session ended. Does not commit the provided editor. |
michael@0 | 857 | * |
michael@0 | 858 | * @param environment An environment ID. This allows callers to record the |
michael@0 | 859 | * end of a session due to an observed environment change. |
michael@0 | 860 | */ |
michael@0 | 861 | public void recordSessionEnd(String reason, SharedPreferences.Editor editor, final int environment) { |
michael@0 | 862 | Log.d(LOG_TAG, "Recording session end: " + reason); |
michael@0 | 863 | if (state != State.INITIALIZED) { |
michael@0 | 864 | // Something has gone awry. |
michael@0 | 865 | Log.e(LOG_TAG, "Attempted to record session end without initialized recorder."); |
michael@0 | 866 | return; |
michael@0 | 867 | } |
michael@0 | 868 | |
michael@0 | 869 | final SessionInformation session = this.session; |
michael@0 | 870 | this.session = null; // So it can't be double-recorded. |
michael@0 | 871 | |
michael@0 | 872 | if (session == null) { |
michael@0 | 873 | Log.w(LOG_TAG, "Unable to record session end: no session. Already ended?"); |
michael@0 | 874 | return; |
michael@0 | 875 | } |
michael@0 | 876 | |
michael@0 | 877 | if (session.wallStartTime <= 0) { |
michael@0 | 878 | Log.e(LOG_TAG, "Session start " + session.wallStartTime + " isn't valid! Can't record end."); |
michael@0 | 879 | return; |
michael@0 | 880 | } |
michael@0 | 881 | |
michael@0 | 882 | long realEndTime = android.os.SystemClock.elapsedRealtime(); |
michael@0 | 883 | try { |
michael@0 | 884 | JSONObject json = session.getCompletionJSON(reason, realEndTime); |
michael@0 | 885 | recordSessionEntry("normal", session, environment, json); |
michael@0 | 886 | } catch (JSONException e) { |
michael@0 | 887 | Log.w(LOG_TAG, "Unable to generate session JSON.", e); |
michael@0 | 888 | |
michael@0 | 889 | // Continue so we don't hit it next time. |
michael@0 | 890 | // Future: record this exception in FHR's own error submitter. |
michael@0 | 891 | } |
michael@0 | 892 | |
michael@0 | 893 | // Track the end of this session in shared prefs, so it doesn't get |
michael@0 | 894 | // double-counted on next run. |
michael@0 | 895 | session.recordCompletion(editor); |
michael@0 | 896 | } |
michael@0 | 897 | } |