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