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