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