Search code examples
javaandroidandroid-notificationsandroid-pendingintent

How can multiple notifications remember data they need for onClick without confusing each other?


I have an SMS service in which my app responds to specific SMS messages. Before SMS queries can be made to the phone number hosting the app, the app attempts to verify the user. The process should be accomplished by displaying a notification prompting action the first time an SMS is received from a new number. The notification provides two options, approve or deny. The approval or denial is save as a boolean in default shared preferences with the sender's phone number as the key.

At least that is what it's supposed to do.

I'm getting stuck with some wierd behavior when the three classes I'm using to acheive the above interact. They are SMSReceiver, NotificationUtils, and SMSAuthReceiver.

SMSReceiver parses and reacts to incoming SMS messages. If it detects an Authorization request from a new user, it creates an instance of NotificationUtils, and uses the showNotification method to display a notification. showNotification takes a Context object and a String named sender, to hold the phone number of the incoming request. The notification provides a deny intent and an approve intent, which are handled by SMSAuthReceiver. Whether request is approved or denied, shared preferences are to be accordingly updated, see code below.

The problematic behavior occurs as follows: After the app is installed, the first time a new user contacts via SMS, the authentication process runs smoothly. However, all successive auth requests fail at the SMSAuthReceiver stage. It always falls back on the data contained in the first notification intent that fired from when the app was installed.

I've tried randomizing the channel ID and notification ID in hopes that they'de be treated seperately, but obviously, I'm missing the boat on something.

How can I acheive the desired behavior with minimal change to the code below???

Relevant lines from SMSReceiver.java:

if (
    (StringUtils.equalsIgnoreCase(message,"myapp sign up")) ||
    (StringUtils.equalsIgnoreCase(message,"myapp signup"))  ||
    (StringUtils.equalsIgnoreCase(message,"myapp start"))
){
    NotificationUtils notificationUtils = new NotificationUtils();
    notificationUtils.showNotification(context,sender); //Problems start here...
    SendSMS(context.getString(R.string.sms_auth_pending));
}

NotificationUtils.java:

package com.myapp.name;

import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.util.Log;

import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;

import org.apache.commons.lang3.RandomStringUtils;

public class NotificationUtils {
    private static final String TAG = NotificationUtils.class.getSimpleName();

    private int notificationID;
    private String channelID;

    public void hideNotification(Context context, int notificationId){
        try {
            NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
            if (notificationId == 0) {
                notificationManager.cancelAll();
            } else {
                notificationManager.cancel(notificationId);
            }
        }catch (Exception ignore){}
    }

    public void showNotification(Context context,String sender){
        createNotificationChannel(context);
        notificationID = getRandomID();
        channelID = String.valueOf(getRandomID());
        Log.d(TAG, "showNotification: Notification ID: "+notificationID);
        Log.d(TAG, "showNotification:      Channel ID: "+channelID);
        Log.d(TAG, "showNotification:          Sender: "+sender);

        Intent approveAuth = new Intent(context, SMSAuthReceiver.class);
        approveAuth.setAction("org.myapp.name.APPROVE_AUTH");
        approveAuth.putExtra("sender",sender);
        approveAuth.putExtra("notification_id",notificationID);
        PendingIntent approveAuthP =
                PendingIntent.getBroadcast(context, 0, approveAuth, 0);

        Intent denyAuth = new Intent(context, SMSAuthReceiver.class);
        denyAuth.setAction("org.myapp.name.DENY_AUTH");
        denyAuth.putExtra("sender",sender);
        denyAuth.putExtra("notification_id",notificationID);
        PendingIntent denyAuthP =
                PendingIntent.getBroadcast(context, 0, denyAuth, 0);

        NotificationCompat.Builder builder = new NotificationCompat.Builder(context, channelID)
                .setSmallIcon(R.drawable.ic_lock_open)
                .setContentTitle(context.getResources().getString(R.string.app_name))
                .setContentText(sender+" "+context.getString(R.string.sms_noti_request))
                .setPriority(NotificationCompat.PRIORITY_HIGH)
                .setContentIntent(approveAuthP)
                .addAction(R.drawable.ic_lock_open, context.getString(R.string.approve), approveAuthP)
                .addAction(R.drawable.ic_lock_close, context.getString(R.string.deny), denyAuthP);

        NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
        notificationManager.notify(notificationID, builder.build());
    }

    private int getRandomID(){
        return Integer.parseInt(
                RandomStringUtils.random(
                        8,
                        '1', '2', '3', '4', '5', '6', '7', '8', '9')
        );
    }

    private void createNotificationChannel(Context context) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            CharSequence name = "myapp Authorization Channel";
            String description = "myapp SMS Service Authorizations";
            int importance = NotificationManager.IMPORTANCE_HIGH;
            NotificationChannel channel = new NotificationChannel(channelID, name, importance);
            channel.setDescription(description);
            NotificationManager notificationManager = context.getSystemService(NotificationManager.class);
            try {
                assert notificationManager != null;
                notificationManager.createNotificationChannel(channel);
            } catch (NullPointerException ex) {
                Log.e(TAG,ex.getMessage(),ex);
            }

        }

    }
}

SMSAuthReceiver.java

package com.myapp.name;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.telephony.SmsManager;
import android.util.Log;
import android.widget.Toast;

import androidx.preference.PreferenceManager;

import java.util.Objects;

public class SMSAuthReceiver extends BroadcastReceiver {
    private static final String TAG = SMSAuthReceiver.class.getSimpleName();

    @Override
    public void onReceive(Context context, Intent intent) {

        try {
            String sender = intent.getStringExtra("sender");
            int id = intent.getIntExtra("notification_id",0);

            /*Todo: bug! for some reason, data is always read from first intent, even if if
            * more request come in. this causes the approval feature to add the same guy a bunch
            * of times, and to mishandle dismissing the notification. (the purpose of this question...)*/

            Log.d(TAG, "onReceive: Sender: "+sender);

            NotificationUtils notificationUtils = new NotificationUtils();
            notificationUtils.hideNotification(context,id);

            SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(context);
            switch (Objects.requireNonNull(intent.getAction())) {
                case "org.myapp.name.APPROVE_AUTH":
                    sharedPrefs.edit().putBoolean(sender,true).apply();
                    Toast.makeText(context, sender+" Approved!", Toast.LENGTH_SHORT).show();
                    SendSMS(context.getString(R.string.sms_invitation),sender);
                    break;
                case "org.myapp.name.DENY_AUTH":
                    sharedPrefs.edit().putBoolean(sender,false).apply();
                    Toast.makeText(context, sender+" Denied!", Toast.LENGTH_SHORT).show();
                    SendSMS(context.getString(R.string.denied),sender);
                    break;
            }
        }catch (Exception e){
            Log.e("SMSAuthReceiver", "onReceive: Error committing sender to preferences! ", e);
        }
    }

    void SendSMS(String smsBody, String phone_number){
        SmsManager manager = SmsManager.getDefault();
        manager.sendTextMessage(phone_number,null,smsBody,null,null);
    }

}

The logging generated by NotificationUtils.java always outputs the current phone number for "sender", whereas that logging generated by SMSAuthReceiver.java always reflects the first phone that the app was tested with. Why...?


Solution

  • Thanks to @MikeM. who clued me in.

    The problem here is the pair of PendingIntent objects which were being used to pass an action to the notification. The second param of their constructor accepts a unique ID that can be used to identify the specific instance of PendingIntent. In my case, the ID was always 0, thus resulting in the same instance being reused in each notification.

    The solution that I used was to apply the random number generated for the notification ID as the second param for the PendingIntent like this:

    PendingIntent approveAuthP =
                    PendingIntent.getBroadcast(context, notificationID, approveAuth, 0);
    

    Instead of what I was using:

    PendingIntent approveAuthP =
                    PendingIntent.getBroadcast(context, 0, approveAuth, 0);
    

    I hope this helps anyone who is experiencing similar trouble with PendingIntent.

    (I do have a really tiny chance of generating the same random for two separate instances, so maybe using a counter or such would be a better solution).