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.sync; michael@0: michael@0: import java.util.ArrayList; michael@0: import java.util.List; michael@0: import java.util.Map; michael@0: import java.util.concurrent.ConcurrentHashMap; michael@0: import java.util.concurrent.atomic.AtomicInteger; michael@0: michael@0: import org.json.simple.JSONArray; michael@0: import org.json.simple.JSONObject; michael@0: import org.mozilla.gecko.BrowserLocaleManager; michael@0: import org.mozilla.gecko.R; michael@0: import org.mozilla.gecko.background.common.log.Logger; michael@0: import org.mozilla.gecko.sync.repositories.NullCursorException; michael@0: import org.mozilla.gecko.sync.repositories.android.ClientsDatabaseAccessor; michael@0: import org.mozilla.gecko.sync.repositories.domain.ClientRecord; michael@0: michael@0: import android.app.Notification; michael@0: import android.app.NotificationManager; michael@0: import android.app.PendingIntent; michael@0: import android.content.Context; michael@0: import android.content.Intent; michael@0: import android.net.Uri; michael@0: michael@0: /** michael@0: * Process commands received from Sync clients. michael@0: *

michael@0: * We need a command processor at two different times: michael@0: *

    michael@0: *
  1. We execute commands during the "clients" engine stage of a Sync. Each michael@0: * command takes a GlobalSession instance as a parameter.
  2. michael@0: *
  3. We queue commands to be executed or propagated to other Sync clients michael@0: * during an activity completely unrelated to a sync (such as michael@0: * SendTabActivity.)
  4. michael@0: *
michael@0: * To provide a processor for both these time frames, we maintain a static michael@0: * long-lived singleton. michael@0: */ michael@0: public class CommandProcessor { michael@0: private static final String LOG_TAG = "Command"; michael@0: private static AtomicInteger currentId = new AtomicInteger(); michael@0: protected ConcurrentHashMap commands = new ConcurrentHashMap(); michael@0: michael@0: private final static CommandProcessor processor = new CommandProcessor(); michael@0: michael@0: /** michael@0: * Get the global singleton command processor. michael@0: * michael@0: * @return the singleton processor. michael@0: */ michael@0: public static CommandProcessor getProcessor() { michael@0: return processor; michael@0: } michael@0: michael@0: public static class Command { michael@0: public final String commandType; michael@0: public final JSONArray args; michael@0: private List argsList; michael@0: michael@0: public Command(String commandType, JSONArray args) { michael@0: this.commandType = commandType; michael@0: this.args = args; michael@0: } michael@0: michael@0: /** michael@0: * Get list of arguments as strings. Individual arguments may be null. michael@0: * michael@0: * @return list of strings. michael@0: */ michael@0: public synchronized List getArgsList() { michael@0: if (argsList == null) { michael@0: ArrayList argsList = new ArrayList(args.size()); michael@0: michael@0: for (int i = 0; i < args.size(); i++) { michael@0: final Object arg = args.get(i); michael@0: if (arg == null) { michael@0: argsList.add(null); michael@0: continue; michael@0: } michael@0: argsList.add(arg.toString()); michael@0: } michael@0: this.argsList = argsList; michael@0: } michael@0: return this.argsList; michael@0: } michael@0: michael@0: @SuppressWarnings("unchecked") michael@0: public JSONObject asJSONObject() { michael@0: JSONObject out = new JSONObject(); michael@0: out.put("command", this.commandType); michael@0: out.put("args", this.args); michael@0: return out; michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Register a command. michael@0: *

michael@0: * Any existing registration is overwritten. michael@0: * michael@0: * @param commandType michael@0: * the name of the command, i.e., "displayURI". michael@0: * @param command michael@0: * the CommandRunner instance that should handle the michael@0: * command. michael@0: */ michael@0: public void registerCommand(String commandType, CommandRunner command) { michael@0: commands.put(commandType, command); michael@0: } michael@0: michael@0: /** michael@0: * Process a command in the context of the given global session. michael@0: * michael@0: * @param session michael@0: * the GlobalSession instance currently executing. michael@0: * @param unparsedCommand michael@0: * command as a ExtendedJSONObject instance. michael@0: */ michael@0: public void processCommand(final GlobalSession session, ExtendedJSONObject unparsedCommand) { michael@0: Command command = parseCommand(unparsedCommand); michael@0: if (command == null) { michael@0: Logger.debug(LOG_TAG, "Invalid command: " + unparsedCommand + " will not be processed."); michael@0: return; michael@0: } michael@0: michael@0: CommandRunner executableCommand = commands.get(command.commandType); michael@0: if (executableCommand == null) { michael@0: Logger.debug(LOG_TAG, "Command \"" + command.commandType + "\" not registered and will not be processed."); michael@0: return; michael@0: } michael@0: michael@0: executableCommand.executeCommand(session, command.getArgsList()); michael@0: } michael@0: michael@0: /** michael@0: * Parse a JSON command into a ParsedCommand object for easier handling. michael@0: * michael@0: * @param unparsedCommand - command as ExtendedJSONObject michael@0: * @return - null if command is invalid, else return ParsedCommand with michael@0: * no null attributes. michael@0: */ michael@0: protected static Command parseCommand(ExtendedJSONObject unparsedCommand) { michael@0: String type = (String) unparsedCommand.get("command"); michael@0: if (type == null) { michael@0: return null; michael@0: } michael@0: michael@0: try { michael@0: JSONArray unparsedArgs = unparsedCommand.getArray("args"); michael@0: if (unparsedArgs == null) { michael@0: return null; michael@0: } michael@0: michael@0: return new Command(type, unparsedArgs); michael@0: } catch (NonArrayJSONException e) { michael@0: Logger.debug(LOG_TAG, "Unable to parse args array. Invalid command"); michael@0: return null; michael@0: } michael@0: } michael@0: michael@0: @SuppressWarnings("unchecked") michael@0: public void sendURIToClientForDisplay(String uri, String clientID, String title, String sender, Context context) { michael@0: Logger.info(LOG_TAG, "Sending URI to client " + clientID + "."); michael@0: if (Logger.LOG_PERSONAL_INFORMATION) { michael@0: Logger.pii(LOG_TAG, "URI is " + uri + "; title is '" + title + "'."); michael@0: } michael@0: michael@0: final JSONArray args = new JSONArray(); michael@0: args.add(uri); michael@0: args.add(sender); michael@0: args.add(title); michael@0: michael@0: final Command displayURICommand = new Command("displayURI", args); michael@0: this.sendCommand(clientID, displayURICommand, context); michael@0: } michael@0: michael@0: /** michael@0: * Validates and sends a command to a client or all clients. michael@0: * michael@0: * Calling this does not actually sync the command data to the server. If the michael@0: * client already has the command/args pair, it won't receive a duplicate michael@0: * command. michael@0: * michael@0: * @param clientID michael@0: * Client ID to send command to. If null, send to all remote michael@0: * clients. michael@0: * @param command michael@0: * Command to invoke on remote clients michael@0: */ michael@0: public void sendCommand(String clientID, Command command, Context context) { michael@0: Logger.debug(LOG_TAG, "In sendCommand."); michael@0: michael@0: CommandRunner commandData = commands.get(command.commandType); michael@0: michael@0: // Don't send commands that we don't know about. michael@0: if (commandData == null) { michael@0: Logger.error(LOG_TAG, "Unknown command to send: " + command); michael@0: return; michael@0: } michael@0: michael@0: // Don't send a command with the wrong number of arguments. michael@0: if (!commandData.argumentsAreValid(command.getArgsList())) { michael@0: Logger.error(LOG_TAG, "Expected " + commandData.argCount + " args for '" + michael@0: command + "', but got " + command.args); michael@0: return; michael@0: } michael@0: michael@0: if (clientID != null) { michael@0: this.sendCommandToClient(clientID, command, context); michael@0: return; michael@0: } michael@0: michael@0: ClientsDatabaseAccessor db = new ClientsDatabaseAccessor(context); michael@0: try { michael@0: Map clientMap = db.fetchAllClients(); michael@0: for (ClientRecord client : clientMap.values()) { michael@0: this.sendCommandToClient(client.guid, command, context); michael@0: } michael@0: } catch (NullCursorException e) { michael@0: Logger.error(LOG_TAG, "NullCursorException when fetching all GUIDs"); michael@0: } finally { michael@0: db.close(); michael@0: } michael@0: } michael@0: michael@0: protected void sendCommandToClient(String clientID, Command command, Context context) { michael@0: Logger.info(LOG_TAG, "Sending " + command.commandType + " to " + clientID); michael@0: michael@0: ClientsDatabaseAccessor db = new ClientsDatabaseAccessor(context); michael@0: try { michael@0: db.store(clientID, command); michael@0: } catch (NullCursorException e) { michael@0: Logger.error(LOG_TAG, "NullCursorException: Unable to send command."); michael@0: } finally { michael@0: db.close(); michael@0: } michael@0: } michael@0: michael@0: private static volatile boolean didUpdateLocale = false; michael@0: michael@0: @SuppressWarnings("deprecation") michael@0: public static void displayURI(final List args, final Context context) { michael@0: // We trust the client sender that these exist. michael@0: final String uri = args.get(0); michael@0: final String clientId = args.get(1); michael@0: michael@0: Logger.pii(LOG_TAG, "Received a URI for display: " + uri + " from " + clientId); michael@0: michael@0: String title = null; michael@0: if (args.size() == 3) { michael@0: title = args.get(2); michael@0: } michael@0: michael@0: // We don't care too much about races, but let's try to avoid michael@0: // unnecessary work. michael@0: if (!didUpdateLocale) { michael@0: BrowserLocaleManager.getInstance().getAndApplyPersistedLocale(context); michael@0: didUpdateLocale = true; michael@0: } michael@0: michael@0: final String ns = Context.NOTIFICATION_SERVICE; michael@0: final NotificationManager notificationManager = (NotificationManager) context.getSystemService(ns); michael@0: michael@0: // Create a Notification. michael@0: final int icon = R.drawable.icon; michael@0: String notificationTitle = context.getString(R.string.sync_new_tab); michael@0: if (title != null) { michael@0: notificationTitle = notificationTitle.concat(": " + title); michael@0: } michael@0: michael@0: final long when = System.currentTimeMillis(); michael@0: Notification notification = new Notification(icon, notificationTitle, when); michael@0: notification.flags = Notification.FLAG_AUTO_CANCEL; michael@0: michael@0: // Set pending intent associated with the notification. michael@0: Intent notificationIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(uri)); michael@0: PendingIntent contentIntent = PendingIntent.getActivity(context, 0, notificationIntent, 0); michael@0: notification.setLatestEventInfo(context, notificationTitle, uri, contentIntent); michael@0: michael@0: // Send notification. michael@0: notificationManager.notify(currentId.getAndIncrement(), notification); michael@0: } michael@0: }