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