UnifiedPushDistributor.java

  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}