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