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