From 2144dc3b675bf6925cb9cab167b8963cc0e74bc5 Mon Sep 17 00:00:00 2001 From: Cody Henthorne Date: Fri, 26 Mar 2021 12:58:17 -0400 Subject: [PATCH] Fix call ringtone not playing on some custom ROMs and Samsung Android 11 devices. --- .../NotificationsPreferenceFragment.java | 5 +- .../CustomNotificationsDialogFragment.java | 7 +- .../securesms/util/RingtoneUtil.java | 121 ++++++++++++++++++ .../webrtc/audio/IncomingRinger.java | 67 ++++++---- app/src/main/res/values/strings.xml | 1 + 5 files changed, 176 insertions(+), 25 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/util/RingtoneUtil.java diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/NotificationsPreferenceFragment.java b/app/src/main/java/org/thoughtcrime/securesms/preferences/NotificationsPreferenceFragment.java index 23709a5666..2a63a896b8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/NotificationsPreferenceFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/NotificationsPreferenceFragment.java @@ -20,6 +20,7 @@ import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.components.SwitchPreferenceCompat; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.notifications.NotificationChannels; +import org.thoughtcrime.securesms.util.RingtoneUtil; import org.thoughtcrime.securesms.util.TextSecurePreferences; import static android.app.Activity.RESULT_OK; @@ -165,10 +166,12 @@ public class NotificationsPreferenceFragment extends ListSummaryPreferenceFragme if (value == null || TextUtils.isEmpty(value.toString())) { preference.setSummary(R.string.preferences__silent); } else { - Ringtone tone = RingtoneManager.getRingtone(getActivity(), value); + Ringtone tone = RingtoneUtil.getRingtone(requireContext(), value); if (tone != null) { preference.setSummary(tone.getTitle(getActivity())); + } else { + preference.setSummary(R.string.preferences__default); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/notifications/CustomNotificationsDialogFragment.java b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/notifications/CustomNotificationsDialogFragment.java index 32363acf57..206721ca54 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/notifications/CustomNotificationsDialogFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/notifications/CustomNotificationsDialogFragment.java @@ -24,16 +24,20 @@ import androidx.lifecycle.ViewModelProviders; import com.annimon.stream.function.Consumer; +import org.signal.core.util.logging.Log; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.database.RecipientDatabase; import org.thoughtcrime.securesms.notifications.NotificationChannels; import org.thoughtcrime.securesms.recipients.RecipientId; +import org.thoughtcrime.securesms.util.RingtoneUtil; import org.thoughtcrime.securesms.util.TextSecurePreferences; import java.util.Objects; public class CustomNotificationsDialogFragment extends DialogFragment { + private static final String TAG = Log.tag(CustomNotificationsDialogFragment.class); + private static final short MESSAGE_RINGTONE_PICKER_REQUEST_CODE = 13562; private static final short CALL_RINGTONE_PICKER_REQUEST_CODE = 23621; @@ -224,8 +228,7 @@ public class CustomNotificationsDialogFragment extends DialogFragment { } else if (ringtone.toString().isEmpty()) { return context.getString(R.string.preferences__silent); } else { - Ringtone tone = RingtoneManager.getRingtone(getActivity(), ringtone); - + Ringtone tone = RingtoneUtil.getRingtone(requireContext(), ringtone); if (tone != null) { return tone.getTitle(context); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/RingtoneUtil.java b/app/src/main/java/org/thoughtcrime/securesms/util/RingtoneUtil.java new file mode 100644 index 0000000000..0df1ad0242 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/util/RingtoneUtil.java @@ -0,0 +1,121 @@ +package org.thoughtcrime.securesms.util; + +import android.annotation.SuppressLint; +import android.content.ContentResolver; +import android.content.Context; +import android.media.Ringtone; +import android.media.RingtoneManager; +import android.net.Uri; +import android.provider.Settings; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.signal.core.util.logging.Log; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +/** + * Some custom ROMs and some Samsung Android 11 devices have quirks around accessing the default ringtone. This attempts to deal + * with them with progressively worse approaches. + */ +public final class RingtoneUtil { + + private static final String TAG = Log.tag(RingtoneUtil.class); + + private RingtoneUtil() {} + + public static @Nullable Ringtone getRingtone(@NonNull Context context, @NonNull Uri uri) { + Ringtone tone; + try { + tone = RingtoneManager.getRingtone(context, uri); + } catch (SecurityException e) { + Log.w(TAG, "Unable to get default ringtone due to permission", e); + tone = RingtoneManager.getRingtone(context, RingtoneUtil.getActualDefaultRingtoneUri(context)); + } + return tone; + } + + public static @Nullable Uri getActualDefaultRingtoneUri(@NonNull Context context) { + Log.i(TAG, "Attempting to get default ringtone directly via normal way"); + try { + return RingtoneManager.getActualDefaultRingtoneUri(context, RingtoneManager.TYPE_RINGTONE); + } catch (SecurityException e) { + Log.w(TAG, "Failed to set ringtone with first fallback approach", e); + } + + Log.i(TAG, "Attempting to get default ringtone directly via reflection"); + String uriString = getStringForUser(context.getContentResolver(), getUserId(context)); + Uri ringtoneUri = uriString != null ? Uri.parse(uriString) : null; + + if (ringtoneUri != null && getUserIdFromAuthority(ringtoneUri.getAuthority(), getUserId(context)) == getUserId(context)) { + ringtoneUri = getUriWithoutUserId(ringtoneUri); + } + + return ringtoneUri; + } + + @SuppressWarnings("JavaReflectionMemberAccess") + @SuppressLint("DiscouragedPrivateApi") + private static @Nullable String getStringForUser(@NonNull ContentResolver resolver, int userHandle) { + try { + Method getStringForUser = Settings.System.class.getMethod("getStringForUser", ContentResolver.class, String.class, int.class); + return (String) getStringForUser.invoke(Settings.System.class, resolver, Settings.System.RINGTONE, userHandle); + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { + Log.w(TAG, "Unable to getStringForUser via reflection", e); + } + return null; + } + + @SuppressWarnings("JavaReflectionMemberAccess") + @SuppressLint("DiscouragedPrivateApi") + private static int getUserId(@NonNull Context context) { + try { + Object userId = Context.class.getMethod("getUserId").invoke(context); + if (userId instanceof Integer) { + return (Integer) userId; + } else { + Log.w(TAG, "getUserId did not return an integer"); + } + } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + Log.w(TAG, "Unable to getUserId via reflection", e); + } + return 0; + } + + private static @Nullable Uri getUriWithoutUserId(@Nullable Uri uri) { + if (uri == null) { + return null; + } + Uri.Builder builder = uri.buildUpon(); + builder.authority(getAuthorityWithoutUserId(uri.getAuthority())); + return builder.build(); + } + + private static @Nullable String getAuthorityWithoutUserId(@Nullable String auth) { + if (auth == null) { + return null; + } + int end = auth.lastIndexOf('@'); + return auth.substring(end + 1); + } + + private static int getUserIdFromAuthority(@Nullable String authority, int defaultUserId) { + if (authority == null) { + return defaultUserId; + } + + int end = authority.lastIndexOf('@'); + if (end == -1) { + return defaultUserId; + } + + String userIdString = authority.substring(0, end); + try { + return Integer.parseInt(userIdString); + } catch (NumberFormatException e) { + return defaultUserId; + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/webrtc/audio/IncomingRinger.java b/app/src/main/java/org/thoughtcrime/securesms/webrtc/audio/IncomingRinger.java index 57cd938f07..e9ba762af3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/webrtc/audio/IncomingRinger.java +++ b/app/src/main/java/org/thoughtcrime/securesms/webrtc/audio/IncomingRinger.java @@ -1,7 +1,6 @@ package org.thoughtcrime.securesms.webrtc.audio; -import android.annotation.TargetApi; import android.content.Context; import android.media.AudioAttributes; import android.media.AudioManager; @@ -9,11 +8,13 @@ import android.media.MediaPlayer; import android.net.Uri; import android.os.Build; import android.os.Vibrator; +import android.provider.Settings; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import org.signal.core.util.logging.Log; +import org.thoughtcrime.securesms.util.RingtoneUtil; import org.thoughtcrime.securesms.util.ServiceUtil; import java.io.IOException; @@ -24,7 +25,7 @@ public class IncomingRinger { private static final long[] VIBRATE_PATTERN = {0, 1000, 1000}; - private final Context context; + private final Context context; private final Vibrator vibrator; private MediaPlayer player; @@ -37,8 +38,13 @@ public class IncomingRinger { public void start(@Nullable Uri uri, boolean vibrate) { AudioManager audioManager = ServiceUtil.getAudioManager(context); - if (player != null) player.release(); - if (uri != null) player = createPlayer(uri); + if (player != null) { + player.release(); + } + + if (uri != null) { + player = createPlayer(uri); + } int ringerMode = audioManager.getRingerMode(); @@ -61,7 +67,7 @@ public class IncomingRinger { player = null; } } else { - Log.w(TAG, "Not ringing, mode: " + ringerMode); + Log.w(TAG, "Not ringing, player: " + (player != null ? "available" : "null") + " mode: " + ringerMode); } } @@ -81,11 +87,6 @@ public class IncomingRinger { return true; } - return shouldVibrateNew(context, ringerMode, vibrate); - } - - @TargetApi(Build.VERSION_CODES.HONEYCOMB) - private boolean shouldVibrateNew(Context context, int ringerMode, boolean vibrate) { Vibrator vibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE); if (vibrator == null || !vibrator.hasVibrator()) { @@ -99,26 +100,24 @@ public class IncomingRinger { } } - private boolean shouldVibrateOld(Context context, boolean vibrate) { - AudioManager audioManager = ServiceUtil.getAudioManager(context); - return vibrate && audioManager.shouldVibrate(AudioManager.VIBRATE_TYPE_RINGER); - } - - private MediaPlayer createPlayer(@NonNull Uri ringtoneUri) { + private @Nullable MediaPlayer createPlayer(@NonNull Uri ringtoneUri) { try { - MediaPlayer mediaPlayer = new MediaPlayer(); + MediaPlayer mediaPlayer = safeCreatePlayer(ringtoneUri); + + if (mediaPlayer == null) { + Log.w(TAG, "Failed to create player for incoming call ringer due to custom rom most likely"); + return null; + } mediaPlayer.setOnErrorListener(new MediaPlayerErrorListener()); - mediaPlayer.setDataSource(context, ringtoneUri); mediaPlayer.setLooping(true); if (Build.VERSION.SDK_INT <= 21) { mediaPlayer.setAudioStreamType(AudioManager.STREAM_RING); } else { - mediaPlayer.setAudioAttributes(new AudioAttributes.Builder() - .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH) - .setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION_SIGNALLING) - .build()); + mediaPlayer.setAudioAttributes(new AudioAttributes.Builder().setContentType(AudioAttributes.CONTENT_TYPE_SPEECH) + .setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION_SIGNALLING) + .build()); } return mediaPlayer; @@ -128,6 +127,30 @@ public class IncomingRinger { } } + private @Nullable MediaPlayer safeCreatePlayer(@NonNull Uri ringtoneUri) throws IOException { + try { + MediaPlayer mediaPlayer = new MediaPlayer(); + mediaPlayer.setDataSource(context, ringtoneUri); + return mediaPlayer; + } catch (SecurityException e) { + Log.w(TAG, "Failed to create player with ringtone the normal way", e); + } + + if (ringtoneUri.equals(Settings.System.DEFAULT_RINGTONE_URI)) { + try { + Uri defaultRingtoneUri = RingtoneUtil.getActualDefaultRingtoneUri(context); + if (defaultRingtoneUri != null) { + MediaPlayer mediaPlayer = new MediaPlayer(); + mediaPlayer.setDataSource(context, defaultRingtoneUri); + return mediaPlayer; + } + } catch (SecurityException e) { + Log.w(TAG, "Failed to set default ringtone with fallback approach", e); + } + } + + return null; + } private class MediaPlayerErrorListener implements MediaPlayer.OnErrorListener { @Override diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1318a5c504..9020c8e5ca 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -2211,6 +2211,7 @@ LED blink pattern Sound Silent + Default Repeat alerts Never One time