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