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;
 31
 32public class UnifiedPushBroker {
 33
 34    // time to expiration before a renewal attempt is made (24 hours)
 35    public static final long TIME_TO_RENEW = 86_400_000L;
 36
 37    // interval for the 'cron tob' that attempts renewals for everything that expires is lass than
 38    // `TIME_TO_RENEW`
 39    public static final long RENEWAL_INTERVAL = 3_600_000L;
 40
 41    private static final ScheduledExecutorService SCHEDULER = Executors.newScheduledThreadPool(1);
 42
 43    private final XmppConnectionService service;
 44
 45    public UnifiedPushBroker(final XmppConnectionService xmppConnectionService) {
 46        this.service = xmppConnectionService;
 47        SCHEDULER.scheduleAtFixedRate(
 48                this::renewUnifiedPushEndpoints,
 49                RENEWAL_INTERVAL,
 50                RENEWAL_INTERVAL,
 51                TimeUnit.MILLISECONDS);
 52    }
 53
 54    public void renewUnifiedPushEndpointsOnBind(final Account account) {
 55        final Optional<Transport> transport = getTransport();
 56        if (transport.isPresent()) {
 57            final Account transportAccount = transport.get().account;
 58            if (transportAccount != null && transportAccount.getUuid().equals(account.getUuid())) {
 59                Log.d(
 60                        Config.LOGTAG,
 61                        account.getJid().asBareJid() + ": trigger endpoint renewal on bind");
 62                renewUnifiedEndpoint(transport.get());
 63            }
 64        }
 65    }
 66
 67    public Optional<Transport> renewUnifiedPushEndpoints() {
 68        final Optional<Transport> transportOptional = getTransport();
 69        if (transportOptional.isPresent()) {
 70            final Transport transport = transportOptional.get();
 71            if (transport.account.isEnabled()) {
 72                renewUnifiedEndpoint(transportOptional.get());
 73            } else {
 74                Log.d(Config.LOGTAG, "skipping UnifiedPush endpoint renewal. Account is disabled");
 75            }
 76        } else {
 77            Log.d(Config.LOGTAG, "skipping UnifiedPush endpoint renewal. No transport selected");
 78        }
 79        return transportOptional;
 80    }
 81
 82    private void renewUnifiedEndpoint(final Transport transport) {
 83        final Account account = transport.account;
 84        final UnifiedPushDatabase unifiedPushDatabase = UnifiedPushDatabase.getInstance(service);
 85        final List<UnifiedPushDatabase.PushTarget> renewals =
 86                unifiedPushDatabase.getRenewals(
 87                        account.getUuid(), transport.transport.toEscapedString());
 88        Log.d(
 89                Config.LOGTAG,
 90                account.getJid().asBareJid()
 91                        + ": "
 92                        + renewals.size()
 93                        + " UnifiedPush endpoints scheduled for renewal on "
 94                        + transport.transport);
 95        for (final UnifiedPushDatabase.PushTarget renewal : renewals) {
 96            Log.d(
 97                    Config.LOGTAG,
 98                    account.getJid().asBareJid() + ": try to renew UnifiedPush " + renewal);
 99            final String hashedApplication =
100                    UnifiedPushDistributor.hash(account.getUuid(), renewal.application);
101            final String hashedInstance =
102                    UnifiedPushDistributor.hash(account.getUuid(), renewal.instance);
103            final IqPacket registration = new IqPacket(IqPacket.TYPE.SET);
104            registration.setTo(transport.transport);
105            final Element register = registration.addChild("register", Namespace.UNIFIED_PUSH);
106            register.setAttribute("application", hashedApplication);
107            register.setAttribute("instance", hashedInstance);
108            this.service.sendIqPacket(
109                    account,
110                    registration,
111                    (a, response) -> processRegistration(transport, renewal, response));
112        }
113    }
114
115    private void processRegistration(
116            final Transport transport,
117            final UnifiedPushDatabase.PushTarget renewal,
118            final IqPacket response) {
119        if (response.getType() == IqPacket.TYPE.RESULT) {
120            final Element registered = response.findChild("registered", Namespace.UNIFIED_PUSH);
121            if (registered == null) {
122                return;
123            }
124            final String endpoint = registered.getAttribute("endpoint");
125            if (Strings.isNullOrEmpty(endpoint)) {
126                Log.w(Config.LOGTAG, "endpoint was null in up registration");
127                return;
128            }
129            final long expiration;
130            try {
131                expiration = AbstractParser.getTimestamp(registered.getAttribute("expiration"));
132            } catch (final IllegalArgumentException | ParseException e) {
133                Log.d(Config.LOGTAG, "could not parse expiration", e);
134                return;
135            }
136            renewUnifiedPushEndpoint(transport, renewal, endpoint, expiration);
137        } else {
138            Log.d(Config.LOGTAG, "could not register UP endpoint " + response.getErrorCondition());
139        }
140    }
141
142    private void renewUnifiedPushEndpoint(
143            final Transport transport,
144            final UnifiedPushDatabase.PushTarget renewal,
145            final String endpoint,
146            final long expiration) {
147        Log.d(Config.LOGTAG, "registered endpoint " + endpoint + " expiration=" + expiration);
148        final UnifiedPushDatabase unifiedPushDatabase = UnifiedPushDatabase.getInstance(service);
149        final boolean modified =
150                unifiedPushDatabase.updateEndpoint(
151                        renewal.instance,
152                        transport.account.getUuid(),
153                        transport.transport.toEscapedString(),
154                        endpoint,
155                        expiration);
156        if (modified) {
157            Log.d(
158                    Config.LOGTAG,
159                    "endpoint for "
160                            + renewal.application
161                            + "/"
162                            + renewal.instance
163                            + " was updated to "
164                            + endpoint);
165            broadcastEndpoint(
166                    renewal.instance,
167                    new UnifiedPushDatabase.ApplicationEndpoint(renewal.application, endpoint));
168        }
169    }
170
171    public boolean reconfigurePushDistributor() {
172        final boolean enabled = getTransport().isPresent();
173        setUnifiedPushDistributorEnabled(enabled);
174        return enabled;
175    }
176
177    private void setUnifiedPushDistributorEnabled(final boolean enabled) {
178        final PackageManager packageManager = service.getPackageManager();
179        final ComponentName componentName =
180                new ComponentName(service, UnifiedPushDistributor.class);
181        if (enabled) {
182            packageManager.setComponentEnabledSetting(
183                    componentName,
184                    PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
185                    PackageManager.DONT_KILL_APP);
186            Log.d(Config.LOGTAG, "UnifiedPushDistributor has been enabled");
187        } else {
188            packageManager.setComponentEnabledSetting(
189                    componentName,
190                    PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
191                    PackageManager.DONT_KILL_APP);
192            Log.d(Config.LOGTAG, "UnifiedPushDistributor has been disabled");
193        }
194    }
195
196    public boolean processPushMessage(
197            final Account account, final Jid transport, final Element push) {
198        final String instance = push.getAttribute("instance");
199        final String application = push.getAttribute("application");
200        if (Strings.isNullOrEmpty(instance) || Strings.isNullOrEmpty(application)) {
201            return false;
202        }
203        final String content = push.getContent();
204        final byte[] payload;
205        if (Strings.isNullOrEmpty(content)) {
206            payload = new byte[0];
207        } else if (BaseEncoding.base64().canDecode(content)) {
208            payload = BaseEncoding.base64().decode(content);
209        } else {
210            Log.d(
211                    Config.LOGTAG,
212                    account.getJid().asBareJid() + ": received invalid unified push payload");
213            return false;
214        }
215        final Optional<UnifiedPushDatabase.PushTarget> pushTarget =
216                getPushTarget(account, transport, application, instance);
217        if (pushTarget.isPresent()) {
218            final UnifiedPushDatabase.PushTarget target = pushTarget.get();
219            // TODO check if app is still installed?
220            Log.d(
221                    Config.LOGTAG,
222                    account.getJid().asBareJid()
223                            + ": broadcasting a "
224                            + payload.length
225                            + " bytes push message to "
226                            + target.application);
227            broadcastPushMessage(target, payload);
228            return true;
229        } else {
230            Log.d(Config.LOGTAG, "could not find application for push");
231            return false;
232        }
233    }
234
235    public Optional<Transport> getTransport() {
236        final SharedPreferences sharedPreferences =
237                PreferenceManager.getDefaultSharedPreferences(service.getApplicationContext());
238        final String accountPreference =
239                sharedPreferences.getString(UnifiedPushDistributor.PREFERENCE_ACCOUNT, "none");
240        final String pushServerPreference =
241                sharedPreferences.getString(
242                        UnifiedPushDistributor.PREFERENCE_PUSH_SERVER,
243                        service.getString(R.string.default_push_server));
244        if (Strings.isNullOrEmpty(accountPreference)
245                || "none".equalsIgnoreCase(accountPreference)
246                || Strings.nullToEmpty(pushServerPreference).trim().isEmpty()) {
247            return Optional.absent();
248        }
249        final Jid transport;
250        final Jid jid;
251        try {
252            transport = Jid.ofEscaped(Strings.nullToEmpty(pushServerPreference).trim());
253            jid = Jid.ofEscaped(Strings.nullToEmpty(accountPreference).trim());
254        } catch (final IllegalArgumentException e) {
255            return Optional.absent();
256        }
257        final Account account = service.findAccountByJid(jid);
258        if (account == null) {
259            return Optional.absent();
260        }
261        return Optional.of(new Transport(account, transport));
262    }
263
264    private Optional<UnifiedPushDatabase.PushTarget> getPushTarget(
265            final Account account,
266            final Jid transport,
267            final String application,
268            final String instance) {
269        final String uuid = account.getUuid();
270        final List<UnifiedPushDatabase.PushTarget> pushTargets =
271                UnifiedPushDatabase.getInstance(service)
272                        .getPushTargets(uuid, transport.toEscapedString());
273        return Iterables.tryFind(
274                pushTargets,
275                pt ->
276                        UnifiedPushDistributor.hash(uuid, pt.application).equals(application)
277                                && UnifiedPushDistributor.hash(uuid, pt.instance).equals(instance));
278    }
279
280    private void broadcastPushMessage(
281            final UnifiedPushDatabase.PushTarget target, final byte[] payload) {
282        final Intent updateIntent = new Intent(UnifiedPushDistributor.ACTION_MESSAGE);
283        updateIntent.setPackage(target.application);
284        updateIntent.putExtra("token", target.instance);
285        updateIntent.putExtra("bytesMessage", payload);
286        updateIntent.putExtra("message", new String(payload, StandardCharsets.UTF_8));
287        service.sendBroadcast(updateIntent);
288    }
289
290    private void broadcastEndpoint(
291            final String instance, final UnifiedPushDatabase.ApplicationEndpoint endpoint) {
292        Log.d(Config.LOGTAG, "broadcasting endpoint to " + endpoint.application);
293        final Intent updateIntent = new Intent(UnifiedPushDistributor.ACTION_NEW_ENDPOINT);
294        updateIntent.setPackage(endpoint.application);
295        updateIntent.putExtra("token", instance);
296        updateIntent.putExtra("endpoint", endpoint.endpoint);
297        service.sendBroadcast(updateIntent);
298    }
299
300    public void rebroadcastEndpoint(final String instance, final Transport transport) {
301        final UnifiedPushDatabase unifiedPushDatabase = UnifiedPushDatabase.getInstance(service);
302        final UnifiedPushDatabase.ApplicationEndpoint endpoint =
303                unifiedPushDatabase.getEndpoint(
304                        transport.account.getUuid(),
305                        transport.transport.toEscapedString(),
306                        instance);
307        if (endpoint != null) {
308            broadcastEndpoint(instance, endpoint);
309        }
310    }
311
312    public static class Transport {
313        public final Account account;
314        public final Jid transport;
315
316        public Transport(Account account, Jid transport) {
317            this.account = account;
318            this.transport = transport;
319        }
320    }
321}