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