Skip to content

Commit 3507d16

Browse files
authored
fix(android): Play Store mic discoverability, safer FCM logs, avatar auth via headers (#7171)
1 parent eefbb9b commit 3507d16

7 files changed

Lines changed: 58 additions & 41 deletions

File tree

android/app/src/main/AndroidManifest.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
2424
<uses-permission android:name="android.permission.MANAGE_OWN_CALLS" />
2525
<uses-feature android:name="android.hardware.audio.output" />
26-
<uses-feature android:name="android.hardware.microphone" />
26+
<uses-feature android:name="android.hardware.microphone" android:required="false" />
2727

2828
<!-- android 13 notifications -->
2929
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />

android/app/src/main/java/chat/rocket/reactnative/notification/CustomPushNotification.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -404,8 +404,8 @@ private void cancelPreviousFallbackNotifications(Ejson ejson) {
404404
}
405405
}
406406

407-
private Bitmap getAvatar(String uri) {
408-
return NotificationHelper.fetchAvatarBitmap(mContext, uri, largeIcon());
407+
private Bitmap getAvatar(String uri, Ejson ejson) {
408+
return NotificationHelper.fetchAvatarBitmap(mContext, uri, ejson, largeIcon());
409409
}
410410

411411
private Bitmap largeIcon() {
@@ -426,7 +426,7 @@ private void notificationIcons(Notification.Builder notification, Bundle bundle)
426426
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
427427
String avatarUri = ejson != null ? ejson.getAvatarUri() : null;
428428
if (avatarUri != null) {
429-
Bitmap avatar = getAvatar(avatarUri);
429+
Bitmap avatar = getAvatar(avatarUri, ejson);
430430
if (avatar != null) {
431431
notification.setLargeIcon(avatar);
432432
}
@@ -506,7 +506,7 @@ private void notificationStyle(Notification.Builder notification, int notId, Bun
506506
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
507507
messageStyle.addMessage(m, timestamp, displaySenderName);
508508
} else {
509-
Bitmap avatar = getAvatar(avatarUri);
509+
Bitmap avatar = getAvatar(avatarUri, ejson);
510510
Person.Builder senderBuilder = new Person.Builder()
511511
.setKey(senderId)
512512
.setName(displaySenderName);

android/app/src/main/java/chat/rocket/reactnative/notification/Ejson.java

Lines changed: 5 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -53,26 +53,13 @@ private MMKV getMMKV() {
5353
return MMKV.mmkvWithID("default", MMKV.SINGLE_PROCESS_MODE);
5454
}
5555

56-
/**
57-
* Helper method to build avatar URI from avatar path.
58-
* Validates server URL and credentials, then constructs the full URI.
59-
*/
60-
private String buildAvatarUri(String avatarPath, String errorContext, int sizePx) {
56+
private String buildAvatarUri(String avatarPath, int sizePx) {
6157
String server = serverURL();
6258
if (server == null || server.isEmpty()) {
63-
Log.w(TAG, "Cannot generate " + errorContext + " avatar URI: serverURL is null");
59+
Log.w(TAG, "Cannot generate avatar URI: serverURL is null");
6460
return null;
6561
}
66-
67-
String userToken = token();
68-
String uid = userId();
69-
70-
String finalUri = server + avatarPath + "?format=png&size=" + sizePx;
71-
if (!userToken.isEmpty() && !uid.isEmpty()) {
72-
finalUri += "&rc_token=" + userToken + "&rc_uid=" + uid;
73-
}
74-
75-
return finalUri;
62+
return server + avatarPath + "?format=png&size=" + sizePx;
7663
}
7764

7865
public String getAvatarUri() {
@@ -102,7 +89,7 @@ public String getAvatarUri() {
10289
}
10390
}
10491

105-
return buildAvatarUri(avatarPath, "", 100);
92+
return buildAvatarUri(avatarPath, 100);
10693
}
10794

10895
/**
@@ -140,7 +127,7 @@ public String getCallerAvatarUri(int sizePx) {
140127

141128
try {
142129
String avatarPath = "/avatar/" + URLEncoder.encode(caller.username, "UTF-8");
143-
return buildAvatarUri(avatarPath, "caller", sizePx);
130+
return buildAvatarUri(avatarPath, sizePx);
144131
} catch (UnsupportedEncodingException e) {
145132
Log.e(TAG, "Failed to encode caller username", e);
146133
return null;

android/app/src/main/java/chat/rocket/reactnative/notification/NotificationHelper.java

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
import androidx.annotation.Nullable;
88

99
import com.bumptech.glide.Glide;
10+
import com.bumptech.glide.load.model.GlideUrl;
11+
import com.bumptech.glide.load.model.LazyHeaders;
1012
import com.bumptech.glide.load.resource.bitmap.RoundedCorners;
1113
import com.bumptech.glide.request.RequestOptions;
1214

@@ -51,26 +53,48 @@ public static String getUserAgent() {
5153
}
5254

5355
/**
54-
* Fetches avatar bitmap from URI using Glide.
55-
* Uses a 3-second timeout to avoid blocking the FCM service for too long.
56-
*
57-
* @param context The application context
58-
* @param uri The avatar URI to fetch
59-
* @param fallbackIcon Optional fallback bitmap (null if no fallback desired)
60-
* @return Avatar bitmap, or fallbackIcon if fetch fails, or null if no fallback provided
56+
* Build a Glide load model for an avatar URL, sending rc_token/rc_uid as HTTP request
57+
* headers via GlideUrl + LazyHeaders. The JS codebase appends these as query params
58+
* (getAvatarUrl.ts) for convenience; native uses headers because Glide supports it cleanly.
6159
*/
62-
public static Bitmap fetchAvatarBitmap(Context context, String uri, @Nullable Bitmap fallbackIcon) {
60+
public static Object avatarLoadModel(String uri, @Nullable Ejson ejson) {
61+
if (uri == null || uri.isEmpty()) {
62+
return uri;
63+
}
64+
String rcToken = ejson != null ? ejson.token() : "";
65+
String rcUid = ejson != null ? ejson.userId() : "";
66+
if (rcToken.isEmpty() || rcUid.isEmpty()) {
67+
return uri;
68+
}
69+
LazyHeaders headers = new LazyHeaders.Builder()
70+
.addHeader("rc_token", rcToken)
71+
.addHeader("rc_uid", rcUid)
72+
.build();
73+
return new GlideUrl(uri, headers);
74+
}
75+
76+
/**
77+
* Fetches avatar bitmap with a 3-second timeout so we don't block the FCM service
78+
* past its 10-second lifetime. Returns fallbackIcon on failure.
79+
*/
80+
public static Bitmap fetchAvatarBitmap(
81+
Context context,
82+
String uri,
83+
@Nullable Ejson ejson,
84+
@Nullable Bitmap fallbackIcon) {
6385
if (uri == null || uri.isEmpty()) {
6486
return fallbackIcon;
6587
}
66-
88+
89+
Object loadModel = avatarLoadModel(uri, ejson);
90+
6791
try {
6892
// Use a 3-second timeout to avoid blocking the FCM service for too long
6993
// FCM has a 10-second limit, so we need to fail fast and use fallback icon
7094
Bitmap avatar = Glide.with(context)
7195
.asBitmap()
7296
.apply(RequestOptions.bitmapTransform(new RoundedCorners(10)))
73-
.load(uri)
97+
.load(loadModel)
7498
.submit(100, 100)
7599
.get(3, TimeUnit.SECONDS);
76100

android/app/src/main/java/chat/rocket/reactnative/notification/RCFirebaseMessagingService.kt

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import android.os.Bundle
44
import android.util.Log
55
import com.google.firebase.messaging.FirebaseMessagingService
66
import com.google.firebase.messaging.RemoteMessage
7+
import chat.rocket.reactnative.BuildConfig
78
import chat.rocket.reactnative.voip.VoipNotification
89
import chat.rocket.reactnative.voip.VoipPayload
910

@@ -21,8 +22,11 @@ class RCFirebaseMessagingService : FirebaseMessagingService() {
2122
}
2223

2324
override fun onMessageReceived(remoteMessage: RemoteMessage) {
24-
// TODO: remove data
25-
Log.d(TAG, "FCM message received from: ${remoteMessage.from} data: ${remoteMessage.data}")
25+
if (BuildConfig.DEBUG) {
26+
Log.d(TAG, "FCM message received from: ${remoteMessage.from} data: ${remoteMessage.data}")
27+
} else {
28+
Log.d(TAG, "FCM message received from: ${remoteMessage.from}")
29+
}
2630

2731
val data = remoteMessage.data
2832
if (data.isEmpty()) {

android/app/src/main/java/chat/rocket/reactnative/notification/VideoConfNotification.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ class VideoConfNotification(private val context: Context) {
159159
// Fetch caller avatar
160160
val avatarUri = ejson.getCallerAvatarUri()
161161
val avatarBitmap = if (avatarUri != null) {
162-
getAvatar(avatarUri)
162+
getAvatar(avatarUri, ejson)
163163
} else {
164164
null
165165
}
@@ -212,8 +212,8 @@ class VideoConfNotification(private val context: Context) {
212212
* Fetches avatar bitmap from URI using Glide.
213213
* Returns null if fetch fails or times out, in which case notification will display without avatar.
214214
*/
215-
private fun getAvatar(uri: String): Bitmap? {
216-
return NotificationHelper.fetchAvatarBitmap(context, uri, null)
215+
private fun getAvatar(uri: String, ejson: Ejson): Bitmap? {
216+
return NotificationHelper.fetchAvatarBitmap(context, uri, ejson, null)
217217
}
218218

219219
/**

android/app/src/main/java/chat/rocket/reactnative/voip/IncomingCallActivity.kt

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import com.bumptech.glide.Glide
2525
import chat.rocket.reactnative.R
2626
import android.graphics.Typeface
2727
import chat.rocket.reactnative.notification.Ejson
28+
import chat.rocket.reactnative.notification.NotificationHelper
2829

2930
/**
3031
* Full-screen Activity displayed when an incoming VoIP call arrives.
@@ -179,12 +180,13 @@ class IncomingCallActivity : Activity() {
179180
val container = findViewById<FrameLayout>(R.id.avatar_container)
180181
val imageView = findViewById<ImageView>(R.id.avatar)
181182
val sizePx = (120 * resources.displayMetrics.density).toInt().coerceIn(120, 480)
182-
val avatarUrl = Ejson.forCallerAvatar(payload.host, payload.username)?.getCallerAvatarUri(sizePx)
183-
?: return
183+
val ejson = Ejson.forCallerAvatar(payload.host, payload.username) ?: return
184+
val avatarUrl = ejson.getCallerAvatarUri(sizePx) ?: return
185+
val loadModel = NotificationHelper.avatarLoadModel(avatarUrl, ejson)
184186
val cornerRadiusPx = (8 * resources.displayMetrics.density).toFloat()
185187

186188
Glide.with(this)
187-
.load(avatarUrl)
189+
.load(loadModel)
188190
.into(object : com.bumptech.glide.request.target.CustomTarget<android.graphics.drawable.Drawable>(sizePx, sizePx) {
189191
override fun onResourceReady(
190192
resource: android.graphics.drawable.Drawable,

0 commit comments

Comments
 (0)