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