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 im.conversations.android.xmpp.model.stanza.Iq;
 36import im.conversations.android.xmpp.model.stanza.Presence;
 37
 38import java.nio.charset.StandardCharsets;
 39import java.text.ParseException;
 40import java.util.List;
 41import java.util.concurrent.Executors;
 42import java.util.concurrent.ScheduledExecutorService;
 43import java.util.concurrent.TimeUnit;
 44
 45public class UnifiedPushBroker {
 46
 47    // time to expiration before a renewal attempt is made (24 hours)
 48    public static final long TIME_TO_RENEW = 86_400_000L;
 49
 50    // interval for the 'cron tob' that attempts renewals for everything that expires is lass than
 51    // `TIME_TO_RENEW`
 52    public static final long RENEWAL_INTERVAL = 3_600_000L;
 53
 54    private static final ScheduledExecutorService SCHEDULER = Executors.newScheduledThreadPool(1);
 55
 56    private final XmppConnectionService service;
 57
 58    public UnifiedPushBroker(final XmppConnectionService xmppConnectionService) {
 59        this.service = xmppConnectionService;
 60        SCHEDULER.scheduleAtFixedRate(
 61                this::renewUnifiedPushEndpoints,
 62                RENEWAL_INTERVAL,
 63                RENEWAL_INTERVAL,
 64                TimeUnit.MILLISECONDS);
 65    }
 66
 67    public void renewUnifiedPushEndpointsOnBind(final Account account) {
 68        final Optional<Transport> transportOptional = getTransport();
 69        if (transportOptional.isPresent()) {
 70            final Transport transport = transportOptional.get();
 71            final Account transportAccount = transport.account;
 72            if (transportAccount != null && transportAccount.getUuid().equals(account.getUuid())) {
 73                final UnifiedPushDatabase database = UnifiedPushDatabase.getInstance(service);
 74                if (database.hasEndpoints(transport)) {
 75                    sendDirectedPresence(transportAccount, transport.transport);
 76                }
 77                Log.d(
 78                        Config.LOGTAG,
 79                        account.getJid().asBareJid() + ": trigger endpoint renewal on bind");
 80                renewUnifiedEndpoint(transportOptional.get(), null);
 81            }
 82        }
 83    }
 84
 85    private void sendDirectedPresence(final Account account, Jid to) {
 86        final var presence = new Presence();
 87        presence.setTo(to);
 88        service.sendPresencePacket(account, presence);
 89    }
 90
 91    public void renewUnifiedPushEndpoints() {
 92        renewUnifiedPushEndpoints(null);
 93    }
 94
 95    public Optional<Transport> renewUnifiedPushEndpoints(@Nullable final PushTargetMessenger pushTargetMessenger) {
 96        final Optional<Transport> transportOptional = getTransport();
 97        if (transportOptional.isPresent()) {
 98            final Transport transport = transportOptional.get();
 99            if (transport.account.isEnabled()) {
100                renewUnifiedEndpoint(transportOptional.get(), pushTargetMessenger);
101            } else {
102                if (pushTargetMessenger != null && pushTargetMessenger.messenger != null) {
103                    sendRegistrationDelayed(pushTargetMessenger.messenger,"account is disabled");
104                }
105                Log.d(Config.LOGTAG, "skipping UnifiedPush endpoint renewal. Account is disabled");
106            }
107        } else {
108            if (pushTargetMessenger != null && pushTargetMessenger.messenger != null) {
109                sendRegistrationDelayed(pushTargetMessenger.messenger,"no transport selected");
110            }
111            Log.d(Config.LOGTAG, "skipping UnifiedPush endpoint renewal. No transport selected");
112        }
113        return transportOptional;
114    }
115
116    private void sendRegistrationDelayed(final Messenger messenger, final String error) {
117        final Intent intent = new Intent(UnifiedPushDistributor.ACTION_REGISTRATION_DELAYED);
118        intent.putExtra(UnifiedPushDistributor.EXTRA_MESSAGE, error);
119        final var message = new Message();
120        message.obj = intent;
121        try {
122            messenger.send(message);
123        } catch (final RemoteException e) {
124            Log.d(Config.LOGTAG,"unable to tell messenger of delayed registration",e);
125        }
126    }
127
128    private void renewUnifiedEndpoint(final Transport transport, final PushTargetMessenger pushTargetMessenger) {
129        final Account account = transport.account;
130        final UnifiedPushDatabase unifiedPushDatabase = UnifiedPushDatabase.getInstance(service);
131        final List<UnifiedPushDatabase.PushTarget> renewals =
132                unifiedPushDatabase.getRenewals(
133                        account.getUuid(), transport.transport.toEscapedString());
134        Log.d(
135                Config.LOGTAG,
136                account.getJid().asBareJid()
137                        + ": "
138                        + renewals.size()
139                        + " UnifiedPush endpoints scheduled for renewal on "
140                        + transport.transport);
141        for (final UnifiedPushDatabase.PushTarget renewal : renewals) {
142            Log.d(
143                    Config.LOGTAG,
144                    account.getJid().asBareJid() + ": try to renew UnifiedPush " + renewal);
145            UnifiedPushDistributor.quickLog(service,String.format("%s: try to renew UnifiedPush %s", 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.toEscapedString(),
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(final Messenger messenger, String instance, final UnifiedPushDatabase.ApplicationEndpoint applicationEndpoint) {
235        if (messenger != null) {
236            Log.d(Config.LOGTAG,"using messenger instead of broadcast to communicate endpoint to "+applicationEndpoint.application);
237            final Message message = new Message();
238            message.obj = endpointIntent(instance, applicationEndpoint);
239            try {
240                messenger.send(message);
241            } catch (final RemoteException e) {
242                Log.d(Config.LOGTAG,"messenger failed. falling back to broadcast");
243                broadcastEndpoint(instance, applicationEndpoint);
244            }
245        } else {
246            broadcastEndpoint(instance, applicationEndpoint);
247        }
248    }
249
250    public boolean reconfigurePushDistributor() {
251        final boolean enabled = getTransport().isPresent();
252        setUnifiedPushDistributorEnabled(enabled);
253        if (!enabled) {
254            unregisterCurrentPushTargets();
255        }
256        return enabled;
257    }
258
259    private void setUnifiedPushDistributorEnabled(final boolean enabled) {
260        final PackageManager packageManager = service.getPackageManager();
261        final ComponentName componentName =
262                new ComponentName(service, UnifiedPushDistributor.class);
263        if (enabled) {
264            packageManager.setComponentEnabledSetting(
265                    componentName,
266                    PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
267                    PackageManager.DONT_KILL_APP);
268            Log.d(Config.LOGTAG, "UnifiedPushDistributor has been enabled");
269        } else {
270            packageManager.setComponentEnabledSetting(
271                    componentName,
272                    PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
273                    PackageManager.DONT_KILL_APP);
274            Log.d(Config.LOGTAG, "UnifiedPushDistributor has been disabled");
275        }
276    }
277
278    private void unregisterCurrentPushTargets() {
279        final var future = deletePushTargets();
280        Futures.addCallback(
281                future,
282                new FutureCallback<>() {
283                    @Override
284                    public void onSuccess(
285                            final List<UnifiedPushDatabase.PushTarget> pushTargets) {
286                        broadcastUnregistered(pushTargets);
287                    }
288
289                    @Override
290                    public void onFailure(@NonNull Throwable throwable) {
291                        Log.d(
292                                Config.LOGTAG,
293                                "could not delete endpoints after UnifiedPushDistributor was disabled");
294                    }
295                },
296                MoreExecutors.directExecutor());
297    }
298
299    private ListenableFuture<List<UnifiedPushDatabase.PushTarget>> deletePushTargets() {
300        return Futures.submit(() -> UnifiedPushDatabase.getInstance(service).deletePushTargets(),SCHEDULER);
301    }
302
303    private void broadcastUnregistered(final List<UnifiedPushDatabase.PushTarget> pushTargets) {
304        for(final UnifiedPushDatabase.PushTarget pushTarget : pushTargets) {
305            Log.d(Config.LOGTAG,"sending unregistered to "+pushTarget);
306            broadcastUnregistered(pushTarget);
307        }
308    }
309
310    private void broadcastUnregistered(final UnifiedPushDatabase.PushTarget pushTarget) {
311        final var intent = unregisteredIntent(pushTarget);
312        service.sendBroadcast(intent);
313    }
314
315    public boolean processPushMessage(
316            final Account account, final Jid transport, final Element push) {
317        final String instance = push.getAttribute("instance");
318        final String application = push.getAttribute("application");
319        if (Strings.isNullOrEmpty(instance) || Strings.isNullOrEmpty(application)) {
320            return false;
321        }
322        final String content = push.getContent();
323        final byte[] payload;
324        if (Strings.isNullOrEmpty(content)) {
325            payload = new byte[0];
326        } else if (BaseEncoding.base64().canDecode(content)) {
327            payload = BaseEncoding.base64().decode(content);
328        } else {
329            Log.d(
330                    Config.LOGTAG,
331                    account.getJid().asBareJid() + ": received invalid unified push payload");
332            return false;
333        }
334        final Optional<UnifiedPushDatabase.PushTarget> pushTarget =
335                getPushTarget(account, transport, application, instance);
336        if (pushTarget.isPresent()) {
337            final UnifiedPushDatabase.PushTarget target = pushTarget.get();
338            // TODO check if app is still installed?
339            Log.d(
340                    Config.LOGTAG,
341                    account.getJid().asBareJid()
342                            + ": broadcasting a "
343                            + payload.length
344                            + " bytes push message to "
345                            + target.application);
346            broadcastPushMessage(target, payload);
347            return true;
348        } else {
349            Log.d(Config.LOGTAG, "could not find application for push");
350            return false;
351        }
352    }
353
354    public Optional<Transport> getTransport() {
355        final SharedPreferences sharedPreferences =
356                PreferenceManager.getDefaultSharedPreferences(service.getApplicationContext());
357        final String accountPreference =
358                sharedPreferences.getString(UnifiedPushDistributor.PREFERENCE_ACCOUNT, "none");
359        final String pushServerPreference =
360                sharedPreferences.getString(
361                        UnifiedPushDistributor.PREFERENCE_PUSH_SERVER,
362                        service.getString(R.string.default_push_server));
363        if (Strings.isNullOrEmpty(accountPreference)
364                || "none".equalsIgnoreCase(accountPreference)
365                || Strings.nullToEmpty(pushServerPreference).trim().isEmpty()) {
366            return Optional.absent();
367        }
368        final Jid transport;
369        final Jid jid;
370        try {
371            transport = Jid.ofEscaped(Strings.nullToEmpty(pushServerPreference).trim());
372            jid = Jid.ofEscaped(Strings.nullToEmpty(accountPreference).trim());
373        } catch (final IllegalArgumentException e) {
374            return Optional.absent();
375        }
376        final Account account = service.findAccountByJid(jid);
377        if (account == null) {
378            return Optional.absent();
379        }
380        return Optional.of(new Transport(account, transport));
381    }
382
383    private Optional<UnifiedPushDatabase.PushTarget> getPushTarget(
384            final Account account,
385            final Jid transport,
386            final String application,
387            final String instance) {
388        if (transport == null || application == null || instance == null) {
389            return Optional.absent();
390        }
391        final String uuid = account.getUuid();
392        final List<UnifiedPushDatabase.PushTarget> pushTargets =
393                UnifiedPushDatabase.getInstance(service)
394                        .getPushTargets(uuid, transport.toEscapedString());
395        return Iterables.tryFind(
396                pushTargets,
397                pt ->
398                        UnifiedPushDistributor.hash(uuid, pt.application).equals(application)
399                                && UnifiedPushDistributor.hash(uuid, pt.instance).equals(instance));
400    }
401
402    private void broadcastPushMessage(
403            final UnifiedPushDatabase.PushTarget target, final byte[] payload) {
404        final Intent updateIntent = new Intent(UnifiedPushDistributor.ACTION_MESSAGE);
405        updateIntent.setPackage(target.application);
406        updateIntent.putExtra("token", target.instance);
407        updateIntent.putExtra("bytesMessage", payload);
408        updateIntent.putExtra("message", new String(payload, StandardCharsets.UTF_8));
409        final var distributorVerificationIntent = new Intent();
410        distributorVerificationIntent.setPackage(service.getPackageName());
411        final var pendingIntent =
412                PendingIntent.getBroadcast(
413                        service, 0, distributorVerificationIntent, PendingIntent.FLAG_IMMUTABLE);
414        updateIntent.putExtra("distributor", pendingIntent);
415        service.sendBroadcast(updateIntent);
416    }
417
418    private void broadcastEndpoint(
419            final String instance, final UnifiedPushDatabase.ApplicationEndpoint endpoint) {
420        Log.d(Config.LOGTAG, "broadcasting endpoint to " + endpoint.application);
421        final Intent updateIntent = endpointIntent(instance, endpoint);
422        service.sendBroadcast(updateIntent);
423    }
424
425    private Intent endpointIntent(final String instance, final UnifiedPushDatabase.ApplicationEndpoint endpoint) {
426        final Intent intent = new Intent(UnifiedPushDistributor.ACTION_NEW_ENDPOINT);
427        intent.setPackage(endpoint.application);
428        intent.putExtra("token", instance);
429        intent.putExtra("endpoint", endpoint.endpoint);
430        final var distributorVerificationIntent = new Intent();
431        distributorVerificationIntent.setPackage(service.getPackageName());
432        final var pendingIntent =
433                PendingIntent.getBroadcast(
434                        service, 0, distributorVerificationIntent, PendingIntent.FLAG_IMMUTABLE);
435        intent.putExtra("distributor", pendingIntent);
436        return intent;
437    }
438
439    private Intent unregisteredIntent(final UnifiedPushDatabase.PushTarget pushTarget) {
440        final Intent intent = new Intent(UnifiedPushDistributor.ACTION_UNREGISTERED);
441        intent.setPackage(pushTarget.application);
442        intent.putExtra("token", pushTarget.instance);
443        final var distributorVerificationIntent = new Intent();
444        distributorVerificationIntent.setPackage(service.getPackageName());
445        final var pendingIntent =
446                PendingIntent.getBroadcast(
447                        service, 0, distributorVerificationIntent, PendingIntent.FLAG_IMMUTABLE);
448        intent.putExtra("distributor", pendingIntent);
449        return intent;
450    }
451
452    public void rebroadcastEndpoint(final Messenger messenger, final String instance, final Transport transport) {
453        final UnifiedPushDatabase unifiedPushDatabase = UnifiedPushDatabase.getInstance(service);
454        final UnifiedPushDatabase.ApplicationEndpoint endpoint =
455                unifiedPushDatabase.getEndpoint(
456                        transport.account.getUuid(),
457                        transport.transport.toEscapedString(),
458                        instance);
459        if (endpoint != null) {
460            sendEndpoint(messenger, instance, endpoint);
461        }
462    }
463
464    public static class Transport {
465        public final Account account;
466        public final Jid transport;
467
468        public Transport(Account account, Jid transport) {
469            this.account = account;
470            this.transport = transport;
471        }
472    }
473
474    public static class PushTargetMessenger {
475        private final UnifiedPushDatabase.PushTarget pushTarget;
476        public final Messenger messenger;
477
478        public PushTargetMessenger(UnifiedPushDatabase.PushTarget pushTarget, Messenger messenger) {
479            this.pushTarget = pushTarget;
480            this.messenger = messenger;
481        }
482    }
483}