Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ public class FirebaseMessagingService extends EnhancedIntentService {
"com.google.firebase.messaging.RECEIVE_DIRECT_BOOT";

static final String ACTION_NEW_TOKEN = "com.google.firebase.messaging.NEW_TOKEN";
static final String ACTION_FCM_REGISTERED = "com.google.firebase.messaging.FCM_REGISTERED";
static final String ACTION_FCM_UNREGISTERED = "com.google.firebase.messaging.FCM_UNREGISTERED";
static final String EXTRA_TOKEN = "token";

private static final int RECENTLY_RECEIVED_MESSAGE_IDS_MAX_SIZE = 10;
Expand Down Expand Up @@ -159,10 +161,56 @@ public void onSendError(@NonNull String msgId, @NonNull Exception exception) {}
*
* @param token The token used for sending messages to this application instance. This token is
* the same as the one retrieved by {@link FirebaseMessaging#getToken()}.
* @deprecated Use {@link #onRegistered(String)} instead.
*/
@WorkerThread
@Deprecated
public void onNewToken(@NonNull String token) {}

/**
* Called when the current app instance has been successfully registered with FCM.
*
* <p>This method provides the unique Firebase Installation ID (FID), which should be used to
* target this app instance for direct-send messaging.
*
* <p>This callback is triggered in the following scenarios:
*
* <ul>
* <li>When the registration first succeeds after app install (if auto-init is enabled).
* <li>When the registration is refreshed due to invalidation or updates (if auto-init is
* enabled).
* <li>Immediately after a direct call to {@link FirebaseMessaging#register()}.
* </ul>
*
* <p>Ensure the provided `installationId` is uploaded if it hasn't been previously or it might
* have been deleted on 404s.
*
* <p><b>Note:</b> To use this API, you must enable it by adding {@code <meta-data
* android:name="firebase_messaging_installation_id_enabled" android:value="true" />} to your
* app's manifest.
*
* @param installationId The Firebase Installation ID used for sending messages to the current app
* instance.
*/
@WorkerThread
public void onRegistered(@NonNull String installationId) {}

/**
* Called when the current app instance has been successfully unregistered from FCM via a call to
* {@code FirebaseMessaging.unregister()}.
*
* <p>This method confirms that the specified FID is no longer active for receiving FCM messages.
*
* <p><b>Note:</b> To use this API, you must enable it by adding {@code <meta-data
* android:name="firebase_messaging_installation_id_enabled" android:value="true" />} to your
* app's manifest.
*
* @param installationId The Firebase Installation ID of the current app instance that was
* unregistered with FCM.
*/
@WorkerThread
public void onUnregistered(@NonNull String installationId) {}

/** @hide */
@Override
protected Intent getStartCommandIntent(Intent originalIntent) {
Expand All @@ -179,6 +227,10 @@ public void handleIntent(Intent intent) {
handleMessageIntent(intent);
} else if (ACTION_NEW_TOKEN.equals(action)) {
onNewToken(intent.getStringExtra(EXTRA_TOKEN));
} else if (ACTION_FCM_REGISTERED.equals(action)) {
onRegistered(intent.getStringExtra(EXTRA_TOKEN));
} else if (ACTION_FCM_UNREGISTERED.equals(action)) {
onUnregistered(intent.getStringExtra(EXTRA_TOKEN));
} else {
Log.d(TAG, "Unknown intent action: " + intent.getAction());
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
package com.google.firebase.messaging;

import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.WorkerThread;
import com.google.android.gms.cloudmessaging.CloudMessaging;
import com.google.android.gms.cloudmessaging.CloudMessagingClient;
import com.google.android.gms.cloudmessaging.RegisterRequest;
import com.google.android.gms.cloudmessaging.UnregisterRequest;
import com.google.android.gms.tasks.Task;
import com.google.android.gms.tasks.TaskCompletionSource;
import com.google.android.gms.tasks.Tasks;
import com.google.firebase.BuildConfig;
import com.google.firebase.FirebaseApp;
import com.google.firebase.installations.FirebaseInstallationsApi;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;

/** A client for the CloudMessaging API to make FCM registration calls. */
public class GmsRegistrationClient {
static final String MANIFEST_METADATA_FIREBASE_MESSAGING_INSTALLATION_ID_ENABLED =
"firebase_messaging_installation_id_enabled";
private final CloudMessagingClient client;
private final FirebaseApp app;
private final FirebaseInstallationsApi firebaseInstallations;
private final GmsRpc gmsRpc;

GmsRegistrationClient(
@NonNull Context context,
@NonNull FirebaseApp app,
@NonNull FirebaseInstallationsApi firebaseInstallations,
@NonNull GmsRpc gmsRpc) {
this(context, app, firebaseInstallations, gmsRpc, CloudMessaging.getClient(context));
}

@androidx.annotation.VisibleForTesting
GmsRegistrationClient(
@NonNull Context context,
@NonNull FirebaseApp app,
@NonNull FirebaseInstallationsApi firebaseInstallations,
@NonNull GmsRpc gmsRpc,
@NonNull CloudMessagingClient client) {
this.client = client;
this.app = app;
this.firebaseInstallations = firebaseInstallations;
this.gmsRpc = gmsRpc;
}

/** Checks whether the installed gmscore supports v1 registration. */
private boolean haveV1RegistrationSupport() {
// TODO:: Figure out the gmscore version which supports V1 registration.
// return metadata.getGmsVersionCode() > 1;...
return true;
}

/** Reads the Manifest metadata to check whether FCM V1 registration is enabled or not. */
public boolean isV1RegistrationEnabled() {
Context applicationContext = app.getApplicationContext();
try {
PackageManager packageManager = applicationContext.getPackageManager();
if (packageManager != null) {
ApplicationInfo applicationInfo =
packageManager.getApplicationInfo(
applicationContext.getPackageName(), PackageManager.GET_META_DATA);
if (applicationInfo.metaData != null
&& applicationInfo.metaData.containsKey(
MANIFEST_METADATA_FIREBASE_MESSAGING_INSTALLATION_ID_ENABLED)) {
return applicationInfo.metaData.getBoolean(
MANIFEST_METADATA_FIREBASE_MESSAGING_INSTALLATION_ID_ENABLED);
}
}
} catch (PackageManager.NameNotFoundException e) {
// This shouldn't happen since it's this app's package, but fall through to default if so.
}

return false;
}

/**
* Registers this app to receive push messages.
*
* @return The registration token for sending messages to this app instance.
*/
@NonNull
public Task<String> register() {
boolean useV1 = isV1RegistrationEnabled();
if (!useV1 || !haveV1RegistrationSupport()) {
// Legacy registration flow.
return gmsRpc.getToken(useV1);
}

// Proceeding with V1 registration.
TaskCompletionSource<String> taskCompletionSource = new TaskCompletionSource<>();
ExecutorService executorService = FcmExecutors.newNetworkIOExecutor();
executorService.execute(
() -> {
try {
String installationId = Tasks.await(firebaseInstallations.getId());
String registrationToken = Tasks.await(registerOverV1(installationId));

// For V1 registration, the token received should be the same as the FID.
if (!TextUtils.isEmpty(registrationToken)
&& registrationToken.contains(installationId)) {
// The registration token will be in format projects/**/$fid. But the actual token
// for sending messages to will be the FID. So returning the FID.
taskCompletionSource.setResult(installationId);
} else {
taskCompletionSource.setException(
new ExecutionException(
new IllegalArgumentException("FID not matching with received token!")));
}
} catch (ExecutionException | InterruptedException e) {
taskCompletionSource.setException(e);
}
});

return taskCompletionSource.getTask();
}

@NonNull
private Task<String> registerOverV1(String installationId)
throws ExecutionException, InterruptedException {
String installationAuthToken = Tasks.await(firebaseInstallations.getToken(false)).getToken();
String apiKey = app.getOptions().getApiKey();
String gmpAppId = app.getOptions().getApplicationId();
String senderId = Metadata.getDefaultSenderId(app);
String sdkVersion = BuildConfig.VERSION_NAME;

RegisterRequest request =
new RegisterRequest(
senderId, gmpAppId, apiKey, installationId, installationAuthToken, sdkVersion);
return client.register(request);
}

/** Unregisters this app from receiving push messages. */
@NonNull
public Task<?> unregister() {
boolean useV1 = isV1RegistrationEnabled();

if (!useV1 || !haveV1RegistrationSupport()) {
// Legacy un-registration flow.
return gmsRpc.deleteToken(useV1);
}

// Proceeding with V1 un-registration.
TaskCompletionSource<Void> taskCompletionSource = new TaskCompletionSource<>();
ExecutorService executorService = FcmExecutors.newNetworkIOExecutor();
executorService.execute(
() -> {
try {
Tasks.await(unregisterOverV1());
taskCompletionSource.setResult(null);
} catch (ExecutionException | InterruptedException e) {
taskCompletionSource.setException(e);
}
});

return taskCompletionSource.getTask();
}

@NonNull
@WorkerThread
private Task<Void> unregisterOverV1() throws ExecutionException, InterruptedException {
String installationId = Tasks.await(firebaseInstallations.getId());
String installationAuthToken = Tasks.await(firebaseInstallations.getToken(false)).getToken();
String apiKey = app.getOptions().getApiKey();
String projectId = Metadata.getDefaultSenderId(app);

UnregisterRequest request =
new UnregisterRequest(projectId, apiKey, installationId, installationAuthToken);
return client.unregister(request);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,8 @@ class GmsRpc {
/** hashed value of developer chosen (nick)name of Firebase Core SDK (a.k.a. FirebaseApp) */
private static final String PARAM_FIREBASE_APP_NAME_HASH = "firebase-app-name-hash";

private static final String PARAM_API_KEY = "Goog-Api-Key";

// --- End of the params for /register3

/**
Expand Down Expand Up @@ -187,45 +189,23 @@ class GmsRpc {
this.firebaseInstallations = firebaseInstallations;
}

Task<String> getToken() {
Task<String> getToken(boolean useV1Registration) {
Task<Bundle> rpcTask =
startRpc(Metadata.getDefaultSenderId(app), SCOPE_ALL, /* extras= */ new Bundle());
startRpc(
Metadata.getDefaultSenderId(app),
SCOPE_ALL,
/* extras= */ new Bundle(),
useV1Registration);
return extractResponseWhenComplete(rpcTask);
}

Task<?> deleteToken() {
Task<?> deleteToken(boolean useV1Registration) {
Bundle extras = new Bundle();
// Server looks at both delete and X-delete so don't need to include both
extras.putString(EXTRA_DELETE, "1");

Task<Bundle> rpcTask = startRpc(Metadata.getDefaultSenderId(app), SCOPE_ALL, extras);
return extractResponseWhenComplete(rpcTask);
}

Task<?> subscribeToTopic(String cachedToken, String topic) {
Bundle extras = new Bundle();
// registration servlet expects this for topics
extras.putString(EXTRA_TOPIC, TOPIC_PREFIX + topic);
// Sends the request to registration servlet and throws on failure.
// We do not cache the topic subscription requests and simply make the
// server request each time.

String to = cachedToken;
String scope = TOPIC_PREFIX + topic;
Task<Bundle> rpcTask = startRpc(to, scope, extras);
return extractResponseWhenComplete(rpcTask);
}

Task<?> unsubscribeFromTopic(String cachedToken, String topic) {
Bundle extras = new Bundle();
// registration servlet expects this for topics
extras.putString(EXTRA_TOPIC, TOPIC_PREFIX + topic);
extras.putString(EXTRA_DELETE, "1");

String to = cachedToken;
String scope = TOPIC_PREFIX + topic;

Task<Bundle> rpcTask = startRpc(to, scope, extras);
Task<Bundle> rpcTask =
startRpc(Metadata.getDefaultSenderId(app), SCOPE_ALL, extras, useV1Registration);
return extractResponseWhenComplete(rpcTask);
}

Expand All @@ -237,9 +217,9 @@ Task<CloudMessage> getProxyNotificationData() {
return rpc.getProxiedNotificationData();
}

private Task<Bundle> startRpc(String to, String scope, Bundle extras) {
private Task<Bundle> startRpc(String to, String scope, Bundle extras, boolean useV1Registration) {
try {
setDefaultAttributesToBundle(to, scope, extras);
setDefaultAttributesToBundle(to, scope, extras, useV1Registration);
} catch (InterruptedException | ExecutionException e) {
return Tasks.forException(e);
}
Expand All @@ -261,7 +241,8 @@ private String getHashedFirebaseAppName() {
}
}

private void setDefaultAttributesToBundle(String to, String scope, Bundle extras)
private void setDefaultAttributesToBundle(
String to, String scope, Bundle extras, boolean useV1Registration)
throws ExecutionException, InterruptedException { // Thrown by Tasks.await() on errors.
extras.putString(EXTRA_SCOPE, scope);
extras.putString(EXTRA_SENDER, to);
Expand All @@ -276,6 +257,11 @@ private void setDefaultAttributesToBundle(String to, String scope, Bundle extras
extras.putString(PARAM_APP_VER_NAME, metadata.getAppVersionName());
extras.putString(PARAM_FIREBASE_APP_NAME_HASH, getHashedFirebaseAppName());

if (useV1Registration) {
// Means developer opted for v1 registration. So we need to add api key.
extras.putString(PARAM_API_KEY, app.getOptions().getApiKey());
}

try {
String fisAuthToken = Tasks.await(firebaseInstallations.getToken(false)).getToken();
if (!TextUtils.isEmpty(fisAuthToken)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,11 @@ public void writeToParcel(@NonNull Parcel out, int flags) {
* Gets the Sender ID for the sender of this message.
*
* @return the message Sender ID
* @deprecated Please use FirebaseOptions.getGcmSenderId() instead to retrieve the sender ID for
* your app
*/
@Nullable
@Deprecated
public String getSenderId() {
return bundle.getString(MessagePayloadKeys.SENDER_ID);
}
Expand Down
Loading
Loading