In previous tutorials in this series, we began the development of an Android chat application that allows users to do the following:
- Register and verify user email addresses; login to the system
- Fetch chat conversations from other users
- Send and receive messages within the conversations
As a next step in the app’s development, we need to set up a system that sends users instant notifications for new messages.
We’ll do this by allowing the Android app to continuously check for new messages sent for the logged in user. If a new message has arrived, the app will sendsa notification showing the number of received messages and the user(s) that sent them.
The outline of this tutorial is as follows:
- Revising the users Table
- Adding New Columns in the users Table to Check New Messages
- Editing the send() Function
- Editing the receive_chats Function
- Updating the Server to Check for New Messages
- Complete Code of the Flask Server
- Background Thread for Checking New Messages in the Android App
- NewMessagesBackgroundNotification Class
The GitHub repo for this project is available on this page. Each tutorial has a separate folder in which the client (Android app) and the server (Python Flask) are available.
The Android app is available on the Google Play Store under the name HiAi Chat. You can download and try it out here:
Revising the Users Table
The primary goal of this tutorial is to allow users to get instant notifications when new messages arrive. A new message in this case means a message that the user has not yet been notified of. So first, we need to figure out a way to know whether the user has been notified or not.
To build this feature, we have to revise the users table we created in the chat_db MySQL database. This table was created in the first tutorial of this series, and the code for building this table is shown below. You’ll need to change the values assigned to the arguments of the mysql.connector.connect() method to match your configuration.
import mysql.connector
import sys
try:
chat_db = mysql.connector.connect(host="localhost", user="root", passwd="ahmedgad", database="chat_db")
except:
sys.exit("Error connecting to the database. Please check your inputs.")
db_cursor = chat_db.cursor()
# Users Table
try:
db_cursor.execute("CREATE TABLE users (id INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY, first_name VARCHAR(255) NOT NULL, last_name VARCHAR(255) NOT NULL, username VARCHAR(255) NOT NULL UNIQUE, password VARCHAR(32) NOT NULL, registration_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP")
print("users table created successfully.")
except mysql.connector.DatabaseError:
sys.exit("Error creating the table. Please check if it already exists.")
As seen in the previous code snippet, the 6 columns included in the users table are listed below.
- id
- first_name
- last_name
- username
- password
- registration_date
Note that in addition to the previous 6 columns, there is another column named email, which we created in the email verification part of this series.
So the total number of fields in this table adds up to 7. As you can see, there isn’t a column to help to identify whether the user is notified by a message or not. We’ll add these columns in the next section.
Adding New Columns in the Users Table to Check New Messages
To know whether the logged-in user has been notified of a given message or not, we add two new columns to the users table, named last_date_conversations_fetched and notified_by_new_messages.
The datatype of the last_date_conversations_fetched column is a TIMESTAMP that holds the last date when the user received messages. If new messages are sent to the user after this date, then they’re classified as new messages, and thus the Android app will send a notification to the user.
The datatype of the other column named notified_by_new_messages isa BOOLEAN set to true when the user is notified of the new messages.
The following code alters the users table to add the new columns:
import mysql.connector
import sys
try:
chat_db = mysql.connector.connect(host="localhost", user="root", passwd="ahmedgad", database="chat_db")
except:
sys.exit("Error connecting to the database. Please check your inputs.")
db_cursor = chat_db.cursor()
add_column_query = "ALTER TABLE users ADD last_date_conversations_fetched TIMESTAMP DEFAULT CURRENT_TIMESTAMP"
db_cursor.execute(add_column_query)
chat_db.commit()
add_column_query = "ALTER TABLE users ADD notified_by_new_messages BOOLEAN DEFAULT false"
db_cursor.execute(add_column_query)
chat_db.commit()
After adding such a new column, here is the list of the 9 columns in the users table.
- id
- first_name
- last_name
- username
- password
- registration_date
- last_date_conversations_fetched
- notified_by_new_messages
The next section discusses how the notified_by_new_messages column is used.
Editing the send() Function
When user A sends a message to another user B, this means that the receiver (user B) has a new message that requires a notification to be sent. For this reason, the notified_by_new_messages column for the receiver (user B) should be set to false immediately after inserting the message into the messages table.
From a previous tutorial in this series titled Sending text messages on Android between verified users, when a user sends a message to another user, the send() function is called to insert the message into the messages table. Thus, we have to edit this function to update the notified_by_new_messages column:
The new send() function is listed below, with the previous code snippet added at its end:
def send(msg_received):
text_msg = msg_received["msg"]
sender_username = msg_received["sender_username"]
receiver_username = msg_received["receiver_username"]
select_query = "SELECT * FROM users where username = " + "'" + receiver_username + "'"
db_cursor.execute(select_query)
records = db_cursor.fetchall()
if len(records) == 0:
return "Invalid receiver username."
select_query = "SELECT * FROM users where username = " + "'" + sender_username + "'"
db_cursor.execute(select_query)
records = db_cursor.fetchall()
if len(records) == 0:
return "Invalid sender username."
insert_query = "INSERT INTO messages (sender_username, receiver_username, message) VALUES (%s, %s, %s)"
insert_values = (sender_username, receiver_username, text_msg)
try:
db_cursor.execute(insert_query, insert_values)
chat_db.commit()
print(db_cursor.rowcount, "New record inserted successfully.")
except Exception as e:
print("Error while inserting the new record :", repr(e))
return "Error while processing the request."
update_query = "UPDATE users SET notified_by_new_messages = false WHERE username = " + "'" + receiver_username + "'"
db_cursor.execute(update_query)
chat_db.commit()
print("Received message from :", sender_username, "(", text_msg, ") to be sent to ", receiver_username)
return "success"
This is used for setting the value of the notified_by_new_messages value to false. When the user is notified of the new messages, its value is set to true, which is discussed further in the next section.
Editing the receive_chats Function
Inside the send() function, the value of the notified_by_new_messages column is set to false to reflect that the user hasn’t notified of such newly sent messages.
When the Android app fetches chats from the server for such a user, the value of the column notified_by_new_messages should be changed back to true, meaning there are no new messages awaiting the user.
We also need to change the value of the last_date_conversations_fetched column to the current timestamp to reflect the last date the conversations were fetched.
This leads us to another question—where in the code does the Android app fetch the chats? The answer is inside the receive_chats() function. We implemented this function in the previous tutorial titled Fetching and listing conversations for an Android chat application.
The code that changes the value of the 2 newly-added columns is as follows. As a note, it should be executed directly after fetching the conversations from the messages table.
The edited receive_chats() function is shown below:
def receive_chats(msg_received):
receiver_username = msg_received["receiver_username"]
receiver_username = decrypt_text(receiver_username)
select_query = "SELECT first_name, last_name FROM users where username = " + "'" + receiver_username + "'"
db_cursor.execute(select_query)
records = db_cursor.fetchall()
if len(records) == 0:
return "Invalid receiver username."
select_query = "SELECT DISTINCT sender_username FROM messages where receiver_username = " + "'" + receiver_username + "'"
db_cursor.execute(select_query)
records = db_cursor.fetchall()
if len(records) == 0:
print("No messages delivered for username " + receiver_username)
return "0"
update_query = "UPDATE users SET notified_by_new_messages = true, last_date_conversations_fetched = CURRENT_TIMESTAMP WHERE username = " + "'" + receiver_username + "'"
db_cursor.execute(update_query)
chat_db.commit()
all_chats = {}
for record_idx in range(len(records)):
curr_record = records[record_idx]
sender_username = curr_record[0]
curr_chat = {}
curr_chat["username"] = sender_username
curr_chat["message"] = "MESSAGE"
curr_chat["date"] = "DATE"
all_chats[str(record_idx)] = curr_chat
all_chats = json.dumps(all_chats)
print("Sending Chat(s) :", all_chats)
return all_chats
Using the notified_by_new_messages column, we’re able to decide whether new messages are sent to the user or not. If the value if false, then there are some new messages that the user has not been notified of. If true, then the user has been notified of all new messages.
Note that using the notified_by_new_messages column, we’re only able to know whether a user has new messages or not. But there’s still a question: What are these new messages? We can answer this question using the newly-added column last_date_conversations_fetched.
In the previous tutorial titled Sending text messages on Android between verified users, we created a messages table in which the sent and received messages between all users are stored. For each message in this table, there’s a column named receive_date that defaults to the date and time of when the message was actually received.
When a new message arrives with its receive_date column set to a value greater than the current value in the last_date_conversations_fetched column, then this message is classified as a new message and a notification should appear to the user.
After the user is notified of this new message, the value of the last_date_conversations_fetched column is set equal to the CURRENT_TIMESTAMP to reflect the last date on which the user was notified of a new incoming message. Also, the value of the notified_by_new_messages column is set to true. This avoids notifying the user of the same message more than once.
If the value of the receive_date column is smaller than the value in the last_date_conversations_fetched column, then it’s an indication that the user has been notified, and thus no new notification is required.
When another new message arrives, its value in the receive_date column will be greater than the value in the last_date_conversations_fetched column, and thus the user will be notified.
Again, the value of the last_date_conversations_fetched column will be updated and set to the CURRENT_TIMESTAMP, and the value of notified_by_new_messages is set to true to avoid duplicated notifications.
The next section builds a new function named check_new_messages() to check whether the user has new messages or not. Based on its return result, the app then decides whether a notification should be created or not.
Updating the Server to Check for New Messages
Next, we need to create a new function named check_new_messages() in the Flask server that returns a JSON object holding the number of new messages and their senders.
Before discussing its implementation, it’s worth mentioning that any request sent from the Android app to the Flask server is first processed by a function named chat() that listens to the root directory / of the server. Its job is to call the appropriate function that serves the request based on a field called subject. Here’s the new implementation of the chat() function:
@app.route('/', methods = ['GET', 'POST'])
def chat():
msg_received = flask.request.get_json()
msg_subject = msg_received["subject"]
if msg_subject == "register":
return register(msg_received)
elif msg_subject == "login":
return login(msg_received)
elif msg_subject == "verify":
return verify(msg_received)
elif msg_subject == "send":
return send(msg_received)
elif msg_subject == "receive_chats":
return receive_chats(msg_received)
elif msg_subject == "receive_messages":
return receive_messages(msg_received)
elif msg_subject == "check_new_messages":
return check_new_messages(msg_received)
else:
return "Invalid request."
When the subject of the received request is set to check_new_messages, then the function named check_new_messages() will be called. The implementation of this function is shown below. Note that the purpose of this function is simpy informing the Android app of newly-available messages for the user. Its purpose is not in actually sending the new messages. Let’s discuss this function in more detail.
def check_new_messages(msg_received):
receiver_username = msg_received["receiver_username"]
select_query = "SELECT last_date_conversations_fetched FROM users WHERE username = " + "'" + receiver_username + "' AND notified_by_new_messages = false"
db_cursor.execute(select_query)
records = db_cursor.fetchall()
reply_json = {}
if len(records) == 0:
reply_json["num_messages"] = "0" # 0 means no new messages for the username
reply_json["senders"] = ""
reply_json = json.dumps(reply_json)
return reply_json
last_date_conversations_fetched = str(records[0][0])
update_query = "UPDATE users SET notified_by_new_messages = true WHERE username = " + "'" + receiver_username + "'"
db_cursor.execute(update_query)
chat_db.commit()
select_query = "SELECT sender_username FROM messages WHERE receiver_username = " + "'" + receiver_username + "' AND receive_date > " + "'" + last_date_conversations_fetched + "'"
db_cursor.execute(select_query)
records = db_cursor.fetchall()
if len(records) == 0:
reply_json["num_messages"] = "0" # 0 means no new messages for the username
reply_json["senders"] = ""
reply_json = json.dumps(reply_json)
return reply_json
else:
select_query = "SELECT DISTINCT sender_username FROM messages WHERE receiver_username = " + "'" + receiver_username + "' and receive_date > " + "'" + last_date_conversations_fetched + "'"
db_cursor.execute(select_query)
records_usernames = db_cursor.fetchall()
reply_json["num_messages"] = str(len(records)) # returns the number of new messages sent to the username
if len(records_usernames) == 1:
reply_json["senders"] = records_usernames[0][0]
elif len(records_usernames) == 2:
reply_json["senders"] = records_usernames[0][0] + " and " + records_usernames[1][0]
else:
reply_json["senders"] = records_usernames[0][0] + ", " + records_usernames[1][0] + ", and " + str(len(records_usernames)-2) + " more"
reply_json = json.dumps(reply_json)
return reply_json
The first line in the function retrieves the username of the user that’s currently logged into the Android app.
The next lines fetches the current value in the last_date_conversations_fetched column from the users table:
The condition notified_by_new_messages = false helps return a result if the user hasn’t been notified of some/any messages. If there are no new messages for the user, then the value of the notified_by_new_messages column will be true.
As a result, the length of the records variable returned from the previous code is 0. For this reason, the check_new_messages() function returns a JSON object indicating that the number of new messages is 0 (i.e. no new messages) and no need for a notification. The number of new messages is saved into the num_messages field of the JSON object.
If the value of the notified_by_new_messages column is true, then there are new messages sent to the user. For this reason, the value of the last_date_conversations_fetched column is returned using the next line:
The next lines update the value of the notified_by_new_messages column to set it to true, meaning that the user has been notified of the new messages. The user isn’t actually notified of the messages at the current moment but will be notified immediately upon the execution of that code.
The following code fetches the senders’ names of the newly-sent messages. Note that we didn’t fetch the actual messages, as the purpose of the check_new_messages() function is to simply to check for new messages, not to return the new messages to the Android app.
The following code prepares a JSON object with 2 fields:
- num_messages: Number of new messages.
- senders: Username(s) of the sender(s) of the new messages. If the number of senders is more than 2, then just the usernames of the first 2 senders are returned.
if len(records) == 0:
reply_json["num_messages"] = "0" # 0 means no new messages for the username
reply_json["senders"] = ""
reply_json = json.dumps(reply_json)
return reply_json
else:
select_query = "SELECT DISTINCT sender_username FROM messages WHERE receiver_username = " + "'" + receiver_username + "' and receive_date > " + "'" + last_date_conversations_fetched + "'"
db_cursor.execute(select_query)
records_usernames = db_cursor.fetchall()
reply_json["num_messages"] = str(len(records)) # returns the number of new messages sent to the username
if len(records_usernames) == 1:
reply_json["senders"] = records_usernames[0][0]
elif len(records_usernames) == 2:
reply_json["senders"] = records_usernames[0][0] + " and " + records_usernames[1][0]
else:
reply_json["senders"] = records_usernames[0][0] + ", " + records_usernames[1][0] + ", and " + str(len(records_usernames)-2) + " more"
reply_json = json.dumps(reply_json)
return reply_json
At this point, the Flask server is able to respond to the Android app with the number of new messages.
Complete Code for the Flask Server
Before discussing the Android app, here’s the complete code for the Flask server. You can either create your own key files (encryption_key1.key and encryption_key2.key) or just use the ones available in the GitHub project.
To learn more about how these files are created, read this tutorial: Email Verification for an Android App Registration System.
import flask
import mysql.connector
import sys
import json
import smtplib, ssl
from email.mime.text import MIMEText
import cryptography.fernet
app = flask.Flask(__name__)
@app.route('/', methods = ['GET', 'POST'])
def chat():
msg_received = flask.request.get_json()
msg_subject = msg_received["subject"]
if msg_subject == "register":
return register(msg_received)
elif msg_subject == "login":
return login(msg_received)
elif msg_subject == "verify":
return verify(msg_received)
elif msg_subject == "send":
return send(msg_received)
elif msg_subject == "receive_chats":
return receive_chats(msg_received)
elif msg_subject == "receive_messages":
return receive_messages(msg_received)
elif msg_subject == "check_new_messages":
return check_new_messages(msg_received)
else:
return "Invalid request."
def register(msg_received):
first_name = msg_received["first_name"]
lastname = msg_received["last_name"]
username = msg_received["username"]
email = msg_received["email"]
select_query = "SELECT * FROM users where username = " + "'" + username + "'"
db_cursor.execute(select_query)
records = db_cursor.fetchall()
if len(records) != 0:
return "username" # "Username already exists. Please chose another username."
select_query = "SELECT * FROM users where email = " + "'" + email + "'"
db_cursor.execute(select_query)
records = db_cursor.fetchall()
if len(records) != 0:
return "email" # "E-mail is already registered."
send_verification_code(first_name, lastname, username, email) # Encrypt e-mail using 2 keys and send the code to the user e-mail.
return "success"
def verify(msg_received):
firstname = msg_received["firstname"]
lastname = msg_received["lastname"]
username = msg_received["username"]
email = msg_received["email"]
password = msg_received["password"]
verification_code = msg_received["verification_code"]
select_query = "SELECT * FROM users where username = " + "'" + username + "'"
db_cursor.execute(select_query)
records = db_cursor.fetchall()
if len(records) != 0:
return "Another user just used the username. Please chose another username."
select_query = "SELECT * FROM users where email = " + "'" + email + "'"
db_cursor.execute(select_query)
records = db_cursor.fetchall()
if len(records) != 0:
return "Another user just registered using the e-mail entered."
verification_state = verify_code(email, verification_code)
if verification_state == True:
insert_query = "INSERT INTO users (first_name, last_name, username, email, password, email_confirmed) VALUES (%s, %s, %s, %s, MD5(%s), %s)"
insert_values = (firstname, lastname, username, email, password, True)
try:
db_cursor.execute(insert_query, insert_values)
chat_db.commit()
print("This e-mail", email, " is verified and user is registered successfully.")
subject = "Welcome to HiAi"
body = "Hi " + firstname + " " + lastname + ",nnYour account is created successfully at HiAi. You can login with your username and password into the Android app to send and receive messages. nnYour username is " + username + "nnIf you forgot your password, just contact [email protected] for resetting it. nnGood luck.nHiAi [email protected]"
send_email(subject, body, email)
return "success"
except Exception as e:
print("Error while inserting the new record :", repr(e))
return "Error while processing the request."
else:
print("This e-mail", email, " verification failed.")
return "failure"
def verify_code(email, verification_code):
decoded_email = decrypt_text(verification_code)
if decoded_email == email:
return True # Email verification succeeded.
else:
return False # Email verification failed.
def send(msg_received):
text_msg = msg_received["msg"]
sender_username = msg_received["sender_username"]
receiver_username = msg_received["receiver_username"]
select_query = "SELECT * FROM users where username = " + "'" + receiver_username + "'"
db_cursor.execute(select_query)
records = db_cursor.fetchall()
if len(records) == 0:
return "Invalid receiver username."
select_query = "SELECT * FROM users where username = " + "'" + sender_username + "'"
db_cursor.execute(select_query)
records = db_cursor.fetchall()
if len(records) == 0:
return "Invalid sender username."
insert_query = "INSERT INTO messages (sender_username, receiver_username, message) VALUES (%s, %s, %s)"
insert_values = (sender_username, receiver_username, text_msg)
try:
db_cursor.execute(insert_query, insert_values)
chat_db.commit()
print(db_cursor.rowcount, "New record inserted successfully.")
except Exception as e:
print("Error while inserting the new record :", repr(e))
return "Error while processing the request."
update_query = "UPDATE users SET notified_by_new_messages = false WHERE username = " + "'" + receiver_username + "'"
db_cursor.execute(update_query)
chat_db.commit()
print("Received message from :", sender_username, "(", text_msg, ") to be sent to ", receiver_username)
return "success"
def receive_chats(msg_received):
receiver_username = msg_received["receiver_username"]
receiver_username = decrypt_text(receiver_username)
select_query = "SELECT first_name, last_name FROM users where username = " + "'" + receiver_username + "'"
db_cursor.execute(select_query)
records = db_cursor.fetchall()
if len(records) == 0:
return "Invalid receiver username."
select_query = "SELECT DISTINCT sender_username FROM messages where receiver_username = " + "'" + receiver_username + "'"
db_cursor.execute(select_query)
records = db_cursor.fetchall()
if len(records) == 0:
print("No messages delivered for username " + receiver_username)
return "0"
update_query = "UPDATE users SET notified_by_new_messages = true, last_date_conversations_fetched = CURRENT_TIMESTAMP WHERE username = " + "'" + receiver_username + "'"
db_cursor.execute(update_query)
chat_db.commit()
all_chats = {}
for record_idx in range(len(records)):
curr_record = records[record_idx]
sender_username = curr_record[0]
curr_chat = {}
curr_chat["username"] = sender_username
curr_chat["message"] = "MESSAGE"
curr_chat["date"] = "DATE"
all_chats[str(record_idx)] = curr_chat
all_chats = json.dumps(all_chats)
print("Sending Chat(s) :", all_chats)
return all_chats
def receive_messages(msg_received):
receiver_username = msg_received["receiver_username"]
sender_username = msg_received["sender_username"]
select_query = "SELECT * FROM users where username = " + "'" + receiver_username + "'"
db_cursor.execute(select_query)
records = db_cursor.fetchall()
if len(records) == 0:
return "Invalid receiver username."
select_query = "SELECT message, receive_date, sender_username FROM messages where (receiver_username = " + "'" + receiver_username + "' AND sender_username = " + "'" + sender_username + "') OR (receiver_username = " + "'" + sender_username + "' AND sender_username = " + "'" + receiver_username + "') ORDER BY receive_date DESC"
db_cursor.execute(select_query)
records = db_cursor.fetchall()
if len(records) == 0:
print("No messages delivered for username " + receiver_username)
return "0"
all_messages = {}
for record_idx in range(len(records)):
curr_record = records[record_idx]
message = curr_record[0]
receiveDate = curr_record[1]
sender_username = curr_record[2]
curr_message = {}
curr_message["message"] = message
curr_message["date"] = str(receiveDate)
curr_message["sender_username"] = sender_username
all_messages[str(record_idx)] = curr_message
all_messages = json.dumps(all_messages)
print("Sending message(s) :", all_messages)
return all_messages
def check_new_messages(msg_received):
receiver_username = msg_received["receiver_username"]
select_query = "SELECT last_date_conversations_fetched FROM users WHERE username = " + "'" + receiver_username + "' AND notified_by_new_messages = false"
db_cursor.execute(select_query)
records = db_cursor.fetchall()
reply_json = {}
if len(records) == 0:
reply_json["num_messages"] = "0" # 0 means no new messages for the username
reply_json["senders"] = ""
reply_json = json.dumps(reply_json)
return reply_json
last_date_conversations_fetched = str(records[0][0])
update_query = "UPDATE users SET notified_by_new_messages = true WHERE username = " + "'" + receiver_username + "'"
db_cursor.execute(update_query)
chat_db.commit()
select_query = "SELECT sender_username FROM messages WHERE receiver_username = " + "'" + receiver_username + "' AND receive_date > " + "'" + last_date_conversations_fetched + "'"
db_cursor.execute(select_query)
records = db_cursor.fetchall()
if len(records) == 0:
reply_json["num_messages"] = "0" # 0 means no new messages for the username
reply_json["senders"] = ""
reply_json = json.dumps(reply_json)
return reply_json
else:
select_query = "SELECT DISTINCT sender_username FROM messages WHERE receiver_username = " + "'" + receiver_username + "' and receive_date > " + "'" + last_date_conversations_fetched + "'"
db_cursor.execute(select_query)
records_usernames = db_cursor.fetchall()
reply_json["num_messages"] = str(len(records)) # returns the number of new messages sent to the username
if len(records_usernames) == 1:
reply_json["senders"] = records_usernames[0][0]
elif len(records_usernames) == 2:
reply_json["senders"] = records_usernames[0][0] + " and " + records_usernames[1][0]
else:
reply_json["senders"] = records_usernames[0][0] + ", " + records_usernames[1][0] + ", and " + str(len(records_usernames)-2) + " more"
reply_json = json.dumps(reply_json)
return reply_json
def encrypt_text(text_to_encrypt):
key_file1 = open("encryption_key1.key", "rb")
encryption_key1 = key_file1.read()
key_file1.close()
key_file2 = open("encryption_key2.key", "rb")
encryption_key2 = key_file2.read()
key_file2.close()
encoded_email = text_to_encrypt.encode()
f = cryptography.fernet.MultiFernet([cryptography.fernet.Fernet(encryption_key1), cryptography.fernet.Fernet(encryption_key2)])
encrypted_text = f.encrypt(encoded_email)
return encrypted_text
def decrypt_text(text_to_decrypt):
key_file1 = open("encryption_key1.key", "rb")
encryption_key1 = key_file1.read()
key_file1.close()
key_file2 = open("encryption_key2.key", "rb")
encryption_key2 = key_file2.read()
key_file2.close()
f = cryptography.fernet.MultiFernet([cryptography.fernet.Fernet(encryption_key1), cryptography.fernet.Fernet(encryption_key2)])
try:
decrypted_text = f.decrypt(text_to_decrypt.encode('utf-8'))
except:
return False # Email verification failed.
decoded_text = decrypted_text.decode()
return decoded_text
def send_verification_code(firstname, lastname, username, email):
encrypted_email = encrypt_text(email)
body = "Hi " + firstname + " " + lastname + ",nnThanks for registering for HiAi Chat System.nnYour username is " + username + ".nTo verify your account, just copy the verification code found below, return back to the Android app, paste the code, and finally click the Verify button.nnn" + encrypted_email.decode()
subject = "HiAi Verification"
send_email(subject, body, email)
def login(msg_received):
username = msg_received["username"]
password = msg_received["password"]
select_query = "SELECT first_name, last_name FROM users where username = " + "'" + username + "' and password = " + "MD5('" + password + "')"
db_cursor.execute(select_query)
records = db_cursor.fetchall()
if len(records) == 0:
return "failure"
else:
return "success"
def send_email(subject, body, email):
port = 465 # For SSL
# smtp_server = "smtp.gmail.com"
smtp_server = "hiai.website"
sender_email = "[email protected]" # Enter your address
# sender_email = "[email protected]" # Enter your address
password = "..."
msg_body = body
msg = MIMEText(msg_body, 'plain', 'utf-8')
# add in the actual person name to the message template
msg['From'] = sender_email
msg['To'] = email
msg['Subject'] = subject
context = ssl.create_default_context()
with smtplib.SMTP_SSL(smtp_server, port, context=context) as server:
server.login(sender_email, password)
server.sendmail(sender_email, email, msg.as_string())
try:
chat_db = mysql.connector.connect(host="localhost", user="root", passwd="ahmedgad", database="chat_db")
except:
sys.exit("Error connecting to the database. Please check your inputs.")
db_cursor = chat_db.cursor()
app.run(host="0.0.0.0", port=5000, debug=True, threaded=True)
The next section discusses the necessary changes for the Android app to actually implement our notification system.
Background Thread for Checking New Messages in the Android App
In the MainActivity of our project, we create a thread by implementing the Runnable interface. This thread will run in the background and get called every 1000 milliseconds (i.e. 1 second) according to the postDelayed() method.
This means the Android app will send a request to the server every 1 second to check for new messages. You can change this value to meet your needs. Note that a handler is created as a private class variable named handler.
private Handler handler;
class Task implements Runnable {
@Override
public void run() {
handler.post(new Runnable() {
@Override
public void run() {
NewMessagesBackgroundNotification newMessagesBackgroundNotification = new NewMessagesBackgroundNotification(); newMessagesBackgroundNotification.checkNewMessages(mainActivityContext);
handler.postDelayed(this, 1000);
}
});
}
}
Within the run() callback method, an instance from the NewMessagesBackgroundNotification class is created in which the code that sends a request to the server is found in the checkNewMessages() method. This method accepts the MainActivity context.
Before discussing the implementation of this class, let’s examine the necessary changes in the MainActivity. A method named setAlarmForNewMessages() is created, which is responsible for starting the thread.
The setAlarmForNewMessages() method is expected to be called inside the onCreate() method activate the thread. Note that the thread will work in the background as long as the application is not stopped by force:
The final change in the MainActivity is to build a method named showNotification() to push notifications to the status bar. It accepts the notification title and message as String arguments. Note that you have to assign a small icon to the notification builder using the setSmallIcon() method. So make sure you have an icon available in your project.
public void showNotification(final Context context, String message, String messageTitle) {
Uri uri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);
PendingIntent contentIntent = PendingIntent.getActivity(context, 0,
new Intent(context, MainActivity.class), 0);
String channelId = "MessageChannel";
CharSequence channelName = "New Message";
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
int importance = NotificationManager.IMPORTANCE_DEFAULT;
NotificationChannel notificationChannel = new NotificationChannel(channelId,
channelName,
importance);
final NotificationManager notificationManager = (NotificationManager) context.getSystemService(NOTIFICATION_SERVICE);
notificationManager.createNotificationChannel(notificationChannel);
final NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(context, channelId);
notificationBuilder.setContentTitle(messageTitle);
notificationBuilder.setContentText(message);
notificationBuilder.setSmallIcon(R.mipmap.ic_launcher);
notificationBuilder.setChannelId(channelId);
notificationBuilder.setAutoCancel(true);
notificationBuilder.setContentIntent(contentIntent);
runOnUiThread(new Runnable() {
@Override
public void run() {
notificationManager.notify(1, notificationBuilder.build());
}
});
} else {
final Notification.Builder notificationBuilder = new Notification.Builder(context);
notificationBuilder.setContentTitle(messageTitle);
notificationBuilder.setContentText(message);
notificationBuilder.setSmallIcon(R.mipmap.ic_launcher);
notificationBuilder.setAutoCancel(true);
notificationBuilder.setContentIntent(contentIntent);
final NotificationManager notificationManager = (NotificationManager) context.getSystemService(NOTIFICATION_SERVICE);
runOnUiThread(new Runnable() {
@Override
public void run() {
notificationManager.notify(1, notificationBuilder.build());
}
});
}
}
At this time, all work necessary in the MainActivity is done. Before discussing the implementation of the NewMessagesBackgroundNotification class, here’s the complete code for our MainActivity.
After the user is logged into the app, three text files are created:
- login_username.txt: Holds the username of the user.
- first_name.txt: User’s first name.
- last_name.txt: User’s last name.
When the user is logged out, these files are removed. Thus, as long as these files exist, this means the user is currently logged into the app.
package gad.hiai.chat.hiaichat;
import android.Manifest;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.media.RingtoneManager;
import android.net.Uri;
import android.os.Build;
import android.os.Handler;
import android.support.v4.app.ActivityCompat;
import android.support.v4.app.NotificationCompat;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
public class MainActivity extends AppCompatActivity {
public static String loginUserNameSuccessEncrypted = "";
public static String firstName = "";
public static String lastName = "";
public static Context mainActivityContext;
private Handler handler;
static String postUrl = "https://hiai.website/chat";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// ActivityCompat.requestPermissions(MainActivity.this, new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, 1);
ActivityCompat.requestPermissions(MainActivity.this, new String[]{Manifest.permission.INTERNET}, 2);
ActivityCompat.requestPermissions(MainActivity.this, new String[]{Manifest.permission.SET_ALARM}, 3);
mainActivityContext = this;
loginUserNameSuccessEncrypted = readTextFromFile(mainActivityContext, "login_username.txt"); // Save username in a text file in the internal storage.
firstName = readTextFromFile(mainActivityContext, "first_name.txt"); // Save first name in a text file in the internal storage.
lastName = readTextFromFile(mainActivityContext, "last_name.txt"); // Save last name in a text file in the internal storage.
TextView loginUsernameSuccess = findViewById(R.id.loginUsernameSuccess);
if (loginUserNameSuccessEncrypted.equals("")) {
loginUsernameSuccess.setText("No login yet.");
} else {
loginUsernameSuccess.setText("Welcome " + firstName + " " + lastName + " :)");
}
enableDisableButtons();
setAlarmForNewMessages();
}
public void setAlarmForNewMessages() {
Log.d("ALARM", "Alarm is activated to run in the background for checking for new messages.");
handler = new Handler();
new Thread(new Task()).start();
}
class Task implements Runnable {
@Override
public void run() {
handler.post(new Runnable() {
@Override
public void run() {
NewMessagesBackgroundNotification newMessagesBackgroundNotification = new NewMessagesBackgroundNotification();
newMessagesBackgroundNotification.checkNewMessages(mainActivityContext);
handler.postDelayed(this, 1000);
// handler.removeCallbacks(this); // Cancel the background alarm.
}
});
}
}
private void enableDisableButtons() {
Button registerButton = findViewById(R.id.register);
Button loginButton = findViewById(R.id.login);
Button logoutButton = findViewById(R.id.logout);
Button sendMessageButton = findViewById(R.id.sendMessage);
Button showConversationsButton = findViewById(R.id.receiveChat);
if (loginUserNameSuccessEncrypted.equals("")) {
registerButton.setEnabled(true);
loginButton.setEnabled(true);
logoutButton.setEnabled(false);
sendMessageButton.setEnabled(false);
showConversationsButton.setEnabled(false);
} else {
registerButton.setEnabled(false);
loginButton.setEnabled(false);
logoutButton.setEnabled(true);
sendMessageButton.setEnabled(true);
showConversationsButton.setEnabled(true);
}
}
@Override
public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) {
switch (requestCode) {
case 1: {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// Toast.makeText(getApplicationContext(), "Access to Storage Permission Granted. Thanks.", Toast.LENGTH_SHORT).show();
} else {
// Toast.makeText(getApplicationContext(), "Access to Storage Permission Denied.", Toast.LENGTH_SHORT).show();
}
return;
}
case 2: {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// Toast.makeText(getApplicationContext(), "Access to Internet Permission Granted. Thanks.", Toast.LENGTH_SHORT).show();
} else {
// Toast.makeText(getApplicationContext(), "Access to Internet Permission Denied.", Toast.LENGTH_SHORT).show();
}
return;
}
}
}
public void login(View v) {
Intent intent = new Intent(this, LoginActivity.class);
startActivityForResult(intent, 2);
}
private boolean deleteTextFile(String fileName) {
try {
File dir = this.getFilesDir();
File file = new File(dir, fileName);
file.delete();
Log.d("USERNAME", "File named " + fileName + " deleted successfully.");
return true;
} catch (Exception e) {
Log.d("USERNAME", "Cannot delete file named " + fileName);
return false;
}
}
public void logout(View v) {
TextView loginUsernameSuccess = findViewById(R.id.loginUsernameSuccess);
boolean userNameFileDeleted = deleteTextFile("login_username.txt");
boolean firstNameFileDeleted = deleteTextFile("first_name.txt");
boolean lastNameFileDeleted = deleteTextFile("last_name.txt");
if (userNameFileDeleted == true && firstNameFileDeleted == true && lastNameFileDeleted == true) {
Log.d("USERNAME", "Successful Logout.");
loginUserNameSuccessEncrypted = "";
firstName = "";
lastName = "";
loginUsernameSuccess.setText("No login yet.");
} else {
Log.d("USERNAME", "Logout Failed.");
loginUsernameSuccess.setText("Logout Failed. Please try again.");
}
enableDisableButtons();
}
public void register(View v) {
Intent intent = new Intent(this, RegisterActivity.class);
startActivityForResult(intent, 1);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
// check if the request code is same as what is passed here it is 2
TextView responseText = findViewById(R.id.responseText);
if (requestCode == 1 && data != null) { // Register
String registrationStatus = data.getStringExtra("status");
responseText.setText(registrationStatus);
} else if (requestCode == 2 && data != null) { // Login
loginUserNameSuccessEncrypted = data.getStringExtra("username_enc");
firstName = data.getStringExtra("firstname");
lastName = data.getStringExtra("lastname");
Toast.makeText(getApplicationContext(), "Welcome " + firstName + " " + lastName, Toast.LENGTH_LONG).show();
// String username = data.getStringExtra("username"); // Equals MainActivity.loginUserNameSuccess
TextView loginUsernameSuccess = findViewById(R.id.loginUsernameSuccess);
loginUsernameSuccess.setText("Welcome " + firstName + " " + lastName + " :)");
responseText.setText("Successful Login.");
saveTextToFile(loginUserNameSuccessEncrypted, "login_username.txt"); // Save username in a text file in the internal storage.
saveTextToFile(firstName, "first_name.txt"); // Save first name in a text file in the internal storage.
saveTextToFile(lastName, "last_name.txt"); // Save last name in a text file in the internal storage.
enableDisableButtons();
} else {
responseText.setText("Invalid or no data entered. Please try again.");
}
}
void saveTextToFile(String text, String fileName) {
try {
File usernameFile = new File(this.getFilesDir(), fileName);
FileWriter usernameFileWriter = new FileWriter(usernameFile);
usernameFileWriter.write(text);
usernameFileWriter.flush();
usernameFileWriter.close();
Log.d("USER", "File named " + fileName + " is created successfully.");
} catch (Exception e) {
Log.d("USER", "Error creating the file named " + fileName + " : " + e.getMessage());
}
}
String readTextFromFile(Context context, String fileName) {
Log.d("USER", "Inside readTextFromFile() for reading a text file named " + fileName);
String returnString = "";
try {
InputStream inputStream = context.openFileInput(fileName);
if (inputStream != null) {
InputStreamReader inputStreamReader = new InputStreamReader(inputStream);
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
String receiveString = "";
StringBuilder stringBuilder = new StringBuilder();
while ((receiveString = bufferedReader.readLine()) != null) {
stringBuilder.append(receiveString);
}
inputStream.close();
returnString = stringBuilder.toString();
Log.d("USER", "Data read from the text file named " + fileName + " inside is : " + returnString);
}
} catch (FileNotFoundException e) {
Log.e("USER", "No text file named " + fileName + " is found : " + e.toString());
} catch (IOException e) {
Log.e("USER", "Can not read the text file named " + fileName + " : " + e.toString());
}
return returnString;
}
public void receiveChat(View v) {
TextView responseText = findViewById(R.id.responseText);
if (loginUserNameSuccessEncrypted.equals("")) {
responseText.setText("Please login first.");
return;
}
responseText.setText("Fetching conversations. Please wait ...");
JSONObject messageContent = new JSONObject();
try {
messageContent.put("subject", "receive_chats");
messageContent.put("receiver_username", loginUserNameSuccessEncrypted);
} catch (JSONException e) {
e.printStackTrace();
}
RequestBody body = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), messageContent.toString());
postRequest(MainActivity.postUrl, body);
}
public void sendMessage(View v) {
TextView responseText = findViewById(R.id.responseText);
if (loginUserNameSuccessEncrypted.equals("")) {
responseText.setText("Please login first.");
return;
}
Intent intent = new Intent(this, SendMessageActivity.class);
startActivity(intent);
}
public void postRequest(String postUrl, RequestBody postBody) {
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
.url(postUrl)
.post(postBody)
.header("Accept", "application/json")
.header("Content-Type", "application/json")
.build();
client.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
// Cancel the post on failure.
call.cancel();
Log.d("FAIL", e.getMessage());
// In order to access the TextView inside the UI thread, the code is executed inside runOnUiThread()
runOnUiThread(new Runnable() {
@Override
public void run() {
TextView responseText = findViewById(R.id.responseText);
responseText.setText("Failed to Connect to Server. Please Try Again.");
}
});
}
@Override
public void onResponse(Call call, final Response response) {
// In order to access the TextView inside the UI thread, the code is executed inside runOnUiThread()
TextView responseText = findViewById(R.id.responseText);
try {
String responseString = response.body().string().trim();
if (responseString.equals("0")) {
responseText.setText("No conversations for " + firstName + " " + lastName); // A message indicating that no messages are delivered for the user.
return;
}
Intent showChatsIntent = new Intent(getApplicationContext(), ChatListViewActivity.class);
ArrayList<String> sendersUsernames = new ArrayList<>();
JSONObject messageContent = new JSONObject(responseString);
try {
for (int i = 0; i < messageContent.length(); i++) {
JSONObject currMessage = messageContent.getJSONObject(i + "");
String senderUsername = currMessage.getString("username");
sendersUsernames.add(senderUsername);
}
} catch (JSONException e) {
e.printStackTrace();
}
Log.d("CHATS", sendersUsernames.toString());
showChatsIntent.putExtra("receivedChats", sendersUsernames);
// showNotification(mainActivityContext, "You can see all your conversations", "Conversations Fetched");
responseText.setText("Conversations Fetched.");
startActivity(showChatsIntent);
} catch (Exception e) {
e.printStackTrace();
}
}
});
}
public void showNotification(final Context context, String message, String messageTitle) {
Uri uri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);
PendingIntent contentIntent = PendingIntent.getActivity(context, 0,
new Intent(context, MainActivity.class), 0);
String channelId = "MessageChannel";
CharSequence channelName = "New Message";
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
int importance = NotificationManager.IMPORTANCE_DEFAULT;
NotificationChannel notificationChannel = new NotificationChannel(channelId,
channelName,
importance);
final NotificationManager notificationManager = (NotificationManager) context.getSystemService(NOTIFICATION_SERVICE);
notificationManager.createNotificationChannel(notificationChannel);
final NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(context, channelId);
notificationBuilder.setContentTitle(messageTitle);
notificationBuilder.setContentText(message);
notificationBuilder.setSmallIcon(R.mipmap.ic_launcher);
notificationBuilder.setChannelId(channelId);
notificationBuilder.setAutoCancel(true);
notificationBuilder.setContentIntent(contentIntent);
runOnUiThread(new Runnable() {
@Override
public void run() {
notificationManager.notify(1, notificationBuilder.build());
}
});
} else {
final Notification.Builder notificationBuilder = new Notification.Builder(context);
notificationBuilder.setContentTitle(messageTitle);
notificationBuilder.setContentText(message);
notificationBuilder.setSmallIcon(R.mipmap.ic_launcher);
notificationBuilder.setAutoCancel(true);
notificationBuilder.setContentIntent(contentIntent);
final NotificationManager notificationManager = (NotificationManager) context.getSystemService(NOTIFICATION_SERVICE);
runOnUiThread(new Runnable() {
@Override
public void run() {
notificationManager.notify(1, notificationBuilder.build());
}
});
}
}
}
The implementation of the NewMessagesBackgroundNotification class is discussed in the next section.
NewMessagesBackgroundNotification Class
The implementation of the class is listed below. Simply, the checkNewMessages() method reads the text file named login_username.txt to return the username of the user. It then builds a request to the Flask server to check for new messages for that username. If the number of messages received from the server is 0, then no notification is created. Otherwise, a notification is created displaying the number of new messages, in addition to the username of the users that sent such messages.
Note that if the login_username.txt doesn’t exist, this means no user is logged into the app, and thus there’s no need to send a request to the server.
package gad.hiai.chat.hiaichat;
import android.content.Context;
import android.util.Log;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
public class NewMessagesBackgroundNotification {
MainActivity mainActivity = new MainActivity();
String readTextFromFile(Context context, String fileName) {
Log.d("ALARM", "Inside readTextFromFile() for reading a text file named " + fileName);
String returnString = "";
try {
InputStream inputStream = context.openFileInput(fileName);
if (inputStream != null) {
InputStreamReader inputStreamReader = new InputStreamReader(inputStream);
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
String receiveString = "";
StringBuilder stringBuilder = new StringBuilder();
while ((receiveString = bufferedReader.readLine()) != null) {
stringBuilder.append(receiveString);
}
inputStream.close();
returnString = stringBuilder.toString();
Log.d("ALARM", "Data read from the text file named " + fileName + " is : " + returnString);
}
} catch (FileNotFoundException e) {
Log.e("ALARM", "No text file named " + fileName + " is found : " + e.toString());
} catch (IOException e) {
Log.e("ALARM", "Can not read the text file named " + fileName + " : " + e.toString());
}
return returnString;
}
public void checkNewMessages(Context context) {
String username = readTextFromFile(context, "login_username.txt"); // Save username in a text file in the internal storage.
String firstName = readTextFromFile(context, "first_name.txt"); // Save first name in a text file in the internal storage.
String lastName = readTextFromFile(context, "last_name.txt"); // Save last name in a text file in the internal storage.
// Toast.makeText(context, username, Toast.LENGTH_SHORT).show();
if (username.equals("") || firstName.equals("") || lastName.equals("")) {
Log.d("ALARM", "No user is logged in.");
// Toast.makeText(context, "Please login first.", Toast.LENGTH_SHORT).show();
return;
}
Log.d("ALARM", "A user is logged in with first name " + firstName + " and last name " + lastName);
JSONObject messageContent = new JSONObject();
try {
messageContent.put("subject", "check_new_messages");
messageContent.put("receiver_username", username);
} catch (JSONException e) {
e.printStackTrace();
}
RequestBody body = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), messageContent.toString());
Log.d("ALARM", "Checking if there are new messages sent to user " + firstName + " " + lastName);
postRequest(MainActivity.postUrl, body, context, firstName, lastName);
}
public void postRequest(String postUrl, RequestBody postBody, final Context context, final String firstName, final String lastName) {
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
.url(postUrl)
.post(postBody)
.header("Accept", "application/json")
.header("Content-Type", "application/json")
.build();
client.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
// Cancel the post on failure.
call.cancel();
Log.d("ALARM", "Failed to Connect to Server. The device may not be connected to the Internet : " + e.getMessage());
}
@Override
public void onResponse(Call call, final Response response) {
// In order to access the TextView inside the UI thread, the code is executed inside runOnUiThread()
try {
String responseString = response.body().string();
Log.d("ALARM", "Response from the server : " + responseString);
JSONObject loginJSONObject = new JSONObject(responseString);
String numMessages = loginJSONObject.getString("num_messages");
if (numMessages.equals("0")) {
Log.d("ALARM", "There are no new messages for the user.");
// mainActivity.showNotification(context, "No new messages", "No New Messages");
return;
}
Log.d("ALARM", numMessages + " new message(s) are sent to the user from " + loginJSONObject.getString("senders"));
mainActivity.showNotification(context, numMessages + " new message(s) from " + loginJSONObject.getString("senders"), "New Messages");
} catch (Exception e) {
Log.d("ALARM", "Unexpected failure for checking for the new messages. Where are you app/Server developer (Ahmed Gad)?");
e.printStackTrace();
}
}
});
}
}
The next figure shows what it looks like when receiving a notification:
Conclusion
This tutorial allowed our Android chat application to send instant notifications for users currently logged into the application.
Firstly, the users table in the MySQL database was edited to add 2 columns (last_date_conversations_fetched and notified_by_new_messages) that help in identifying whether there’s a new message.
Secondly, we created a new function named check_new_messages() in the Flask server to return the number of newly-sent messages and the senders’ usernames.
Finally, we created a background thread in the Android app to continuously check for new messages. If a new message is available, then a notification is shown.
Currently, the chat isn’t updated automatically when a new message arrives. The user has to close the activity and open it again to fetch the messages. In the next tutorial, we’ll tackle this issue head on.
Comments 0 Responses