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