Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
9f024c7
feat(floating-video): add system-wide mini-player draft
aliIsazadeh Sep 26, 2025
a111b5e
Merge branch 'element-hq:develop' into minimized-video
aliIsazadeh Sep 27, 2025
16db3ab
fixed the bug of video being full size at first
aliIsazadeh Sep 27, 2025
bbc5458
Merge remote-tracking branch 'origin/minimized-video' into minimized-…
aliIsazadeh Sep 27, 2025
0f3fec9
Merge branch 'develop' into minimized-video
aliIsazadeh Sep 30, 2025
fe18ef7
-remove unused setMinimize
aliIsazadeh Sep 30, 2025
49a765d
-removed added icons and string and using existing icons and strings
aliIsazadeh Sep 30, 2025
daf7461
-adjusting maximize button size
aliIsazadeh Sep 30, 2025
46d226b
Merge branch 'element-hq:develop' into minimized-video
aliIsazadeh Sep 30, 2025
d07dc1e
-remove extra service declaration of service in app manifest
aliIsazadeh Sep 30, 2025
b99e67f
Merge remote-tracking branch 'origin/minimized-video' into minimized-…
aliIsazadeh Sep 30, 2025
2f9f56d
-remove extra import
aliIsazadeh Sep 30, 2025
7c13c1f
-remove extra string file
aliIsazadeh Oct 2, 2025
b9f415f
refactor: Convert floating video overlay to Compose with direct view …
aliIsazadeh Oct 4, 2025
a90c19a
Merge branch 'develop' into minimized-video
aliIsazadeh Oct 6, 2025
6a3cb61
-revert changes in gradle.properties and project
aliIsazadeh Oct 11, 2025
f91d167
- Broke down large FloatingVideoService into multiple smaller files f…
aliIsazadeh Oct 12, 2025
1466e74
Merge remote-tracking branch 'origin/minimized-video' into minimized-…
aliIsazadeh Oct 12, 2025
b33720c
fix(video): stabilize floating video behavior
aliIsazadeh Oct 13, 2025
d4228a8
-Converted VideoDataRepository to @SingleIn(AppScope::class) with @In…
aliIsazadeh Oct 16, 2025
19defb1
-on video completes the minimized video gose away
aliIsazadeh Oct 17, 2025
2cf92d5
-Toast for display over apps persmission(string needs to be added to …
aliIsazadeh Oct 17, 2025
44105d2
Merge branch 'element-hq:develop' into minimized-video
aliIsazadeh Oct 17, 2025
1978751
Merge branch 'develop' into minimized-video
aliIsazadeh Oct 20, 2025
e75cf85
-get url functions
aliIsazadeh Nov 15, 2025
eb0e785
changes on floating video overlay and floating service based on the c…
aliIsazadeh Nov 15, 2025
7432367
changes on floating video overlay and floating service based on the c…
aliIsazadeh Nov 15, 2025
d39251e
clearing maximize and minimize flows of the overlay video
aliIsazadeh Nov 16, 2025
0e5d9cd
remove video model strore owner from the service and also move the vi…
aliIsazadeh Nov 16, 2025
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 .idea/codeStyles/Project.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@

<!-- To be able to install APK from the application -->
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />

<application
android:name=".ElementXApplication"
android:allowBackup="false"
Expand Down
19 changes: 19 additions & 0 deletions libraries/mediaviewer/impl/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright 2025 New Vector Ltd.
~
~ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
~ Please see LICENSE files in the repository root for full details.
-->

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="io.element.android.libraries.mediaviewer.impl">
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />

<application>
<service
android:name=".floatingvideo.FloatingVideoService"
android:enabled="true"
android:exported="false" />
</application>
</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,280 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/

package io.element.android.libraries.mediaviewer.impl.floatingvideo

import android.annotation.SuppressLint
import android.app.Service
import android.content.Context
import android.content.Intent
import android.graphics.PixelFormat
import android.os.Build
import android.os.IBinder
import android.provider.Settings
import android.view.Gravity
import android.view.View
import android.view.WindowManager
import android.widget.Toast
import android.widget.VideoView
import androidx.compose.ui.platform.ComposeView
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LifecycleRegistry
import androidx.lifecycle.setViewTreeLifecycleOwner
import io.element.android.libraries.mediaviewer.impl.viewer.MediaViewerPageData
import timber.log.Timber
import androidx.savedstate.SavedStateRegistry
import androidx.savedstate.SavedStateRegistryController
import androidx.savedstate.SavedStateRegistryOwner
import androidx.savedstate.setViewTreeSavedStateRegistryOwner
import androidx.core.net.toUri
import io.element.android.libraries.mediaviewer.impl.floatingvideo.ui.FloatingVideoOverlay
import io.element.android.libraries.mediaviewer.impl.floatingvideo.util.getScreenHeight
import dev.zacsweers.metro.Inject
import io.element.android.libraries.architecture.bindings
import io.element.android.libraries.mediaviewer.impl.floatingvideo.util.getUri
import io.element.android.libraries.mediaviewer.impl.floatingvideo.util.maximizeWindowHelper
import io.element.android.libraries.mediaviewer.impl.floatingvideo.util.minimizeWindowHelper
import io.element.android.libraries.mediaviewer.impl.floatingvideo.util.movePosition
import io.element.android.libraries.mediaviewer.impl.floatingvideo.util.updateWindowSize

class FloatingVideoService : Service(), LifecycleOwner, SavedStateRegistryOwner {
private var windowManager: WindowManager? = null
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
private var windowManager: WindowManager? = null
private val windowManager = getSystemService<WindowManager>()!!

The code will be simplified by this change, and I believe that the system always provides the WindowManager service.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for your recomendation but it raised and error while creating the service
java.lang.RuntimeException: Unable to create service io.element.android.libraries.mediaviewer.impl.floatingvideo.FloatingVideoService: java.lang.NullPointerException: Attempt to invoke virtual method 'java.lang.String android.content.Context.getSystemServiceName(java.lang.Class)' on a null object reference

private var floatingView: View? = null
private var videoView: VideoView? = null
private var currentVideoData: MediaViewerPageData.MediaViewerData? = null
private var currentPosition: Long = 0L
private var isMinimized = true

private lateinit var windowLayoutParams: WindowManager.LayoutParams
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's possible to remove lateinit, change to val and initialize the value here.


companion object {
const val ACTION_START_FLOATING = "START_FLOATING"
const val ACTION_STOP_FLOATING = "STOP_FLOATING"
const val ACTION_UPDATE_POSITION = "UPDATE_POSITION"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It appears to be dead code, can you remove please?

const val EXTRA_VIDEO_ID = "video_id"
const val EXTRA_POSITION = "position"

@SuppressLint("ObsoleteSdkInt")
fun startFloating(
context: Context, videoData: MediaViewerPageData.MediaViewerData, position: Long = 0L
) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !Settings.canDrawOverlays(context)) {

//the message needs to be added into commonStrings as notice for permission needed
Toast.makeText(context, "To show the floating video, please allow 'Display over other apps' permission.", Toast.LENGTH_LONG).show()

// Request overlay permission
val intent = Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION).apply {
data = "package:${context.packageName}".toUri()
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}


context.startActivity(intent)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code should be moved to SystemUtils.kt and the exception for ActivityNotFoundException should be caught.

return
}


// Store the video data in repository via DI
val videoId = context.bindings<FloatingVideoServiceBindings>().videoDataRepository().storeVideoData(videoData)

val intent = Intent(context, FloatingVideoService::class.java).apply {
action = ACTION_START_FLOATING
putExtra(EXTRA_VIDEO_ID, videoId) // Pass only the ID, not the whole object
putExtra(EXTRA_POSITION, position)
}
context.startService(intent)
}
}

@Inject lateinit var videoDataRepository: VideoDataRepository

override fun onBind(intent: Intent?): IBinder? = null

private val lifecycleRegistry = LifecycleRegistry(this)
private val savedStateRegistryController = SavedStateRegistryController.create(this)
override val lifecycle: Lifecycle
get() = lifecycleRegistry

override val savedStateRegistry: SavedStateRegistry
get() = savedStateRegistryController.savedStateRegistry

override fun onCreate() {
super.onCreate()
bindings<FloatingVideoServiceBindings>().inject(this)
windowManager = getSystemService(WINDOW_SERVICE) as WindowManager
// 1. Attach controller
savedStateRegistryController.performAttach()

// 2. Restore state (if any)
savedStateRegistryController.performRestore(null)

// 3. Now move lifecycle forward
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
}

private var currentVideoId: String? = null
private var eventId: String? = null
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not used, remove?


override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
when (intent?.action) {
ACTION_START_FLOATING -> {
val videoId = intent.getStringExtra(EXTRA_VIDEO_ID)
val position = intent.getLongExtra(EXTRA_POSITION, 0L)

if (videoId != null) {
// Get video data from repository using the ID
val videoData = videoDataRepository.getVideoData(videoId)
if (videoData != null) {
eventId = videoData.eventId?.value ?: ""
currentVideoData = videoData
currentVideoId = videoId
currentPosition = position
createFloatingView()
}
}
}

ACTION_STOP_FLOATING -> {
// Clean up stored data
currentVideoId?.let { videoId ->
videoDataRepository.removeVideoData(videoId)
}
removeFloatingView()
stopSelf()
}

ACTION_UPDATE_POSITION -> {
val position = intent.getLongExtra(EXTRA_POSITION, 0L)
currentPosition = position
videoView?.seekTo(position.toInt())
}
}
return START_STICKY
}

private fun createFloatingView() {
removeFloatingView()
windowManager = getSystemService(WINDOW_SERVICE) as WindowManager

windowLayoutParams = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
WindowManager.LayoutParams(
WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY,
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN or WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED,
PixelFormat.TRANSLUCENT
)
} else {
@Suppress("DEPRECATION")
WindowManager.LayoutParams(
WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.TYPE_PHONE,
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN or WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED,
PixelFormat.TRANSLUCENT
)
}

windowLayoutParams.gravity = Gravity.TOP or Gravity.START
windowLayoutParams.x = 0
windowLayoutParams.y = windowManager.getScreenHeight() - dpToPx(300)

val composeView = ComposeView(this).apply {
setViewTreeLifecycleOwner(this@FloatingVideoService)
setViewTreeSavedStateRegistryOwner(this@FloatingVideoService)
setContent {
FloatingVideoOverlay(
onClose = {
removeFloatingView()
stopSelf()
},
onToggleFullScreen = {aspectRatio ->
if (isMinimized) {
maximizeWindow(aspectRatio)
} else {
minimizeWindow(aspectRatio)
}
},
onCompleted = {
removeFloatingView()
stopSelf()
},
updateAspectRatio = {
updateWindowSize(
aspectRatio = it,
isMinimized = isMinimized,
floatingView = floatingView,
windowManager = windowManager,
windowLayoutParams = windowLayoutParams
)
},
uri = currentVideoData.getUri(),
movePosition = { x, y ->
movePosition(x = x, y = y, windowLayoutParams = windowLayoutParams, floatingView = floatingView, windowManager = windowManager)
}
)
}
}


floatingView = composeView


try {
windowManager?.addView(floatingView, windowLayoutParams)
} catch (e: Exception) {
Timber.tag("FloatingVideoService").e(e, "Error adding floating view")
}
}

private fun removeFloatingView() {
floatingView?.let { view ->
try {
windowManager?.removeView(view)
} catch (e: Exception) {
Timber.tag("FloatingVideoService").e(e, "Error removing floating view")
}
floatingView = null
videoView = null
}
}

override fun onDestroy() {
super.onDestroy()
onVideoComplete()
}

private fun dpToPx(dp: Int): Int {
return (dp * resources.displayMetrics.density).toInt()
}

private fun onVideoComplete() {
removeFloatingView()
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
}

private fun minimizeWindow(aspectRatio: Float) {
isMinimized = true
minimizeWindowHelper(
aspectRatio = aspectRatio,
windowManager = windowManager,
windowLayoutParams = windowLayoutParams,
floatingView = floatingView
)
}
private fun maximizeWindow(aspectRatio: Float) {
isMinimized = false
maximizeWindowHelper(
aspectRatio = aspectRatio,
windowManager = windowManager,
windowLayoutParams = windowLayoutParams,
floatingView = floatingView
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/

package io.element.android.libraries.mediaviewer.impl.floatingvideo

import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesTo

@ContributesTo(AppScope::class)
interface FloatingVideoServiceBindings {
fun inject(service: FloatingVideoService)
fun videoDataRepository(): VideoDataRepository
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fun can be removed if you handle my other remarks.

}


Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/

package io.element.android.libraries.mediaviewer.impl.floatingvideo

import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.SingleIn
import io.element.android.libraries.mediaviewer.impl.viewer.MediaViewerPageData
import java.util.UUID

@SingleIn(AppScope::class)
@Inject
class VideoDataRepository {

private val videoDataMap = mutableMapOf<String, MediaViewerPageData.MediaViewerData>()

fun storeVideoData(data: MediaViewerPageData.MediaViewerData) : String{
val id = UUID.randomUUID().toString()
videoDataMap[id] = data
return id
}

fun getVideoData(videoId: String): MediaViewerPageData.MediaViewerData? {
return videoDataMap[videoId]
}

fun removeVideoData(videoId: String) {
videoDataMap.remove(videoId)
}

fun clear() {
videoDataMap.clear()
}
}

Loading