chromium/components/gcm_driver/android/java/src/org/chromium/components/gcm_driver/GoogleCloudMessagingV2.java

// Copyright 2014 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.components.gcm_driver;

import android.app.PendingIntent;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.Messenger;

import androidx.annotation.Nullable;

import org.chromium.base.ContextUtils;
import org.chromium.base.IntentUtils;
import org.chromium.base.PackageUtils;

import java.io.IOException;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;

/**
 * Temporary code for sending subtypes when (un)subscribing with GCM.
 * Subtypes are experimental and may change without notice!
 * TODO(johnme): Remove this file, once we switch to the GMS client library.
 */
public class GoogleCloudMessagingV2 implements GoogleCloudMessagingSubscriber {
    private static final String GOOGLE_PLAY_SERVICES_PACKAGE = "com.google.android.gms";
    private static final long REGISTER_TIMEOUT = 5000;
    private static final String ACTION_C2DM_REGISTER = "com.google.android.c2dm.intent.REGISTER";
    private static final String C2DM_EXTRA_ERROR = "error";
    private static final String INTENT_PARAM_APP = "app";
    private static final String ERROR_MAIN_THREAD = "MAIN_THREAD";
    private static final String ERROR_SERVICE_NOT_AVAILABLE = "SERVICE_NOT_AVAILABLE";
    private static final String EXTRA_DELETE = "delete";
    private static final String EXTRA_REGISTRATION_ID = "registration_id";
    private static final String EXTRA_SENDER = "sender";
    private static final String EXTRA_MESSENGER = "google.messenger";
    private static final String EXTRA_SUBTYPE = "subtype";
    private static final String EXTRA_SUBSCRIPTION = "subscription";

    private PendingIntent mAppPendingIntent;
    private final Object mAppPendingIntentLock = new Object();

    public GoogleCloudMessagingV2() {}

    @Override
    public String subscribe(String source, String subtype, @Nullable Bundle data)
            throws IOException {
        if (data == null) {
            data = new Bundle();
        }
        data.putString(EXTRA_SUBTYPE, subtype);
        Bundle result = subscribe(source, data);
        return result.getString(EXTRA_REGISTRATION_ID);
    }

    @Override
    public void unsubscribe(String source, String subtype, @Nullable Bundle data)
            throws IOException {
        if (data == null) {
            data = new Bundle();
        }
        data.putString(EXTRA_SUBTYPE, subtype);
        unsubscribe(source, data);
        return;
    }

    /**
     * Subscribe to receive GCM messages from a specific source.
     * <p>
     * Source Types:
     * <ul>
     * <li>Sender ID - if you have multiple senders you can call this method
     * for each additional sender. Each sender can use the corresponding
     * {@link #REGISTRATION_ID} returned in the bundle to send messages
     * from the server.</li>
     * <li>Cloud Pub/Sub topic - You can subscribe to a topic and receive
     * notifications from the owner of that topic, when something changes.
     * For more information see
     * <a href="https://cloud.google.com/pubsub">Cloud Pub/Sub</a>.</li>
     * </ul>
     * This function is blocking and should not be called on the main thread.
     *
     * @param source of the desired notifications.
     * @param data (optional) additional information.
     * @return Bundle containing subscription information including {@link #REGISTRATION_ID}
     * @throws IOException if the request fails.
     */
    public Bundle subscribe(String source, Bundle data) throws IOException {
        if (data == null) {
            data = new Bundle();
        }
        // Expected by older versions of GMS and servlet
        data.putString(EXTRA_SENDER, source);
        // New name of the sender parameter
        data.putString(EXTRA_SUBSCRIPTION, source);
        // DB buster for older versions of GCM.
        if (data.getString(EXTRA_SUBTYPE) == null) {
            data.putString(EXTRA_SUBTYPE, source);
        }

        Intent resultIntent = registerRpc(data);
        getExtraOrThrow(resultIntent, EXTRA_REGISTRATION_ID);
        return resultIntent.getExtras();
    }

    /**
     * Unsubscribe from a source to stop receiving messages from it.
     * <p>
     * This function is blocking and should not be called on the main thread.
     *
     * @param source to unsubscribe
     * @param data (optional) additional information.
     * @throws IOException if the request fails.
     */
    public void unsubscribe(String source, Bundle data) throws IOException {
        if (data == null) {
            data = new Bundle();
        }
        // Use the register servlet, with 'delete=true'.
        // Registration service returns a registration_id on success - or an error code.
        data.putString(EXTRA_DELETE, "1");
        subscribe(source, data);
    }

    private Intent registerRpc(Bundle data) throws IOException {
        if (Looper.getMainLooper() == Looper.myLooper()) {
            throw new IOException(ERROR_MAIN_THREAD);
        }
        if (!PackageUtils.isPackageInstalled(GOOGLE_PLAY_SERVICES_PACKAGE)) {
            throw new IOException("Google Play Services missing");
        }
        if (data == null) {
            data = new Bundle();
        }

        final BlockingQueue<Intent> responseResult = new LinkedBlockingQueue<Intent>();
        Handler responseHandler =
                new Handler(Looper.getMainLooper()) {
                    @Override
                    public void handleMessage(Message msg) {
                        Intent res = (Intent) msg.obj;
                        responseResult.add(res);
                    }
                };
        Messenger responseMessenger = new Messenger(responseHandler);

        Intent intent = new Intent(ACTION_C2DM_REGISTER);
        intent.setPackage(GOOGLE_PLAY_SERVICES_PACKAGE);
        setPackageNameExtra(intent);
        intent.putExtras(data);
        intent.putExtra(EXTRA_MESSENGER, responseMessenger);
        ContextUtils.getApplicationContext().startService(intent);
        try {
            return responseResult.poll(REGISTER_TIMEOUT, TimeUnit.MILLISECONDS);
        } catch (InterruptedException e) {
            throw new IOException(e.getMessage());
        }
    }

    private String getExtraOrThrow(Intent intent, String extraKey) throws IOException {
        if (intent == null) {
            throw new IOException(ERROR_SERVICE_NOT_AVAILABLE);
        }

        String extraValue = intent.getStringExtra(extraKey);
        if (extraValue != null) {
            return extraValue;
        }

        String err = intent.getStringExtra(C2DM_EXTRA_ERROR);
        if (err != null) {
            throw new IOException(err);
        } else {
            throw new IOException(ERROR_SERVICE_NOT_AVAILABLE);
        }
    }

    private void setPackageNameExtra(Intent intent) {
        synchronized (mAppPendingIntentLock) {
            if (mAppPendingIntent == null) {
                Intent target = new Intent();
                // Fill in the package, to prevent the intent from being used.
                target.setPackage("com.google.example.invalidpackage");
                mAppPendingIntent =
                        PendingIntent.getBroadcast(
                                ContextUtils.getApplicationContext(),
                                0,
                                target,
                                IntentUtils.getPendingIntentMutabilityFlag(false));
            }
        }
        intent.putExtra(INTENT_PARAM_APP, mAppPendingIntent);
    }
}