1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/mobile/android/base/home/HomePanelsManager.java Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,360 @@ 1.4 +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- 1.5 + * This Source Code Form is subject to the terms of the Mozilla Public 1.6 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.7 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.8 + 1.9 +package org.mozilla.gecko.home; 1.10 + 1.11 +import static org.mozilla.gecko.home.HomeConfig.createBuiltinPanelConfig; 1.12 + 1.13 +import java.util.ArrayList; 1.14 +import java.util.HashSet; 1.15 +import java.util.List; 1.16 +import java.util.Queue; 1.17 +import java.util.Set; 1.18 +import java.util.concurrent.ConcurrentLinkedQueue; 1.19 + 1.20 +import org.json.JSONException; 1.21 +import org.json.JSONObject; 1.22 +import org.mozilla.gecko.db.HomeProvider; 1.23 +import org.mozilla.gecko.GeckoAppShell; 1.24 +import org.mozilla.gecko.home.HomeConfig.PanelConfig; 1.25 +import org.mozilla.gecko.home.PanelInfoManager.PanelInfo; 1.26 +import org.mozilla.gecko.home.PanelInfoManager.RequestCallback; 1.27 +import org.mozilla.gecko.util.GeckoEventListener; 1.28 +import org.mozilla.gecko.util.ThreadUtils; 1.29 + 1.30 +import android.content.ContentResolver; 1.31 +import android.content.Context; 1.32 +import android.os.Handler; 1.33 +import android.util.Log; 1.34 + 1.35 +public class HomePanelsManager implements GeckoEventListener { 1.36 + public static final String LOGTAG = "HomePanelsManager"; 1.37 + 1.38 + private static final HomePanelsManager sInstance = new HomePanelsManager(); 1.39 + 1.40 + private static final int INVALIDATION_DELAY_MSEC = 500; 1.41 + private static final int PANEL_INFO_TIMEOUT_MSEC = 1000; 1.42 + 1.43 + private static final String EVENT_HOMEPANELS_INSTALL = "HomePanels:Install"; 1.44 + private static final String EVENT_HOMEPANELS_UNINSTALL = "HomePanels:Uninstall"; 1.45 + private static final String EVENT_HOMEPANELS_UPDATE = "HomePanels:Update"; 1.46 + private static final String EVENT_HOMEPANELS_REFRESH = "HomePanels:RefreshDataset"; 1.47 + 1.48 + private static final String JSON_KEY_PANEL = "panel"; 1.49 + private static final String JSON_KEY_PANEL_ID = "id"; 1.50 + 1.51 + private enum ChangeType { 1.52 + UNINSTALL, 1.53 + INSTALL, 1.54 + UPDATE, 1.55 + REFRESH 1.56 + } 1.57 + 1.58 + private enum InvalidationMode { 1.59 + DELAYED, 1.60 + IMMEDIATE 1.61 + } 1.62 + 1.63 + private static class ConfigChange { 1.64 + private final ChangeType type; 1.65 + private final Object target; 1.66 + 1.67 + public ConfigChange(ChangeType type) { 1.68 + this(type, null); 1.69 + } 1.70 + 1.71 + public ConfigChange(ChangeType type, Object target) { 1.72 + this.type = type; 1.73 + this.target = target; 1.74 + } 1.75 + } 1.76 + 1.77 + private Context mContext; 1.78 + private HomeConfig mHomeConfig; 1.79 + 1.80 + private final Queue<ConfigChange> mPendingChanges = new ConcurrentLinkedQueue<ConfigChange>(); 1.81 + private final Runnable mInvalidationRunnable = new InvalidationRunnable(); 1.82 + 1.83 + public static HomePanelsManager getInstance() { 1.84 + return sInstance; 1.85 + } 1.86 + 1.87 + public void init(Context context) { 1.88 + mContext = context; 1.89 + mHomeConfig = HomeConfig.getDefault(context); 1.90 + 1.91 + GeckoAppShell.getEventDispatcher().registerEventListener(EVENT_HOMEPANELS_INSTALL, this); 1.92 + GeckoAppShell.getEventDispatcher().registerEventListener(EVENT_HOMEPANELS_UNINSTALL, this); 1.93 + GeckoAppShell.getEventDispatcher().registerEventListener(EVENT_HOMEPANELS_UPDATE, this); 1.94 + GeckoAppShell.getEventDispatcher().registerEventListener(EVENT_HOMEPANELS_REFRESH, this); 1.95 + } 1.96 + 1.97 + public void onLocaleReady(final String locale) { 1.98 + ThreadUtils.getBackgroundHandler().post(new Runnable() { 1.99 + @Override 1.100 + public void run() { 1.101 + final String configLocale = mHomeConfig.getLocale(); 1.102 + if (configLocale == null || !configLocale.equals(locale)) { 1.103 + handleLocaleChange(); 1.104 + } 1.105 + } 1.106 + }); 1.107 + } 1.108 + 1.109 + @Override 1.110 + public void handleMessage(String event, JSONObject message) { 1.111 + try { 1.112 + if (event.equals(EVENT_HOMEPANELS_INSTALL)) { 1.113 + Log.d(LOGTAG, EVENT_HOMEPANELS_INSTALL); 1.114 + handlePanelInstall(createPanelConfigFromMessage(message), InvalidationMode.DELAYED); 1.115 + } else if (event.equals(EVENT_HOMEPANELS_UNINSTALL)) { 1.116 + Log.d(LOGTAG, EVENT_HOMEPANELS_UNINSTALL); 1.117 + final String panelId = message.getString(JSON_KEY_PANEL_ID); 1.118 + handlePanelUninstall(panelId); 1.119 + } else if (event.equals(EVENT_HOMEPANELS_UPDATE)) { 1.120 + Log.d(LOGTAG, EVENT_HOMEPANELS_UPDATE); 1.121 + handlePanelUpdate(createPanelConfigFromMessage(message)); 1.122 + } else if (event.equals(EVENT_HOMEPANELS_REFRESH)) { 1.123 + Log.d(LOGTAG, EVENT_HOMEPANELS_REFRESH); 1.124 + handleDatasetRefresh(message); 1.125 + } 1.126 + } catch (Exception e) { 1.127 + Log.e(LOGTAG, "Failed to handle event " + event, e); 1.128 + } 1.129 + } 1.130 + 1.131 + private PanelConfig createPanelConfigFromMessage(JSONObject message) throws JSONException { 1.132 + final JSONObject json = message.getJSONObject(JSON_KEY_PANEL); 1.133 + return new PanelConfig(json); 1.134 + } 1.135 + 1.136 + /** 1.137 + * Adds a new PanelConfig to the HomeConfig. 1.138 + * 1.139 + * This posts the invalidation of HomeConfig immediately. 1.140 + * 1.141 + * @param panelConfig panel to add 1.142 + */ 1.143 + public void installPanel(PanelConfig panelConfig) { 1.144 + Log.d(LOGTAG, "installPanel: " + panelConfig.getTitle()); 1.145 + handlePanelInstall(panelConfig, InvalidationMode.IMMEDIATE); 1.146 + } 1.147 + 1.148 + /** 1.149 + * Runs in the gecko thread. 1.150 + */ 1.151 + private void handlePanelInstall(PanelConfig panelConfig, InvalidationMode mode) { 1.152 + mPendingChanges.offer(new ConfigChange(ChangeType.INSTALL, panelConfig)); 1.153 + Log.d(LOGTAG, "handlePanelInstall: " + mPendingChanges.size()); 1.154 + 1.155 + scheduleInvalidation(mode); 1.156 + } 1.157 + 1.158 + /** 1.159 + * Runs in the gecko thread. 1.160 + */ 1.161 + private void handlePanelUninstall(String panelId) { 1.162 + mPendingChanges.offer(new ConfigChange(ChangeType.UNINSTALL, panelId)); 1.163 + Log.d(LOGTAG, "handlePanelUninstall: " + mPendingChanges.size()); 1.164 + 1.165 + scheduleInvalidation(InvalidationMode.DELAYED); 1.166 + } 1.167 + 1.168 + /** 1.169 + * Runs in the gecko thread. 1.170 + */ 1.171 + private void handlePanelUpdate(PanelConfig panelConfig) { 1.172 + mPendingChanges.offer(new ConfigChange(ChangeType.UPDATE, panelConfig)); 1.173 + Log.d(LOGTAG, "handlePanelUpdate: " + mPendingChanges.size()); 1.174 + 1.175 + scheduleInvalidation(InvalidationMode.DELAYED); 1.176 + } 1.177 + 1.178 + /** 1.179 + * Runs in the background thread. 1.180 + */ 1.181 + private void handleLocaleChange() { 1.182 + mPendingChanges.offer(new ConfigChange(ChangeType.REFRESH)); 1.183 + Log.d(LOGTAG, "handleLocaleChange: " + mPendingChanges.size()); 1.184 + 1.185 + scheduleInvalidation(InvalidationMode.IMMEDIATE); 1.186 + } 1.187 + 1.188 + 1.189 + /** 1.190 + * Handles a dataset refresh request from Gecko. This is usually 1.191 + * triggered by a HomeStorage.save() call in an add-on. 1.192 + * 1.193 + * Runs in the gecko thread. 1.194 + */ 1.195 + private void handleDatasetRefresh(JSONObject message) { 1.196 + final String datasetId; 1.197 + try { 1.198 + datasetId = message.getString("datasetId"); 1.199 + } catch (JSONException e) { 1.200 + Log.e(LOGTAG, "Failed to handle dataset refresh", e); 1.201 + return; 1.202 + } 1.203 + 1.204 + Log.d(LOGTAG, "Refresh request for dataset: " + datasetId); 1.205 + 1.206 + final ContentResolver cr = mContext.getContentResolver(); 1.207 + cr.notifyChange(HomeProvider.getDatasetNotificationUri(datasetId), null); 1.208 + } 1.209 + 1.210 + /** 1.211 + * Runs in the gecko or main thread. 1.212 + */ 1.213 + private void scheduleInvalidation(InvalidationMode mode) { 1.214 + final Handler handler = ThreadUtils.getBackgroundHandler(); 1.215 + 1.216 + handler.removeCallbacks(mInvalidationRunnable); 1.217 + 1.218 + if (mode == InvalidationMode.IMMEDIATE) { 1.219 + handler.post(mInvalidationRunnable); 1.220 + } else { 1.221 + handler.postDelayed(mInvalidationRunnable, INVALIDATION_DELAY_MSEC); 1.222 + } 1.223 + 1.224 + Log.d(LOGTAG, "scheduleInvalidation: scheduled new invalidation: " + mode); 1.225 + } 1.226 + 1.227 + /** 1.228 + * Runs in the background thread. 1.229 + */ 1.230 + private void executePendingChanges(HomeConfig.Editor editor) { 1.231 + boolean shouldRefresh = false; 1.232 + 1.233 + while (!mPendingChanges.isEmpty()) { 1.234 + final ConfigChange pendingChange = mPendingChanges.poll(); 1.235 + 1.236 + switch (pendingChange.type) { 1.237 + case UNINSTALL: { 1.238 + final String panelId = (String) pendingChange.target; 1.239 + if (editor.uninstall(panelId)) { 1.240 + Log.d(LOGTAG, "executePendingChanges: uninstalled panel " + panelId); 1.241 + } 1.242 + break; 1.243 + } 1.244 + 1.245 + case INSTALL: { 1.246 + final PanelConfig panelConfig = (PanelConfig) pendingChange.target; 1.247 + if (editor.install(panelConfig)) { 1.248 + Log.d(LOGTAG, "executePendingChanges: added panel " + panelConfig.getId()); 1.249 + } 1.250 + break; 1.251 + } 1.252 + 1.253 + case UPDATE: { 1.254 + final PanelConfig panelConfig = (PanelConfig) pendingChange.target; 1.255 + if (editor.update(panelConfig)) { 1.256 + Log.w(LOGTAG, "executePendingChanges: updated panel " + panelConfig.getId()); 1.257 + } 1.258 + break; 1.259 + } 1.260 + 1.261 + case REFRESH: { 1.262 + shouldRefresh = true; 1.263 + } 1.264 + } 1.265 + } 1.266 + 1.267 + // The editor still represents the default HomeConfig 1.268 + // configuration and hasn't been changed by any operation 1.269 + // above. No need to refresh as the HomeConfig backend will 1.270 + // take of forcing all existing HomeConfigLoader instances to 1.271 + // refresh their contents. 1.272 + if (shouldRefresh && !editor.isDefault()) { 1.273 + executeRefresh(editor); 1.274 + } 1.275 + } 1.276 + 1.277 + /** 1.278 + * Runs in the background thread. 1.279 + */ 1.280 + private void refreshFromPanelInfos(HomeConfig.Editor editor, List<PanelInfo> panelInfos) { 1.281 + Log.d(LOGTAG, "refreshFromPanelInfos"); 1.282 + 1.283 + for (PanelConfig panelConfig : editor) { 1.284 + PanelConfig refreshedPanelConfig = null; 1.285 + 1.286 + if (panelConfig.isDynamic()) { 1.287 + for (PanelInfo panelInfo : panelInfos) { 1.288 + if (panelInfo.getId().equals(panelConfig.getId())) { 1.289 + refreshedPanelConfig = panelInfo.toPanelConfig(); 1.290 + Log.d(LOGTAG, "refreshFromPanelInfos: refreshing from panel info: " + panelInfo.getId()); 1.291 + break; 1.292 + } 1.293 + } 1.294 + } else { 1.295 + refreshedPanelConfig = createBuiltinPanelConfig(mContext, panelConfig.getType()); 1.296 + Log.d(LOGTAG, "refreshFromPanelInfos: refreshing built-in panel: " + panelConfig.getId()); 1.297 + } 1.298 + 1.299 + if (refreshedPanelConfig == null) { 1.300 + Log.d(LOGTAG, "refreshFromPanelInfos: no refreshed panel, falling back: " + panelConfig.getId()); 1.301 + continue; 1.302 + } 1.303 + 1.304 + Log.d(LOGTAG, "refreshFromPanelInfos: refreshed panel " + refreshedPanelConfig.getId()); 1.305 + editor.update(refreshedPanelConfig); 1.306 + } 1.307 + } 1.308 + 1.309 + /** 1.310 + * Runs in the background thread. 1.311 + */ 1.312 + private void executeRefresh(HomeConfig.Editor editor) { 1.313 + if (editor.isEmpty()) { 1.314 + return; 1.315 + } 1.316 + 1.317 + Log.d(LOGTAG, "executeRefresh"); 1.318 + 1.319 + final Set<String> ids = new HashSet<String>(); 1.320 + for (PanelConfig panelConfig : editor) { 1.321 + ids.add(panelConfig.getId()); 1.322 + } 1.323 + 1.324 + final Object panelRequestLock = new Object(); 1.325 + final List<PanelInfo> latestPanelInfos = new ArrayList<PanelInfo>(); 1.326 + 1.327 + final PanelInfoManager pm = new PanelInfoManager(); 1.328 + pm.requestPanelsById(ids, new RequestCallback() { 1.329 + @Override 1.330 + public void onComplete(List<PanelInfo> panelInfos) { 1.331 + synchronized(panelRequestLock) { 1.332 + latestPanelInfos.addAll(panelInfos); 1.333 + Log.d(LOGTAG, "executeRefresh: fetched panel infos: " + panelInfos.size()); 1.334 + 1.335 + panelRequestLock.notifyAll(); 1.336 + } 1.337 + } 1.338 + }); 1.339 + 1.340 + try { 1.341 + synchronized(panelRequestLock) { 1.342 + panelRequestLock.wait(PANEL_INFO_TIMEOUT_MSEC); 1.343 + 1.344 + Log.d(LOGTAG, "executeRefresh: done fetching panel infos"); 1.345 + refreshFromPanelInfos(editor, latestPanelInfos); 1.346 + } 1.347 + } catch (InterruptedException e) { 1.348 + Log.e(LOGTAG, "Failed to fetch panels from gecko", e); 1.349 + } 1.350 + } 1.351 + 1.352 + /** 1.353 + * Runs in the background thread. 1.354 + */ 1.355 + private class InvalidationRunnable implements Runnable { 1.356 + @Override 1.357 + public void run() { 1.358 + final HomeConfig.Editor editor = mHomeConfig.load().edit(); 1.359 + executePendingChanges(editor); 1.360 + editor.commit(); 1.361 + } 1.362 + }; 1.363 +}