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            final String hashedApplication =
116                    UnifiedPushDistributor.hash(account.getUuid(), renewal.application);
117            final String hashedInstance =
118                    UnifiedPushDistributor.hash(account.getUuid(), renewal.instance);
119            final IqPacket registration = new IqPacket(IqPacket.TYPE.SET);
120            registration.setTo(transport.transport);
121            final Element register = registration.addChild("register", Namespace.UNIFIED_PUSH);
122            register.setAttribute("application", hashedApplication);
123            register.setAttribute("instance", hashedInstance);
124            final Messenger messenger;
125            if (pushTargetMessenger != null && renewal.equals(pushTargetMessenger.pushTarget)) {
126                messenger = pushTargetMessenger.messenger;
127            } else {
128                messenger = null;
129            }
130            this.service.sendIqPacket(
131                    account,
132                    registration,
133                    (a, response) -> processRegistration(transport, renewal, messenger, response));
134        }
135    }
136
137    private void processRegistration(
138            final Transport transport,
139            final UnifiedPushDatabase.PushTarget renewal,
140            final Messenger messenger,
141            final IqPacket response) {
142        if (response.getType() == IqPacket.TYPE.RESULT) {
143            final Element registered = response.findChild("registered", Namespace.UNIFIED_PUSH);
144            if (registered == null) {
145                return;
146            }
147            final String endpoint = registered.getAttribute("endpoint");
148            if (Strings.isNullOrEmpty(endpoint)) {
149                Log.w(Config.LOGTAG, "endpoint was null in up registration");
150                return;
151            }
152            final long expiration;
153            try {
154                expiration = AbstractParser.getTimestamp(registered.getAttribute("expiration"));
155            } catch (final IllegalArgumentException | ParseException e) {
156                Log.d(Config.LOGTAG, "could not parse expiration", e);
157                return;
158            }
159            renewUnifiedPushEndpoint(transport, renewal, messenger, endpoint, expiration);
160        } else {
161            Log.d(Config.LOGTAG, "could not register UP endpoint " + response.getErrorCondition());
162        }
163    }
164
165    private void renewUnifiedPushEndpoint(
166            final Transport transport,
167            final UnifiedPushDatabase.PushTarget renewal,
168            final Messenger messenger,
169            final String endpoint,
170            final long expiration) {
171        Log.d(Config.LOGTAG, "registered endpoint " + endpoint + " expiration=" + expiration);
172        final UnifiedPushDatabase unifiedPushDatabase = UnifiedPushDatabase.getInstance(service);
173        final boolean modified =
174                unifiedPushDatabase.updateEndpoint(
175                        renewal.instance,
176                        transport.account.getUuid(),
177                        transport.transport.toEscapedString(),
178                        endpoint,
179                        expiration);
180        if (modified) {
181            Log.d(
182                    Config.LOGTAG,
183                    "endpoint for "
184                            + renewal.application
185                            + "/"
186                            + renewal.instance
187                            + " was updated to "
188                            + endpoint);
189            final UnifiedPushDatabase.ApplicationEndpoint applicationEndpoint = new UnifiedPushDatabase.ApplicationEndpoint(renewal.application, endpoint);
190            sendEndpoint(messenger, renewal.instance, applicationEndpoint);
191        }
192    }
193
194    private void sendEndpoint(final Messenger messenger, String instance, final UnifiedPushDatabase.ApplicationEndpoint applicationEndpoint) {
195        if (messenger != null) {
196            Log.d(Config.LOGTAG,"using messenger instead of broadcast to communicate endpoint to "+applicationEndpoint.application);
197            final Message message = new Message();
198            message.obj = endpointIntent(instance, applicationEndpoint);
199            try {
200                messenger.send(message);
201            } catch (final RemoteException e) {
202                Log.d(Config.LOGTAG,"messenger failed. falling back to broadcast");
203                broadcastEndpoint(instance, applicationEndpoint);
204            }
205        } else {
206            broadcastEndpoint(instance, applicationEndpoint);
207        }
208    }
209
210    public boolean reconfigurePushDistributor() {
211        final boolean enabled = getTransport().isPresent();
212        setUnifiedPushDistributorEnabled(enabled);
213        return enabled;
214    }
215
216    private void setUnifiedPushDistributorEnabled(final boolean enabled) {
217        final PackageManager packageManager = service.getPackageManager();
218        final ComponentName componentName =
219                new ComponentName(service, UnifiedPushDistributor.class);
220        if (enabled) {
221            packageManager.setComponentEnabledSetting(
222                    componentName,
223                    PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
224                    PackageManager.DONT_KILL_APP);
225            Log.d(Config.LOGTAG, "UnifiedPushDistributor has been enabled");
226        } else {
227            packageManager.setComponentEnabledSetting(
228                    componentName,
229                    PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
230                    PackageManager.DONT_KILL_APP);
231            Log.d(Config.LOGTAG, "UnifiedPushDistributor has been disabled");
232        }
233    }
234
235    public boolean processPushMessage(
236            final Account account, final Jid transport, final Element push) {
237        final String instance = push.getAttribute("instance");
238        final String application = push.getAttribute("application");
239        if (Strings.isNullOrEmpty(instance) || Strings.isNullOrEmpty(application)) {
240            return false;
241        }
242        final String content = push.getContent();
243        final byte[] payload;
244        if (Strings.isNullOrEmpty(content)) {
245            payload = new byte[0];
246        } else if (BaseEncoding.base64().canDecode(content)) {
247            payload = BaseEncoding.base64().decode(content);
248        } else {
249            Log.d(
250                    Config.LOGTAG,
251                    account.getJid().asBareJid() + ": received invalid unified push payload");
252            return false;
253        }
254        final Optional<UnifiedPushDatabase.PushTarget> pushTarget =
255                getPushTarget(account, transport, application, instance);
256        if (pushTarget.isPresent()) {
257            final UnifiedPushDatabase.PushTarget target = pushTarget.get();
258            // TODO check if app is still installed?
259            Log.d(
260                    Config.LOGTAG,
261                    account.getJid().asBareJid()
262                            + ": broadcasting a "
263                            + payload.length
264                            + " bytes push message to "
265                            + target.application);
266            broadcastPushMessage(target, payload);
267            return true;
268        } else {
269            Log.d(Config.LOGTAG, "could not find application for push");
270            return false;
271        }
272    }
273
274    public Optional<Transport> getTransport() {
275        final SharedPreferences sharedPreferences =
276                PreferenceManager.getDefaultSharedPreferences(service.getApplicationContext());
277        final String accountPreference =
278                sharedPreferences.getString(UnifiedPushDistributor.PREFERENCE_ACCOUNT, "none");
279        final String pushServerPreference =
280                sharedPreferences.getString(
281                        UnifiedPushDistributor.PREFERENCE_PUSH_SERVER,
282                        service.getString(R.string.default_push_server));
283        if (Strings.isNullOrEmpty(accountPreference)
284                || "none".equalsIgnoreCase(accountPreference)
285                || Strings.nullToEmpty(pushServerPreference).trim().isEmpty()) {
286            return Optional.absent();
287        }
288        final Jid transport;
289        final Jid jid;
290        try {
291            transport = Jid.ofEscaped(Strings.nullToEmpty(pushServerPreference).trim());
292            jid = Jid.ofEscaped(Strings.nullToEmpty(accountPreference).trim());
293        } catch (final IllegalArgumentException e) {
294            return Optional.absent();
295        }
296        final Account account = service.findAccountByJid(jid);
297        if (account == null) {
298            return Optional.absent();
299        }
300        return Optional.of(new Transport(account, transport));
301    }
302
303    private Optional<UnifiedPushDatabase.PushTarget> getPushTarget(
304            final Account account,
305            final Jid transport,
306            final String application,
307            final String instance) {
308        if (transport == null || application == null || instance == null) {
309            return Optional.absent();
310        }
311        final String uuid = account.getUuid();
312        final List<UnifiedPushDatabase.PushTarget> pushTargets =
313                UnifiedPushDatabase.getInstance(service)
314                        .getPushTargets(uuid, transport.toEscapedString());
315        return Iterables.tryFind(
316                pushTargets,
317                pt ->
318                        UnifiedPushDistributor.hash(uuid, pt.application).equals(application)
319                                && UnifiedPushDistributor.hash(uuid, pt.instance).equals(instance));
320    }
321
322    private void broadcastPushMessage(
323            final UnifiedPushDatabase.PushTarget target, final byte[] payload) {
324        final Intent updateIntent = new Intent(UnifiedPushDistributor.ACTION_MESSAGE);
325        updateIntent.setPackage(target.application);
326        updateIntent.putExtra("token", target.instance);
327        updateIntent.putExtra("bytesMessage", payload);
328        updateIntent.putExtra("message", new String(payload, StandardCharsets.UTF_8));
329        service.sendBroadcast(updateIntent);
330    }
331
332    private void broadcastEndpoint(
333            final String instance, final UnifiedPushDatabase.ApplicationEndpoint endpoint) {
334        Log.d(Config.LOGTAG, "broadcasting endpoint to " + endpoint.application);
335        final Intent updateIntent = endpointIntent(instance, endpoint);
336        service.sendBroadcast(updateIntent);
337    }
338
339    private static Intent endpointIntent(final String instance, final UnifiedPushDatabase.ApplicationEndpoint endpoint) {
340        final Intent intent = new Intent(UnifiedPushDistributor.ACTION_NEW_ENDPOINT);
341        intent.setPackage(endpoint.application);
342        intent.putExtra("token", instance);
343        intent.putExtra("endpoint", endpoint.endpoint);
344        return intent;
345    }
346
347    public void rebroadcastEndpoint(final Messenger messenger, final String instance, final Transport transport) {
348        final UnifiedPushDatabase unifiedPushDatabase = UnifiedPushDatabase.getInstance(service);
349        final UnifiedPushDatabase.ApplicationEndpoint endpoint =
350                unifiedPushDatabase.getEndpoint(
351                        transport.account.getUuid(),
352                        transport.transport.toEscapedString(),
353                        instance);
354        if (endpoint != null) {
355            sendEndpoint(messenger, instance, endpoint);
356        }
357    }
358
359    public static class Transport {
360        public final Account account;
361        public final Jid transport;
362
363        public Transport(Account account, Jid transport) {
364            this.account = account;
365            this.transport = transport;
366        }
367    }
368
369    public static class PushTargetMessenger {
370        private final UnifiedPushDatabase.PushTarget pushTarget;
371        private final Messenger messenger;
372
373        public PushTargetMessenger(UnifiedPushDatabase.PushTarget pushTarget, Messenger messenger) {
374            this.pushTarget = pushTarget;
375            this.messenger = messenger;
376        }
377    }
378}