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