多个通知如何在不混淆彼此的情况下记住它们 onClick 所需的数据?

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

我有一个 SMS 服务,我的应用可以在其中响应特定的 SMS 消息。 在可以向托管该应用程序的 phone 号码进行短信查询之前,该应用程序会尝试验证用户。 该过程应通过在第一次 SMS 时显示通知提示操作来完成 从一个新号码收到。通知提供两个选项,批准或拒绝。 批准或拒绝在默认共享首选项中保存为布尔值 以发件人的 phone 号码作为密钥。

至少这是它应该做的。

当我用来实现上述目标的三个 类 相互作用时,我遇到了一些奇怪的行为。 它们是 SMSReceiver、NotificationUtils 和 SMSAuthReceiver。

SMSReceiver 解析传入的 SMS 消息并做出反应。如果它检测到来自新用户的授权请求, 它创建了一个 NotificationUtils 的实例,并使用 showNotification 方法来显示通知。 showNotification 需要一个 Context 对象和一个 String 命名发件人,以保存传入请求的 phone 号码。 通知提供拒绝意图和批准意图,由 SMSAuthReceiver 处理。 无论请求被批准还是拒绝,共享偏好都会相应更新,请参见下面的代码。

问题行为发生如下: 安装应用程序后,新用户第一次通过短信联系时,身份验证过程会顺利进行。 但是,所有连续的身份验证请求都在 SMSAuthReceiver 阶段失败。它总是依赖于包含在 安装应用程序时触发的第一个通知意图。

我已经尝试随机化频道 ID 和通知 ID,希望它们能被单独处理,但显然,我错过了一些事情。

如何在对以下代码进行最少更改的情况下实现所需的行为???

来自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);
    }

}

NotificationUtils.java 生成的日志始终输出 "sender" 的当前 phone 编号,而 SMSAuthReceiver.java 生成的日志始终反映第一个 phone该应用程序经过测试。 为什么...?

感谢@MikeM。谁告诉我的。

这里的问题是一对 PendingIntent 对象,它们被用来将操作传递给通知。他们的构造函数的第二个参数接受一个唯一的 ID,可以用来识别 PendingIntent 的特定实例。在我的例子中,ID 始终是 0,因此导致在每个通知中重复使用相同的实例。

我使用的解决方案是应用为通知 ID 生成的随机数作为 PendingIntent 的第二个参数,如下所示:

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

而不是我使用的:

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

我希望这对遇到与 PendingIntent 类似问题的任何人有所帮助。

(我确实有很小的机会为两个单独的实例生成相同的随机数,所以也许使用计数器或类似的方法会是更好的解决方案)。