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