简体   繁体   中英

Chat App using Firebase: Get notification when new message received - Android

I am developing a chat app using Firebase Realtime Database. I have been able to send and receive messages properly. Now, I want to implement notification whenever new message is received. For that, I have created a Service which listens to database changes using ChildEventListener and creates notification. The problem is that I am creating notification in onChildAdded method and this method fires both for existing node in database and new one. This is causing notification to be created multiple times for same message whenever user navigate back and forth from app.

Here is how I am implementing it:

chatMsgsRef.orderByChild(FirebaseDBKeys.LOCATION_LAST_UPDATED).addChildEventListener(new ChildEventListener() {

            @Override
            public void onChildAdded(DataSnapshot dataSnapshot, String s) {

                ChatMessage message = dataSnapshot.getValue(ChatMessage.class);

                if (!message.getSenderId().equals(currentUserId)) {

                    mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);

                    NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(NotificationsService.this)
                            .setSmallIcon(R.drawable.message)
                            .setContentTitle("New Message from " + message.getReceipientName())
                            .setContentText(message.getMessage())
                            .setOnlyAlertOnce(true)
                            .setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION));
                    mBuilder.setAutoCancel(true);
                    mBuilder.setLocalOnly(false);


                    mNotificationManager.notify(NOTIFICATION_ID, mBuilder.build());

                }
            }

            @Override
            public void onChildChanged(DataSnapshot dataSnapshot, String s) {

            }

            @Override
            public void onChildRemoved(DataSnapshot dataSnapshot) {

            }

            @Override
            public void onChildMoved(DataSnapshot dataSnapshot, String s) {

            }

            @Override
            public void onCancelled(DatabaseError databaseError) {

            }
        });

How can I implement notifications in the way it works in other chat applications like whatsapp, etc.??

The correct way to do this is to have a push data message to the client with the chat when there is a new chat sent to them.

This blog post explains how to accomplish this. Basically you need to set up a server to listen to the chat firebase ref and send push notifications when it is updated. This way your clients can be in the app or out of the app and still get the push.

If you use a service there are a number of potential issues.

First of all you will have to keep the phone awake. This will drain the battery.

Second, Android can kill your background service at any time, so your app may stop working suddenly.

Third, with Doze mode Android will block network activity and stop your app from running in the background.

A better answer occurred to me:

What you want in this case isn't necessarily to know new messages. It's to know UNREAD messages.

Set a "read" flag in the ChatMessage object (or conversely, have a value somewhere that gives the timestamp or ID of the most recent read message).

Now whenever onChildAdded is fired, check to see if read == false. If so, show a notification that the message is unread (remember to update the notification if it exists, so only one notification will be displayed and it will show the most recent one--oh, and also remember to remove the notification when the child is changed to read.)

If the user is using your app on multiple devices, it will correctly check the read state. If he reads the latest message on a phone and then goes to the tablet, it will not show that new message notification.

If you wanted to, you could even use this functionality to indicate that the recipient read your message to them.

How do you know when it is read? Perhaps simply when you add it on the screen. Perhaps you make sure it is in view (not scrolled off the screen) and is visible for several seconds.

Have you tried addValueEventListener?

https://www.firebase.com/docs/android/guide/retrieving-data.html#section-start

// Get a reference to our posts
Firebase ref = new Firebase("https://docs-examples.firebaseio.com/web/saving-data/fireblog/posts");
// Attach an listener to read the data at our posts reference
ref.addValueEventListener(new ValueEventListener() {
    @Override
    public void onDataChange(DataSnapshot snapshot) {
        System.out.println(snapshot.getValue());
    }
    @Override
    public void onCancelled(FirebaseError firebaseError) {
        System.out.println("The read failed: " + firebaseError.getMessage());
    }
});

"This method will be called anytime new data is added to our Firebase reference, and we don't need to write any extra code to make this happen."

EDIT: "It is triggered once with the initial data and again every time the data changes." I haven't tried it, but my thought would be to set a flag when the method fires the first time. Then whenever the method and that flag is set (in other words, the second time, third time, fourth time, etc.) get the most recent object from the snapshot.

A combination of the answer of @Chad Shultz and your original code, with some database structural additions.

You will, as you originally intended, use a Query

chatMsgsRef.orderByChild(...)

However, change the data structure to have the following layout

>root
    > users
        > userID_001
            > chats
                > chat_001:true
        > userID_002
            > chats
                > chat_001:true
    > chats
        > chat_001
            > participants
                > userID_001:true
                > userID_002:true
            > messages
                > msg_001
                    > sender:"userID_001"
                    > text:"some message"
                    > time:"some time"
    > message_read_states
        > chat_001
            > msg_001
                > userID_001:true
                > userID_002:false

So whenever a message is sent, it gets pushed to the "messages" node. Next, the system gets all the "participants", and pushes them under the "message_read_states" node for that chat/message, with a value of false for everyone except the sender. When someone reads the message, they change their value to true there.

Now the Service needs to determine for which messages to notify a user. The Service will place a listener on message_read_states/chat_X, and order it by the value of the child field "userID_X" (depending on who the current user is). We'll make the Query only return the message ID's, for each chat, for which there is a value "userID_X:false" under it

So in this example, for "userID_002", the query will return "msg_001" , but for "userID_001" it will return nothing, because the required value is not found for that key (he/she was the sender).

The Query will be constructed as follows, inside your Service's onStartCommand:

    //TODO: Get the ID of the chat the user is taking part in
    String chatID = "chat_001";

    // Check for new messages
    FirebaseUser currentUser = FirebaseAuth.getInstance().getCurrentUser();
    if (currentUser != null){
        String UID = currentUser.getUid();
        DatabaseReference rootRef = FirebaseDatabase.getInstance().getReference();
        Query query = rootRef.child("message_read_states").child(chatID).orderByChild(UID).equalTo(false);
        query.addChildEventListener(new ChildEventListener() {
            @Override
            public void onChildAdded(DataSnapshot dataSnapshot, String s) {
                String messageID = dataSnapshot.getKey();
                //TODO: Handle Notification here, using the messageID
                // A datasnapshot received here will be a new message that the user has not read
                // If you want to display data about the message or chat,
                // Use the chatID and/or messageID and declare a new 
                // SingleValueEventListener here, and add it to the chat/message DatabaseReference.
            }

            @Override
            public void onChildChanged(DataSnapshot dataSnapshot, String s) {
                String messageID = dataSnapshot.getKey();
                //TODO: Remove the notification
                // If the user reads the message in the app, before checking the notification
                // then the notification is no longer relevant, remove it here.
                // In onChildAdded you could use the messageID(s) to keep track of the notifications
            }

            @Override
            public void onChildRemoved(DataSnapshot dataSnapshot) {

            }

            @Override
            public void onChildMoved(DataSnapshot dataSnapshot, String s) {

            }

            @Override
            public void onCancelled(DatabaseError databaseError) {

            }
        });
    }

Now of course it's very likely a user will be involved in multiple chats. You'll have to iterate over all the chats associated with the user, and implement a Query for each chat.

Source : I'm currently working on a Firebase app with a chat system

YOU CAN DO IT FOR FREE AND WITHOUT SERVICES!!

What Shmuel says is the right way to do it. However, to do what is described in that blog post he links, you need Cloud functions. And they are not free on Firebase, you have to pay.

What the others describe in their answers is working but it's not good because it is not reliable (eg if the user restarts his phone it won't work) and such background services slow down the device by a lot.


SOLUTION:

You can pretty much do what you would do in these cloud functions on the user backend.

This is also not perfect because you need to store sensitive information in the code. So malicious users could get the information and abuse it for sending notifications to your users themselves.

But it is free, reliable and doesn't slow down the device at all.

Below is the implementation.


Module:app dependencies

implementation 'com.squareup.retrofit2:retrofit:2.6.0'
implementation 'com.squareup.retrofit2:converter-gson:2.6.0'

ApiClient

public class ApiClient {

    private static final String BASE_URL = "https://fcm.googleapis.com/";
    private static Retrofit retrofit = null;

    public static Retrofit getClient() {
        if (retrofit == null) {
            retrofit = new Retrofit.Builder()
                    .baseUrl(BASE_URL)
                    .addConverterFactory(GsonConverterFactory.create())
                    .build();
        }
        return retrofit;
    }
}

ApiInterface

public interface ApiInterface {

    @Headers({"Authorization: key=" + ConstantKey.SERVER_KEY, "Content-Type:application/json"})
    @POST("fcm/send")
    Call<ResponseBody> sendNotification(@Body RootModel root);
}

RootModel

public class RootModel {

    @SerializedName("to") //  "to" changed to token
    private String token;

    @SerializedName("notification")
    private NotificationModel notification;

    @SerializedName("data")
    private DataModel data;

    public RootModel(String token, NotificationModel notification, DataModel data) {
        this.token = token;
        this.notification = notification;
        this.data = data;
    }

    public String getToken() {
        return token;
    }

    public void setToken(String token) {
        this.token = token;
    }

    public NotificationModel getNotification() {
        return notification;
    }

    public void setNotification(NotificationModel notification) {
        this.notification = notification;
    }

    public DataModel getData() {
        return data;
    }

    public void setData(DataModel data) {
        this.data = data;
    }
}

NotificationModel

public class NotificationModel {

    private String title;
    private String body;

    public NotificationModel(String title, String body) {
        this.title = title;
        this.body = body;
    }

    public String getBody() {
        return body;
    }

    public void setBody(String body) {
        this.body = body;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

}

DataModel

public class DataModel {

    private String name;
    private String age;

    public DataModel(String name, String age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getAge() {
        return age;
    }

    public void setAge(String age) {
        this.age = age;
    }

}

Send notification by using this method

private void sendNotificationToUser(String token) {
    RootModel rootModel = new RootModel(token, new NotificationModel("Title", "Body"), new DataModel("Name", "30"));

    ApiInterface apiService =  ApiClient.getClient().create(ApiInterface.class);
    retrofit2.Call<ResponseBody> responseBodyCall = apiService.sendNotification(rootModel);

    responseBodyCall.enqueue(new Callback<ResponseBody>() {
        @Override
        public void onResponse(retrofit2.Call<ResponseBody> call, retrofit2.Response<ResponseBody> response) {
            Log.d(TAG,"Successfully notification send by using retrofit.");
        }

        @Override
        public void onFailure(retrofit2.Call<ResponseBody> call, Throwable t) {

        }
    });
}

You can get ConstantKey.SERVER_KEY in your Firebase Project settings in the Messaging tab.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM