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