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