|
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/. */ |
|
4 |
|
5 package org.mozilla.gecko.sync; |
|
6 |
|
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; |
|
12 |
|
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; |
|
21 |
|
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; |
|
28 |
|
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>(); |
|
47 |
|
48 private final static CommandProcessor processor = new CommandProcessor(); |
|
49 |
|
50 /** |
|
51 * Get the global singleton command processor. |
|
52 * |
|
53 * @return the singleton processor. |
|
54 */ |
|
55 public static CommandProcessor getProcessor() { |
|
56 return processor; |
|
57 } |
|
58 |
|
59 public static class Command { |
|
60 public final String commandType; |
|
61 public final JSONArray args; |
|
62 private List<String> argsList; |
|
63 |
|
64 public Command(String commandType, JSONArray args) { |
|
65 this.commandType = commandType; |
|
66 this.args = args; |
|
67 } |
|
68 |
|
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()); |
|
77 |
|
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 } |
|
90 |
|
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 } |
|
99 |
|
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 } |
|
114 |
|
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 } |
|
129 |
|
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 } |
|
135 |
|
136 executableCommand.executeCommand(session, command.getArgsList()); |
|
137 } |
|
138 |
|
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 } |
|
151 |
|
152 try { |
|
153 JSONArray unparsedArgs = unparsedCommand.getArray("args"); |
|
154 if (unparsedArgs == null) { |
|
155 return null; |
|
156 } |
|
157 |
|
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 } |
|
164 |
|
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 } |
|
171 |
|
172 final JSONArray args = new JSONArray(); |
|
173 args.add(uri); |
|
174 args.add(sender); |
|
175 args.add(title); |
|
176 |
|
177 final Command displayURICommand = new Command("displayURI", args); |
|
178 this.sendCommand(clientID, displayURICommand, context); |
|
179 } |
|
180 |
|
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."); |
|
196 |
|
197 CommandRunner commandData = commands.get(command.commandType); |
|
198 |
|
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 } |
|
204 |
|
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 } |
|
211 |
|
212 if (clientID != null) { |
|
213 this.sendCommandToClient(clientID, command, context); |
|
214 return; |
|
215 } |
|
216 |
|
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 } |
|
229 |
|
230 protected void sendCommandToClient(String clientID, Command command, Context context) { |
|
231 Logger.info(LOG_TAG, "Sending " + command.commandType + " to " + clientID); |
|
232 |
|
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 } |
|
242 |
|
243 private static volatile boolean didUpdateLocale = false; |
|
244 |
|
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); |
|
250 |
|
251 Logger.pii(LOG_TAG, "Received a URI for display: " + uri + " from " + clientId); |
|
252 |
|
253 String title = null; |
|
254 if (args.size() == 3) { |
|
255 title = args.get(2); |
|
256 } |
|
257 |
|
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 } |
|
264 |
|
265 final String ns = Context.NOTIFICATION_SERVICE; |
|
266 final NotificationManager notificationManager = (NotificationManager) context.getSystemService(ns); |
|
267 |
|
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 } |
|
274 |
|
275 final long when = System.currentTimeMillis(); |
|
276 Notification notification = new Notification(icon, notificationTitle, when); |
|
277 notification.flags = Notification.FLAG_AUTO_CANCEL; |
|
278 |
|
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); |
|
283 |
|
284 // Send notification. |
|
285 notificationManager.notify(currentId.getAndIncrement(), notification); |
|
286 } |
|
287 } |