mobile/android/base/home/HomePanelsManager.java

changeset 0
6474c204b198
equal deleted inserted replaced
-1:000000000000 0:cb057c4f0a94
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 }

mercurial