1package eu.siacs.conversations.receiver;
2
3import android.app.PendingIntent;
4import android.content.BroadcastReceiver;
5import android.content.Context;
6import android.content.Intent;
7import android.content.pm.PackageManager;
8import android.content.pm.ResolveInfo;
9import android.net.Uri;
10import android.os.Build;
11import android.os.Message;
12import android.os.Messenger;
13import android.os.Parcelable;
14import android.os.RemoteException;
15import android.util.Log;
16import com.google.common.base.Charsets;
17import com.google.common.base.Joiner;
18import com.google.common.base.Strings;
19import com.google.common.collect.Lists;
20import com.google.common.hash.Hashing;
21import com.google.common.io.BaseEncoding;
22import eu.siacs.conversations.Config;
23import eu.siacs.conversations.persistance.UnifiedPushDatabase;
24import eu.siacs.conversations.services.XmppConnectionService;
25import eu.siacs.conversations.utils.Compatibility;
26import java.util.Arrays;
27import java.util.Collection;
28import java.util.List;
29
30public class UnifiedPushDistributor extends BroadcastReceiver {
31
32 // distributor actions (these are actions 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 pi = intent.getParcelableExtra("pi");
70 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
71 final String sentFromPackage = getSentFromPackage();
72 if (Strings.isNullOrEmpty(sentFromPackage)) {
73 final var fallback = asApplication(pi);
74 if (Strings.isNullOrEmpty(fallback)) {
75 Log.d(Config.LOGTAG, "register/unregister command did not include application");
76 return;
77 }
78 final var targetSdk = getTargetSdk(context, fallback);
79 if (targetSdk >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
80 Log.d(
81 Config.LOGTAG,
82 "not accepting fallback because " + fallback + " targets " + targetSdk);
83 return;
84 }
85 application = fallback;
86 } else {
87 application = sentFromPackage;
88 }
89 } else {
90 application = asApplication(pi);
91 }
92 if (Strings.isNullOrEmpty(application)) {
93 Log.d(Config.LOGTAG, "register/unregister command did not include application");
94 return;
95 }
96 final Parcelable messenger = intent.getParcelableExtra("messenger");
97 final String instance = intent.getStringExtra("token");
98 final List<String> features = intent.getStringArrayListExtra("features");
99 switch (Strings.nullToEmpty(action)) {
100 case ACTION_REGISTER -> register(context, application, instance, features, messenger);
101 case ACTION_UNREGISTER -> unregister(context, instance);
102 case Intent.ACTION_PACKAGE_FULLY_REMOVED ->
103 unregisterApplication(context, intent.getData());
104 default ->
105 Log.d(
106 Config.LOGTAG,
107 "UnifiedPushDistributor received unknown action " + action);
108 }
109 }
110
111 private static String asApplication(final Parcelable parcelable) {
112 if (parcelable instanceof PendingIntent pendingIntent) {
113 return Strings.emptyToNull(pendingIntent.getIntentSender().getCreatorPackage());
114 } else {
115 return null;
116 }
117 }
118
119 private static int getTargetSdk(final Context context, final String application) {
120 try {
121 return context.getPackageManager().getApplicationInfo(application, 0).targetSdkVersion;
122 } catch (final PackageManager.NameNotFoundException e) {
123 // this will def. be over our max sdk of 34
124 return Integer.MIN_VALUE;
125 }
126 }
127
128 private void register(
129 final Context context,
130 final String application,
131 final String instance,
132 final Collection<String> features,
133 final Parcelable messenger) {
134 if (Strings.isNullOrEmpty(application) || Strings.isNullOrEmpty(instance)) {
135 Log.w(Config.LOGTAG, "ignoring invalid UnifiedPush registration");
136 return;
137 }
138 final List<String> receivers = getBroadcastReceivers(context, application);
139 if (receivers.contains(application)) {
140 final boolean byteMessage = features != null && features.contains(ACTION_BYTE_MESSAGE);
141 Log.d(
142 Config.LOGTAG,
143 "received up registration from "
144 + application
145 + "/"
146 + instance
147 + " features: "
148 + features);
149 if (UnifiedPushDatabase.getInstance(context).register(application, instance)) {
150 Log.d(
151 Config.LOGTAG,
152 "successfully created UnifiedPush entry. waking up XmppConnectionService");
153 quickLog(
154 context,
155 String.format(
156 "successfully registered %s (token = %s) for UnifiedPushed",
157 application, instance));
158 final Intent serviceIntent = new Intent(context, XmppConnectionService.class);
159 serviceIntent.setAction(XmppConnectionService.ACTION_RENEW_UNIFIED_PUSH_ENDPOINTS);
160 serviceIntent.putExtra("instance", instance);
161 serviceIntent.putExtra("application", application);
162 if (messenger instanceof Messenger) {
163 serviceIntent.putExtra("messenger", messenger);
164 }
165 Compatibility.startService(context, serviceIntent);
166 } else {
167 Log.d(Config.LOGTAG, "not successful. sending error message back to application");
168 final Intent registrationFailed = new Intent(ACTION_REGISTRATION_FAILED);
169 registrationFailed.putExtra(EXTRA_MESSAGE, "instance already exits");
170 registrationFailed.setPackage(application);
171 registrationFailed.putExtra("token", instance);
172 if (messenger instanceof Messenger m) {
173 final var message = new Message();
174 message.obj = registrationFailed;
175 try {
176 m.send(message);
177 } catch (final RemoteException e) {
178 context.sendBroadcast(registrationFailed);
179 }
180 } else {
181 context.sendBroadcast(registrationFailed);
182 }
183 }
184 } else {
185 if (messenger instanceof Messenger m) {
186 sendRegistrationFailed(m, "Your application is not registered to receive messages");
187 }
188 Log.d(
189 Config.LOGTAG,
190 "ignoring invalid UnifiedPush registration. Unknown application "
191 + application);
192 }
193 }
194
195 private void sendRegistrationFailed(final Messenger messenger, final String error) {
196 final Intent intent = new Intent(ACTION_REGISTRATION_FAILED);
197 intent.putExtra(EXTRA_MESSAGE, error);
198 final var message = new Message();
199 message.obj = intent;
200 try {
201 messenger.send(message);
202 } catch (final RemoteException e) {
203 Log.d(Config.LOGTAG, "unable to tell messenger of failed registration", e);
204 }
205 }
206
207 private List<String> getBroadcastReceivers(final Context context, final String application) {
208 final Intent messageIntent = new Intent(ACTION_MESSAGE);
209 messageIntent.setPackage(application);
210 final List<ResolveInfo> resolveInfo =
211 context.getPackageManager().queryBroadcastReceivers(messageIntent, 0);
212 return Lists.transform(
213 resolveInfo, ri -> ri.activityInfo == null ? null : ri.activityInfo.packageName);
214 }
215
216 private void unregister(final Context context, final String instance) {
217 if (Strings.isNullOrEmpty(instance)) {
218 Log.w(Config.LOGTAG, "ignoring invalid UnifiedPush un-registration");
219 return;
220 }
221 final UnifiedPushDatabase unifiedPushDatabase = UnifiedPushDatabase.getInstance(context);
222 if (unifiedPushDatabase.deleteInstance(instance)) {
223 quickLog(
224 context,
225 String.format(
226 "successfully unregistered token %s from UnifiedPushed (application"
227 + " requested unregister)",
228 instance));
229 Log.d(Config.LOGTAG, "successfully removed " + instance + " from UnifiedPush");
230 // TODO send UNREGISTERED broadcast back to app?!
231 }
232 }
233
234 private void unregisterApplication(final Context context, final Uri uri) {
235 if (uri != null && "package".equalsIgnoreCase(uri.getScheme())) {
236 final String application = uri.getSchemeSpecificPart();
237 if (Strings.isNullOrEmpty(application)) {
238 return;
239 }
240 Log.d(Config.LOGTAG, "app " + application + " has been removed from the system");
241 final UnifiedPushDatabase database = UnifiedPushDatabase.getInstance(context);
242 if (database.deleteApplication(application)) {
243 quickLog(
244 context,
245 String.format(
246 "successfully removed %s from UnifiedPushed"
247 + " (ACTION_PACKAGE_FULLY_REMOVED)",
248 application));
249 Log.d(Config.LOGTAG, "successfully removed " + application + " from UnifiedPush");
250 }
251 }
252 }
253
254 public static String hash(String... components) {
255 return BaseEncoding.base64()
256 .encode(
257 Hashing.sha256()
258 .hashString(Joiner.on('\0').join(components), Charsets.UTF_8)
259 .asBytes());
260 }
261
262 public static void quickLog(final Context context, final String message) {
263 final Intent intent = new Intent(context, XmppConnectionService.class);
264 intent.setAction(XmppConnectionService.ACTION_QUICK_LOG);
265 intent.putExtra("message", message);
266 Compatibility.startService(context, intent);
267 }
268}