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 im.conversations.android.xmpp.model.up.Push;
35import java.nio.charset.StandardCharsets;
36import java.text.ParseException;
37import java.util.List;
38import java.util.concurrent.Executors;
39import java.util.concurrent.ScheduledExecutorService;
40import java.util.concurrent.TimeUnit;
41
42public class UnifiedPushBroker {
43
44 // time to expiration before a renewal attempt is made (24 hours)
45 public static final long TIME_TO_RENEW = 86_400_000L;
46
47 // interval for the 'cron tob' that attempts renewals for everything that expires is lass than
48 // `TIME_TO_RENEW`
49 public static final long RENEWAL_INTERVAL = 3_600_000L;
50
51 private static final ScheduledExecutorService SCHEDULER = Executors.newScheduledThreadPool(1);
52
53 private final XmppConnectionService service;
54
55 public UnifiedPushBroker(final XmppConnectionService xmppConnectionService) {
56 this.service = xmppConnectionService;
57 SCHEDULER.scheduleAtFixedRate(
58 this::renewUnifiedPushEndpoints,
59 RENEWAL_INTERVAL,
60 RENEWAL_INTERVAL,
61 TimeUnit.MILLISECONDS);
62 }
63
64 public void renewUnifiedPushEndpointsOnBind(final Account account) {
65 final Optional<Transport> transportOptional = getTransport();
66 if (transportOptional.isPresent()) {
67 final Transport transport = transportOptional.get();
68 final Account transportAccount = transport.account;
69 if (transportAccount != null && transportAccount.getUuid().equals(account.getUuid())) {
70 final UnifiedPushDatabase database = UnifiedPushDatabase.getInstance(service);
71 if (database.hasEndpoints(transport)) {
72 sendDirectedPresence(transportAccount, transport.transport);
73 }
74 Log.d(
75 Config.LOGTAG,
76 account.getJid().asBareJid() + ": trigger endpoint renewal on bind");
77 renewUnifiedEndpoint(transportOptional.get(), null);
78 }
79 }
80 }
81
82 private void sendDirectedPresence(final Account account, Jid to) {
83 final var presence = new Presence();
84 presence.setTo(to);
85 service.sendPresencePacket(account, presence);
86 }
87
88 public void renewUnifiedPushEndpoints() {
89 renewUnifiedPushEndpoints(null);
90 }
91
92 public Optional<Transport> renewUnifiedPushEndpoints(
93 @Nullable final PushTargetMessenger pushTargetMessenger) {
94 final Optional<Transport> transportOptional = getTransport();
95 if (transportOptional.isPresent()) {
96 final Transport transport = transportOptional.get();
97 if (transport.account.isEnabled()) {
98 renewUnifiedEndpoint(transportOptional.get(), pushTargetMessenger);
99 } else {
100 if (pushTargetMessenger != null && pushTargetMessenger.messenger != null) {
101 sendRegistrationDelayed(pushTargetMessenger.messenger, "account is disabled");
102 }
103 Log.d(Config.LOGTAG, "skipping UnifiedPush endpoint renewal. Account is disabled");
104 }
105 } else {
106 if (pushTargetMessenger != null && pushTargetMessenger.messenger != null) {
107 sendRegistrationDelayed(pushTargetMessenger.messenger, "no transport selected");
108 }
109 Log.d(Config.LOGTAG, "skipping UnifiedPush endpoint renewal. No transport selected");
110 }
111 return transportOptional;
112 }
113
114 private void sendRegistrationDelayed(final Messenger messenger, final String error) {
115 final Intent intent = new Intent(UnifiedPushDistributor.ACTION_REGISTRATION_DELAYED);
116 intent.putExtra(UnifiedPushDistributor.EXTRA_MESSAGE, error);
117 final var message = new Message();
118 message.obj = intent;
119 try {
120 messenger.send(message);
121 } catch (final RemoteException e) {
122 Log.d(Config.LOGTAG, "unable to tell messenger of delayed registration", e);
123 }
124 }
125
126 private void renewUnifiedEndpoint(
127 final Transport transport, final PushTargetMessenger pushTargetMessenger) {
128 final Account account = transport.account;
129 final UnifiedPushDatabase unifiedPushDatabase = UnifiedPushDatabase.getInstance(service);
130 final List<UnifiedPushDatabase.PushTarget> renewals =
131 unifiedPushDatabase.getRenewals(account.getUuid(), transport.transport.toString());
132 Log.d(
133 Config.LOGTAG,
134 account.getJid().asBareJid()
135 + ": "
136 + renewals.size()
137 + " UnifiedPush endpoints scheduled for renewal on "
138 + transport.transport);
139 for (final UnifiedPushDatabase.PushTarget renewal : renewals) {
140 Log.d(
141 Config.LOGTAG,
142 account.getJid().asBareJid() + ": try to renew UnifiedPush " + renewal);
143 UnifiedPushDistributor.quickLog(
144 service,
145 String.format(
146 "%s: try to renew UnifiedPush %s",
147 account.getJid(), renewal.toString()));
148 final String hashedApplication =
149 UnifiedPushDistributor.hash(account.getUuid(), renewal.application);
150 final String hashedInstance =
151 UnifiedPushDistributor.hash(account.getUuid(), renewal.instance);
152 final Iq registration = new Iq(Iq.Type.SET);
153 registration.setTo(transport.transport);
154 final Element register = registration.addChild("register", Namespace.UNIFIED_PUSH);
155 register.setAttribute("application", hashedApplication);
156 register.setAttribute("instance", hashedInstance);
157 final Messenger messenger;
158 if (pushTargetMessenger != null && renewal.equals(pushTargetMessenger.pushTarget)) {
159 messenger = pushTargetMessenger.messenger;
160 } else {
161 messenger = null;
162 }
163 this.service.sendIqPacket(
164 account,
165 registration,
166 (response) -> processRegistration(transport, renewal, messenger, response));
167 }
168 }
169
170 private void processRegistration(
171 final Transport transport,
172 final UnifiedPushDatabase.PushTarget renewal,
173 final Messenger messenger,
174 final Iq response) {
175 if (response.getType() == Iq.Type.RESULT) {
176 final Element registered = response.findChild("registered", Namespace.UNIFIED_PUSH);
177 if (registered == null) {
178 return;
179 }
180 final String endpoint = registered.getAttribute("endpoint");
181 if (Strings.isNullOrEmpty(endpoint)) {
182 Log.w(Config.LOGTAG, "endpoint was null in up registration");
183 return;
184 }
185 final long expiration;
186 try {
187 expiration = AbstractParser.getTimestamp(registered.getAttribute("expiration"));
188 } catch (final IllegalArgumentException | ParseException e) {
189 Log.d(Config.LOGTAG, "could not parse expiration", e);
190 return;
191 }
192 renewUnifiedPushEndpoint(transport, renewal, messenger, endpoint, expiration);
193 } else {
194 Log.d(Config.LOGTAG, "could not register UP endpoint " + response.getErrorCondition());
195 }
196 }
197
198 private void renewUnifiedPushEndpoint(
199 final Transport transport,
200 final UnifiedPushDatabase.PushTarget renewal,
201 final Messenger messenger,
202 final String endpoint,
203 final long expiration) {
204 Log.d(Config.LOGTAG, "registered endpoint " + endpoint + " expiration=" + expiration);
205 final UnifiedPushDatabase unifiedPushDatabase = UnifiedPushDatabase.getInstance(service);
206 final boolean modified =
207 unifiedPushDatabase.updateEndpoint(
208 renewal.instance,
209 transport.account.getUuid(),
210 transport.transport.toString(),
211 endpoint,
212 expiration);
213 if (modified) {
214 Log.d(
215 Config.LOGTAG,
216 "endpoint for "
217 + renewal.application
218 + "/"
219 + renewal.instance
220 + " was updated to "
221 + endpoint);
222 UnifiedPushDistributor.quickLog(
223 service,
224 "endpoint for "
225 + renewal.application
226 + "/"
227 + renewal.instance
228 + " was updated to "
229 + endpoint);
230 final UnifiedPushDatabase.ApplicationEndpoint applicationEndpoint =
231 new UnifiedPushDatabase.ApplicationEndpoint(renewal.application, endpoint);
232 sendEndpoint(messenger, renewal.instance, applicationEndpoint);
233 }
234 }
235
236 private void sendEndpoint(
237 final Messenger messenger,
238 String instance,
239 final UnifiedPushDatabase.ApplicationEndpoint applicationEndpoint) {
240 if (messenger != null) {
241 Log.d(
242 Config.LOGTAG,
243 "using messenger instead of broadcast to communicate endpoint to "
244 + applicationEndpoint.application);
245 final Message message = new Message();
246 message.obj = endpointIntent(instance, applicationEndpoint);
247 try {
248 messenger.send(message);
249 } catch (final RemoteException e) {
250 Log.d(Config.LOGTAG, "messenger failed. falling back to broadcast");
251 broadcastEndpoint(instance, applicationEndpoint);
252 }
253 } else {
254 broadcastEndpoint(instance, applicationEndpoint);
255 }
256 }
257
258 public boolean reconfigurePushDistributor() {
259 final boolean enabled = getTransport().isPresent();
260 setUnifiedPushDistributorEnabled(enabled);
261 if (!enabled) {
262 unregisterCurrentPushTargets();
263 }
264 return enabled;
265 }
266
267 private void setUnifiedPushDistributorEnabled(final boolean enabled) {
268 final PackageManager packageManager = service.getPackageManager();
269 final ComponentName componentName =
270 new ComponentName(service, UnifiedPushDistributor.class);
271 if (enabled) {
272 packageManager.setComponentEnabledSetting(
273 componentName,
274 PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
275 PackageManager.DONT_KILL_APP);
276 Log.d(Config.LOGTAG, "UnifiedPushDistributor has been enabled");
277 } else {
278 packageManager.setComponentEnabledSetting(
279 componentName,
280 PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
281 PackageManager.DONT_KILL_APP);
282 Log.d(Config.LOGTAG, "UnifiedPushDistributor has been disabled");
283 }
284 }
285
286 private void unregisterCurrentPushTargets() {
287 final var future = deletePushTargets();
288 Futures.addCallback(
289 future,
290 new FutureCallback<>() {
291 @Override
292 public void onSuccess(final List<UnifiedPushDatabase.PushTarget> pushTargets) {
293 broadcastUnregistered(pushTargets);
294 }
295
296 @Override
297 public void onFailure(@NonNull Throwable throwable) {
298 Log.d(
299 Config.LOGTAG,
300 "could not delete endpoints after UnifiedPushDistributor was"
301 + " disabled");
302 }
303 },
304 MoreExecutors.directExecutor());
305 }
306
307 private ListenableFuture<List<UnifiedPushDatabase.PushTarget>> deletePushTargets() {
308 return Futures.submit(
309 () -> UnifiedPushDatabase.getInstance(service).deletePushTargets(), SCHEDULER);
310 }
311
312 private void broadcastUnregistered(final List<UnifiedPushDatabase.PushTarget> pushTargets) {
313 for (final UnifiedPushDatabase.PushTarget pushTarget : pushTargets) {
314 Log.d(Config.LOGTAG, "sending unregistered to " + pushTarget);
315 broadcastUnregistered(pushTarget);
316 }
317 }
318
319 private void broadcastUnregistered(final UnifiedPushDatabase.PushTarget pushTarget) {
320 final var intent = unregisteredIntent(pushTarget);
321 service.sendBroadcast(intent);
322 }
323
324 public boolean processPushMessage(final Account account, final Jid transport, final Push 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}