UnifiedPushBroker.java

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