UnifiedPushBroker.java

  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}