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 java.nio.charset.StandardCharsets;
 35import java.text.ParseException;
 36import java.util.List;
 37import java.util.concurrent.Executors;
 38import java.util.concurrent.ScheduledExecutorService;
 39import java.util.concurrent.TimeUnit;
 40
 41public class UnifiedPushBroker {
 42
 43    // time to expiration before a renewal attempt is made (24 hours)
 44    public static final long TIME_TO_RENEW = 86_400_000L;
 45
 46    // interval for the 'cron tob' that attempts renewals for everything that expires is lass than
 47    // `TIME_TO_RENEW`
 48    public static final long RENEWAL_INTERVAL = 3_600_000L;
 49
 50    private static final ScheduledExecutorService SCHEDULER = Executors.newScheduledThreadPool(1);
 51
 52    private final XmppConnectionService service;
 53
 54    public UnifiedPushBroker(final XmppConnectionService xmppConnectionService) {
 55        this.service = xmppConnectionService;
 56        SCHEDULER.scheduleAtFixedRate(
 57                this::renewUnifiedPushEndpoints,
 58                RENEWAL_INTERVAL,
 59                RENEWAL_INTERVAL,
 60                TimeUnit.MILLISECONDS);
 61    }
 62
 63    public void renewUnifiedPushEndpointsOnBind(final Account account) {
 64        final Optional<Transport> transportOptional = getTransport();
 65        if (transportOptional.isPresent()) {
 66            final Transport transport = transportOptional.get();
 67            final Account transportAccount = transport.account;
 68            if (transportAccount != null && transportAccount.getUuid().equals(account.getUuid())) {
 69                final UnifiedPushDatabase database = UnifiedPushDatabase.getInstance(service);
 70                if (database.hasEndpoints(transport)) {
 71                    sendDirectedPresence(transportAccount, transport.transport);
 72                }
 73                Log.d(
 74                        Config.LOGTAG,
 75                        account.getJid().asBareJid() + ": trigger endpoint renewal on bind");
 76                renewUnifiedEndpoint(transportOptional.get(), null);
 77            }
 78        }
 79    }
 80
 81    private void sendDirectedPresence(final Account account, Jid to) {
 82        final var presence = new Presence();
 83        presence.setTo(to);
 84        service.sendPresencePacket(account, presence);
 85    }
 86
 87    public void renewUnifiedPushEndpoints() {
 88        renewUnifiedPushEndpoints(null);
 89    }
 90
 91    public Optional<Transport> renewUnifiedPushEndpoints(
 92            @Nullable 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 != null && 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 != null && 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(
126            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(account.getUuid(), transport.transport.toString());
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(
143                    service,
144                    String.format(
145                            "%s: try to renew UnifiedPush %s",
146                            account.getJid(), renewal.toString()));
147            final String hashedApplication =
148                    UnifiedPushDistributor.hash(account.getUuid(), renewal.application);
149            final String hashedInstance =
150                    UnifiedPushDistributor.hash(account.getUuid(), renewal.instance);
151            final Iq registration = new Iq(Iq.Type.SET);
152            registration.setTo(transport.transport);
153            final Element register = registration.addChild("register", Namespace.UNIFIED_PUSH);
154            register.setAttribute("application", hashedApplication);
155            register.setAttribute("instance", hashedInstance);
156            final Messenger messenger;
157            if (pushTargetMessenger != null && renewal.equals(pushTargetMessenger.pushTarget)) {
158                messenger = pushTargetMessenger.messenger;
159            } else {
160                messenger = null;
161            }
162            this.service.sendIqPacket(
163                    account,
164                    registration,
165                    (response) -> processRegistration(transport, renewal, messenger, response));
166        }
167    }
168
169    private void processRegistration(
170            final Transport transport,
171            final UnifiedPushDatabase.PushTarget renewal,
172            final Messenger messenger,
173            final Iq response) {
174        if (response.getType() == Iq.Type.RESULT) {
175            final Element registered = response.findChild("registered", Namespace.UNIFIED_PUSH);
176            if (registered == null) {
177                return;
178            }
179            final String endpoint = registered.getAttribute("endpoint");
180            if (Strings.isNullOrEmpty(endpoint)) {
181                Log.w(Config.LOGTAG, "endpoint was null in up registration");
182                return;
183            }
184            final long expiration;
185            try {
186                expiration = AbstractParser.getTimestamp(registered.getAttribute("expiration"));
187            } catch (final IllegalArgumentException | ParseException e) {
188                Log.d(Config.LOGTAG, "could not parse expiration", e);
189                return;
190            }
191            renewUnifiedPushEndpoint(transport, renewal, messenger, endpoint, expiration);
192        } else {
193            Log.d(Config.LOGTAG, "could not register UP endpoint " + response.getErrorCondition());
194        }
195    }
196
197    private void renewUnifiedPushEndpoint(
198            final Transport transport,
199            final UnifiedPushDatabase.PushTarget renewal,
200            final Messenger messenger,
201            final String endpoint,
202            final long expiration) {
203        Log.d(Config.LOGTAG, "registered endpoint " + endpoint + " expiration=" + expiration);
204        final UnifiedPushDatabase unifiedPushDatabase = UnifiedPushDatabase.getInstance(service);
205        final boolean modified =
206                unifiedPushDatabase.updateEndpoint(
207                        renewal.instance,
208                        transport.account.getUuid(),
209                        transport.transport.toString(),
210                        endpoint,
211                        expiration);
212        if (modified) {
213            Log.d(
214                    Config.LOGTAG,
215                    "endpoint for "
216                            + renewal.application
217                            + "/"
218                            + renewal.instance
219                            + " was updated to "
220                            + endpoint);
221            UnifiedPushDistributor.quickLog(
222                    service,
223                    "endpoint for "
224                            + renewal.application
225                            + "/"
226                            + renewal.instance
227                            + " was updated to "
228                            + endpoint);
229            final UnifiedPushDatabase.ApplicationEndpoint applicationEndpoint =
230                    new UnifiedPushDatabase.ApplicationEndpoint(renewal.application, endpoint);
231            sendEndpoint(messenger, renewal.instance, applicationEndpoint);
232        }
233    }
234
235    private void sendEndpoint(
236            final Messenger messenger,
237            String instance,
238            final UnifiedPushDatabase.ApplicationEndpoint applicationEndpoint) {
239        if (messenger != null) {
240            Log.d(
241                    Config.LOGTAG,
242                    "using messenger instead of broadcast to communicate endpoint to "
243                            + applicationEndpoint.application);
244            final Message message = new Message();
245            message.obj = endpointIntent(instance, applicationEndpoint);
246            try {
247                messenger.send(message);
248            } catch (final RemoteException e) {
249                Log.d(Config.LOGTAG, "messenger failed. falling back to broadcast");
250                broadcastEndpoint(instance, applicationEndpoint);
251            }
252        } else {
253            broadcastEndpoint(instance, applicationEndpoint);
254        }
255    }
256
257    public boolean reconfigurePushDistributor() {
258        final boolean enabled = getTransport().isPresent();
259        setUnifiedPushDistributorEnabled(enabled);
260        if (!enabled) {
261            unregisterCurrentPushTargets();
262        }
263        return enabled;
264    }
265
266    private void setUnifiedPushDistributorEnabled(final boolean enabled) {
267        final PackageManager packageManager = service.getPackageManager();
268        final ComponentName componentName =
269                new ComponentName(service, UnifiedPushDistributor.class);
270        if (enabled) {
271            packageManager.setComponentEnabledSetting(
272                    componentName,
273                    PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
274                    PackageManager.DONT_KILL_APP);
275            Log.d(Config.LOGTAG, "UnifiedPushDistributor has been enabled");
276        } else {
277            packageManager.setComponentEnabledSetting(
278                    componentName,
279                    PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
280                    PackageManager.DONT_KILL_APP);
281            Log.d(Config.LOGTAG, "UnifiedPushDistributor has been disabled");
282        }
283    }
284
285    private void unregisterCurrentPushTargets() {
286        final var future = deletePushTargets();
287        Futures.addCallback(
288                future,
289                new FutureCallback<>() {
290                    @Override
291                    public void onSuccess(final List<UnifiedPushDatabase.PushTarget> pushTargets) {
292                        broadcastUnregistered(pushTargets);
293                    }
294
295                    @Override
296                    public void onFailure(@NonNull Throwable throwable) {
297                        Log.d(
298                                Config.LOGTAG,
299                                "could not delete endpoints after UnifiedPushDistributor was"
300                                        + " disabled");
301                    }
302                },
303                MoreExecutors.directExecutor());
304    }
305
306    private ListenableFuture<List<UnifiedPushDatabase.PushTarget>> deletePushTargets() {
307        return Futures.submit(
308                () -> UnifiedPushDatabase.getInstance(service).deletePushTargets(), SCHEDULER);
309    }
310
311    private void broadcastUnregistered(final List<UnifiedPushDatabase.PushTarget> pushTargets) {
312        for (final UnifiedPushDatabase.PushTarget pushTarget : pushTargets) {
313            Log.d(Config.LOGTAG, "sending unregistered to " + pushTarget);
314            broadcastUnregistered(pushTarget);
315        }
316    }
317
318    private void broadcastUnregistered(final UnifiedPushDatabase.PushTarget pushTarget) {
319        final var intent = unregisteredIntent(pushTarget);
320        service.sendBroadcast(intent);
321    }
322
323    public boolean processPushMessage(
324            final Account account, final Jid transport, final Element 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}