1package eu.siacs.conversations.services;
2
3import android.app.PendingIntent;
4import android.content.ComponentName;
5import android.content.Intent;
6import android.content.SharedPreferences;
7import android.content.pm.PackageManager;
8import android.os.Message;
9import android.os.Messenger;
10import android.os.RemoteException;
11import android.preference.PreferenceManager;
12import android.util.Log;
13import com.google.common.base.Optional;
14import com.google.common.base.Strings;
15import com.google.common.collect.Iterables;
16import com.google.common.io.BaseEncoding;
17import eu.siacs.conversations.Config;
18import eu.siacs.conversations.R;
19import eu.siacs.conversations.entities.Account;
20import eu.siacs.conversations.parser.AbstractParser;
21import eu.siacs.conversations.persistance.UnifiedPushDatabase;
22import eu.siacs.conversations.xml.Element;
23import eu.siacs.conversations.xml.Namespace;
24import eu.siacs.conversations.xmpp.Jid;
25import eu.siacs.conversations.xmpp.stanzas.IqPacket;
26import eu.siacs.conversations.xmpp.stanzas.PresencePacket;
27import java.nio.charset.StandardCharsets;
28import java.text.ParseException;
29import java.util.List;
30import java.util.concurrent.Executors;
31import java.util.concurrent.ScheduledExecutorService;
32import java.util.concurrent.TimeUnit;
33
34public class UnifiedPushBroker {
35
36 // time to expiration before a renewal attempt is made (24 hours)
37 public static final long TIME_TO_RENEW = 86_400_000L;
38
39 // interval for the 'cron tob' that attempts renewals for everything that expires is lass than
40 // `TIME_TO_RENEW`
41 public static final long RENEWAL_INTERVAL = 3_600_000L;
42
43 private static final ScheduledExecutorService SCHEDULER = Executors.newScheduledThreadPool(1);
44
45 private final XmppConnectionService service;
46
47 public UnifiedPushBroker(final XmppConnectionService xmppConnectionService) {
48 this.service = xmppConnectionService;
49 SCHEDULER.scheduleAtFixedRate(
50 this::renewUnifiedPushEndpoints,
51 RENEWAL_INTERVAL,
52 RENEWAL_INTERVAL,
53 TimeUnit.MILLISECONDS);
54 }
55
56 public void renewUnifiedPushEndpointsOnBind(final Account account) {
57 final Optional<Transport> transportOptional = getTransport();
58 if (transportOptional.isPresent()) {
59 final Transport transport = transportOptional.get();
60 final Account transportAccount = transport.account;
61 if (transportAccount != null && transportAccount.getUuid().equals(account.getUuid())) {
62 final UnifiedPushDatabase database = UnifiedPushDatabase.getInstance(service);
63 if (database.hasEndpoints(transport)) {
64 sendDirectedPresence(transportAccount, transport.transport);
65 }
66 Log.d(
67 Config.LOGTAG,
68 account.getJid().asBareJid() + ": trigger endpoint renewal on bind");
69 renewUnifiedEndpoint(transportOptional.get(), null);
70 }
71 }
72 }
73
74 private void sendDirectedPresence(final Account account, Jid to) {
75 final PresencePacket presence = new PresencePacket();
76 presence.setTo(to);
77 service.sendPresencePacket(account, presence);
78 }
79
80 public Optional<Transport> renewUnifiedPushEndpoints() {
81 return renewUnifiedPushEndpoints(null);
82 }
83
84 public Optional<Transport> renewUnifiedPushEndpoints(final PushTargetMessenger pushTargetMessenger) {
85 final Optional<Transport> transportOptional = getTransport();
86 if (transportOptional.isPresent()) {
87 final Transport transport = transportOptional.get();
88 if (transport.account.isEnabled()) {
89 renewUnifiedEndpoint(transportOptional.get(), pushTargetMessenger);
90 } else {
91 if (pushTargetMessenger.messenger != null) {
92 sendRegistrationDelayed(pushTargetMessenger.messenger,"account is disabled");
93 }
94 Log.d(Config.LOGTAG, "skipping UnifiedPush endpoint renewal. Account is disabled");
95 }
96 } else {
97 if (pushTargetMessenger.messenger != null) {
98 sendRegistrationDelayed(pushTargetMessenger.messenger,"no transport selected");
99 }
100 Log.d(Config.LOGTAG, "skipping UnifiedPush endpoint renewal. No transport selected");
101 }
102 return transportOptional;
103 }
104
105 private void sendRegistrationDelayed(final Messenger messenger, final String error) {
106 final Intent intent = new Intent(UnifiedPushDistributor.ACTION_REGISTRATION_DELAYED);
107 intent.putExtra(UnifiedPushDistributor.EXTRA_MESSAGE, error);
108 final var message = new Message();
109 message.obj = intent;
110 try {
111 messenger.send(message);
112 } catch (final RemoteException e) {
113 Log.d(Config.LOGTAG,"unable to tell messenger of delayed registration",e);
114 }
115 }
116
117 private void renewUnifiedEndpoint(final Transport transport, final PushTargetMessenger pushTargetMessenger) {
118 final Account account = transport.account;
119 final UnifiedPushDatabase unifiedPushDatabase = UnifiedPushDatabase.getInstance(service);
120 final List<UnifiedPushDatabase.PushTarget> renewals =
121 unifiedPushDatabase.getRenewals(
122 account.getUuid(), transport.transport.toEscapedString());
123 Log.d(
124 Config.LOGTAG,
125 account.getJid().asBareJid()
126 + ": "
127 + renewals.size()
128 + " UnifiedPush endpoints scheduled for renewal on "
129 + transport.transport);
130 for (final UnifiedPushDatabase.PushTarget renewal : renewals) {
131 Log.d(
132 Config.LOGTAG,
133 account.getJid().asBareJid() + ": try to renew UnifiedPush " + renewal);
134 UnifiedPushDistributor.quickLog(service,String.format("%s: try to renew UnifiedPush %s", account.getJid(), renewal.toString()));
135 final String hashedApplication =
136 UnifiedPushDistributor.hash(account.getUuid(), renewal.application);
137 final String hashedInstance =
138 UnifiedPushDistributor.hash(account.getUuid(), renewal.instance);
139 final IqPacket registration = new IqPacket(IqPacket.TYPE.SET);
140 registration.setTo(transport.transport);
141 final Element register = registration.addChild("register", Namespace.UNIFIED_PUSH);
142 register.setAttribute("application", hashedApplication);
143 register.setAttribute("instance", hashedInstance);
144 final Messenger messenger;
145 if (pushTargetMessenger != null && renewal.equals(pushTargetMessenger.pushTarget)) {
146 messenger = pushTargetMessenger.messenger;
147 } else {
148 messenger = null;
149 }
150 this.service.sendIqPacket(
151 account,
152 registration,
153 (a, response) -> processRegistration(transport, renewal, messenger, response));
154 }
155 }
156
157 private void processRegistration(
158 final Transport transport,
159 final UnifiedPushDatabase.PushTarget renewal,
160 final Messenger messenger,
161 final IqPacket response) {
162 if (response.getType() == IqPacket.TYPE.RESULT) {
163 final Element registered = response.findChild("registered", Namespace.UNIFIED_PUSH);
164 if (registered == null) {
165 return;
166 }
167 final String endpoint = registered.getAttribute("endpoint");
168 if (Strings.isNullOrEmpty(endpoint)) {
169 Log.w(Config.LOGTAG, "endpoint was null in up registration");
170 return;
171 }
172 final long expiration;
173 try {
174 expiration = AbstractParser.getTimestamp(registered.getAttribute("expiration"));
175 } catch (final IllegalArgumentException | ParseException e) {
176 Log.d(Config.LOGTAG, "could not parse expiration", e);
177 return;
178 }
179 renewUnifiedPushEndpoint(transport, renewal, messenger, endpoint, expiration);
180 } else {
181 Log.d(Config.LOGTAG, "could not register UP endpoint " + response.getErrorCondition());
182 }
183 }
184
185 private void renewUnifiedPushEndpoint(
186 final Transport transport,
187 final UnifiedPushDatabase.PushTarget renewal,
188 final Messenger messenger,
189 final String endpoint,
190 final long expiration) {
191 Log.d(Config.LOGTAG, "registered endpoint " + endpoint + " expiration=" + expiration);
192 final UnifiedPushDatabase unifiedPushDatabase = UnifiedPushDatabase.getInstance(service);
193 final boolean modified =
194 unifiedPushDatabase.updateEndpoint(
195 renewal.instance,
196 transport.account.getUuid(),
197 transport.transport.toEscapedString(),
198 endpoint,
199 expiration);
200 if (modified) {
201 Log.d(
202 Config.LOGTAG,
203 "endpoint for "
204 + renewal.application
205 + "/"
206 + renewal.instance
207 + " was updated to "
208 + endpoint);
209 UnifiedPushDistributor.quickLog(
210 service,
211 "endpoint for "
212 + renewal.application
213 + "/"
214 + renewal.instance
215 + " was updated to "
216 + endpoint);
217 final UnifiedPushDatabase.ApplicationEndpoint applicationEndpoint =
218 new UnifiedPushDatabase.ApplicationEndpoint(renewal.application, endpoint);
219 sendEndpoint(messenger, renewal.instance, applicationEndpoint);
220 }
221 }
222
223 private void sendEndpoint(final Messenger messenger, String instance, final UnifiedPushDatabase.ApplicationEndpoint applicationEndpoint) {
224 if (messenger != null) {
225 Log.d(Config.LOGTAG,"using messenger instead of broadcast to communicate endpoint to "+applicationEndpoint.application);
226 final Message message = new Message();
227 message.obj = endpointIntent(instance, applicationEndpoint);
228 try {
229 messenger.send(message);
230 } catch (final RemoteException e) {
231 Log.d(Config.LOGTAG,"messenger failed. falling back to broadcast");
232 broadcastEndpoint(instance, applicationEndpoint);
233 }
234 } else {
235 broadcastEndpoint(instance, applicationEndpoint);
236 }
237 }
238
239 public boolean reconfigurePushDistributor() {
240 final boolean enabled = getTransport().isPresent();
241 setUnifiedPushDistributorEnabled(enabled);
242 return enabled;
243 }
244
245 private void setUnifiedPushDistributorEnabled(final boolean enabled) {
246 final PackageManager packageManager = service.getPackageManager();
247 final ComponentName componentName =
248 new ComponentName(service, UnifiedPushDistributor.class);
249 if (enabled) {
250 packageManager.setComponentEnabledSetting(
251 componentName,
252 PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
253 PackageManager.DONT_KILL_APP);
254 Log.d(Config.LOGTAG, "UnifiedPushDistributor has been enabled");
255 } else {
256 packageManager.setComponentEnabledSetting(
257 componentName,
258 PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
259 PackageManager.DONT_KILL_APP);
260 Log.d(Config.LOGTAG, "UnifiedPushDistributor has been disabled");
261 }
262 }
263
264 public boolean processPushMessage(
265 final Account account, final Jid transport, final Element push) {
266 final String instance = push.getAttribute("instance");
267 final String application = push.getAttribute("application");
268 if (Strings.isNullOrEmpty(instance) || Strings.isNullOrEmpty(application)) {
269 return false;
270 }
271 final String content = push.getContent();
272 final byte[] payload;
273 if (Strings.isNullOrEmpty(content)) {
274 payload = new byte[0];
275 } else if (BaseEncoding.base64().canDecode(content)) {
276 payload = BaseEncoding.base64().decode(content);
277 } else {
278 Log.d(
279 Config.LOGTAG,
280 account.getJid().asBareJid() + ": received invalid unified push payload");
281 return false;
282 }
283 final Optional<UnifiedPushDatabase.PushTarget> pushTarget =
284 getPushTarget(account, transport, application, instance);
285 if (pushTarget.isPresent()) {
286 final UnifiedPushDatabase.PushTarget target = pushTarget.get();
287 // TODO check if app is still installed?
288 Log.d(
289 Config.LOGTAG,
290 account.getJid().asBareJid()
291 + ": broadcasting a "
292 + payload.length
293 + " bytes push message to "
294 + target.application);
295 broadcastPushMessage(target, payload);
296 return true;
297 } else {
298 Log.d(Config.LOGTAG, "could not find application for push");
299 return false;
300 }
301 }
302
303 public Optional<Transport> getTransport() {
304 final SharedPreferences sharedPreferences =
305 PreferenceManager.getDefaultSharedPreferences(service.getApplicationContext());
306 final String accountPreference =
307 sharedPreferences.getString(UnifiedPushDistributor.PREFERENCE_ACCOUNT, "none");
308 final String pushServerPreference =
309 sharedPreferences.getString(
310 UnifiedPushDistributor.PREFERENCE_PUSH_SERVER,
311 service.getString(R.string.default_push_server));
312 if (Strings.isNullOrEmpty(accountPreference)
313 || "none".equalsIgnoreCase(accountPreference)
314 || Strings.nullToEmpty(pushServerPreference).trim().isEmpty()) {
315 return Optional.absent();
316 }
317 final Jid transport;
318 final Jid jid;
319 try {
320 transport = Jid.ofEscaped(Strings.nullToEmpty(pushServerPreference).trim());
321 jid = Jid.ofEscaped(Strings.nullToEmpty(accountPreference).trim());
322 } catch (final IllegalArgumentException e) {
323 return Optional.absent();
324 }
325 final Account account = service.findAccountByJid(jid);
326 if (account == null) {
327 return Optional.absent();
328 }
329 return Optional.of(new Transport(account, transport));
330 }
331
332 private Optional<UnifiedPushDatabase.PushTarget> getPushTarget(
333 final Account account,
334 final Jid transport,
335 final String application,
336 final String instance) {
337 if (transport == null || application == null || instance == null) {
338 return Optional.absent();
339 }
340 final String uuid = account.getUuid();
341 final List<UnifiedPushDatabase.PushTarget> pushTargets =
342 UnifiedPushDatabase.getInstance(service)
343 .getPushTargets(uuid, transport.toEscapedString());
344 return Iterables.tryFind(
345 pushTargets,
346 pt ->
347 UnifiedPushDistributor.hash(uuid, pt.application).equals(application)
348 && UnifiedPushDistributor.hash(uuid, pt.instance).equals(instance));
349 }
350
351 private void broadcastPushMessage(
352 final UnifiedPushDatabase.PushTarget target, final byte[] payload) {
353 final Intent updateIntent = new Intent(UnifiedPushDistributor.ACTION_MESSAGE);
354 updateIntent.setPackage(target.application);
355 updateIntent.putExtra("token", target.instance);
356 updateIntent.putExtra("bytesMessage", payload);
357 updateIntent.putExtra("message", new String(payload, StandardCharsets.UTF_8));
358 service.sendBroadcast(updateIntent);
359 }
360
361 private void broadcastEndpoint(
362 final String instance, final UnifiedPushDatabase.ApplicationEndpoint endpoint) {
363 Log.d(Config.LOGTAG, "broadcasting endpoint to " + endpoint.application);
364 final Intent updateIntent = endpointIntent(instance, endpoint);
365 service.sendBroadcast(updateIntent);
366 }
367
368 private Intent endpointIntent(final String instance, final UnifiedPushDatabase.ApplicationEndpoint endpoint) {
369 final Intent intent = new Intent(UnifiedPushDistributor.ACTION_NEW_ENDPOINT);
370 intent.setPackage(endpoint.application);
371 intent.putExtra("token", instance);
372 intent.putExtra("endpoint", endpoint.endpoint);
373 final var distributorVerificationIntent = new Intent();
374 distributorVerificationIntent.setPackage(service.getPackageName());
375 final var pendingIntent =
376 PendingIntent.getBroadcast(
377 service, 0, distributorVerificationIntent, PendingIntent.FLAG_IMMUTABLE);
378 intent.putExtra("distributor", pendingIntent);
379 return intent;
380 }
381
382 public void rebroadcastEndpoint(final Messenger messenger, final String instance, final Transport transport) {
383 final UnifiedPushDatabase unifiedPushDatabase = UnifiedPushDatabase.getInstance(service);
384 final UnifiedPushDatabase.ApplicationEndpoint endpoint =
385 unifiedPushDatabase.getEndpoint(
386 transport.account.getUuid(),
387 transport.transport.toEscapedString(),
388 instance);
389 if (endpoint != null) {
390 sendEndpoint(messenger, instance, endpoint);
391 }
392 }
393
394 public static class Transport {
395 public final Account account;
396 public final Jid transport;
397
398 public Transport(Account account, Jid transport) {
399 this.account = account;
400 this.transport = transport;
401 }
402 }
403
404 public static class PushTargetMessenger {
405 private final UnifiedPushDatabase.PushTarget pushTarget;
406 public final Messenger messenger;
407
408 public PushTargetMessenger(UnifiedPushDatabase.PushTarget pushTarget, Messenger messenger) {
409 this.pushTarget = pushTarget;
410 this.messenger = messenger;
411 }
412 }
413}