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;
18import java.util.concurrent.Executors;
19import java.util.concurrent.ScheduledExecutorService;
20import java.util.concurrent.TimeUnit;
21
22import eu.siacs.conversations.Config;
23import eu.siacs.conversations.R;
24import eu.siacs.conversations.entities.Account;
25import eu.siacs.conversations.parser.AbstractParser;
26import eu.siacs.conversations.persistance.UnifiedPushDatabase;
27import eu.siacs.conversations.xml.Element;
28import eu.siacs.conversations.xml.Namespace;
29import eu.siacs.conversations.xmpp.Jid;
30import eu.siacs.conversations.xmpp.stanzas.IqPacket;
31import eu.siacs.conversations.xmpp.stanzas.PresencePacket;
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());
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 final Optional<Transport> transportOptional = getTransport();
81 if (transportOptional.isPresent()) {
82 final Transport transport = transportOptional.get();
83 if (transport.account.isEnabled()) {
84 renewUnifiedEndpoint(transportOptional.get());
85 } else {
86 Log.d(Config.LOGTAG, "skipping UnifiedPush endpoint renewal. Account is disabled");
87 }
88 } else {
89 Log.d(Config.LOGTAG, "skipping UnifiedPush endpoint renewal. No transport selected");
90 }
91 return transportOptional;
92 }
93
94 private void renewUnifiedEndpoint(final Transport transport) {
95 final Account account = transport.account;
96 final UnifiedPushDatabase unifiedPushDatabase = UnifiedPushDatabase.getInstance(service);
97 final List<UnifiedPushDatabase.PushTarget> renewals =
98 unifiedPushDatabase.getRenewals(
99 account.getUuid(), transport.transport.toEscapedString());
100 Log.d(
101 Config.LOGTAG,
102 account.getJid().asBareJid()
103 + ": "
104 + renewals.size()
105 + " UnifiedPush endpoints scheduled for renewal on "
106 + transport.transport);
107 for (final UnifiedPushDatabase.PushTarget renewal : renewals) {
108 Log.d(
109 Config.LOGTAG,
110 account.getJid().asBareJid() + ": try to renew UnifiedPush " + renewal);
111 final String hashedApplication =
112 UnifiedPushDistributor.hash(account.getUuid(), renewal.application);
113 final String hashedInstance =
114 UnifiedPushDistributor.hash(account.getUuid(), renewal.instance);
115 final IqPacket registration = new IqPacket(IqPacket.TYPE.SET);
116 registration.setTo(transport.transport);
117 final Element register = registration.addChild("register", Namespace.UNIFIED_PUSH);
118 register.setAttribute("application", hashedApplication);
119 register.setAttribute("instance", hashedInstance);
120 this.service.sendIqPacket(
121 account,
122 registration,
123 (a, response) -> processRegistration(transport, renewal, response));
124 }
125 }
126
127 private void processRegistration(
128 final Transport transport,
129 final UnifiedPushDatabase.PushTarget renewal,
130 final IqPacket response) {
131 if (response.getType() == IqPacket.TYPE.RESULT) {
132 final Element registered = response.findChild("registered", Namespace.UNIFIED_PUSH);
133 if (registered == null) {
134 return;
135 }
136 final String endpoint = registered.getAttribute("endpoint");
137 if (Strings.isNullOrEmpty(endpoint)) {
138 Log.w(Config.LOGTAG, "endpoint was null in up registration");
139 return;
140 }
141 final long expiration;
142 try {
143 expiration = AbstractParser.getTimestamp(registered.getAttribute("expiration"));
144 } catch (final IllegalArgumentException | ParseException e) {
145 Log.d(Config.LOGTAG, "could not parse expiration", e);
146 return;
147 }
148 renewUnifiedPushEndpoint(transport, renewal, endpoint, expiration);
149 } else {
150 Log.d(Config.LOGTAG, "could not register UP endpoint " + response.getErrorCondition());
151 }
152 }
153
154 private void renewUnifiedPushEndpoint(
155 final Transport transport,
156 final UnifiedPushDatabase.PushTarget renewal,
157 final String endpoint,
158 final long expiration) {
159 Log.d(Config.LOGTAG, "registered endpoint " + endpoint + " expiration=" + expiration);
160 final UnifiedPushDatabase unifiedPushDatabase = UnifiedPushDatabase.getInstance(service);
161 final boolean modified =
162 unifiedPushDatabase.updateEndpoint(
163 renewal.instance,
164 transport.account.getUuid(),
165 transport.transport.toEscapedString(),
166 endpoint,
167 expiration);
168 if (modified) {
169 Log.d(
170 Config.LOGTAG,
171 "endpoint for "
172 + renewal.application
173 + "/"
174 + renewal.instance
175 + " was updated to "
176 + endpoint);
177 broadcastEndpoint(
178 renewal.instance,
179 new UnifiedPushDatabase.ApplicationEndpoint(renewal.application, endpoint));
180 }
181 }
182
183 public boolean reconfigurePushDistributor() {
184 final boolean enabled = getTransport().isPresent();
185 setUnifiedPushDistributorEnabled(enabled);
186 return enabled;
187 }
188
189 private void setUnifiedPushDistributorEnabled(final boolean enabled) {
190 final PackageManager packageManager = service.getPackageManager();
191 final ComponentName componentName =
192 new ComponentName(service, UnifiedPushDistributor.class);
193 if (enabled) {
194 packageManager.setComponentEnabledSetting(
195 componentName,
196 PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
197 PackageManager.DONT_KILL_APP);
198 Log.d(Config.LOGTAG, "UnifiedPushDistributor has been enabled");
199 } else {
200 packageManager.setComponentEnabledSetting(
201 componentName,
202 PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
203 PackageManager.DONT_KILL_APP);
204 Log.d(Config.LOGTAG, "UnifiedPushDistributor has been disabled");
205 }
206 }
207
208 public boolean processPushMessage(
209 final Account account, final Jid transport, final Element push) {
210 final String instance = push.getAttribute("instance");
211 final String application = push.getAttribute("application");
212 if (Strings.isNullOrEmpty(instance) || Strings.isNullOrEmpty(application)) {
213 return false;
214 }
215 final String content = push.getContent();
216 final byte[] payload;
217 if (Strings.isNullOrEmpty(content)) {
218 payload = new byte[0];
219 } else if (BaseEncoding.base64().canDecode(content)) {
220 payload = BaseEncoding.base64().decode(content);
221 } else {
222 Log.d(
223 Config.LOGTAG,
224 account.getJid().asBareJid() + ": received invalid unified push payload");
225 return false;
226 }
227 final Optional<UnifiedPushDatabase.PushTarget> pushTarget =
228 getPushTarget(account, transport, application, instance);
229 if (pushTarget.isPresent()) {
230 final UnifiedPushDatabase.PushTarget target = pushTarget.get();
231 // TODO check if app is still installed?
232 Log.d(
233 Config.LOGTAG,
234 account.getJid().asBareJid()
235 + ": broadcasting a "
236 + payload.length
237 + " bytes push message to "
238 + target.application);
239 broadcastPushMessage(target, payload);
240 return true;
241 } else {
242 Log.d(Config.LOGTAG, "could not find application for push");
243 return false;
244 }
245 }
246
247 public Optional<Transport> getTransport() {
248 final SharedPreferences sharedPreferences =
249 PreferenceManager.getDefaultSharedPreferences(service.getApplicationContext());
250 final String accountPreference =
251 sharedPreferences.getString(UnifiedPushDistributor.PREFERENCE_ACCOUNT, "none");
252 final String pushServerPreference =
253 sharedPreferences.getString(
254 UnifiedPushDistributor.PREFERENCE_PUSH_SERVER,
255 service.getString(R.string.default_push_server));
256 if (Strings.isNullOrEmpty(accountPreference)
257 || "none".equalsIgnoreCase(accountPreference)
258 || Strings.nullToEmpty(pushServerPreference).trim().isEmpty()) {
259 return Optional.absent();
260 }
261 final Jid transport;
262 final Jid jid;
263 try {
264 transport = Jid.ofEscaped(Strings.nullToEmpty(pushServerPreference).trim());
265 jid = Jid.ofEscaped(Strings.nullToEmpty(accountPreference).trim());
266 } catch (final IllegalArgumentException e) {
267 return Optional.absent();
268 }
269 final Account account = service.findAccountByJid(jid);
270 if (account == null) {
271 return Optional.absent();
272 }
273 return Optional.of(new Transport(account, transport));
274 }
275
276 private Optional<UnifiedPushDatabase.PushTarget> getPushTarget(
277 final Account account,
278 final Jid transport,
279 final String application,
280 final String instance) {
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}