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