1package eu.siacs.conversations.services;
2
3import android.content.ComponentName;
4import android.content.Intent;
5import android.content.SharedPreferences;
6import android.content.pm.PackageManager;
7import android.os.Message;
8import android.os.Messenger;
9import android.os.RemoteException;
10import android.preference.PreferenceManager;
11import android.util.Log;
12import com.google.common.base.Optional;
13import com.google.common.base.Strings;
14import com.google.common.collect.Iterables;
15import com.google.common.io.BaseEncoding;
16import eu.siacs.conversations.Config;
17import eu.siacs.conversations.R;
18import eu.siacs.conversations.entities.Account;
19import eu.siacs.conversations.parser.AbstractParser;
20import eu.siacs.conversations.persistance.UnifiedPushDatabase;
21import eu.siacs.conversations.xml.Element;
22import eu.siacs.conversations.xml.Namespace;
23import eu.siacs.conversations.xmpp.Jid;
24import eu.siacs.conversations.xmpp.stanzas.IqPacket;
25import eu.siacs.conversations.xmpp.stanzas.PresencePacket;
26import java.nio.charset.StandardCharsets;
27import java.text.ParseException;
28import java.util.List;
29import java.util.concurrent.Executors;
30import java.util.concurrent.ScheduledExecutorService;
31import java.util.concurrent.TimeUnit;
32
33public class UnifiedPushBroker {
34
35 // time to expiration before a renewal attempt is made (24 hours)
36 public static final long TIME_TO_RENEW = 86_400_000L;
37
38 // interval for the 'cron tob' that attempts renewals for everything that expires is lass than
39 // `TIME_TO_RENEW`
40 public static final long RENEWAL_INTERVAL = 3_600_000L;
41
42 private static final ScheduledExecutorService SCHEDULER = Executors.newScheduledThreadPool(1);
43
44 private final XmppConnectionService service;
45
46 public UnifiedPushBroker(final XmppConnectionService xmppConnectionService) {
47 this.service = xmppConnectionService;
48 SCHEDULER.scheduleAtFixedRate(
49 this::renewUnifiedPushEndpoints,
50 RENEWAL_INTERVAL,
51 RENEWAL_INTERVAL,
52 TimeUnit.MILLISECONDS);
53 }
54
55 public void renewUnifiedPushEndpointsOnBind(final Account account) {
56 final Optional<Transport> transportOptional = getTransport();
57 if (transportOptional.isPresent()) {
58 final Transport transport = transportOptional.get();
59 final Account transportAccount = transport.account;
60 if (transportAccount != null && transportAccount.getUuid().equals(account.getUuid())) {
61 final UnifiedPushDatabase database = UnifiedPushDatabase.getInstance(service);
62 if (database.hasEndpoints(transport)) {
63 sendDirectedPresence(transportAccount, transport.transport);
64 }
65 Log.d(
66 Config.LOGTAG,
67 account.getJid().asBareJid() + ": trigger endpoint renewal on bind");
68 renewUnifiedEndpoint(transportOptional.get(), null);
69 }
70 }
71 }
72
73 private void sendDirectedPresence(final Account account, Jid to) {
74 final PresencePacket presence = new PresencePacket();
75 presence.setTo(to);
76 service.sendPresencePacket(account, presence);
77 }
78
79 public Optional<Transport> renewUnifiedPushEndpoints() {
80 return renewUnifiedPushEndpoints(null);
81 }
82
83 public Optional<Transport> renewUnifiedPushEndpoints(final PushTargetMessenger pushTargetMessenger) {
84 final Optional<Transport> transportOptional = getTransport();
85 if (transportOptional.isPresent()) {
86 final Transport transport = transportOptional.get();
87 if (transport.account.isEnabled()) {
88 renewUnifiedEndpoint(transportOptional.get(), pushTargetMessenger);
89 } else {
90 Log.d(Config.LOGTAG, "skipping UnifiedPush endpoint renewal. Account is disabled");
91 }
92 } else {
93 Log.d(Config.LOGTAG, "skipping UnifiedPush endpoint renewal. No transport selected");
94 }
95 return transportOptional;
96 }
97
98 private void renewUnifiedEndpoint(final Transport transport, final PushTargetMessenger pushTargetMessenger) {
99 final Account account = transport.account;
100 final UnifiedPushDatabase unifiedPushDatabase = UnifiedPushDatabase.getInstance(service);
101 final List<UnifiedPushDatabase.PushTarget> renewals =
102 unifiedPushDatabase.getRenewals(
103 account.getUuid(), transport.transport.toEscapedString());
104 Log.d(
105 Config.LOGTAG,
106 account.getJid().asBareJid()
107 + ": "
108 + renewals.size()
109 + " UnifiedPush endpoints scheduled for renewal on "
110 + transport.transport);
111 for (final UnifiedPushDatabase.PushTarget renewal : renewals) {
112 Log.d(
113 Config.LOGTAG,
114 account.getJid().asBareJid() + ": try to renew UnifiedPush " + renewal);
115 UnifiedPushDistributor.quickLog(service,String.format("%s: try to renew UnifiedPush %s", account.getJid(), renewal.toString()));
116 final String hashedApplication =
117 UnifiedPushDistributor.hash(account.getUuid(), renewal.application);
118 final String hashedInstance =
119 UnifiedPushDistributor.hash(account.getUuid(), renewal.instance);
120 final IqPacket registration = new IqPacket(IqPacket.TYPE.SET);
121 registration.setTo(transport.transport);
122 final Element register = registration.addChild("register", Namespace.UNIFIED_PUSH);
123 register.setAttribute("application", hashedApplication);
124 register.setAttribute("instance", hashedInstance);
125 final Messenger messenger;
126 if (pushTargetMessenger != null && renewal.equals(pushTargetMessenger.pushTarget)) {
127 messenger = pushTargetMessenger.messenger;
128 } else {
129 messenger = null;
130 }
131 this.service.sendIqPacket(
132 account,
133 registration,
134 (a, response) -> processRegistration(transport, renewal, messenger, response));
135 }
136 }
137
138 private void processRegistration(
139 final Transport transport,
140 final UnifiedPushDatabase.PushTarget renewal,
141 final Messenger messenger,
142 final IqPacket response) {
143 if (response.getType() == IqPacket.TYPE.RESULT) {
144 final Element registered = response.findChild("registered", Namespace.UNIFIED_PUSH);
145 if (registered == null) {
146 return;
147 }
148 final String endpoint = registered.getAttribute("endpoint");
149 if (Strings.isNullOrEmpty(endpoint)) {
150 Log.w(Config.LOGTAG, "endpoint was null in up registration");
151 return;
152 }
153 final long expiration;
154 try {
155 expiration = AbstractParser.getTimestamp(registered.getAttribute("expiration"));
156 } catch (final IllegalArgumentException | ParseException e) {
157 Log.d(Config.LOGTAG, "could not parse expiration", e);
158 return;
159 }
160 renewUnifiedPushEndpoint(transport, renewal, messenger, endpoint, expiration);
161 } else {
162 Log.d(Config.LOGTAG, "could not register UP endpoint " + response.getErrorCondition());
163 }
164 }
165
166 private void renewUnifiedPushEndpoint(
167 final Transport transport,
168 final UnifiedPushDatabase.PushTarget renewal,
169 final Messenger messenger,
170 final String endpoint,
171 final long expiration) {
172 Log.d(Config.LOGTAG, "registered endpoint " + endpoint + " expiration=" + expiration);
173 final UnifiedPushDatabase unifiedPushDatabase = UnifiedPushDatabase.getInstance(service);
174 final boolean modified =
175 unifiedPushDatabase.updateEndpoint(
176 renewal.instance,
177 transport.account.getUuid(),
178 transport.transport.toEscapedString(),
179 endpoint,
180 expiration);
181 if (modified) {
182 Log.d(
183 Config.LOGTAG,
184 "endpoint for "
185 + renewal.application
186 + "/"
187 + renewal.instance
188 + " was updated to "
189 + endpoint);
190 UnifiedPushDistributor.quickLog(
191 service,
192 "endpoint for "
193 + renewal.application
194 + "/"
195 + renewal.instance
196 + " was updated to "
197 + endpoint);
198 final UnifiedPushDatabase.ApplicationEndpoint applicationEndpoint =
199 new UnifiedPushDatabase.ApplicationEndpoint(renewal.application, endpoint);
200 sendEndpoint(messenger, renewal.instance, applicationEndpoint);
201 }
202 }
203
204 private void sendEndpoint(final Messenger messenger, String instance, final UnifiedPushDatabase.ApplicationEndpoint applicationEndpoint) {
205 if (messenger != null) {
206 Log.d(Config.LOGTAG,"using messenger instead of broadcast to communicate endpoint to "+applicationEndpoint.application);
207 final Message message = new Message();
208 message.obj = endpointIntent(instance, applicationEndpoint);
209 try {
210 messenger.send(message);
211 } catch (final RemoteException e) {
212 Log.d(Config.LOGTAG,"messenger failed. falling back to broadcast");
213 broadcastEndpoint(instance, applicationEndpoint);
214 }
215 } else {
216 broadcastEndpoint(instance, applicationEndpoint);
217 }
218 }
219
220 public boolean reconfigurePushDistributor() {
221 final boolean enabled = getTransport().isPresent();
222 setUnifiedPushDistributorEnabled(enabled);
223 return enabled;
224 }
225
226 private void setUnifiedPushDistributorEnabled(final boolean enabled) {
227 final PackageManager packageManager = service.getPackageManager();
228 final ComponentName componentName =
229 new ComponentName(service, UnifiedPushDistributor.class);
230 if (enabled) {
231 packageManager.setComponentEnabledSetting(
232 componentName,
233 PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
234 PackageManager.DONT_KILL_APP);
235 Log.d(Config.LOGTAG, "UnifiedPushDistributor has been enabled");
236 } else {
237 packageManager.setComponentEnabledSetting(
238 componentName,
239 PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
240 PackageManager.DONT_KILL_APP);
241 Log.d(Config.LOGTAG, "UnifiedPushDistributor has been disabled");
242 }
243 }
244
245 public boolean processPushMessage(
246 final Account account, final Jid transport, final Element push) {
247 final String instance = push.getAttribute("instance");
248 final String application = push.getAttribute("application");
249 if (Strings.isNullOrEmpty(instance) || Strings.isNullOrEmpty(application)) {
250 return false;
251 }
252 final String content = push.getContent();
253 final byte[] payload;
254 if (Strings.isNullOrEmpty(content)) {
255 payload = new byte[0];
256 } else if (BaseEncoding.base64().canDecode(content)) {
257 payload = BaseEncoding.base64().decode(content);
258 } else {
259 Log.d(
260 Config.LOGTAG,
261 account.getJid().asBareJid() + ": received invalid unified push payload");
262 return false;
263 }
264 final Optional<UnifiedPushDatabase.PushTarget> pushTarget =
265 getPushTarget(account, transport, application, instance);
266 if (pushTarget.isPresent()) {
267 final UnifiedPushDatabase.PushTarget target = pushTarget.get();
268 // TODO check if app is still installed?
269 Log.d(
270 Config.LOGTAG,
271 account.getJid().asBareJid()
272 + ": broadcasting a "
273 + payload.length
274 + " bytes push message to "
275 + target.application);
276 broadcastPushMessage(target, payload);
277 return true;
278 } else {
279 Log.d(Config.LOGTAG, "could not find application for push");
280 return false;
281 }
282 }
283
284 public Optional<Transport> getTransport() {
285 final SharedPreferences sharedPreferences =
286 PreferenceManager.getDefaultSharedPreferences(service.getApplicationContext());
287 final String accountPreference =
288 sharedPreferences.getString(UnifiedPushDistributor.PREFERENCE_ACCOUNT, "none");
289 final String pushServerPreference =
290 sharedPreferences.getString(
291 UnifiedPushDistributor.PREFERENCE_PUSH_SERVER,
292 service.getString(R.string.default_push_server));
293 if (Strings.isNullOrEmpty(accountPreference)
294 || "none".equalsIgnoreCase(accountPreference)
295 || Strings.nullToEmpty(pushServerPreference).trim().isEmpty()) {
296 return Optional.absent();
297 }
298 final Jid transport;
299 final Jid jid;
300 try {
301 transport = Jid.ofEscaped(Strings.nullToEmpty(pushServerPreference).trim());
302 jid = Jid.ofEscaped(Strings.nullToEmpty(accountPreference).trim());
303 } catch (final IllegalArgumentException e) {
304 return Optional.absent();
305 }
306 final Account account = service.findAccountByJid(jid);
307 if (account == null) {
308 return Optional.absent();
309 }
310 return Optional.of(new Transport(account, transport));
311 }
312
313 private Optional<UnifiedPushDatabase.PushTarget> getPushTarget(
314 final Account account,
315 final Jid transport,
316 final String application,
317 final String instance) {
318 if (transport == null || application == null || instance == null) {
319 return Optional.absent();
320 }
321 final String uuid = account.getUuid();
322 final List<UnifiedPushDatabase.PushTarget> pushTargets =
323 UnifiedPushDatabase.getInstance(service)
324 .getPushTargets(uuid, transport.toEscapedString());
325 return Iterables.tryFind(
326 pushTargets,
327 pt ->
328 UnifiedPushDistributor.hash(uuid, pt.application).equals(application)
329 && UnifiedPushDistributor.hash(uuid, pt.instance).equals(instance));
330 }
331
332 private void broadcastPushMessage(
333 final UnifiedPushDatabase.PushTarget target, final byte[] payload) {
334 final Intent updateIntent = new Intent(UnifiedPushDistributor.ACTION_MESSAGE);
335 updateIntent.setPackage(target.application);
336 updateIntent.putExtra("token", target.instance);
337 updateIntent.putExtra("bytesMessage", payload);
338 updateIntent.putExtra("message", new String(payload, StandardCharsets.UTF_8));
339 service.sendBroadcast(updateIntent);
340 }
341
342 private void broadcastEndpoint(
343 final String instance, final UnifiedPushDatabase.ApplicationEndpoint endpoint) {
344 Log.d(Config.LOGTAG, "broadcasting endpoint to " + endpoint.application);
345 final Intent updateIntent = endpointIntent(instance, endpoint);
346 service.sendBroadcast(updateIntent);
347 }
348
349 private static Intent endpointIntent(final String instance, final UnifiedPushDatabase.ApplicationEndpoint endpoint) {
350 final Intent intent = new Intent(UnifiedPushDistributor.ACTION_NEW_ENDPOINT);
351 intent.setPackage(endpoint.application);
352 intent.putExtra("token", instance);
353 intent.putExtra("endpoint", endpoint.endpoint);
354 return intent;
355 }
356
357 public void rebroadcastEndpoint(final Messenger messenger, final String instance, final Transport transport) {
358 final UnifiedPushDatabase unifiedPushDatabase = UnifiedPushDatabase.getInstance(service);
359 final UnifiedPushDatabase.ApplicationEndpoint endpoint =
360 unifiedPushDatabase.getEndpoint(
361 transport.account.getUuid(),
362 transport.transport.toEscapedString(),
363 instance);
364 if (endpoint != null) {
365 sendEndpoint(messenger, instance, endpoint);
366 }
367 }
368
369 public static class Transport {
370 public final Account account;
371 public final Jid transport;
372
373 public Transport(Account account, Jid transport) {
374 this.account = account;
375 this.transport = transport;
376 }
377 }
378
379 public static class PushTargetMessenger {
380 private final UnifiedPushDatabase.PushTarget pushTarget;
381 private final Messenger messenger;
382
383 public PushTargetMessenger(UnifiedPushDatabase.PushTarget pushTarget, Messenger messenger) {
384 this.pushTarget = pushTarget;
385 this.messenger = messenger;
386 }
387 }
388}