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    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}