Wed, 31 Dec 2014 07:22:50 +0100
Correct previous dual key logic pending first delivery installment.
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 package org.mozilla.gecko.sync;
7 import java.util.ArrayList;
8 import java.util.List;
9 import java.util.Map;
10 import java.util.concurrent.ConcurrentHashMap;
11 import java.util.concurrent.atomic.AtomicInteger;
13 import org.json.simple.JSONArray;
14 import org.json.simple.JSONObject;
15 import org.mozilla.gecko.BrowserLocaleManager;
16 import org.mozilla.gecko.R;
17 import org.mozilla.gecko.background.common.log.Logger;
18 import org.mozilla.gecko.sync.repositories.NullCursorException;
19 import org.mozilla.gecko.sync.repositories.android.ClientsDatabaseAccessor;
20 import org.mozilla.gecko.sync.repositories.domain.ClientRecord;
22 import android.app.Notification;
23 import android.app.NotificationManager;
24 import android.app.PendingIntent;
25 import android.content.Context;
26 import android.content.Intent;
27 import android.net.Uri;
29 /**
30 * Process commands received from Sync clients.
31 * <p>
32 * We need a command processor at two different times:
33 * <ol>
34 * <li>We execute commands during the "clients" engine stage of a Sync. Each
35 * command takes a <code>GlobalSession</code> instance as a parameter.</li>
36 * <li>We queue commands to be executed or propagated to other Sync clients
37 * during an activity completely unrelated to a sync (such as
38 * <code>SendTabActivity</code>.)</li>
39 * </ol>
40 * To provide a processor for both these time frames, we maintain a static
41 * long-lived singleton.
42 */
43 public class CommandProcessor {
44 private static final String LOG_TAG = "Command";
45 private static AtomicInteger currentId = new AtomicInteger();
46 protected ConcurrentHashMap<String, CommandRunner> commands = new ConcurrentHashMap<String, CommandRunner>();
48 private final static CommandProcessor processor = new CommandProcessor();
50 /**
51 * Get the global singleton command processor.
52 *
53 * @return the singleton processor.
54 */
55 public static CommandProcessor getProcessor() {
56 return processor;
57 }
59 public static class Command {
60 public final String commandType;
61 public final JSONArray args;
62 private List<String> argsList;
64 public Command(String commandType, JSONArray args) {
65 this.commandType = commandType;
66 this.args = args;
67 }
69 /**
70 * Get list of arguments as strings. Individual arguments may be null.
71 *
72 * @return list of strings.
73 */
74 public synchronized List<String> getArgsList() {
75 if (argsList == null) {
76 ArrayList<String> argsList = new ArrayList<String>(args.size());
78 for (int i = 0; i < args.size(); i++) {
79 final Object arg = args.get(i);
80 if (arg == null) {
81 argsList.add(null);
82 continue;
83 }
84 argsList.add(arg.toString());
85 }
86 this.argsList = argsList;
87 }
88 return this.argsList;
89 }
91 @SuppressWarnings("unchecked")
92 public JSONObject asJSONObject() {
93 JSONObject out = new JSONObject();
94 out.put("command", this.commandType);
95 out.put("args", this.args);
96 return out;
97 }
98 }
100 /**
101 * Register a command.
102 * <p>
103 * Any existing registration is overwritten.
104 *
105 * @param commandType
106 * the name of the command, i.e., "displayURI".
107 * @param command
108 * the <code>CommandRunner</code> instance that should handle the
109 * command.
110 */
111 public void registerCommand(String commandType, CommandRunner command) {
112 commands.put(commandType, command);
113 }
115 /**
116 * Process a command in the context of the given global session.
117 *
118 * @param session
119 * the <code>GlobalSession</code> instance currently executing.
120 * @param unparsedCommand
121 * command as a <code>ExtendedJSONObject</code> instance.
122 */
123 public void processCommand(final GlobalSession session, ExtendedJSONObject unparsedCommand) {
124 Command command = parseCommand(unparsedCommand);
125 if (command == null) {
126 Logger.debug(LOG_TAG, "Invalid command: " + unparsedCommand + " will not be processed.");
127 return;
128 }
130 CommandRunner executableCommand = commands.get(command.commandType);
131 if (executableCommand == null) {
132 Logger.debug(LOG_TAG, "Command \"" + command.commandType + "\" not registered and will not be processed.");
133 return;
134 }
136 executableCommand.executeCommand(session, command.getArgsList());
137 }
139 /**
140 * Parse a JSON command into a ParsedCommand object for easier handling.
141 *
142 * @param unparsedCommand - command as ExtendedJSONObject
143 * @return - null if command is invalid, else return ParsedCommand with
144 * no null attributes.
145 */
146 protected static Command parseCommand(ExtendedJSONObject unparsedCommand) {
147 String type = (String) unparsedCommand.get("command");
148 if (type == null) {
149 return null;
150 }
152 try {
153 JSONArray unparsedArgs = unparsedCommand.getArray("args");
154 if (unparsedArgs == null) {
155 return null;
156 }
158 return new Command(type, unparsedArgs);
159 } catch (NonArrayJSONException e) {
160 Logger.debug(LOG_TAG, "Unable to parse args array. Invalid command");
161 return null;
162 }
163 }
165 @SuppressWarnings("unchecked")
166 public void sendURIToClientForDisplay(String uri, String clientID, String title, String sender, Context context) {
167 Logger.info(LOG_TAG, "Sending URI to client " + clientID + ".");
168 if (Logger.LOG_PERSONAL_INFORMATION) {
169 Logger.pii(LOG_TAG, "URI is " + uri + "; title is '" + title + "'.");
170 }
172 final JSONArray args = new JSONArray();
173 args.add(uri);
174 args.add(sender);
175 args.add(title);
177 final Command displayURICommand = new Command("displayURI", args);
178 this.sendCommand(clientID, displayURICommand, context);
179 }
181 /**
182 * Validates and sends a command to a client or all clients.
183 *
184 * Calling this does not actually sync the command data to the server. If the
185 * client already has the command/args pair, it won't receive a duplicate
186 * command.
187 *
188 * @param clientID
189 * Client ID to send command to. If null, send to all remote
190 * clients.
191 * @param command
192 * Command to invoke on remote clients
193 */
194 public void sendCommand(String clientID, Command command, Context context) {
195 Logger.debug(LOG_TAG, "In sendCommand.");
197 CommandRunner commandData = commands.get(command.commandType);
199 // Don't send commands that we don't know about.
200 if (commandData == null) {
201 Logger.error(LOG_TAG, "Unknown command to send: " + command);
202 return;
203 }
205 // Don't send a command with the wrong number of arguments.
206 if (!commandData.argumentsAreValid(command.getArgsList())) {
207 Logger.error(LOG_TAG, "Expected " + commandData.argCount + " args for '" +
208 command + "', but got " + command.args);
209 return;
210 }
212 if (clientID != null) {
213 this.sendCommandToClient(clientID, command, context);
214 return;
215 }
217 ClientsDatabaseAccessor db = new ClientsDatabaseAccessor(context);
218 try {
219 Map<String, ClientRecord> clientMap = db.fetchAllClients();
220 for (ClientRecord client : clientMap.values()) {
221 this.sendCommandToClient(client.guid, command, context);
222 }
223 } catch (NullCursorException e) {
224 Logger.error(LOG_TAG, "NullCursorException when fetching all GUIDs");
225 } finally {
226 db.close();
227 }
228 }
230 protected void sendCommandToClient(String clientID, Command command, Context context) {
231 Logger.info(LOG_TAG, "Sending " + command.commandType + " to " + clientID);
233 ClientsDatabaseAccessor db = new ClientsDatabaseAccessor(context);
234 try {
235 db.store(clientID, command);
236 } catch (NullCursorException e) {
237 Logger.error(LOG_TAG, "NullCursorException: Unable to send command.");
238 } finally {
239 db.close();
240 }
241 }
243 private static volatile boolean didUpdateLocale = false;
245 @SuppressWarnings("deprecation")
246 public static void displayURI(final List<String> args, final Context context) {
247 // We trust the client sender that these exist.
248 final String uri = args.get(0);
249 final String clientId = args.get(1);
251 Logger.pii(LOG_TAG, "Received a URI for display: " + uri + " from " + clientId);
253 String title = null;
254 if (args.size() == 3) {
255 title = args.get(2);
256 }
258 // We don't care too much about races, but let's try to avoid
259 // unnecessary work.
260 if (!didUpdateLocale) {
261 BrowserLocaleManager.getInstance().getAndApplyPersistedLocale(context);
262 didUpdateLocale = true;
263 }
265 final String ns = Context.NOTIFICATION_SERVICE;
266 final NotificationManager notificationManager = (NotificationManager) context.getSystemService(ns);
268 // Create a Notification.
269 final int icon = R.drawable.icon;
270 String notificationTitle = context.getString(R.string.sync_new_tab);
271 if (title != null) {
272 notificationTitle = notificationTitle.concat(": " + title);
273 }
275 final long when = System.currentTimeMillis();
276 Notification notification = new Notification(icon, notificationTitle, when);
277 notification.flags = Notification.FLAG_AUTO_CANCEL;
279 // Set pending intent associated with the notification.
280 Intent notificationIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(uri));
281 PendingIntent contentIntent = PendingIntent.getActivity(context, 0, notificationIntent, 0);
282 notification.setLatestEventInfo(context, notificationTitle, uri, contentIntent);
284 // Send notification.
285 notificationManager.notify(currentId.getAndIncrement(), notification);
286 }
287 }