|
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.home; |
|
7 |
|
8 import static org.mozilla.gecko.home.HomeConfig.createBuiltinPanelConfig; |
|
9 |
|
10 import java.util.ArrayList; |
|
11 import java.util.HashSet; |
|
12 import java.util.List; |
|
13 import java.util.Queue; |
|
14 import java.util.Set; |
|
15 import java.util.concurrent.ConcurrentLinkedQueue; |
|
16 |
|
17 import org.json.JSONException; |
|
18 import org.json.JSONObject; |
|
19 import org.mozilla.gecko.db.HomeProvider; |
|
20 import org.mozilla.gecko.GeckoAppShell; |
|
21 import org.mozilla.gecko.home.HomeConfig.PanelConfig; |
|
22 import org.mozilla.gecko.home.PanelInfoManager.PanelInfo; |
|
23 import org.mozilla.gecko.home.PanelInfoManager.RequestCallback; |
|
24 import org.mozilla.gecko.util.GeckoEventListener; |
|
25 import org.mozilla.gecko.util.ThreadUtils; |
|
26 |
|
27 import android.content.ContentResolver; |
|
28 import android.content.Context; |
|
29 import android.os.Handler; |
|
30 import android.util.Log; |
|
31 |
|
32 public class HomePanelsManager implements GeckoEventListener { |
|
33 public static final String LOGTAG = "HomePanelsManager"; |
|
34 |
|
35 private static final HomePanelsManager sInstance = new HomePanelsManager(); |
|
36 |
|
37 private static final int INVALIDATION_DELAY_MSEC = 500; |
|
38 private static final int PANEL_INFO_TIMEOUT_MSEC = 1000; |
|
39 |
|
40 private static final String EVENT_HOMEPANELS_INSTALL = "HomePanels:Install"; |
|
41 private static final String EVENT_HOMEPANELS_UNINSTALL = "HomePanels:Uninstall"; |
|
42 private static final String EVENT_HOMEPANELS_UPDATE = "HomePanels:Update"; |
|
43 private static final String EVENT_HOMEPANELS_REFRESH = "HomePanels:RefreshDataset"; |
|
44 |
|
45 private static final String JSON_KEY_PANEL = "panel"; |
|
46 private static final String JSON_KEY_PANEL_ID = "id"; |
|
47 |
|
48 private enum ChangeType { |
|
49 UNINSTALL, |
|
50 INSTALL, |
|
51 UPDATE, |
|
52 REFRESH |
|
53 } |
|
54 |
|
55 private enum InvalidationMode { |
|
56 DELAYED, |
|
57 IMMEDIATE |
|
58 } |
|
59 |
|
60 private static class ConfigChange { |
|
61 private final ChangeType type; |
|
62 private final Object target; |
|
63 |
|
64 public ConfigChange(ChangeType type) { |
|
65 this(type, null); |
|
66 } |
|
67 |
|
68 public ConfigChange(ChangeType type, Object target) { |
|
69 this.type = type; |
|
70 this.target = target; |
|
71 } |
|
72 } |
|
73 |
|
74 private Context mContext; |
|
75 private HomeConfig mHomeConfig; |
|
76 |
|
77 private final Queue<ConfigChange> mPendingChanges = new ConcurrentLinkedQueue<ConfigChange>(); |
|
78 private final Runnable mInvalidationRunnable = new InvalidationRunnable(); |
|
79 |
|
80 public static HomePanelsManager getInstance() { |
|
81 return sInstance; |
|
82 } |
|
83 |
|
84 public void init(Context context) { |
|
85 mContext = context; |
|
86 mHomeConfig = HomeConfig.getDefault(context); |
|
87 |
|
88 GeckoAppShell.getEventDispatcher().registerEventListener(EVENT_HOMEPANELS_INSTALL, this); |
|
89 GeckoAppShell.getEventDispatcher().registerEventListener(EVENT_HOMEPANELS_UNINSTALL, this); |
|
90 GeckoAppShell.getEventDispatcher().registerEventListener(EVENT_HOMEPANELS_UPDATE, this); |
|
91 GeckoAppShell.getEventDispatcher().registerEventListener(EVENT_HOMEPANELS_REFRESH, this); |
|
92 } |
|
93 |
|
94 public void onLocaleReady(final String locale) { |
|
95 ThreadUtils.getBackgroundHandler().post(new Runnable() { |
|
96 @Override |
|
97 public void run() { |
|
98 final String configLocale = mHomeConfig.getLocale(); |
|
99 if (configLocale == null || !configLocale.equals(locale)) { |
|
100 handleLocaleChange(); |
|
101 } |
|
102 } |
|
103 }); |
|
104 } |
|
105 |
|
106 @Override |
|
107 public void handleMessage(String event, JSONObject message) { |
|
108 try { |
|
109 if (event.equals(EVENT_HOMEPANELS_INSTALL)) { |
|
110 Log.d(LOGTAG, EVENT_HOMEPANELS_INSTALL); |
|
111 handlePanelInstall(createPanelConfigFromMessage(message), InvalidationMode.DELAYED); |
|
112 } else if (event.equals(EVENT_HOMEPANELS_UNINSTALL)) { |
|
113 Log.d(LOGTAG, EVENT_HOMEPANELS_UNINSTALL); |
|
114 final String panelId = message.getString(JSON_KEY_PANEL_ID); |
|
115 handlePanelUninstall(panelId); |
|
116 } else if (event.equals(EVENT_HOMEPANELS_UPDATE)) { |
|
117 Log.d(LOGTAG, EVENT_HOMEPANELS_UPDATE); |
|
118 handlePanelUpdate(createPanelConfigFromMessage(message)); |
|
119 } else if (event.equals(EVENT_HOMEPANELS_REFRESH)) { |
|
120 Log.d(LOGTAG, EVENT_HOMEPANELS_REFRESH); |
|
121 handleDatasetRefresh(message); |
|
122 } |
|
123 } catch (Exception e) { |
|
124 Log.e(LOGTAG, "Failed to handle event " + event, e); |
|
125 } |
|
126 } |
|
127 |
|
128 private PanelConfig createPanelConfigFromMessage(JSONObject message) throws JSONException { |
|
129 final JSONObject json = message.getJSONObject(JSON_KEY_PANEL); |
|
130 return new PanelConfig(json); |
|
131 } |
|
132 |
|
133 /** |
|
134 * Adds a new PanelConfig to the HomeConfig. |
|
135 * |
|
136 * This posts the invalidation of HomeConfig immediately. |
|
137 * |
|
138 * @param panelConfig panel to add |
|
139 */ |
|
140 public void installPanel(PanelConfig panelConfig) { |
|
141 Log.d(LOGTAG, "installPanel: " + panelConfig.getTitle()); |
|
142 handlePanelInstall(panelConfig, InvalidationMode.IMMEDIATE); |
|
143 } |
|
144 |
|
145 /** |
|
146 * Runs in the gecko thread. |
|
147 */ |
|
148 private void handlePanelInstall(PanelConfig panelConfig, InvalidationMode mode) { |
|
149 mPendingChanges.offer(new ConfigChange(ChangeType.INSTALL, panelConfig)); |
|
150 Log.d(LOGTAG, "handlePanelInstall: " + mPendingChanges.size()); |
|
151 |
|
152 scheduleInvalidation(mode); |
|
153 } |
|
154 |
|
155 /** |
|
156 * Runs in the gecko thread. |
|
157 */ |
|
158 private void handlePanelUninstall(String panelId) { |
|
159 mPendingChanges.offer(new ConfigChange(ChangeType.UNINSTALL, panelId)); |
|
160 Log.d(LOGTAG, "handlePanelUninstall: " + mPendingChanges.size()); |
|
161 |
|
162 scheduleInvalidation(InvalidationMode.DELAYED); |
|
163 } |
|
164 |
|
165 /** |
|
166 * Runs in the gecko thread. |
|
167 */ |
|
168 private void handlePanelUpdate(PanelConfig panelConfig) { |
|
169 mPendingChanges.offer(new ConfigChange(ChangeType.UPDATE, panelConfig)); |
|
170 Log.d(LOGTAG, "handlePanelUpdate: " + mPendingChanges.size()); |
|
171 |
|
172 scheduleInvalidation(InvalidationMode.DELAYED); |
|
173 } |
|
174 |
|
175 /** |
|
176 * Runs in the background thread. |
|
177 */ |
|
178 private void handleLocaleChange() { |
|
179 mPendingChanges.offer(new ConfigChange(ChangeType.REFRESH)); |
|
180 Log.d(LOGTAG, "handleLocaleChange: " + mPendingChanges.size()); |
|
181 |
|
182 scheduleInvalidation(InvalidationMode.IMMEDIATE); |
|
183 } |
|
184 |
|
185 |
|
186 /** |
|
187 * Handles a dataset refresh request from Gecko. This is usually |
|
188 * triggered by a HomeStorage.save() call in an add-on. |
|
189 * |
|
190 * Runs in the gecko thread. |
|
191 */ |
|
192 private void handleDatasetRefresh(JSONObject message) { |
|
193 final String datasetId; |
|
194 try { |
|
195 datasetId = message.getString("datasetId"); |
|
196 } catch (JSONException e) { |
|
197 Log.e(LOGTAG, "Failed to handle dataset refresh", e); |
|
198 return; |
|
199 } |
|
200 |
|
201 Log.d(LOGTAG, "Refresh request for dataset: " + datasetId); |
|
202 |
|
203 final ContentResolver cr = mContext.getContentResolver(); |
|
204 cr.notifyChange(HomeProvider.getDatasetNotificationUri(datasetId), null); |
|
205 } |
|
206 |
|
207 /** |
|
208 * Runs in the gecko or main thread. |
|
209 */ |
|
210 private void scheduleInvalidation(InvalidationMode mode) { |
|
211 final Handler handler = ThreadUtils.getBackgroundHandler(); |
|
212 |
|
213 handler.removeCallbacks(mInvalidationRunnable); |
|
214 |
|
215 if (mode == InvalidationMode.IMMEDIATE) { |
|
216 handler.post(mInvalidationRunnable); |
|
217 } else { |
|
218 handler.postDelayed(mInvalidationRunnable, INVALIDATION_DELAY_MSEC); |
|
219 } |
|
220 |
|
221 Log.d(LOGTAG, "scheduleInvalidation: scheduled new invalidation: " + mode); |
|
222 } |
|
223 |
|
224 /** |
|
225 * Runs in the background thread. |
|
226 */ |
|
227 private void executePendingChanges(HomeConfig.Editor editor) { |
|
228 boolean shouldRefresh = false; |
|
229 |
|
230 while (!mPendingChanges.isEmpty()) { |
|
231 final ConfigChange pendingChange = mPendingChanges.poll(); |
|
232 |
|
233 switch (pendingChange.type) { |
|
234 case UNINSTALL: { |
|
235 final String panelId = (String) pendingChange.target; |
|
236 if (editor.uninstall(panelId)) { |
|
237 Log.d(LOGTAG, "executePendingChanges: uninstalled panel " + panelId); |
|
238 } |
|
239 break; |
|
240 } |
|
241 |
|
242 case INSTALL: { |
|
243 final PanelConfig panelConfig = (PanelConfig) pendingChange.target; |
|
244 if (editor.install(panelConfig)) { |
|
245 Log.d(LOGTAG, "executePendingChanges: added panel " + panelConfig.getId()); |
|
246 } |
|
247 break; |
|
248 } |
|
249 |
|
250 case UPDATE: { |
|
251 final PanelConfig panelConfig = (PanelConfig) pendingChange.target; |
|
252 if (editor.update(panelConfig)) { |
|
253 Log.w(LOGTAG, "executePendingChanges: updated panel " + panelConfig.getId()); |
|
254 } |
|
255 break; |
|
256 } |
|
257 |
|
258 case REFRESH: { |
|
259 shouldRefresh = true; |
|
260 } |
|
261 } |
|
262 } |
|
263 |
|
264 // The editor still represents the default HomeConfig |
|
265 // configuration and hasn't been changed by any operation |
|
266 // above. No need to refresh as the HomeConfig backend will |
|
267 // take of forcing all existing HomeConfigLoader instances to |
|
268 // refresh their contents. |
|
269 if (shouldRefresh && !editor.isDefault()) { |
|
270 executeRefresh(editor); |
|
271 } |
|
272 } |
|
273 |
|
274 /** |
|
275 * Runs in the background thread. |
|
276 */ |
|
277 private void refreshFromPanelInfos(HomeConfig.Editor editor, List<PanelInfo> panelInfos) { |
|
278 Log.d(LOGTAG, "refreshFromPanelInfos"); |
|
279 |
|
280 for (PanelConfig panelConfig : editor) { |
|
281 PanelConfig refreshedPanelConfig = null; |
|
282 |
|
283 if (panelConfig.isDynamic()) { |
|
284 for (PanelInfo panelInfo : panelInfos) { |
|
285 if (panelInfo.getId().equals(panelConfig.getId())) { |
|
286 refreshedPanelConfig = panelInfo.toPanelConfig(); |
|
287 Log.d(LOGTAG, "refreshFromPanelInfos: refreshing from panel info: " + panelInfo.getId()); |
|
288 break; |
|
289 } |
|
290 } |
|
291 } else { |
|
292 refreshedPanelConfig = createBuiltinPanelConfig(mContext, panelConfig.getType()); |
|
293 Log.d(LOGTAG, "refreshFromPanelInfos: refreshing built-in panel: " + panelConfig.getId()); |
|
294 } |
|
295 |
|
296 if (refreshedPanelConfig == null) { |
|
297 Log.d(LOGTAG, "refreshFromPanelInfos: no refreshed panel, falling back: " + panelConfig.getId()); |
|
298 continue; |
|
299 } |
|
300 |
|
301 Log.d(LOGTAG, "refreshFromPanelInfos: refreshed panel " + refreshedPanelConfig.getId()); |
|
302 editor.update(refreshedPanelConfig); |
|
303 } |
|
304 } |
|
305 |
|
306 /** |
|
307 * Runs in the background thread. |
|
308 */ |
|
309 private void executeRefresh(HomeConfig.Editor editor) { |
|
310 if (editor.isEmpty()) { |
|
311 return; |
|
312 } |
|
313 |
|
314 Log.d(LOGTAG, "executeRefresh"); |
|
315 |
|
316 final Set<String> ids = new HashSet<String>(); |
|
317 for (PanelConfig panelConfig : editor) { |
|
318 ids.add(panelConfig.getId()); |
|
319 } |
|
320 |
|
321 final Object panelRequestLock = new Object(); |
|
322 final List<PanelInfo> latestPanelInfos = new ArrayList<PanelInfo>(); |
|
323 |
|
324 final PanelInfoManager pm = new PanelInfoManager(); |
|
325 pm.requestPanelsById(ids, new RequestCallback() { |
|
326 @Override |
|
327 public void onComplete(List<PanelInfo> panelInfos) { |
|
328 synchronized(panelRequestLock) { |
|
329 latestPanelInfos.addAll(panelInfos); |
|
330 Log.d(LOGTAG, "executeRefresh: fetched panel infos: " + panelInfos.size()); |
|
331 |
|
332 panelRequestLock.notifyAll(); |
|
333 } |
|
334 } |
|
335 }); |
|
336 |
|
337 try { |
|
338 synchronized(panelRequestLock) { |
|
339 panelRequestLock.wait(PANEL_INFO_TIMEOUT_MSEC); |
|
340 |
|
341 Log.d(LOGTAG, "executeRefresh: done fetching panel infos"); |
|
342 refreshFromPanelInfos(editor, latestPanelInfos); |
|
343 } |
|
344 } catch (InterruptedException e) { |
|
345 Log.e(LOGTAG, "Failed to fetch panels from gecko", e); |
|
346 } |
|
347 } |
|
348 |
|
349 /** |
|
350 * Runs in the background thread. |
|
351 */ |
|
352 private class InvalidationRunnable implements Runnable { |
|
353 @Override |
|
354 public void run() { |
|
355 final HomeConfig.Editor editor = mHomeConfig.load().edit(); |
|
356 executePendingChanges(editor); |
|
357 editor.commit(); |
|
358 } |
|
359 }; |
|
360 } |