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