1package eu.siacs.conversations.services;
2
3import android.app.PendingIntent;
4import android.content.BroadcastReceiver;
5import android.content.Context;
6import android.content.Intent;
7import android.content.pm.ResolveInfo;
8import android.net.Uri;
9import android.os.Message;
10import android.os.Messenger;
11import android.os.Parcelable;
12import android.os.RemoteException;
13import android.util.Log;
14
15import com.google.common.base.Charsets;
16import com.google.common.base.Joiner;
17import com.google.common.base.Strings;
18import com.google.common.collect.Lists;
19import com.google.common.hash.Hashing;
20import com.google.common.io.BaseEncoding;
21
22import java.util.Arrays;
23import java.util.Collection;
24import java.util.List;
25
26import eu.siacs.conversations.Config;
27import eu.siacs.conversations.persistance.UnifiedPushDatabase;
28import eu.siacs.conversations.utils.Compatibility;
29
30public class UnifiedPushDistributor extends BroadcastReceiver {
31
32 // distributor actions (these are actios used for connector->distributor broadcasts)
33 // we, the distributor, have a broadcast receiver listening for those actions
34
35 public static final String ACTION_REGISTER = "org.unifiedpush.android.distributor.REGISTER";
36 public static final String ACTION_UNREGISTER = "org.unifiedpush.android.distributor.UNREGISTER";
37
38 // connector actions (these are actions used for distributor->connector broadcasts)
39 public static final String ACTION_UNREGISTERED =
40 "org.unifiedpush.android.connector.UNREGISTERED";
41 public static final String ACTION_BYTE_MESSAGE =
42 "org.unifiedpush.android.distributor.feature.BYTES_MESSAGE";
43 public static final String ACTION_REGISTRATION_FAILED =
44 "org.unifiedpush.android.connector.REGISTRATION_FAILED";
45
46 // this action is only used in 'messenger' communication to tell the app that a registration is
47 // probably fine but can not be processed right now; for example due to spotty internet
48 public static final String ACTION_REGISTRATION_DELAYED =
49 "org.unifiedpush.android.connector.REGISTRATION_DELAYED";
50 public static final String ACTION_MESSAGE = "org.unifiedpush.android.connector.MESSAGE";
51 public static final String ACTION_NEW_ENDPOINT =
52 "org.unifiedpush.android.connector.NEW_ENDPOINT";
53
54 public static final String EXTRA_MESSAGE = "message";
55
56 public static final String PREFERENCE_ACCOUNT = "up_push_account";
57 public static final String PREFERENCE_PUSH_SERVER = "up_push_server";
58
59 public static final List<String> PREFERENCES =
60 Arrays.asList(PREFERENCE_ACCOUNT, PREFERENCE_PUSH_SERVER);
61
62 @Override
63 public void onReceive(final Context context, final Intent intent) {
64 if (intent == null) {
65 return;
66 }
67 final String action = intent.getAction();
68 final String application;
69 final Parcelable appVerification = intent.getParcelableExtra("app");
70 if (appVerification instanceof PendingIntent pendingIntent) {
71 application = pendingIntent.getIntentSender().getCreatorPackage();
72 Log.d(Config.LOGTAG, "received application name via pending intent " + application);
73 } else {
74 application = intent.getStringExtra("application");
75 }
76 final Parcelable messenger = intent.getParcelableExtra("messenger");
77 final String instance = intent.getStringExtra("token");
78 final List<String> features = intent.getStringArrayListExtra("features");
79 switch (Strings.nullToEmpty(action)) {
80 case ACTION_REGISTER -> register(context, application, instance, features, messenger);
81 case ACTION_UNREGISTER -> unregister(context, instance);
82 case Intent.ACTION_PACKAGE_FULLY_REMOVED -> unregisterApplication(
83 context, intent.getData());
84 default -> Log.d(
85 Config.LOGTAG, "UnifiedPushDistributor received unknown action " + action);
86 }
87 }
88
89 private void register(
90 final Context context,
91 final String application,
92 final String instance,
93 final Collection<String> features,
94 final Parcelable messenger) {
95 if (Strings.isNullOrEmpty(application) || Strings.isNullOrEmpty(instance)) {
96 Log.w(Config.LOGTAG, "ignoring invalid UnifiedPush registration");
97 return;
98 }
99 final List<String> receivers = getBroadcastReceivers(context, application);
100 if (receivers.contains(application)) {
101 final boolean byteMessage = features != null && features.contains(ACTION_BYTE_MESSAGE);
102 Log.d(
103 Config.LOGTAG,
104 "received up registration from "
105 + application
106 + "/"
107 + instance
108 + " features: "
109 + features);
110 if (UnifiedPushDatabase.getInstance(context).register(application, instance)) {
111 Log.d(
112 Config.LOGTAG,
113 "successfully created UnifiedPush entry. waking up XmppConnectionService");
114 quickLog(
115 context,
116 String.format(
117 "successfully registered %s (token = %s) for UnifiedPushed",
118 application, instance));
119 final Intent serviceIntent = new Intent(context, XmppConnectionService.class);
120 serviceIntent.setAction(XmppConnectionService.ACTION_RENEW_UNIFIED_PUSH_ENDPOINTS);
121 serviceIntent.putExtra("instance", instance);
122 serviceIntent.putExtra("application", application);
123 if (messenger instanceof Messenger) {
124 serviceIntent.putExtra("messenger", messenger);
125 }
126 Compatibility.startService(context, serviceIntent);
127 } else {
128 Log.d(Config.LOGTAG, "not successful. sending error message back to application");
129 final Intent registrationFailed = new Intent(ACTION_REGISTRATION_FAILED);
130 registrationFailed.putExtra(EXTRA_MESSAGE, "instance already exits");
131 registrationFailed.setPackage(application);
132 registrationFailed.putExtra("token", instance);
133 if (messenger instanceof Messenger m) {
134 final var message = new Message();
135 message.obj = registrationFailed;
136 try {
137 m.send(message);
138 } catch (final RemoteException e) {
139 context.sendBroadcast(registrationFailed);
140 }
141 } else {
142 context.sendBroadcast(registrationFailed);
143 }
144 }
145 } else {
146 if (messenger instanceof Messenger m) {
147 sendRegistrationFailed(m, "Your application is not registered to receive messages");
148 }
149 Log.d(
150 Config.LOGTAG,
151 "ignoring invalid UnifiedPush registration. Unknown application "
152 + application);
153 }
154 }
155
156 private void sendRegistrationFailed(final Messenger messenger, final String error) {
157 final Intent intent = new Intent(ACTION_REGISTRATION_FAILED);
158 intent.putExtra(EXTRA_MESSAGE, error);
159 final var message = new Message();
160 message.obj = intent;
161 try {
162 messenger.send(message);
163 } catch (final RemoteException e) {
164 Log.d(Config.LOGTAG, "unable to tell messenger of failed registration", e);
165 }
166 }
167
168 private List<String> getBroadcastReceivers(final Context context, final String application) {
169 final Intent messageIntent = new Intent(ACTION_MESSAGE);
170 messageIntent.setPackage(application);
171 final List<ResolveInfo> resolveInfo =
172 context.getPackageManager().queryBroadcastReceivers(messageIntent, 0);
173 return Lists.transform(
174 resolveInfo, ri -> ri.activityInfo == null ? null : ri.activityInfo.packageName);
175 }
176
177 private void unregister(final Context context, final String instance) {
178 if (Strings.isNullOrEmpty(instance)) {
179 Log.w(Config.LOGTAG, "ignoring invalid UnifiedPush un-registration");
180 return;
181 }
182 final UnifiedPushDatabase unifiedPushDatabase = UnifiedPushDatabase.getInstance(context);
183 if (unifiedPushDatabase.deleteInstance(instance)) {
184 quickLog(
185 context,
186 String.format(
187 "successfully unregistered token %s from UnifiedPushed (application requested unregister)",
188 instance));
189 Log.d(Config.LOGTAG, "successfully removed " + instance + " from UnifiedPush");
190 // TODO send UNREGISTERED broadcast back to app?!
191 }
192 }
193
194 private void unregisterApplication(final Context context, final Uri uri) {
195 if (uri != null && "package".equalsIgnoreCase(uri.getScheme())) {
196 final String application = uri.getSchemeSpecificPart();
197 if (Strings.isNullOrEmpty(application)) {
198 return;
199 }
200 Log.d(Config.LOGTAG, "app " + application + " has been removed from the system");
201 final UnifiedPushDatabase database = UnifiedPushDatabase.getInstance(context);
202 if (database.deleteApplication(application)) {
203 quickLog(
204 context,
205 String.format(
206 "successfully removed %s from UnifiedPushed (ACTION_PACKAGE_FULLY_REMOVED)",
207 application));
208 Log.d(Config.LOGTAG, "successfully removed " + application + " from UnifiedPush");
209 }
210 }
211 }
212
213 public static String hash(String... components) {
214 return BaseEncoding.base64()
215 .encode(
216 Hashing.sha256()
217 .hashString(Joiner.on('\0').join(components), Charsets.UTF_8)
218 .asBytes());
219 }
220
221 public static void quickLog(final Context context, final String message) {
222 final Intent intent = new Intent(context, XmppConnectionService.class);
223 intent.setAction(XmppConnectionService.ACTION_QUICK_LOG);
224 intent.putExtra("message", message);
225 Compatibility.startService(context, intent);
226 }
227}