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