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