UnifiedPushDistributor.java

  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}