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