无法以编程方式为 Android Oreo 上的通知频道更新声音

Can't update sound programmatically for Notification Channel on Android Oreo

我在将频道的通知声音更新为 Android Oreo 时遇到了一些问题。我知道用户可以通过打开应用程序通知屏幕手动设置声音,但我想通过在默认设置 activity 中使用 RingtonePreference 以编程方式执行此操作(用户能够从 activity 在我的应用程序中)。

问题是应用程序发出的第一个通知从 PreferenceManager.getDefaultSharedPreferences() 中获取默认声音值,在手动将其更改为其他媒体(使用 RingtonePreference 屏幕)后,它仍会播放原来的声音最初是在该频道上创建的,而不是用户选择的新频道。

我不明白为什么 NotificationChannel 声音没有根据新的声音值更新,因为我正在做这样的事情 NotificationChannel mChannel = new NotificationChannel(id, title, importance); mChannel.setSound(ringtoneUri, audioAttributes);

完整代码如下:

 public static void sendNotification(Context context, NotificationType notificationType) {
        String id = "channel_1"; // default_channel_id
        String title = "Doppler Channel"; // Default Channel
        Intent intent;
        PendingIntent pendingIntent;
        NotificationCompat.Builder builder;

        SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
        Uri ringtoneUri = Uri.parse(preferences.getString("notifications_new_message_ringtone", "DEFAULT_RINGTONE_URI"));
        boolean isNotificationSticky = !Boolean.parseBoolean(preferences.getAll().get("stickyNotification").toString());

        AudioAttributes audioAttributes = new AudioAttributes.Builder()
                .setUsage(AudioAttributes.USAGE_NOTIFICATION)
                .build();

        NotificationManager notifManager = (NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE);

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {

            int importance = NotificationManager.IMPORTANCE_DEFAULT;

            NotificationChannel mChannel = notifManager.getNotificationChannel(id);

                mChannel = new NotificationChannel(id, title, importance);
                mChannel.setSound(ringtoneUri, audioAttributes);
                notifManager.createNotificationChannel(mChannel);

            builder = new NotificationCompat.Builder(context, id);
            intent = new Intent(context, MainActivity.class);
            intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
            pendingIntent = PendingIntent.getActivity(context, 0, intent, 0);
            builder .setSmallIcon(notificationType.getNotificationIcon())
                    .setContentTitle(notificationType.getNotificationContent())
                    .setSubText(notificationType.getNotificationTitle())
                    .setOnlyAlertOnce(true)
                    .setOngoing(isNotificationSticky)
                    .setAutoCancel(true)
                    .setSound(ringtoneUri)
                    .setColor(notificationType.getNotificationColor())
                    .setContentIntent(pendingIntent);
        }
Notification notification = builder.build();
notifManager.notify(1, notification);

}

我能够更新声音的唯一方法是在每次触发通知时给频道 ID 一个随机值 UUID.randomUUID().toString(),但这会在用户手动检查应用程序通知时造成大量垃圾屏幕。

对此的提示将不胜感激。

非常感谢!

I don't understand why the NotificationChannel sound is not updated according with the new sound value

在大多数情况下,NotificationChannel 是一次写入 API。创建后,您无法修改其大部分特性。

but I want to do this programmatically by using RingtonePreference into a default Settings activity

我建议从您的应用中删除此功能,或仅在 Android 7.1 及更早版本的设备上提供此功能。

有一种方法可以 "trick" Android 允许这样做。如果您使用 WhatsApp 应用程序,您会发现它们允许您从应用程序内更改消息通知的声音。如果您稍微调查一下,您会发现他们实际上删除并创建了频道。

但是,如果您删除并重新创建 相同的 频道(换句话说,"new" 频道与 "old" 频道具有相同的频道 ID ) 那么您的新更改将不适用。 Android 显然会跟踪所有已删除的内容,并防止在重新创建时更新这些内容。因此,假设您拥有自己的频道(并可以访问您应用中的各个字段)。如果您希望用户更新频道的声音,您需要做的是:

  • 删除旧频道
  • 使用不同的频道 ID 创建一个新频道,但新的更新声音和所有其他用户可见字段与刚刚删除的频道相同。

因此,如果我们假设您有两个定义了 "Base ID":s 的频道,我们将基于它们的所有频道 ID。然后,每当您的应用程序启动时,您都可以检查通道是否已经存在,如果存在,则检索它们,否则创建它们。当您添加更多频道时,它需要一些人工劳动,也许有更好的方法来做到这一点。但是,如果您这样做,您可以为每个更改创建一个新频道并为其提供一个新的递增 ID。下面是一个 class 的完整工作示例,它负责所有这些工作。在这个例子中,我们有两个通道及其 baseId,它们用声音 #1 和声音 #2 初始化,但每次我们实例化 class 我们更新第二个通道并给它声音 #3.

public class TestNotificationHandler {

    private static TestNotificationHandler instance;
    private NotificationManager mNotificationManager;
    private Context mContext;

    private Uri NOTIFICATION_SOUND_1, NOTIFICATION_SOUND_2, NOTIFICATION_SOUND_3, NOTIFICATION_SOUND_4;

    private static final String TAG = TestNotificationHandler.class.getSimpleName();

    public static TestNotificationHandler getInstance(Context context) {
        if (instance == null) {
            instance = new TestNotificationHandler(context);
        }
        return instance;
    }

    private TestNotificationHandler(Context context) {
        mContext = context;
        mNotificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);

        NOTIFICATION_SOUND_1 = Uri.parse("android.resource://" + mContext.getPackageName() + "/" + R.raw.sound_1);
        NOTIFICATION_SOUND_2 = Uri.parse("android.resource://" + mContext.getPackageName() + "/" + R.raw.sound_2);
        NOTIFICATION_SOUND_3 = Uri.parse("android.resource://" + mContext.getPackageName() + "/" + R.raw.sound_3);
        NOTIFICATION_SOUND_4 = Uri.parse("android.resource://" + mContext.getPackageName() + "/" + R.raw.sound_4);

        getOrCreateChannels();

        // Only added here for testing, this should of course be done when user actually performs a change from within the app
        setNewChannelSound(testChannel2, NOTIFICATION_SOUND_3);

       // Remember that we now effectively have deleted testChannel2, so running this will not work, b/c no channel with the id that the testChannel2 object has currently exists so we cannot delete it
        setNewChannelSound(testChannel2, NOTIFICATION_SOUND_4);

      // The easy and ugly way would be to simply do this which will update our objects
       getOrCreateChannels();

      // And this will now work
      setNewChannelSound(testChannel2, NOTIFICATION_SOUND_4);


      // If we changed so that setNewChannelSound and updateChannel actually returned the updated 
      // channel (or the already existing one if could not update), then we could do something like: 
      // testChannel2 = setNewChannelSound(testChannel2, NOTIFICATION_SOUND_3);
    }


    AudioAttributes mAudioAttributes = new AudioAttributes.Builder()
            .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
            .setUsage(AudioAttributes.USAGE_ALARM)
            .build();

    private NotificationChannel testChannel;
    private String baseTestChannelId = "TEST_CHANNEL_ID-";

    private NotificationChannel testChannel2;
    private String baseTest2ChannelId = "TEST_CHANNEL_2_ID-";

    private void getOrCreateChannels() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            for (NotificationChannel channel : mNotificationManager.getNotificationChannels()) {
                // Since we'll incrementally update the ID we'll look for the correct channel with startsWith
                if (channel.getId().startsWith(baseTestChannelId)) {
                    testChannel = channel;
                } else if (channel.getId().startsWith(baseTest2ChannelId)) {
                    testChannel2 = channel;
                }
            }
            if (testChannel == null) {
                // This should only happen the first time the app is launched or if you just added this channel to your app
                // We'll update the ID incrementally so if it doesn't exist will simply create it as number 0

                testChannel = new NotificationChannel(baseTestChannelId + "0", "TEST CHANNEL", NotificationManager.IMPORTANCE_HIGH);
                testChannel.setDescription("First test channel");
                testChannel.setSound(NOTIFICATION_SOUND_1, mAudioAttributes);
                mNotificationManager.createNotificationChannel(testChannel);
            }
            if (testChannel2 == null) {
                // This should only happen the first time the app is launched or if you just added this channel to your app
                // We'll update the ID incrementally so if it doesn't exist will simply create it as number 0
                testChannel2 = new NotificationChannel(baseTest2ChannelId + "0", "TEST CHANNEL 2", NotificationManager.IMPORTANCE_HIGH);
                testChannel2.setDescription("Second test channel");
                testChannel2.setSound(NOTIFICATION_SOUND_2, mAudioAttributes);
                mNotificationManager.createNotificationChannel(testChannel2);
            } 
        }
    }

    private boolean setNewChannelSound(NotificationChannel notificationChannel, Uri newSound) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            notificationChannel.setSound(newSound, mAudioAttributes);
            return updateChannel(notificationChannel);
        }
        return false;
    }


    private boolean updateChannel(NotificationChannel notificationChannel) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            String baseChannelId = getBaseChannelId(notificationChannel);
            if (baseChannelId == null) {
                Log.e(TAG, "Could not find baseChannelId from id: " + notificationChannel.getId());
                return false;
            }
            int oldIndex = getChannelIncrementedIndex(notificationChannel, baseChannelId);
            if (oldIndex == -1) {
                Log.e(TAG, String.format("Could not get old channel index from id: %s and baseChannelId: %d", notificationChannel.getId(), baseChannelId));
                return false;
            }
            NotificationChannel updatedChannel = new NotificationChannel(baseChannelId+(oldIndex+1), notificationChannel.getName(), NotificationManager.IMPORTANCE_HIGH);
            updatedChannel.setDescription(notificationChannel.getDescription());
            updatedChannel.setVibrationPattern(notificationChannel.getVibrationPattern());
            updatedChannel.setSound(notificationChannel.getSound(), mAudioAttributes);
            mNotificationManager.deleteNotificationChannel(notificationChannel.getId());
            mNotificationManager.createNotificationChannel(updatedChannel);
            return true;
        }
        return false;
    }

    /**********************************************************
     Some helper methods
     **********************************************************/
    private String getBaseChannelId(NotificationChannel notificationChannel) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            if (notificationChannel.getId().startsWith(baseTestChannelId)) {
                return baseTestChannelId;
            } else if (notificationChannel.getId().startsWith(baseTest2ChannelId)) {
                return baseTest2ChannelId;
            }
        }
        return null;
    }

    private int getChannelIncrementedIndex(NotificationChannel channel, String baseChannelId) {
        int index = -1;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            String channelIncIdx = channel.getId().substring(baseChannelId.length(), channel.getId().length());

            try {
                index = Integer.parseInt(channelIncIdx);
            } catch (Exception e) {
                Log.e(TAG, String.format("Could not parse channel index %s", channelIncIdx));
            }
        }
        return index;
    }
}

我想说唯一的缺点是:

  • 您实际上必须添加这个额外的逻辑来维护您的频道(而不是简单地创建它们并让用户在 OS 中进行更改)。
  • 仅适用于 >= OREO 的版本。所以 运行 这个例子在旧设备上不会完成任何事情。所以我们需要添加功能来处理这个问题。在那种情况下,我们需要将我们的 "updated" 通知类型存储在首选项或数据库中,以便使用与我们添加的默认内容不同的内容通知 OS。
  • Android 将在 App 通知设置中显示一个计数器,通知用户频道被删除的次数。我会说大多数用户永远不会关心这个,甚至不会看到它,但可能会有一些用户对此感到恼火。