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.preference.PreferenceManager;
  8import android.util.Log;
  9
 10import com.google.common.base.Optional;
 11import com.google.common.base.Strings;
 12import com.google.common.collect.Iterables;
 13import com.google.common.io.BaseEncoding;
 14
 15import java.nio.charset.StandardCharsets;
 16import java.text.ParseException;
 17import java.util.List;
 18import java.util.concurrent.Executors;
 19import java.util.concurrent.ScheduledExecutorService;
 20import java.util.concurrent.TimeUnit;
 21
 22import eu.siacs.conversations.Config;
 23import eu.siacs.conversations.R;
 24import eu.siacs.conversations.entities.Account;
 25import eu.siacs.conversations.parser.AbstractParser;
 26import eu.siacs.conversations.persistance.UnifiedPushDatabase;
 27import eu.siacs.conversations.xml.Element;
 28import eu.siacs.conversations.xml.Namespace;
 29import eu.siacs.conversations.xmpp.Jid;
 30import eu.siacs.conversations.xmpp.stanzas.IqPacket;
 31import eu.siacs.conversations.xmpp.stanzas.PresencePacket;
 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());
 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        final Optional<Transport> transportOptional = getTransport();
 81        if (transportOptional.isPresent()) {
 82            final Transport transport = transportOptional.get();
 83            if (transport.account.isEnabled()) {
 84                renewUnifiedEndpoint(transportOptional.get());
 85            } else {
 86                Log.d(Config.LOGTAG, "skipping UnifiedPush endpoint renewal. Account is disabled");
 87            }
 88        } else {
 89            Log.d(Config.LOGTAG, "skipping UnifiedPush endpoint renewal. No transport selected");
 90        }
 91        return transportOptional;
 92    }
 93
 94    private void renewUnifiedEndpoint(final Transport transport) {
 95        final Account account = transport.account;
 96        final UnifiedPushDatabase unifiedPushDatabase = UnifiedPushDatabase.getInstance(service);
 97        final List<UnifiedPushDatabase.PushTarget> renewals =
 98                unifiedPushDatabase.getRenewals(
 99                        account.getUuid(), transport.transport.toEscapedString());
100        Log.d(
101                Config.LOGTAG,
102                account.getJid().asBareJid()
103                        + ": "
104                        + renewals.size()
105                        + " UnifiedPush endpoints scheduled for renewal on "
106                        + transport.transport);
107        for (final UnifiedPushDatabase.PushTarget renewal : renewals) {
108            Log.d(
109                    Config.LOGTAG,
110                    account.getJid().asBareJid() + ": try to renew UnifiedPush " + renewal);
111            final String hashedApplication =
112                    UnifiedPushDistributor.hash(account.getUuid(), renewal.application);
113            final String hashedInstance =
114                    UnifiedPushDistributor.hash(account.getUuid(), renewal.instance);
115            final IqPacket registration = new IqPacket(IqPacket.TYPE.SET);
116            registration.setTo(transport.transport);
117            final Element register = registration.addChild("register", Namespace.UNIFIED_PUSH);
118            register.setAttribute("application", hashedApplication);
119            register.setAttribute("instance", hashedInstance);
120            this.service.sendIqPacket(
121                    account,
122                    registration,
123                    (a, response) -> processRegistration(transport, renewal, response));
124        }
125    }
126
127    private void processRegistration(
128            final Transport transport,
129            final UnifiedPushDatabase.PushTarget renewal,
130            final IqPacket response) {
131        if (response.getType() == IqPacket.TYPE.RESULT) {
132            final Element registered = response.findChild("registered", Namespace.UNIFIED_PUSH);
133            if (registered == null) {
134                return;
135            }
136            final String endpoint = registered.getAttribute("endpoint");
137            if (Strings.isNullOrEmpty(endpoint)) {
138                Log.w(Config.LOGTAG, "endpoint was null in up registration");
139                return;
140            }
141            final long expiration;
142            try {
143                expiration = AbstractParser.getTimestamp(registered.getAttribute("expiration"));
144            } catch (final IllegalArgumentException | ParseException e) {
145                Log.d(Config.LOGTAG, "could not parse expiration", e);
146                return;
147            }
148            renewUnifiedPushEndpoint(transport, renewal, endpoint, expiration);
149        } else {
150            Log.d(Config.LOGTAG, "could not register UP endpoint " + response.getErrorCondition());
151        }
152    }
153
154    private void renewUnifiedPushEndpoint(
155            final Transport transport,
156            final UnifiedPushDatabase.PushTarget renewal,
157            final String endpoint,
158            final long expiration) {
159        Log.d(Config.LOGTAG, "registered endpoint " + endpoint + " expiration=" + expiration);
160        final UnifiedPushDatabase unifiedPushDatabase = UnifiedPushDatabase.getInstance(service);
161        final boolean modified =
162                unifiedPushDatabase.updateEndpoint(
163                        renewal.instance,
164                        transport.account.getUuid(),
165                        transport.transport.toEscapedString(),
166                        endpoint,
167                        expiration);
168        if (modified) {
169            Log.d(
170                    Config.LOGTAG,
171                    "endpoint for "
172                            + renewal.application
173                            + "/"
174                            + renewal.instance
175                            + " was updated to "
176                            + endpoint);
177            broadcastEndpoint(
178                    renewal.instance,
179                    new UnifiedPushDatabase.ApplicationEndpoint(renewal.application, endpoint));
180        }
181    }
182
183    public boolean reconfigurePushDistributor() {
184        final boolean enabled = getTransport().isPresent();
185        setUnifiedPushDistributorEnabled(enabled);
186        return enabled;
187    }
188
189    private void setUnifiedPushDistributorEnabled(final boolean enabled) {
190        final PackageManager packageManager = service.getPackageManager();
191        final ComponentName componentName =
192                new ComponentName(service, UnifiedPushDistributor.class);
193        if (enabled) {
194            packageManager.setComponentEnabledSetting(
195                    componentName,
196                    PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
197                    PackageManager.DONT_KILL_APP);
198            Log.d(Config.LOGTAG, "UnifiedPushDistributor has been enabled");
199        } else {
200            packageManager.setComponentEnabledSetting(
201                    componentName,
202                    PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
203                    PackageManager.DONT_KILL_APP);
204            Log.d(Config.LOGTAG, "UnifiedPushDistributor has been disabled");
205        }
206    }
207
208    public boolean processPushMessage(
209            final Account account, final Jid transport, final Element push) {
210        final String instance = push.getAttribute("instance");
211        final String application = push.getAttribute("application");
212        if (Strings.isNullOrEmpty(instance) || Strings.isNullOrEmpty(application)) {
213            return false;
214        }
215        final String content = push.getContent();
216        final byte[] payload;
217        if (Strings.isNullOrEmpty(content)) {
218            payload = new byte[0];
219        } else if (BaseEncoding.base64().canDecode(content)) {
220            payload = BaseEncoding.base64().decode(content);
221        } else {
222            Log.d(
223                    Config.LOGTAG,
224                    account.getJid().asBareJid() + ": received invalid unified push payload");
225            return false;
226        }
227        final Optional<UnifiedPushDatabase.PushTarget> pushTarget =
228                getPushTarget(account, transport, application, instance);
229        if (pushTarget.isPresent()) {
230            final UnifiedPushDatabase.PushTarget target = pushTarget.get();
231            // TODO check if app is still installed?
232            Log.d(
233                    Config.LOGTAG,
234                    account.getJid().asBareJid()
235                            + ": broadcasting a "
236                            + payload.length
237                            + " bytes push message to "
238                            + target.application);
239            broadcastPushMessage(target, payload);
240            return true;
241        } else {
242            Log.d(Config.LOGTAG, "could not find application for push");
243            return false;
244        }
245    }
246
247    public Optional<Transport> getTransport() {
248        final SharedPreferences sharedPreferences =
249                PreferenceManager.getDefaultSharedPreferences(service.getApplicationContext());
250        final String accountPreference =
251                sharedPreferences.getString(UnifiedPushDistributor.PREFERENCE_ACCOUNT, "none");
252        final String pushServerPreference =
253                sharedPreferences.getString(
254                        UnifiedPushDistributor.PREFERENCE_PUSH_SERVER,
255                        service.getString(R.string.default_push_server));
256        if (Strings.isNullOrEmpty(accountPreference)
257                || "none".equalsIgnoreCase(accountPreference)
258                || Strings.nullToEmpty(pushServerPreference).trim().isEmpty()) {
259            return Optional.absent();
260        }
261        final Jid transport;
262        final Jid jid;
263        try {
264            transport = Jid.ofEscaped(Strings.nullToEmpty(pushServerPreference).trim());
265            jid = Jid.ofEscaped(Strings.nullToEmpty(accountPreference).trim());
266        } catch (final IllegalArgumentException e) {
267            return Optional.absent();
268        }
269        final Account account = service.findAccountByJid(jid);
270        if (account == null) {
271            return Optional.absent();
272        }
273        return Optional.of(new Transport(account, transport));
274    }
275
276    private Optional<UnifiedPushDatabase.PushTarget> getPushTarget(
277            final Account account,
278            final Jid transport,
279            final String application,
280            final String instance) {
281        final String uuid = account.getUuid();
282        final List<UnifiedPushDatabase.PushTarget> pushTargets =
283                UnifiedPushDatabase.getInstance(service)
284                        .getPushTargets(uuid, transport.toEscapedString());
285        return Iterables.tryFind(
286                pushTargets,
287                pt ->
288                        UnifiedPushDistributor.hash(uuid, pt.application).equals(application)
289                                && UnifiedPushDistributor.hash(uuid, pt.instance).equals(instance));
290    }
291
292    private void broadcastPushMessage(
293            final UnifiedPushDatabase.PushTarget target, final byte[] payload) {
294        final Intent updateIntent = new Intent(UnifiedPushDistributor.ACTION_MESSAGE);
295        updateIntent.setPackage(target.application);
296        updateIntent.putExtra("token", target.instance);
297        updateIntent.putExtra("bytesMessage", payload);
298        updateIntent.putExtra("message", new String(payload, StandardCharsets.UTF_8));
299        service.sendBroadcast(updateIntent);
300    }
301
302    private void broadcastEndpoint(
303            final String instance, final UnifiedPushDatabase.ApplicationEndpoint endpoint) {
304        Log.d(Config.LOGTAG, "broadcasting endpoint to " + endpoint.application);
305        final Intent updateIntent = new Intent(UnifiedPushDistributor.ACTION_NEW_ENDPOINT);
306        updateIntent.setPackage(endpoint.application);
307        updateIntent.putExtra("token", instance);
308        updateIntent.putExtra("endpoint", endpoint.endpoint);
309        service.sendBroadcast(updateIntent);
310    }
311
312    public void rebroadcastEndpoint(final String instance, final Transport transport) {
313        final UnifiedPushDatabase unifiedPushDatabase = UnifiedPushDatabase.getInstance(service);
314        final UnifiedPushDatabase.ApplicationEndpoint endpoint =
315                unifiedPushDatabase.getEndpoint(
316                        transport.account.getUuid(),
317                        transport.transport.toEscapedString(),
318                        instance);
319        if (endpoint != null) {
320            broadcastEndpoint(instance, endpoint);
321        }
322    }
323
324    public static class Transport {
325        public final Account account;
326        public final Jid transport;
327
328        public Transport(Account account, Jid transport) {
329            this.account = account;
330            this.transport = transport;
331        }
332    }
333}