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