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