michael@0: /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- michael@0: * This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: package org.mozilla.gecko.health; michael@0: michael@0: import android.content.ContentProviderClient; michael@0: import android.content.Context; michael@0: import android.util.Log; michael@0: michael@0: import org.mozilla.gecko.GeckoAppShell; michael@0: import org.mozilla.gecko.GeckoEvent; michael@0: import org.mozilla.gecko.GeckoProfile; michael@0: michael@0: import org.mozilla.gecko.background.healthreport.EnvironmentBuilder; michael@0: import org.mozilla.gecko.background.common.GlobalConstants; michael@0: import org.mozilla.gecko.background.healthreport.HealthReportConstants; michael@0: import org.mozilla.gecko.background.healthreport.HealthReportDatabaseStorage; michael@0: import org.mozilla.gecko.background.healthreport.HealthReportGenerator; michael@0: michael@0: import org.mozilla.gecko.util.GeckoEventListener; michael@0: import org.mozilla.gecko.util.ThreadUtils; michael@0: michael@0: import org.json.JSONException; michael@0: import org.json.JSONObject; michael@0: michael@0: /** michael@0: * BrowserHealthReporter is the browser's interface to the Firefox Health michael@0: * Report report generator. michael@0: * michael@0: * Each instance registers Gecko event listeners, so keep a single instance michael@0: * around for the life of the browser. Java callers should use this globally michael@0: * available singleton. michael@0: */ michael@0: public class BrowserHealthReporter implements GeckoEventListener { michael@0: private static final String LOGTAG = "GeckoHealthRep"; michael@0: michael@0: public static final String EVENT_REQUEST = "HealthReport:Request"; michael@0: public static final String EVENT_RESPONSE = "HealthReport:Response"; michael@0: michael@0: protected final Context context; michael@0: michael@0: public BrowserHealthReporter() { michael@0: GeckoAppShell.registerEventListener(EVENT_REQUEST, this); michael@0: michael@0: context = GeckoAppShell.getContext(); michael@0: if (context == null) { michael@0: throw new IllegalStateException("Null Gecko context"); michael@0: } michael@0: } michael@0: michael@0: public void uninit() { michael@0: GeckoAppShell.unregisterEventListener(EVENT_REQUEST, this); michael@0: } michael@0: michael@0: /** michael@0: * Generate a new Health Report. michael@0: * michael@0: * This method performs IO, so call it from a background thread. michael@0: * michael@0: * @param since timestamp of first day to report (milliseconds since epoch). michael@0: * @param lastPingTime timestamp when last health report was uploaded michael@0: * (milliseconds since epoch). michael@0: * @param profilePath path of the profile to generate report for. michael@0: * @throws JSONException if JSON generation fails. michael@0: * @throws IllegalStateException if the environment does not allow to generate a report. michael@0: * @return non-null report. michael@0: */ michael@0: public JSONObject generateReport(long since, long lastPingTime, String profilePath) throws JSONException { michael@0: // We abuse the life-cycle of an Android ContentProvider slightly by holding michael@0: // onto a ContentProviderClient while we generate a payload. This keeps michael@0: // our database storage alive, while also allowing us to share a database michael@0: // connection with BrowserHealthRecorder and the uploader. michael@0: // The ContentProvider owns all underlying Storage instances, so we don't michael@0: // need to explicitly close them. michael@0: ContentProviderClient client = EnvironmentBuilder.getContentProviderClient(context); michael@0: if (client == null) { michael@0: throw new IllegalStateException("Could not fetch Health Report content provider."); michael@0: } michael@0: michael@0: try { michael@0: // Storage instance is owned by HealthReportProvider, so we don't need michael@0: // to close it. michael@0: HealthReportDatabaseStorage storage = EnvironmentBuilder.getStorage(client, profilePath); michael@0: if (storage == null) { michael@0: throw new IllegalStateException("No storage in Health Reporter."); michael@0: } michael@0: michael@0: HealthReportGenerator generator = new HealthReportGenerator(storage); michael@0: JSONObject report = generator.generateDocument(since, lastPingTime, profilePath); michael@0: if (report == null) { michael@0: throw new IllegalStateException("Not enough profile information to generate report."); michael@0: } michael@0: return report; michael@0: } finally { michael@0: client.release(); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Get last time a health report was successfully uploaded. michael@0: * michael@0: * This is read from shared preferences, so call it from a background michael@0: * thread. Bug 882182 tracks making this work with multiple profiles. michael@0: * michael@0: * @return milliseconds since the epoch, or 0 if never uploaded. michael@0: */ michael@0: protected long getLastUploadLocalTime() { michael@0: return context michael@0: .getSharedPreferences(HealthReportConstants.PREFS_BRANCH, 0) michael@0: .getLong(HealthReportConstants.PREF_LAST_UPLOAD_LOCAL_TIME, 0L); michael@0: } michael@0: michael@0: /** michael@0: * Generate a new Health Report for the current Gecko profile. michael@0: * michael@0: * This method performs IO, so call it from a background thread. michael@0: * michael@0: * @throws JSONException if JSON generation fails. michael@0: * @throws IllegalStateException if the environment does not allow to generate a report. michael@0: * @return non-null Health Report. michael@0: */ michael@0: public JSONObject generateReport() throws JSONException { michael@0: GeckoProfile profile = GeckoAppShell.getGeckoInterface().getProfile(); michael@0: String profilePath = profile.getDir().getAbsolutePath(); michael@0: michael@0: long since = System.currentTimeMillis() - GlobalConstants.MILLISECONDS_PER_SIX_MONTHS; michael@0: long lastPingTime = Math.max(getLastUploadLocalTime(), HealthReportConstants.EARLIEST_LAST_PING); michael@0: michael@0: return generateReport(since, lastPingTime, profilePath); michael@0: } michael@0: michael@0: @Override michael@0: public void handleMessage(String event, JSONObject message) { michael@0: try { michael@0: ThreadUtils.postToBackgroundThread(new Runnable() { michael@0: @Override michael@0: public void run() { michael@0: JSONObject report = null; michael@0: try { michael@0: report = generateReport(); // non-null if it returns. michael@0: } catch (Exception e) { michael@0: Log.e(LOGTAG, "Generating report failed; responding with empty report.", e); michael@0: report = new JSONObject(); michael@0: } michael@0: michael@0: GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent(EVENT_RESPONSE, report.toString())); michael@0: } michael@0: }); michael@0: } catch (Exception e) { michael@0: Log.e(LOGTAG, "Exception handling message \"" + event + "\":", e); michael@0: } michael@0: } michael@0: } michael@0: