1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/mobile/android/base/sync/CommandProcessor.java Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,287 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +package org.mozilla.gecko.sync; 1.9 + 1.10 +import java.util.ArrayList; 1.11 +import java.util.List; 1.12 +import java.util.Map; 1.13 +import java.util.concurrent.ConcurrentHashMap; 1.14 +import java.util.concurrent.atomic.AtomicInteger; 1.15 + 1.16 +import org.json.simple.JSONArray; 1.17 +import org.json.simple.JSONObject; 1.18 +import org.mozilla.gecko.BrowserLocaleManager; 1.19 +import org.mozilla.gecko.R; 1.20 +import org.mozilla.gecko.background.common.log.Logger; 1.21 +import org.mozilla.gecko.sync.repositories.NullCursorException; 1.22 +import org.mozilla.gecko.sync.repositories.android.ClientsDatabaseAccessor; 1.23 +import org.mozilla.gecko.sync.repositories.domain.ClientRecord; 1.24 + 1.25 +import android.app.Notification; 1.26 +import android.app.NotificationManager; 1.27 +import android.app.PendingIntent; 1.28 +import android.content.Context; 1.29 +import android.content.Intent; 1.30 +import android.net.Uri; 1.31 + 1.32 +/** 1.33 + * Process commands received from Sync clients. 1.34 + * <p> 1.35 + * We need a command processor at two different times: 1.36 + * <ol> 1.37 + * <li>We execute commands during the "clients" engine stage of a Sync. Each 1.38 + * command takes a <code>GlobalSession</code> instance as a parameter.</li> 1.39 + * <li>We queue commands to be executed or propagated to other Sync clients 1.40 + * during an activity completely unrelated to a sync (such as 1.41 + * <code>SendTabActivity</code>.)</li> 1.42 + * </ol> 1.43 + * To provide a processor for both these time frames, we maintain a static 1.44 + * long-lived singleton. 1.45 + */ 1.46 +public class CommandProcessor { 1.47 + private static final String LOG_TAG = "Command"; 1.48 + private static AtomicInteger currentId = new AtomicInteger(); 1.49 + protected ConcurrentHashMap<String, CommandRunner> commands = new ConcurrentHashMap<String, CommandRunner>(); 1.50 + 1.51 + private final static CommandProcessor processor = new CommandProcessor(); 1.52 + 1.53 + /** 1.54 + * Get the global singleton command processor. 1.55 + * 1.56 + * @return the singleton processor. 1.57 + */ 1.58 + public static CommandProcessor getProcessor() { 1.59 + return processor; 1.60 + } 1.61 + 1.62 + public static class Command { 1.63 + public final String commandType; 1.64 + public final JSONArray args; 1.65 + private List<String> argsList; 1.66 + 1.67 + public Command(String commandType, JSONArray args) { 1.68 + this.commandType = commandType; 1.69 + this.args = args; 1.70 + } 1.71 + 1.72 + /** 1.73 + * Get list of arguments as strings. Individual arguments may be null. 1.74 + * 1.75 + * @return list of strings. 1.76 + */ 1.77 + public synchronized List<String> getArgsList() { 1.78 + if (argsList == null) { 1.79 + ArrayList<String> argsList = new ArrayList<String>(args.size()); 1.80 + 1.81 + for (int i = 0; i < args.size(); i++) { 1.82 + final Object arg = args.get(i); 1.83 + if (arg == null) { 1.84 + argsList.add(null); 1.85 + continue; 1.86 + } 1.87 + argsList.add(arg.toString()); 1.88 + } 1.89 + this.argsList = argsList; 1.90 + } 1.91 + return this.argsList; 1.92 + } 1.93 + 1.94 + @SuppressWarnings("unchecked") 1.95 + public JSONObject asJSONObject() { 1.96 + JSONObject out = new JSONObject(); 1.97 + out.put("command", this.commandType); 1.98 + out.put("args", this.args); 1.99 + return out; 1.100 + } 1.101 + } 1.102 + 1.103 + /** 1.104 + * Register a command. 1.105 + * <p> 1.106 + * Any existing registration is overwritten. 1.107 + * 1.108 + * @param commandType 1.109 + * the name of the command, i.e., "displayURI". 1.110 + * @param command 1.111 + * the <code>CommandRunner</code> instance that should handle the 1.112 + * command. 1.113 + */ 1.114 + public void registerCommand(String commandType, CommandRunner command) { 1.115 + commands.put(commandType, command); 1.116 + } 1.117 + 1.118 + /** 1.119 + * Process a command in the context of the given global session. 1.120 + * 1.121 + * @param session 1.122 + * the <code>GlobalSession</code> instance currently executing. 1.123 + * @param unparsedCommand 1.124 + * command as a <code>ExtendedJSONObject</code> instance. 1.125 + */ 1.126 + public void processCommand(final GlobalSession session, ExtendedJSONObject unparsedCommand) { 1.127 + Command command = parseCommand(unparsedCommand); 1.128 + if (command == null) { 1.129 + Logger.debug(LOG_TAG, "Invalid command: " + unparsedCommand + " will not be processed."); 1.130 + return; 1.131 + } 1.132 + 1.133 + CommandRunner executableCommand = commands.get(command.commandType); 1.134 + if (executableCommand == null) { 1.135 + Logger.debug(LOG_TAG, "Command \"" + command.commandType + "\" not registered and will not be processed."); 1.136 + return; 1.137 + } 1.138 + 1.139 + executableCommand.executeCommand(session, command.getArgsList()); 1.140 + } 1.141 + 1.142 + /** 1.143 + * Parse a JSON command into a ParsedCommand object for easier handling. 1.144 + * 1.145 + * @param unparsedCommand - command as ExtendedJSONObject 1.146 + * @return - null if command is invalid, else return ParsedCommand with 1.147 + * no null attributes. 1.148 + */ 1.149 + protected static Command parseCommand(ExtendedJSONObject unparsedCommand) { 1.150 + String type = (String) unparsedCommand.get("command"); 1.151 + if (type == null) { 1.152 + return null; 1.153 + } 1.154 + 1.155 + try { 1.156 + JSONArray unparsedArgs = unparsedCommand.getArray("args"); 1.157 + if (unparsedArgs == null) { 1.158 + return null; 1.159 + } 1.160 + 1.161 + return new Command(type, unparsedArgs); 1.162 + } catch (NonArrayJSONException e) { 1.163 + Logger.debug(LOG_TAG, "Unable to parse args array. Invalid command"); 1.164 + return null; 1.165 + } 1.166 + } 1.167 + 1.168 + @SuppressWarnings("unchecked") 1.169 + public void sendURIToClientForDisplay(String uri, String clientID, String title, String sender, Context context) { 1.170 + Logger.info(LOG_TAG, "Sending URI to client " + clientID + "."); 1.171 + if (Logger.LOG_PERSONAL_INFORMATION) { 1.172 + Logger.pii(LOG_TAG, "URI is " + uri + "; title is '" + title + "'."); 1.173 + } 1.174 + 1.175 + final JSONArray args = new JSONArray(); 1.176 + args.add(uri); 1.177 + args.add(sender); 1.178 + args.add(title); 1.179 + 1.180 + final Command displayURICommand = new Command("displayURI", args); 1.181 + this.sendCommand(clientID, displayURICommand, context); 1.182 + } 1.183 + 1.184 + /** 1.185 + * Validates and sends a command to a client or all clients. 1.186 + * 1.187 + * Calling this does not actually sync the command data to the server. If the 1.188 + * client already has the command/args pair, it won't receive a duplicate 1.189 + * command. 1.190 + * 1.191 + * @param clientID 1.192 + * Client ID to send command to. If null, send to all remote 1.193 + * clients. 1.194 + * @param command 1.195 + * Command to invoke on remote clients 1.196 + */ 1.197 + public void sendCommand(String clientID, Command command, Context context) { 1.198 + Logger.debug(LOG_TAG, "In sendCommand."); 1.199 + 1.200 + CommandRunner commandData = commands.get(command.commandType); 1.201 + 1.202 + // Don't send commands that we don't know about. 1.203 + if (commandData == null) { 1.204 + Logger.error(LOG_TAG, "Unknown command to send: " + command); 1.205 + return; 1.206 + } 1.207 + 1.208 + // Don't send a command with the wrong number of arguments. 1.209 + if (!commandData.argumentsAreValid(command.getArgsList())) { 1.210 + Logger.error(LOG_TAG, "Expected " + commandData.argCount + " args for '" + 1.211 + command + "', but got " + command.args); 1.212 + return; 1.213 + } 1.214 + 1.215 + if (clientID != null) { 1.216 + this.sendCommandToClient(clientID, command, context); 1.217 + return; 1.218 + } 1.219 + 1.220 + ClientsDatabaseAccessor db = new ClientsDatabaseAccessor(context); 1.221 + try { 1.222 + Map<String, ClientRecord> clientMap = db.fetchAllClients(); 1.223 + for (ClientRecord client : clientMap.values()) { 1.224 + this.sendCommandToClient(client.guid, command, context); 1.225 + } 1.226 + } catch (NullCursorException e) { 1.227 + Logger.error(LOG_TAG, "NullCursorException when fetching all GUIDs"); 1.228 + } finally { 1.229 + db.close(); 1.230 + } 1.231 + } 1.232 + 1.233 + protected void sendCommandToClient(String clientID, Command command, Context context) { 1.234 + Logger.info(LOG_TAG, "Sending " + command.commandType + " to " + clientID); 1.235 + 1.236 + ClientsDatabaseAccessor db = new ClientsDatabaseAccessor(context); 1.237 + try { 1.238 + db.store(clientID, command); 1.239 + } catch (NullCursorException e) { 1.240 + Logger.error(LOG_TAG, "NullCursorException: Unable to send command."); 1.241 + } finally { 1.242 + db.close(); 1.243 + } 1.244 + } 1.245 + 1.246 + private static volatile boolean didUpdateLocale = false; 1.247 + 1.248 + @SuppressWarnings("deprecation") 1.249 + public static void displayURI(final List<String> args, final Context context) { 1.250 + // We trust the client sender that these exist. 1.251 + final String uri = args.get(0); 1.252 + final String clientId = args.get(1); 1.253 + 1.254 + Logger.pii(LOG_TAG, "Received a URI for display: " + uri + " from " + clientId); 1.255 + 1.256 + String title = null; 1.257 + if (args.size() == 3) { 1.258 + title = args.get(2); 1.259 + } 1.260 + 1.261 + // We don't care too much about races, but let's try to avoid 1.262 + // unnecessary work. 1.263 + if (!didUpdateLocale) { 1.264 + BrowserLocaleManager.getInstance().getAndApplyPersistedLocale(context); 1.265 + didUpdateLocale = true; 1.266 + } 1.267 + 1.268 + final String ns = Context.NOTIFICATION_SERVICE; 1.269 + final NotificationManager notificationManager = (NotificationManager) context.getSystemService(ns); 1.270 + 1.271 + // Create a Notification. 1.272 + final int icon = R.drawable.icon; 1.273 + String notificationTitle = context.getString(R.string.sync_new_tab); 1.274 + if (title != null) { 1.275 + notificationTitle = notificationTitle.concat(": " + title); 1.276 + } 1.277 + 1.278 + final long when = System.currentTimeMillis(); 1.279 + Notification notification = new Notification(icon, notificationTitle, when); 1.280 + notification.flags = Notification.FLAG_AUTO_CANCEL; 1.281 + 1.282 + // Set pending intent associated with the notification. 1.283 + Intent notificationIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(uri)); 1.284 + PendingIntent contentIntent = PendingIntent.getActivity(context, 0, notificationIntent, 0); 1.285 + notification.setLatestEventInfo(context, notificationTitle, uri, contentIntent); 1.286 + 1.287 + // Send notification. 1.288 + notificationManager.notify(currentId.getAndIncrement(), notification); 1.289 + } 1.290 +}