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.home; michael@0: michael@0: import static org.mozilla.gecko.home.HomeConfig.createBuiltinPanelConfig; michael@0: michael@0: import java.util.ArrayList; michael@0: import java.util.HashSet; michael@0: import java.util.List; michael@0: import java.util.Queue; michael@0: import java.util.Set; michael@0: import java.util.concurrent.ConcurrentLinkedQueue; michael@0: michael@0: import org.json.JSONException; michael@0: import org.json.JSONObject; michael@0: import org.mozilla.gecko.db.HomeProvider; michael@0: import org.mozilla.gecko.GeckoAppShell; michael@0: import org.mozilla.gecko.home.HomeConfig.PanelConfig; michael@0: import org.mozilla.gecko.home.PanelInfoManager.PanelInfo; michael@0: import org.mozilla.gecko.home.PanelInfoManager.RequestCallback; michael@0: import org.mozilla.gecko.util.GeckoEventListener; michael@0: import org.mozilla.gecko.util.ThreadUtils; michael@0: michael@0: import android.content.ContentResolver; michael@0: import android.content.Context; michael@0: import android.os.Handler; michael@0: import android.util.Log; michael@0: michael@0: public class HomePanelsManager implements GeckoEventListener { michael@0: public static final String LOGTAG = "HomePanelsManager"; michael@0: michael@0: private static final HomePanelsManager sInstance = new HomePanelsManager(); michael@0: michael@0: private static final int INVALIDATION_DELAY_MSEC = 500; michael@0: private static final int PANEL_INFO_TIMEOUT_MSEC = 1000; michael@0: michael@0: private static final String EVENT_HOMEPANELS_INSTALL = "HomePanels:Install"; michael@0: private static final String EVENT_HOMEPANELS_UNINSTALL = "HomePanels:Uninstall"; michael@0: private static final String EVENT_HOMEPANELS_UPDATE = "HomePanels:Update"; michael@0: private static final String EVENT_HOMEPANELS_REFRESH = "HomePanels:RefreshDataset"; michael@0: michael@0: private static final String JSON_KEY_PANEL = "panel"; michael@0: private static final String JSON_KEY_PANEL_ID = "id"; michael@0: michael@0: private enum ChangeType { michael@0: UNINSTALL, michael@0: INSTALL, michael@0: UPDATE, michael@0: REFRESH michael@0: } michael@0: michael@0: private enum InvalidationMode { michael@0: DELAYED, michael@0: IMMEDIATE michael@0: } michael@0: michael@0: private static class ConfigChange { michael@0: private final ChangeType type; michael@0: private final Object target; michael@0: michael@0: public ConfigChange(ChangeType type) { michael@0: this(type, null); michael@0: } michael@0: michael@0: public ConfigChange(ChangeType type, Object target) { michael@0: this.type = type; michael@0: this.target = target; michael@0: } michael@0: } michael@0: michael@0: private Context mContext; michael@0: private HomeConfig mHomeConfig; michael@0: michael@0: private final Queue mPendingChanges = new ConcurrentLinkedQueue(); michael@0: private final Runnable mInvalidationRunnable = new InvalidationRunnable(); michael@0: michael@0: public static HomePanelsManager getInstance() { michael@0: return sInstance; michael@0: } michael@0: michael@0: public void init(Context context) { michael@0: mContext = context; michael@0: mHomeConfig = HomeConfig.getDefault(context); michael@0: michael@0: GeckoAppShell.getEventDispatcher().registerEventListener(EVENT_HOMEPANELS_INSTALL, this); michael@0: GeckoAppShell.getEventDispatcher().registerEventListener(EVENT_HOMEPANELS_UNINSTALL, this); michael@0: GeckoAppShell.getEventDispatcher().registerEventListener(EVENT_HOMEPANELS_UPDATE, this); michael@0: GeckoAppShell.getEventDispatcher().registerEventListener(EVENT_HOMEPANELS_REFRESH, this); michael@0: } michael@0: michael@0: public void onLocaleReady(final String locale) { michael@0: ThreadUtils.getBackgroundHandler().post(new Runnable() { michael@0: @Override michael@0: public void run() { michael@0: final String configLocale = mHomeConfig.getLocale(); michael@0: if (configLocale == null || !configLocale.equals(locale)) { michael@0: handleLocaleChange(); michael@0: } michael@0: } michael@0: }); michael@0: } michael@0: michael@0: @Override michael@0: public void handleMessage(String event, JSONObject message) { michael@0: try { michael@0: if (event.equals(EVENT_HOMEPANELS_INSTALL)) { michael@0: Log.d(LOGTAG, EVENT_HOMEPANELS_INSTALL); michael@0: handlePanelInstall(createPanelConfigFromMessage(message), InvalidationMode.DELAYED); michael@0: } else if (event.equals(EVENT_HOMEPANELS_UNINSTALL)) { michael@0: Log.d(LOGTAG, EVENT_HOMEPANELS_UNINSTALL); michael@0: final String panelId = message.getString(JSON_KEY_PANEL_ID); michael@0: handlePanelUninstall(panelId); michael@0: } else if (event.equals(EVENT_HOMEPANELS_UPDATE)) { michael@0: Log.d(LOGTAG, EVENT_HOMEPANELS_UPDATE); michael@0: handlePanelUpdate(createPanelConfigFromMessage(message)); michael@0: } else if (event.equals(EVENT_HOMEPANELS_REFRESH)) { michael@0: Log.d(LOGTAG, EVENT_HOMEPANELS_REFRESH); michael@0: handleDatasetRefresh(message); michael@0: } michael@0: } catch (Exception e) { michael@0: Log.e(LOGTAG, "Failed to handle event " + event, e); michael@0: } michael@0: } michael@0: michael@0: private PanelConfig createPanelConfigFromMessage(JSONObject message) throws JSONException { michael@0: final JSONObject json = message.getJSONObject(JSON_KEY_PANEL); michael@0: return new PanelConfig(json); michael@0: } michael@0: michael@0: /** michael@0: * Adds a new PanelConfig to the HomeConfig. michael@0: * michael@0: * This posts the invalidation of HomeConfig immediately. michael@0: * michael@0: * @param panelConfig panel to add michael@0: */ michael@0: public void installPanel(PanelConfig panelConfig) { michael@0: Log.d(LOGTAG, "installPanel: " + panelConfig.getTitle()); michael@0: handlePanelInstall(panelConfig, InvalidationMode.IMMEDIATE); michael@0: } michael@0: michael@0: /** michael@0: * Runs in the gecko thread. michael@0: */ michael@0: private void handlePanelInstall(PanelConfig panelConfig, InvalidationMode mode) { michael@0: mPendingChanges.offer(new ConfigChange(ChangeType.INSTALL, panelConfig)); michael@0: Log.d(LOGTAG, "handlePanelInstall: " + mPendingChanges.size()); michael@0: michael@0: scheduleInvalidation(mode); michael@0: } michael@0: michael@0: /** michael@0: * Runs in the gecko thread. michael@0: */ michael@0: private void handlePanelUninstall(String panelId) { michael@0: mPendingChanges.offer(new ConfigChange(ChangeType.UNINSTALL, panelId)); michael@0: Log.d(LOGTAG, "handlePanelUninstall: " + mPendingChanges.size()); michael@0: michael@0: scheduleInvalidation(InvalidationMode.DELAYED); michael@0: } michael@0: michael@0: /** michael@0: * Runs in the gecko thread. michael@0: */ michael@0: private void handlePanelUpdate(PanelConfig panelConfig) { michael@0: mPendingChanges.offer(new ConfigChange(ChangeType.UPDATE, panelConfig)); michael@0: Log.d(LOGTAG, "handlePanelUpdate: " + mPendingChanges.size()); michael@0: michael@0: scheduleInvalidation(InvalidationMode.DELAYED); michael@0: } michael@0: michael@0: /** michael@0: * Runs in the background thread. michael@0: */ michael@0: private void handleLocaleChange() { michael@0: mPendingChanges.offer(new ConfigChange(ChangeType.REFRESH)); michael@0: Log.d(LOGTAG, "handleLocaleChange: " + mPendingChanges.size()); michael@0: michael@0: scheduleInvalidation(InvalidationMode.IMMEDIATE); michael@0: } michael@0: michael@0: michael@0: /** michael@0: * Handles a dataset refresh request from Gecko. This is usually michael@0: * triggered by a HomeStorage.save() call in an add-on. michael@0: * michael@0: * Runs in the gecko thread. michael@0: */ michael@0: private void handleDatasetRefresh(JSONObject message) { michael@0: final String datasetId; michael@0: try { michael@0: datasetId = message.getString("datasetId"); michael@0: } catch (JSONException e) { michael@0: Log.e(LOGTAG, "Failed to handle dataset refresh", e); michael@0: return; michael@0: } michael@0: michael@0: Log.d(LOGTAG, "Refresh request for dataset: " + datasetId); michael@0: michael@0: final ContentResolver cr = mContext.getContentResolver(); michael@0: cr.notifyChange(HomeProvider.getDatasetNotificationUri(datasetId), null); michael@0: } michael@0: michael@0: /** michael@0: * Runs in the gecko or main thread. michael@0: */ michael@0: private void scheduleInvalidation(InvalidationMode mode) { michael@0: final Handler handler = ThreadUtils.getBackgroundHandler(); michael@0: michael@0: handler.removeCallbacks(mInvalidationRunnable); michael@0: michael@0: if (mode == InvalidationMode.IMMEDIATE) { michael@0: handler.post(mInvalidationRunnable); michael@0: } else { michael@0: handler.postDelayed(mInvalidationRunnable, INVALIDATION_DELAY_MSEC); michael@0: } michael@0: michael@0: Log.d(LOGTAG, "scheduleInvalidation: scheduled new invalidation: " + mode); michael@0: } michael@0: michael@0: /** michael@0: * Runs in the background thread. michael@0: */ michael@0: private void executePendingChanges(HomeConfig.Editor editor) { michael@0: boolean shouldRefresh = false; michael@0: michael@0: while (!mPendingChanges.isEmpty()) { michael@0: final ConfigChange pendingChange = mPendingChanges.poll(); michael@0: michael@0: switch (pendingChange.type) { michael@0: case UNINSTALL: { michael@0: final String panelId = (String) pendingChange.target; michael@0: if (editor.uninstall(panelId)) { michael@0: Log.d(LOGTAG, "executePendingChanges: uninstalled panel " + panelId); michael@0: } michael@0: break; michael@0: } michael@0: michael@0: case INSTALL: { michael@0: final PanelConfig panelConfig = (PanelConfig) pendingChange.target; michael@0: if (editor.install(panelConfig)) { michael@0: Log.d(LOGTAG, "executePendingChanges: added panel " + panelConfig.getId()); michael@0: } michael@0: break; michael@0: } michael@0: michael@0: case UPDATE: { michael@0: final PanelConfig panelConfig = (PanelConfig) pendingChange.target; michael@0: if (editor.update(panelConfig)) { michael@0: Log.w(LOGTAG, "executePendingChanges: updated panel " + panelConfig.getId()); michael@0: } michael@0: break; michael@0: } michael@0: michael@0: case REFRESH: { michael@0: shouldRefresh = true; michael@0: } michael@0: } michael@0: } michael@0: michael@0: // The editor still represents the default HomeConfig michael@0: // configuration and hasn't been changed by any operation michael@0: // above. No need to refresh as the HomeConfig backend will michael@0: // take of forcing all existing HomeConfigLoader instances to michael@0: // refresh their contents. michael@0: if (shouldRefresh && !editor.isDefault()) { michael@0: executeRefresh(editor); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Runs in the background thread. michael@0: */ michael@0: private void refreshFromPanelInfos(HomeConfig.Editor editor, List panelInfos) { michael@0: Log.d(LOGTAG, "refreshFromPanelInfos"); michael@0: michael@0: for (PanelConfig panelConfig : editor) { michael@0: PanelConfig refreshedPanelConfig = null; michael@0: michael@0: if (panelConfig.isDynamic()) { michael@0: for (PanelInfo panelInfo : panelInfos) { michael@0: if (panelInfo.getId().equals(panelConfig.getId())) { michael@0: refreshedPanelConfig = panelInfo.toPanelConfig(); michael@0: Log.d(LOGTAG, "refreshFromPanelInfos: refreshing from panel info: " + panelInfo.getId()); michael@0: break; michael@0: } michael@0: } michael@0: } else { michael@0: refreshedPanelConfig = createBuiltinPanelConfig(mContext, panelConfig.getType()); michael@0: Log.d(LOGTAG, "refreshFromPanelInfos: refreshing built-in panel: " + panelConfig.getId()); michael@0: } michael@0: michael@0: if (refreshedPanelConfig == null) { michael@0: Log.d(LOGTAG, "refreshFromPanelInfos: no refreshed panel, falling back: " + panelConfig.getId()); michael@0: continue; michael@0: } michael@0: michael@0: Log.d(LOGTAG, "refreshFromPanelInfos: refreshed panel " + refreshedPanelConfig.getId()); michael@0: editor.update(refreshedPanelConfig); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Runs in the background thread. michael@0: */ michael@0: private void executeRefresh(HomeConfig.Editor editor) { michael@0: if (editor.isEmpty()) { michael@0: return; michael@0: } michael@0: michael@0: Log.d(LOGTAG, "executeRefresh"); michael@0: michael@0: final Set ids = new HashSet(); michael@0: for (PanelConfig panelConfig : editor) { michael@0: ids.add(panelConfig.getId()); michael@0: } michael@0: michael@0: final Object panelRequestLock = new Object(); michael@0: final List latestPanelInfos = new ArrayList(); michael@0: michael@0: final PanelInfoManager pm = new PanelInfoManager(); michael@0: pm.requestPanelsById(ids, new RequestCallback() { michael@0: @Override michael@0: public void onComplete(List panelInfos) { michael@0: synchronized(panelRequestLock) { michael@0: latestPanelInfos.addAll(panelInfos); michael@0: Log.d(LOGTAG, "executeRefresh: fetched panel infos: " + panelInfos.size()); michael@0: michael@0: panelRequestLock.notifyAll(); michael@0: } michael@0: } michael@0: }); michael@0: michael@0: try { michael@0: synchronized(panelRequestLock) { michael@0: panelRequestLock.wait(PANEL_INFO_TIMEOUT_MSEC); michael@0: michael@0: Log.d(LOGTAG, "executeRefresh: done fetching panel infos"); michael@0: refreshFromPanelInfos(editor, latestPanelInfos); michael@0: } michael@0: } catch (InterruptedException e) { michael@0: Log.e(LOGTAG, "Failed to fetch panels from gecko", e); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Runs in the background thread. michael@0: */ michael@0: private class InvalidationRunnable implements Runnable { michael@0: @Override michael@0: public void run() { michael@0: final HomeConfig.Editor editor = mHomeConfig.load().edit(); michael@0: executePendingChanges(editor); michael@0: editor.commit(); michael@0: } michael@0: }; michael@0: }