UnifiedPushBroker.java

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