How do I handle situation, when user logs out of my application and I no longer want him to receive notifications to the device.
I tried
FirebaseInstanceId.getInstance().deleteToken(FirebaseInstanceId.getInstance().getId(), FirebaseMessaging.INSTANCE_ID_SCOPE)
But I still receive the notifications to my device's registration_id.
I also made sure that this is the token I should delete:
FirebaseInstanceId.getInstance().getToken(FirebaseInstanceId.getInstance().getId(), FirebaseMessaging.INSTANCE_ID_SCOPE)
or simply FirebaseInstanceId.getInstance().getToken()).
I also tried FirebaseInstanceId.getInstance().deleteInstanceId(), but then the next time I call FirebaseInstanceId.getInstance.getToken I receive null (it works on the second try).
I guess, after deleteInstanceId I could immediately call getToken() again, but it looks like a hack. And also there's this answer which states that it shouldn't be done, but it proposes deleting the token which apparently doesn't work.
So what is the right method to handle this?
Okay. So I managed to do some testing and have concluded the following:
deleteToken() is the counterpart of getToken(String, String), but not for getToken().
It only works if the Sender ID you are passing is a different Sender ID (not the same ID that can be seen in your google-services.json). For example, you want to allow a different Server to send to your app, you call getToken("THEIR_SENDER_ID", "FCM") to give them authorization to send to your app. This will return a different registration token that corresponds only to that specific sender.
In the future, if you chose to remove their authorization to send to your app, you'll then have to make use of deleteToken("THEIR_SENDER_ID", "FCM"). This will invalidate the corresponding token, and when the Sender attempts to send a message, as the intended behavior, they will receive a NotRegistered error.
In order to delete the token for your own Sender, the correct handling is to use deleteInstanceId().
Special mentioning this answer by #Prince, specifically the code sample for helping me with this.
As #MichałK already doing in his post, after calling the deleteInstanceId(), getToken() should be called in order to send a request for a new token. However, you don't have to call it the second time. So long as onTokenRefresh() onNewToken() is implemented, it should automatically trigger providing you the new token.
For short, deleteInstanceId() > getToken() > check onTokenRefresh() onNewToken().
Note: Calling deleteInstanceId() will not only delete the token for your own app. It will delete all topic subscriptions and all other tokens associated with the app instance.
Are you positive you're calling deleteToken() properly? The value for audience should be (also seen from my answer that you linked) is "set to the app server's sender ID". You're passing the getId() value which is not the same as the Sender ID (it contains the app instance id value). Also, how are you sending the message (App Server or Notifications Console)?
getToken() and getToken(String, String) returns different tokens. See my answer here.
I also tried FirebaseInstanceId.getInstance().deleteInstanceId(), but then the next time I call FirebaseInstanceId.getInstance.getToken I receive null (it works on the second try).
It's probably because the first time you're calling the getToken(), it's still being generated. It's just the intended behavior.
I guess, after deleteInstanceId I could immediately call getToken() again, but it looks like a hack.
Not really. It's how you'll get the new generated (provided that it is already generated) token. So I think it's fine.
I did a brief research on what would be the most elegant solution to get back the full control (subscribe and unsubscribe to FCM) as before. Enable and disable the FCM after the user logged in or out.
Step 1. - Prevent auto initialization
Firebase now handle the InstanceID and everything else which need to generate a registration token. First of all you need to prevent auto initialization. Based on the official set-up documentation you need to add these meta-data values to your AndroidManifest.xml:
<?xml version="1.0" encoding="utf-8"?>
<application>
<!-- FCM: Disable auto-init -->
<meta-data android:name="firebase_messaging_auto_init_enabled"
android:value="false" />
<meta-data android:name="firebase_analytics_collection_enabled"
android:value="false" />
<!-- FCM: Receive token and messages -->
<service android:name=".FCMService">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT"/>
</intent-filter>
</service>
</application>
Now you disabled the automatic token request process. At the same time you have an option to enable it again at runtime by code.
Step 2. - Implement enableFCM() and disableFCM() functions
If you enable the auto initialization again then you received a new token immediately, so this is a perfect way to implement the enableFCM() method.
All subscribe information assigned to InstanceID, so when you delete it then initiate to unsubscribe all topic. On this way you able to implement disableFCM() method, just turn back off auto-init before you delete it.
public class FCMHandler {
public void enableFCM(){
// Enable FCM via enable Auto-init service which generate new token and receive in FCMService
FirebaseMessaging.getInstance().setAutoInitEnabled(true);
}
public void disableFCM(){
// Disable auto init
FirebaseMessaging.getInstance().setAutoInitEnabled(false);
new Thread(() -> {
try {
// Remove InstanceID initiate to unsubscribe all topic
// TODO: May be a better way to use FirebaseMessaging.getInstance().unsubscribeFromTopic()
FirebaseInstanceId.getInstance().deleteInstanceId();
} catch (IOException e) {
e.printStackTrace();
}
}).start();
}
}
Step 3. - FCMService implementation - token and message receiving
In the last step you need to receive the new token and send direct to your server.
Other hand you'll receive your data message and just do it what you want.
public class FCMService extends FirebaseMessagingService {
#Override
public void onNewToken(String token) {
super.onNewToken(token);
// TODO: send your new token to the server
}
#Override
public void onMessageReceived(RemoteMessage remoteMessage) {
super.onMessageReceived(remoteMessage);
String from = remoteMessage.getFrom();
Map data = remoteMessage.getData();
if (data != null) {
// TODO: handle your message and data
sendMessageNotification(message, messageId);
}
}
private void sendMessageNotification(String msg, long messageId) {
// TODO: show notification using NotificationCompat
}
}
I think this solution is clear, simple and transparent. I tested in a production environment and it's works. I hope it was helpful.
I was working on the same problem, when I had done my logout() from my application. But the problem was that after logging out, I was still getting push notifications from Firebase. I tried to delete the Firebase token. But after deleting the token in my logout() method, it is null when I query for it in my login() method. After working 2 days I finally got a solution.
In your logout() method, delete the Firebase token in the background because you can not delete Firebase token from the main thread
new AsyncTask<Void,Void,Void>() {
#Override
protected Void doInBackground(Void... params) {
try
{
FirebaseInstanceId.getInstance().deleteInstanceId();
} catch (IOException e)
{
e.printStackTrace();
}
return null;
}
#Override
protected void onPostExecute(Void result) {
// Call your Activity where you want to land after log out
}
}.execute();
In your login() method, generate the Firebase token again.
new AsyncTask<Void,Void,Void>() {
#Override
protected Void doInBackground(Void... params) {
String token = FirebaseInstanceId.getInstance().getToken();
// Used to get firebase token until its null so it will save you from null pointer exeption
while(token == null) {
token = FirebaseInstanceId.getInstance().getToken();
}
return null;
}
#Override
protected void onPostExecute(Void result) {
}
}.execute();
Developers should never unregister the client app as a mechanism for
logout or for switching between users, for the following reasons:
A registration token isn't associated with a particular logged in user. If the client app unregisters and then re-registers, the app can
receive the same registration token or a different registration token.
Unregistration and re-registration may each take up to five minutes to propagate. During this time messages may be rejected due to the
unregistered state, and messages may go to the wrong user. To make
sure that messages go to the intended user:
The app server can maintain a mapping between the current user and the registration token.
The client app can then check to ensure that messages it receives match the logged in user.
this quote is from a deprecated google documentation
But there is reasons to believe this is still true - even if the documentation above is deprecated.
You can observe this here - check out how they do it in this codelab https://github.com/firebase/functions-samples/blob/master/fcm-notifications/functions/index.js
and here
https://github.com/firebase/friendlychat-web/blob/master/cloud-functions/public/scripts/main.js
Since the getToken() is deprecated, use getInstanceId() to regenerate new token instead. It has same effect.
public static void resetInstanceId() {
new Thread(new Runnable() {
#Override
public void run() {
try {
FirebaseInstanceId.getInstance().deleteInstanceId();
FirebaseInstanceId.getInstance().getInstanceId();
Helper.log(TAG, "InstanceId removed and regenerated.");
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
}
Use this methods.
This is my solution, and I referred this at here
When you sign-up, use initFirebaseMessage,. and when log-out or delete
use removeFirebaseMessage().
private fun removeFirebaseMessage(){
CoroutineScope(Dispatchers.Default).launch {
FirebaseMessaging.getInstance().isAutoInitEnabled = false
FirebaseInstallations.getInstance().delete()
FirebaseMessaging.getInstance().deleteToken()
}
}
private fun initFirebaseMessage(){
val fcm = FirebaseMessaging.getInstance()
fcm.isAutoInitEnabled = true
fcm.subscribeToTopic("all")
fcm.subscribeToTopic("")
}
Another handy way to clear the firebase token and regenerated a new one using FirebaseMessaging.getInstance()
fun clearFirebaseToken() {
FirebaseMessaging.getInstance().apply {
deleteToken().addOnCompleteListener { it ->
Log.d("TAG++", "firebase token deleted ${it.result}")
token.addOnCompleteListener {
Log.d("TAG++", "firebase token generated ${it.result}")
if (it.result != null) saveTokenGenerated(it.result!!)
}
}
}
}
Just call deleteToken method on a background Thread upon Logout:
https://firebase.google.com/docs/reference/android/com/google/firebase/iid/FirebaseInstanceId.html#public-void-deletetoken-string-senderid,-string-scope
FirebaseInstanceId.getInstance().deleteToken(getString(R.string.gcm_defaultSenderId), "FCM")
The first argument takes the SenderID as it is defined in your FireBaseConsole
It takes a few seconds to update - and after that, you will no longer get FCM notifications.
I know I am late for the party. deleteInstanceId() should be called from the background thread since it's a blocking call. Just check the method deleteInstanceId() in FirebaseInstanceId() class.
#WorkerThread
public void deleteInstanceId() throws IOException {
if (Looper.getMainLooper() == Looper.myLooper()) {
throw new IOException("MAIN_THREAD");
} else {
String var1 = zzh();
this.zza(this.zzal.deleteInstanceId(var1));
this.zzl();
}
}
You can start an IntentService to delete the instance id and the data associated with it.
The firebase.iid package that contains FirebaseInstanceId is now deprecated. Auto-initialization has been migrated from Firebase Instance ID to Firebase Cloud Messaging. Also its behaviour has slighly changed. Before, a call to deleteInstanceId() would automatically generate a new token if auto-initialization was enabled. Now, the new token is only generated on the next app-start or if getToken() is called explicitly.
private suspend fun loginFCM() = withContext(Dispatchers.Default) {
val fcm = FirebaseMessaging.getInstance()
fcm.isAutoInitEnabled = true
fcm.token.await()
}
private suspend fun logoutFCM() = withContext(Dispatchers.Default) {
val fcm = FirebaseMessaging.getInstance()
fcm.isAutoInitEnabled = false // To prevent a new token to be generated automatically in the next app-start (remove if you don't care)
fcm.deleteToken().await()
}
If you want to logout completely from Firebase you can just delete the whole installation afterwards:
private suspend fun logoutFirebase() = withContext(Dispatchers.Default) {
logoutFCM()
val firebase = FirebaseInstallations.getInstance()
firebase.delete().await()
}
To wrap it all up, use background thread to delete the instanceID, the next time you login keep an eye on the Firestore/Realtime DB (if you save your tokens there), they will refresh
public void Logout() {
new Thread(){
#Override
public void run() {
super.run();
try {
FirebaseInstanceId.getInstance().deleteInstanceId();
FirebaseInstanceId.getInstance().getInstanceId();
} catch (final IOException e) {
runOnUiThread(new Runnable() {
#Override
public void run() {
Toast.makeText(Flags.this, e.getMessage(), Toast.LENGTH_SHORT).show();
}
});
}
}
}.start();
FirebaseMessaging.getInstance().setAutoInitEnabled(false);
FirebaseAuth.getInstance().signOut();
SharedPreferences sharedPreferences = getDefaultSharedPreferences(Flags.this);
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.clear();
editor.apply();
startActivity(new Intent(Flags.this, MainActivity.class));
Flags.this.finish();
}
This code below I used it and it helps me, and I used Kotlin coroutine instead of Thread(Runnable{}).start() because it less cost than creating a new thread object
private fun logoutFromFCM() {
GlobalScope.launch(Dispatchers.IO) {
FirebaseInstallations.getInstance().delete()
FirebaseMessaging.getInstance().deleteToken()
FirebaseMessaging.getInstance().token.addOnCompleteListener(OnCompleteListener { task ->
if (!task.isSuccessful) {
Log.w(TAG, "Fetching FCM registration token failed", task.exception)
return#OnCompleteListener
}
// Get new FCM registration token
val token = task.result
saveFirebaseToken(token)
Log.w(TAG, "Token Updated - newToken> $token")
})
}
}
For many situations where the notifications requirements are simple, the issue of handling log out can be implemented much more easily. For example, in my case every user is subscribed to only two topics:
Global alerts topic
User specific topic defined as the users email (with replacement of # with - because # is not allowed in topic string)
For such simple scenarios simply unsubscribe from the unwanted topics on log out:
Future<void> signOut() async {
messaging.unsubscribeFromTopic(emailToTopic(_firebaseAuth.currentUser.email));
await _firebaseAuth.signOut();
}
And of course subscribe to topics only on successful log in or sign up:
Future<String> signIn({String email, String password}) async {
try {
await _firebaseAuth.signInWithEmailAndPassword(
email: email, password: password);
messaging.subscribeToTopic(emailToTopic(email));
return "Signed in";
} on FirebaseAuthException catch (e) {
return e.message;
}
}
According to Firebase cloud messaging documentation, for subscribing a user to a topic I need to call
FirebaseMessaging.getInstance().subscribeToTopic("news");
In my application, I need all users to be subscribed to my cloud
messaging topic. Since return value is void, the question is how
can I understand that subscription was successful?
Is it a bad practice to call subscribeToTopic each time my
application starts?
1. How can I understand that subscription was successful?
Edit:
You could now check if subscription is successful by adding addOnSuccessListener()
FirebaseMessaging.getInstance().subscribeToTopic("news").addOnSuccessListener(new OnSuccessListener<Void>() {
#Override
public void onSuccess(Void aVoid) {
Toast.makeText(getApplicationContext(),"Success",Toast.LENGTH_LONG).show();
}
});
Original:
There is nothing explicitly mentioned in the docs about a response received when the subscription is successful.
However, if you need to mandate all of your users to be subscribed to a specific topic, you should call the subscribeToTopic on your app's first install. This will most likely make sure that there is a connection to the internet (since it's probably been downloaded and installed via the Play Store) and the subscription successful.
However, if you want to make sure, you can also handle he checking via your own App Server. As mentioned in the docs:
You can take advantage of Instance ID APIs to perform basic topic management tasks from the server side. Given the registration token(s) of client app instances, you can do the following:
Find out details about a client app instance's subscriptions, including each topic name and subscribe date. See Get information about app instances.
Check through the registration tokens, if they haven't been successfully subsribed to your topic, send a notification to it where it will trigger your client app to call subscribeToTopic.
2. Is it a bad practice to call subscribeToTopic each time my application starts?
Edit: Adding it in from the comments section: Subscribing on app start should be fine.
Thank you #FrankvanPuffelen for verifying. :)
I have written this function and tested. May be helpful.
private void subscribeToMessaging(){
SharedPreferences prefs = getSharedPreferences(SETTINGS_TITLE, MODE_PRIVATE);
// Getting value from shared preferences
boolean isSubscriptionEnable = prefs.getBoolean(SETTING_NOTIFICATION, true);
// if "isSubscriptionEnable" is true then check whether its already subscribed or not
if (isSubscriptionEnable){
boolean alreadySubscribed = prefs.getBoolean(SETTING_ALREADY_SUBSCRIBED, false);
// if not already subscribed then subscribe to topic and save value to shared preferences
if (!alreadySubscribed){
FirebaseMessaging.getInstance().subscribeToTopic("global").addOnSuccessListener(new OnSuccessListener<Void>() {
#Override
public void onSuccess(Void aVoid) {
Toast.makeText(getApplicationContext(),"Success",Toast.LENGTH_LONG).show();
}
});
SharedPreferences.Editor editor = getSharedPreferences(SETTINGS_TITLE, MODE_PRIVATE).edit();
editor.putBoolean(SETTING_ALREADY_SUBSCRIBED, true);
editor.apply();
Toast.makeText(this, "Subscribed", Toast.LENGTH_LONG).show();
} else {
Toast.makeText(this, "Already subscribed", Toast.LENGTH_LONG).show();
}
}
}
Don't forget to write these lines above onCreate()
public static final String SETTINGS_TITLE = "settings";
public static final String SETTING_NOTIFICATION = "notification_state";
public static final String SETTING_ALREADY_SUBSCRIBED = "already_subscribed";
Okay so I have an app which on first start takes you through a few welcoming slides, then takes you to a login/register page and then to MainActivity.
I have just implemented FCM and the services generate a token before any of those pages have been seen by the user. How could I make it so that the service runs after I get to MainActivity?
The problem is I'm trying to send the token as soon as it is refreshed to the MySQL DB to the appropriate user account, but since the user hasn't signed in yet, that is null and my message to the server fails. What's a good way to design this? I thought of saving the token in SharedPreferences and sending it to the server after the user has logged in but that creates lots of complications when the token is refreshed at some later point?!
Possible solution:
I'm not sure I completely understand how the 2 services run but say in onTokenRefresh I just save the token into SharedPreferences and in MainActivity I get the value from SP and then I send it to the server. In that case when the token is refreshed the new value will immediately go into SharedPreferences again. But I would still need to check if it's a new value in SP and then reupload it to the server. This is confusing!
Note that you can always retrieve the token with:
FirebaseInstanceID.getInstance().getToken();
This will return null if the token has not yet been generated or the token if it has been generated. In your case it is very likely that the token will be generated by the time the user has signed in. So you should be able to send it to your app server as soon as the user has signed in. If it is not available then you would send it in the onTokenRefresh callback as Chintan Soni mentioned.
Edit
Using the new Firebase SDK (21.0.0) , you will get your token this way :
FirebaseInstallations.getInstance().getToken(false).addOnCompleteListener(new OnCompleteListener<InstallationTokenResult>() {
#Override
public void onComplete(#NonNull Task<InstallationTokenResult> task) {
if(!task.isSuccessful()){
return;
}
// Get new Instance ID token
String token = task.getResult().getToken();
}
});
You better add a listener for more handling on the response .
Yes FCM token is generated automatically. But try to see this in a different angle.
This is how I handled it.
Let FCM generate token as soon as your app starts. OnTokenRefresh will be called and you just save it in your preferences as:
#Override
public void onTokenRefresh() {
// Get updated InstanceID token.
String refreshedToken = FirebaseInstanceId.getInstance().getToken();
Log.d(TAG, "Refreshed token: " + refreshedToken);
sendRegistrationToServer(refreshedToken);
}
private void sendRegistrationToServer(String token) {
// Add custom implementation, as needed.
SharedPreferenceUtils.getInstance(this).setValue(getString(R.string.firebase_cloud_messaging_token), token);
// To implement: Only if user is registered, i.e. UserId is available in preference, update token on server.
int userId = SharedPreferenceUtils.getInstance(this).getIntValue(getString(R.string.user_id), 0);
if(userId != 0){
// Implement code to update registration token to server
}
}
Hope you are clear with the way. Ask if you need more clearance on it.
Edit
Using the new Firebase SDK (21.0.0) , you need to override onNewToken() method instead of onTokenRefresh()
#Override
public void onNewToken(#NonNull String s) {
super.onNewToken(s);
sendRegistrationToServer(s);
}
We handled it like this:
Our server create/update the token value against a user id (primary key)
Use 2 SharedPreferences
String - token String
Boolean (updated) - whether token is updated on server or not.
In case of token refresh we update the token string and set the boolean to false.
Later whenever user login each time we check for boolean (updated), if that is false - we attach the current token to his id and send it to server and set updated to true.
December 2020 update : Using the new Firebase SDK (21.0.0) you can get it by overriding onNewToken() method :
#Override
public void onNewToken(#NonNull String s) {
super.onNewToken(s);
sendRegistrationToServer(s);
}
Or by FirebaseInstallations.getInstance() within your scope :
FirebaseInstallations.getInstance().getToken(false).addOnCompleteListener(new OnCompleteListener<InstallationTokenResult>() {
#Override
public void onComplete(#NonNull Task<InstallationTokenResult> task) {
if(!task.isSuccessful()){
return;
}
// Get new Instance ID token
String token = task.getResult().getToken();
}
});
I'm starting to use Backendless.com mBaaS on Android
I sign in user via Google and I got token and everything is OK, but the logged in user isn't created an Users table, so I can not use it to store user specific data.
So I tried to combine user login from here with user creation from documentation:
if (result.isSuccess()) {
logined = true;
loginInBackendless(result.getSignInAccount());
BackendlessUser user = new BackendlessUser();
user.setProperty("email", result.getSignInAccount().getEmail().toString());
user.setProperty("name", result.getSignInAccount().getDisplayName().toString());
user.setPassword(UUID.randomUUID().toString());
Backendless.UserService.register(user, new AsyncCallback<BackendlessUser>() {
public void handleResponse(BackendlessUser registeredUser) {
// user has been registered and now can login
}
public void handleFault(BackendlessFault fault) {
// an error has occurred, the error code can be retrieved with fault.getCode()
}
});
the question is:
Is it right way to create user? it seems not OK, because every time google user is logged in, a new Backendless user is created (or his record in Users table is updated).
So, my question restated is when you go to Settings -> Accounts & Sync and select the an account that was created that your SyncAdapter is syncing with a cloud server, and select remove account, what happens as far as your SyncAdapter is concerned? There is a dialog that displays asking you to confirm and that the data on the phone associated with that account will be removed. I cannot easily believe that the framework can automatically remove the data my SyncAdapter has stored in the local database, but it seems to imply that removing the account will (and I would agree that is should) remove that data. Is there a addition to my SyncAdapter that will serve sort of as the callback for the account removal to handle deleting all the appropriate data from the local database? Maybe it has to be done through the AccountManager instead; my AccountManager gets notified when the account gets removed and from there I can trigger the data deletion without the SyncAdapter.
EDIT:
On a related note, is the sync manager calling my SyncAdapter for each account that it synchronizes when a new account is added? I see a onPerformSync(...) being executed for previously added accounts along with the just added account when I add an account, and would like to stop that.
I discovered the solution is to make the app's ContentProvider implement OnAccountsUpdateListener. Attach the ContentProvider as a listener in its onCreate method with account_manager.addOnAccountsUpdatedListener(this, null, false) and then implement the interface method like
#Override
public void onAccountsUpdated(final Account[] accounts) {
Ln.i("Accounts updated.");
final Iterable<String> account_list = new Iterable<String>() {
#Override
public Iterator<String> iterator() {
return new Iterator<String>() {
private final Iterator<Account> account_list = Arrays.asList(accounts).iterator();
#Override
public boolean hasNext() {
return account_list.hasNext();
}
/** Extracts the next account name and wraps it in single quotes. */
#Override
public String next() {
return "'" + account_list.next().name + "'";
}
#Override
public void remove() { throw new UnsupportedOperationException("Not implemented"); }
};
}
};
final String account_set = TextUtils.join(", ", account_list);
Ln.i("Current accounts: %s", account_set);
// Removes content that is associated with accounts that are not currently connected
final SelectionBuilder builder = new SelectionBuilder();
builder.table(Tables.CALENDARS)
.where(Calendars.CALENDAR_USER + " NOT IN (?)", account_set);
new SafeAsyncTask() {
#Override
public Void call() throws Exception {
_model.openWritableDatabase();
_model.delete(builder);
return null;
}
}.execute();
getContext().getContentResolver().notifyChange(Calendars.NO_SYNC_URI, null, false);
}
I construct a String of the currently connected accounts, then build a SQL query with that String. I perform a delete on the database in a background thread on that query to remove the data associated with accounts not currently connected. And I notify that content changed, but does not need to synchronized with the server.
No, but your Authenticator does[1]. This method is called before the account is removed:
AbstractAccountAuthenticator.getAccountRemovalAllowed(AccountAuthenticatorResponse, Account)
the Account param is the account being deleted - the default behaviour is to allow removal of the account:
return super.getAccountRemovalAllowed(response, account); // returns Bundle[{booleanResult=true}]
..but I guess it's a hook that you can use to tidy things up or block the account being removed should you wish to.
[1] - this is a dirty hack; please see Dandre's comment.
Another option is to register for the android.accounts.LOGIN_ACCOUNTS_CHANGED broadcast that the AccountManager sends out. Unfortunately, this broadcast is sent out whenever any account is changed and the broadcast does not deliver further information what has changed either.
So you'd have to query the account manager and look how many of "your" accounts it has left and delete the data of the missing ones.