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