|
1 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- |
|
2 * This Source Code Form is subject to the terms of the Mozilla Public |
|
3 * License, v. 2.0. If a copy of the MPL was not distributed with this |
|
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
5 |
|
6 package org.mozilla.gecko.health; |
|
7 |
|
8 import android.content.ContentProviderClient; |
|
9 import android.content.Context; |
|
10 import android.util.Log; |
|
11 |
|
12 import org.mozilla.gecko.GeckoAppShell; |
|
13 import org.mozilla.gecko.GeckoEvent; |
|
14 import org.mozilla.gecko.GeckoProfile; |
|
15 |
|
16 import org.mozilla.gecko.background.healthreport.EnvironmentBuilder; |
|
17 import org.mozilla.gecko.background.common.GlobalConstants; |
|
18 import org.mozilla.gecko.background.healthreport.HealthReportConstants; |
|
19 import org.mozilla.gecko.background.healthreport.HealthReportDatabaseStorage; |
|
20 import org.mozilla.gecko.background.healthreport.HealthReportGenerator; |
|
21 |
|
22 import org.mozilla.gecko.util.GeckoEventListener; |
|
23 import org.mozilla.gecko.util.ThreadUtils; |
|
24 |
|
25 import org.json.JSONException; |
|
26 import org.json.JSONObject; |
|
27 |
|
28 /** |
|
29 * BrowserHealthReporter is the browser's interface to the Firefox Health |
|
30 * Report report generator. |
|
31 * |
|
32 * Each instance registers Gecko event listeners, so keep a single instance |
|
33 * around for the life of the browser. Java callers should use this globally |
|
34 * available singleton. |
|
35 */ |
|
36 public class BrowserHealthReporter implements GeckoEventListener { |
|
37 private static final String LOGTAG = "GeckoHealthRep"; |
|
38 |
|
39 public static final String EVENT_REQUEST = "HealthReport:Request"; |
|
40 public static final String EVENT_RESPONSE = "HealthReport:Response"; |
|
41 |
|
42 protected final Context context; |
|
43 |
|
44 public BrowserHealthReporter() { |
|
45 GeckoAppShell.registerEventListener(EVENT_REQUEST, this); |
|
46 |
|
47 context = GeckoAppShell.getContext(); |
|
48 if (context == null) { |
|
49 throw new IllegalStateException("Null Gecko context"); |
|
50 } |
|
51 } |
|
52 |
|
53 public void uninit() { |
|
54 GeckoAppShell.unregisterEventListener(EVENT_REQUEST, this); |
|
55 } |
|
56 |
|
57 /** |
|
58 * Generate a new Health Report. |
|
59 * |
|
60 * This method performs IO, so call it from a background thread. |
|
61 * |
|
62 * @param since timestamp of first day to report (milliseconds since epoch). |
|
63 * @param lastPingTime timestamp when last health report was uploaded |
|
64 * (milliseconds since epoch). |
|
65 * @param profilePath path of the profile to generate report for. |
|
66 * @throws JSONException if JSON generation fails. |
|
67 * @throws IllegalStateException if the environment does not allow to generate a report. |
|
68 * @return non-null report. |
|
69 */ |
|
70 public JSONObject generateReport(long since, long lastPingTime, String profilePath) throws JSONException { |
|
71 // We abuse the life-cycle of an Android ContentProvider slightly by holding |
|
72 // onto a ContentProviderClient while we generate a payload. This keeps |
|
73 // our database storage alive, while also allowing us to share a database |
|
74 // connection with BrowserHealthRecorder and the uploader. |
|
75 // The ContentProvider owns all underlying Storage instances, so we don't |
|
76 // need to explicitly close them. |
|
77 ContentProviderClient client = EnvironmentBuilder.getContentProviderClient(context); |
|
78 if (client == null) { |
|
79 throw new IllegalStateException("Could not fetch Health Report content provider."); |
|
80 } |
|
81 |
|
82 try { |
|
83 // Storage instance is owned by HealthReportProvider, so we don't need |
|
84 // to close it. |
|
85 HealthReportDatabaseStorage storage = EnvironmentBuilder.getStorage(client, profilePath); |
|
86 if (storage == null) { |
|
87 throw new IllegalStateException("No storage in Health Reporter."); |
|
88 } |
|
89 |
|
90 HealthReportGenerator generator = new HealthReportGenerator(storage); |
|
91 JSONObject report = generator.generateDocument(since, lastPingTime, profilePath); |
|
92 if (report == null) { |
|
93 throw new IllegalStateException("Not enough profile information to generate report."); |
|
94 } |
|
95 return report; |
|
96 } finally { |
|
97 client.release(); |
|
98 } |
|
99 } |
|
100 |
|
101 /** |
|
102 * Get last time a health report was successfully uploaded. |
|
103 * |
|
104 * This is read from shared preferences, so call it from a background |
|
105 * thread. Bug 882182 tracks making this work with multiple profiles. |
|
106 * |
|
107 * @return milliseconds since the epoch, or 0 if never uploaded. |
|
108 */ |
|
109 protected long getLastUploadLocalTime() { |
|
110 return context |
|
111 .getSharedPreferences(HealthReportConstants.PREFS_BRANCH, 0) |
|
112 .getLong(HealthReportConstants.PREF_LAST_UPLOAD_LOCAL_TIME, 0L); |
|
113 } |
|
114 |
|
115 /** |
|
116 * Generate a new Health Report for the current Gecko profile. |
|
117 * |
|
118 * This method performs IO, so call it from a background thread. |
|
119 * |
|
120 * @throws JSONException if JSON generation fails. |
|
121 * @throws IllegalStateException if the environment does not allow to generate a report. |
|
122 * @return non-null Health Report. |
|
123 */ |
|
124 public JSONObject generateReport() throws JSONException { |
|
125 GeckoProfile profile = GeckoAppShell.getGeckoInterface().getProfile(); |
|
126 String profilePath = profile.getDir().getAbsolutePath(); |
|
127 |
|
128 long since = System.currentTimeMillis() - GlobalConstants.MILLISECONDS_PER_SIX_MONTHS; |
|
129 long lastPingTime = Math.max(getLastUploadLocalTime(), HealthReportConstants.EARLIEST_LAST_PING); |
|
130 |
|
131 return generateReport(since, lastPingTime, profilePath); |
|
132 } |
|
133 |
|
134 @Override |
|
135 public void handleMessage(String event, JSONObject message) { |
|
136 try { |
|
137 ThreadUtils.postToBackgroundThread(new Runnable() { |
|
138 @Override |
|
139 public void run() { |
|
140 JSONObject report = null; |
|
141 try { |
|
142 report = generateReport(); // non-null if it returns. |
|
143 } catch (Exception e) { |
|
144 Log.e(LOGTAG, "Generating report failed; responding with empty report.", e); |
|
145 report = new JSONObject(); |
|
146 } |
|
147 |
|
148 GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent(EVENT_RESPONSE, report.toString())); |
|
149 } |
|
150 }); |
|
151 } catch (Exception e) { |
|
152 Log.e(LOGTAG, "Exception handling message \"" + event + "\":", e); |
|
153 } |
|
154 } |
|
155 } |
|
156 |