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