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
39 // connector actions (these are actions used for distributor->connector broadcasts)
40 public static final String ACTION_UNREGISTERED = "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 ->
83 unregisterApplication(context, intent.getData());
84 default ->
85 Log.d(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(context, String.format("successfully registered %s (token = %s) for UnifiedPushed", application, instance));
115 final Intent serviceIntent = new Intent(context, XmppConnectionService.class);
116 serviceIntent.setAction(XmppConnectionService.ACTION_RENEW_UNIFIED_PUSH_ENDPOINTS);
117 serviceIntent.putExtra("instance", instance);
118 serviceIntent.putExtra("application", application);
119 if (messenger instanceof Messenger) {
120 serviceIntent.putExtra("messenger", messenger);
121 }
122 Compatibility.startService(context, serviceIntent);
123 } else {
124 Log.d(Config.LOGTAG, "not successful. sending error message back to application");
125 final Intent registrationFailed = new Intent(ACTION_REGISTRATION_FAILED);
126 registrationFailed.putExtra(EXTRA_MESSAGE, "instance already exits");
127 registrationFailed.setPackage(application);
128 registrationFailed.putExtra("token", instance);
129 if (messenger instanceof Messenger m) {
130 final var message = new Message();
131 message.obj = registrationFailed;
132 try {
133 m.send(message);
134 } catch (final RemoteException e) {
135 context.sendBroadcast(registrationFailed);
136 }
137 } else {
138 context.sendBroadcast(registrationFailed);
139 }
140 }
141 } else {
142 if (messenger instanceof Messenger m) {
143 sendRegistrationFailed(m,"Your application is not registered to receive messages");
144 }
145 Log.d(
146 Config.LOGTAG,
147 "ignoring invalid UnifiedPush registration. Unknown application "
148 + application);
149 }
150 }
151
152 private void sendRegistrationFailed(final Messenger messenger, final String error) {
153 final Intent intent = new Intent(ACTION_REGISTRATION_FAILED);
154 intent.putExtra(EXTRA_MESSAGE, error);
155 final var message = new Message();
156 message.obj = intent;
157 try {
158 messenger.send(message);
159 } catch (final RemoteException e) {
160 Log.d(Config.LOGTAG,"unable to tell messenger of failed registration",e);
161 }
162 }
163
164 private List<String> getBroadcastReceivers(final Context context, final String application) {
165 final Intent messageIntent = new Intent(ACTION_MESSAGE);
166 messageIntent.setPackage(application);
167 final List<ResolveInfo> resolveInfo =
168 context.getPackageManager().queryBroadcastReceivers(messageIntent, 0);
169 return Lists.transform(
170 resolveInfo, ri -> ri.activityInfo == null ? null : ri.activityInfo.packageName);
171 }
172
173 private void unregister(final Context context, final String instance) {
174 if (Strings.isNullOrEmpty(instance)) {
175 Log.w(Config.LOGTAG, "ignoring invalid UnifiedPush un-registration");
176 return;
177 }
178 final UnifiedPushDatabase unifiedPushDatabase = UnifiedPushDatabase.getInstance(context);
179 if (unifiedPushDatabase.deleteInstance(instance)) {
180 quickLog(context, String.format("successfully unregistered token %s from UnifiedPushed (application requested unregister)", instance));
181 Log.d(Config.LOGTAG, "successfully removed " + instance + " from UnifiedPush");
182 // TODO send UNREGISTERED broadcast back to app?!
183 }
184 }
185
186 private void unregisterApplication(final Context context, final Uri uri) {
187 if (uri != null && "package".equalsIgnoreCase(uri.getScheme())) {
188 final String application = uri.getSchemeSpecificPart();
189 if (Strings.isNullOrEmpty(application)) {
190 return;
191 }
192 Log.d(Config.LOGTAG, "app " + application + " has been removed from the system");
193 final UnifiedPushDatabase database = UnifiedPushDatabase.getInstance(context);
194 if (database.deleteApplication(application)) {
195 quickLog(context, String.format("successfully removed %s from UnifiedPushed (ACTION_PACKAGE_FULLY_REMOVED)", application));
196 Log.d(Config.LOGTAG, "successfully removed " + application + " from UnifiedPush");
197 }
198 }
199 }
200
201 public static String hash(String... components) {
202 return BaseEncoding.base64()
203 .encode(
204 Hashing.sha256()
205 .hashString(Joiner.on('\0').join(components), Charsets.UTF_8)
206 .asBytes());
207 }
208
209 public static void quickLog(final Context context, final String message) {
210 final Intent intent = new Intent(context, XmppConnectionService.class);
211 intent.setAction(XmppConnectionService.ACTION_QUICK_LOG);
212 intent.putExtra("message", message);
213 context.startService(intent);
214 }
215}