Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/prettier.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,5 +46,5 @@ jobs:
git config user.name "${{ github.actor }}"
git config user.email "${{ github.actor }}@users.noreply.github.com"
git add .
git commit -m "chore: format code and fix lint issues [skip ci]"
git commit -m "chore: format code and fix lint issues"
git push origin ${{ github.ref_name }}
Comment thread
diegolmello marked this conversation as resolved.
1 change: 1 addition & 0 deletions android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ dependencies {

implementation "com.google.code.gson:gson:2.8.9"
implementation "com.tencent:mmkv-static:1.2.10"
implementation "com.github.bumptech.glide:glide:${rootProject.ext.glideVersion}"
implementation 'com.facebook.soloader:soloader:0.10.4'

// For SecureKeystore (EncryptedSharedPreferences)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ private MMKV getMMKV() {
* Helper method to build avatar URI from avatar path.
* Validates server URL and credentials, then constructs the full URI.
*/
private String buildAvatarUri(String avatarPath, String errorContext) {
private String buildAvatarUri(String avatarPath, String errorContext, int sizePx) {
String server = serverURL();
if (server == null || server.isEmpty()) {
Log.w(TAG, "Cannot generate " + errorContext + " avatar URI: serverURL is null");
Expand All @@ -67,7 +67,7 @@ private String buildAvatarUri(String avatarPath, String errorContext) {
String userToken = token();
String uid = userId();

String finalUri = server + avatarPath + "?format=png&size=100";
String finalUri = server + avatarPath + "?format=png&size=" + sizePx;
if (!userToken.isEmpty() && !uid.isEmpty()) {
finalUri += "&rc_token=" + userToken + "&rc_uid=" + uid;
}
Expand Down Expand Up @@ -102,23 +102,45 @@ public String getAvatarUri() {
}
}

return buildAvatarUri(avatarPath, "");
return buildAvatarUri(avatarPath, "", 100);
}

/**
* Generates avatar URI for video conference caller.
* Factory for building caller avatar URIs from host + username (e.g. VoIP payload).
* Caller is package-private, so this is the only way to get avatar URI from outside the package.
*/
public static Ejson forCallerAvatar(String host, String username) {
if (host == null || host.isEmpty() || username == null || username.isEmpty()) {
return null;
}
Ejson ejson = new Ejson();
ejson.host = host;
ejson.caller = new Caller();
ejson.caller.username = username;
return ejson;
}

/**
* Generates avatar URI for video conference caller (default size 100).
* Returns null if caller username is not available (username is required for avatar endpoint).
*/
public String getCallerAvatarUri() {
// Check if caller exists and has username (required - /avatar/{userId} endpoint doesn't exist)
return getCallerAvatarUri(100);
}

/**
* Generates avatar URI for video conference caller with custom size.
* Returns null if caller username is not available.
*/
public String getCallerAvatarUri(int sizePx) {
if (caller == null || caller.username == null || caller.username.isEmpty()) {
Log.w(TAG, "Cannot generate caller avatar URI: caller or username is null");
return null;
}

try {
String avatarPath = "/avatar/" + URLEncoder.encode(caller.username, "UTF-8");
return buildAvatarUri(avatarPath, "caller");
return buildAvatarUri(avatarPath, "caller", sizePx);
} catch (UnsupportedEncodingException e) {
Log.e(TAG, "Failed to encode caller username", e);
return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ import com.google.gson.GsonBuilder
import chat.rocket.reactnative.voip.VoipNotification
import chat.rocket.reactnative.voip.VoipModule
import chat.rocket.reactnative.voip.VoipPayload
import android.os.Build
import android.app.KeyguardManager
import android.app.Activity

/**
* Handles notification Intent processing from MainActivity.
Expand Down Expand Up @@ -57,6 +60,15 @@ class NotificationIntentHandler {
VoipNotification.cancelById(context, voipPayload.notificationId)
VoipModule.storeInitialEvents(voipPayload)

if (context is Activity) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
context.setShowWhenLocked(true)
context.setTurnScreenOn(true)
val keyguardManager = context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
keyguardManager.requestDismissKeyguard(context, null)
}
}

// Clear the voip flag to prevent re-processing
intent.removeExtra("voipAction")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,25 @@ import android.app.Activity
import android.app.KeyguardManager
import android.content.Context
import android.content.Intent
import android.graphics.drawable.GradientDrawable
import android.media.Ringtone
import android.media.RingtoneManager
import android.os.Build
import android.os.Bundle
import android.view.WindowManager
import android.widget.ImageButton
import android.view.View
import android.widget.ImageView
import androidx.core.content.ContextCompat
import android.widget.LinearLayout
import android.widget.TextView
import android.widget.FrameLayout
import android.util.Log
import androidx.core.content.ContextCompat
import android.view.ViewOutlineProvider
import com.bumptech.glide.Glide
import chat.rocket.reactnative.MainActivity
import chat.rocket.reactnative.R
import chat.rocket.reactnative.notification.Ejson
import android.graphics.Typeface

/**
* Full-screen Activity displayed when an incoming VoIP call arrives.
Expand Down Expand Up @@ -56,6 +63,9 @@ class IncomingCallActivity : Activity() {
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)

setContentView(R.layout.activity_incoming_call)
applyNavigationBar()
applyButtonBackgrounds()
applyInterFont()

val voipPayload = VoipPayload.fromBundle(intent.extras)
if (voipPayload == null || !voipPayload.isVoipIncomingCall()) {
Expand All @@ -72,15 +82,123 @@ class IncomingCallActivity : Activity() {
setupButtons(voipPayload)
}

private fun applyNavigationBar() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) return
window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
val bgColor = ContextCompat.getColor(this, R.color.incoming_call_background)
window.navigationBarColor = bgColor
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val isDarkTheme = (resources.configuration.uiMode and android.content.res.Configuration.UI_MODE_NIGHT_MASK) ==
android.content.res.Configuration.UI_MODE_NIGHT_YES
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && !isDarkTheme) {
@Suppress("DEPRECATION")
window.decorView.systemUiVisibility = window.decorView.systemUiVisibility or View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
window.isNavigationBarContrastEnforced = false
}
}

/**
* Applies button background colors programmatically. Required on some devices (e.g. Samsung
* lock screen) where XML @color references may not resolve correctly in full-screen intent context.
*/
private fun applyButtonBackgrounds() {
val cornerRadiusPx = 8 * resources.displayMetrics.density
findViewById<FrameLayout>(R.id.btn_reject_bg)?.apply {
background = GradientDrawable().apply {
setColor(ContextCompat.getColor(this@IncomingCallActivity, R.color.incoming_call_reject_bg))
cornerRadius = cornerRadiusPx
}
}
findViewById<FrameLayout>(R.id.btn_accept_bg)?.apply {
background = GradientDrawable().apply {
setColor(ContextCompat.getColor(this@IncomingCallActivity, R.color.incoming_call_accept_bg))
cornerRadius = cornerRadiusPx
}
}
}

private fun applyInterFont() {
val interRegular = try {
Typeface.createFromAsset(assets, "fonts/Inter-Regular.otf")
} catch (e: Exception) {
Log.e(TAG, "Failed to load Inter-Regular font", e)
return
}
val interBold = try {
Typeface.createFromAsset(assets, "fonts/Inter-Bold.otf")
} catch (e: Exception) {
Log.e(TAG, "Failed to load Inter-Bold font", e)
interRegular
}
listOf(
R.id.header_text,
R.id.host_name,
R.id.incoming_call_reject_label,
R.id.incoming_call_accept_label
).forEach { id ->
findViewById<TextView>(id)?.setTypeface(interRegular)
}
findViewById<TextView>(R.id.caller_name)?.setTypeface(interBold)
}

private fun updateUI(payload: VoipPayload) {
val callerView = findViewById<TextView>(R.id.caller_name)
callerView?.text = payload.caller

// Try to load avatar if available
// TODO: needs username to load avatar
val avatarView = findViewById<ImageView>(R.id.caller_avatar)
// Avatar loading would require additional data - can be enhanced later
// For now, just show a placeholder or default icon
findViewById<TextView>(R.id.caller_name)?.text = payload.caller.ifEmpty { getString(R.string.incoming_call_unknown_caller) }
findViewById<TextView>(R.id.host_name)?.text = payload.hostName.ifEmpty { getString(R.string.incoming_call_unknown_host) }

loadAvatar(payload)
}

private fun loadAvatar(payload: VoipPayload) {
if (payload.host.isBlank() || payload.username.isBlank()) return

val container = findViewById<FrameLayout>(R.id.avatar_container)
val imageView = findViewById<ImageView>(R.id.avatar)
val sizePx = (120 * resources.displayMetrics.density).toInt().coerceIn(120, 480)
val avatarUrl = Ejson.forCallerAvatar(payload.host, payload.username)?.getCallerAvatarUri(sizePx)
?: return
val cornerRadiusPx = (8 * resources.displayMetrics.density).toFloat()

Glide.with(this)
.load(avatarUrl)
.into(object : com.bumptech.glide.request.target.CustomTarget<android.graphics.drawable.Drawable>(sizePx, sizePx) {
override fun onResourceReady(
resource: android.graphics.drawable.Drawable,
transition: com.bumptech.glide.request.transition.Transition<in android.graphics.drawable.Drawable>?
) {
container.visibility = View.VISIBLE
imageView.setImageDrawable(resource)
applyAvatarRoundCorners(imageView, cornerRadiusPx)
}

override fun onLoadFailed(errorDrawable: android.graphics.drawable.Drawable?) {
container.visibility = View.GONE
}

override fun onLoadCleared(placeholder: android.graphics.drawable.Drawable?) {
container.visibility = View.GONE
}
})
}

/**
* Applies rounded corners via view-level clipping.
* Works for both PNG (BitmapDrawable) and SVG (vector/PictureDrawable) since
* Glide's RoundedCorners bitmap transform only applies to bitmaps.
*/
private fun applyAvatarRoundCorners(imageView: ImageView, cornerRadiusPx: Float) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) return
imageView.post {
val radius = cornerRadiusPx
imageView.outlineProvider = object : ViewOutlineProvider() {
override fun getOutline(view: View, outline: android.graphics.Outline) {
outline.setRoundRect(0, 0, view.width, view.height, radius)
}
}
imageView.clipToOutline = true
}
}

private fun startRingtone() {
Expand All @@ -105,14 +223,11 @@ class IncomingCallActivity : Activity() {
}

private fun setupButtons(payload: VoipPayload) {
val acceptButton = findViewById<ImageButton>(R.id.btn_accept)
val declineButton = findViewById<ImageButton>(R.id.btn_decline)

acceptButton?.setOnClickListener {
findViewById<LinearLayout>(R.id.btn_accept)?.setOnClickListener {
handleAccept(payload)
}

declineButton?.setOnClickListener {
findViewById<LinearLayout>(R.id.btn_decline)?.setOnClickListener {
handleDecline(payload)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,25 @@ import com.google.gson.annotations.SerializedName
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.WritableMap
import chat.rocket.reactnative.utils.CallIdUUID
import android.util.Log

data class VoipPayload(
@SerializedName("callId")
val callId: String,

@SerializedName("caller")
val caller: String,

@SerializedName("username")
val username: String,

@SerializedName("host")
val host: String,

@SerializedName("type")
val type: String
val type: String,

@SerializedName("hostName")
val hostName: String,
) {
val notificationId: Int = callId.hashCode()
val callUUID: String = CallIdUUID.generateUUIDv5(callId)
Expand All @@ -31,8 +36,10 @@ data class VoipPayload(
return Bundle().apply {
putString("callId", callId)
putString("caller", caller)
putString("username", username)
putString("host", host)
putString("type", type)
putString("hostName", hostName)
putString("callUUID", callUUID)
putInt("notificationId", notificationId)
// Useful flag for MainActivity to know it's handling a VoIP action
Expand All @@ -44,33 +51,38 @@ data class VoipPayload(
return Arguments.createMap().apply {
putString("callId", callId)
putString("caller", caller)
putString("username", username)
putString("host", host)
putString("type", type)
putString("hostName", hostName)
putString("callUUID", callUUID)
putInt("notificationId", notificationId)
}
}

companion object {
fun fromMap(data: Map<String, String>): VoipPayload? {
Log.d("RocketChat.VoipPayload", "Parsing VoIP payload from map: $data")
val type = data["type"] ?: return null
val callId = data["callId"] ?: return null
val caller = data["caller"] ?: return null
val username = data["username"] ?: return null
val host = data["host"] ?: return null
val hostName = data["hostName"] ?: return null
if (type != "incoming_call") return null

return VoipPayload(callId, caller, host, type)
return VoipPayload(callId, caller, username, host, type, hostName)
}

fun fromBundle(bundle: Bundle?): VoipPayload? {
if (bundle == null) return null
val callId = bundle.getString("callId") ?: return null
val caller = bundle.getString("caller") ?: ""
val host = bundle.getString("host") ?: ""
val type = bundle.getString("type") ?: ""
val caller = bundle.getString("caller") ?: return null
val username = bundle.getString("username") ?: return null
val host = bundle.getString("host") ?: return null
val type = bundle.getString("type") ?: return null
val hostName = bundle.getString("hostName") ?: return null

return VoipPayload(callId, caller, host, type)
return VoipPayload(callId, caller, username, host, type, hostName)
}
}
}
6 changes: 6 additions & 0 deletions android/app/src/main/res/drawable/bg_avatar_incoming_call.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/incoming_call_avatar_bg"/>
<corners android:radius="8dp"/>
</shape>
6 changes: 6 additions & 0 deletions android/app/src/main/res/drawable/bg_btn_accept.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/incoming_call_accept_bg"/>
<corners android:radius="8dp"/>
</shape>
Loading
Loading