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