Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New Timer Overlay that shows up real count while watching #8

Merged
merged 3 commits into from
Feb 5, 2025
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
17 changes: 17 additions & 0 deletions app/src/main/java/com/scrolless/app/di/ProviderModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
package com.scrolless.app.di

import android.content.Context
import com.scrolless.app.overlay.TimerOverlayManager
import com.scrolless.app.overlay.TimerOverlayManagerImpl
import com.scrolless.app.provider.AppProvider
import com.scrolless.app.provider.AppProviderImpl
import com.scrolless.app.provider.NavigationProvider
Expand All @@ -15,6 +17,8 @@ import com.scrolless.app.provider.ThemeProvider
import com.scrolless.app.provider.ThemeProviderImpl
import com.scrolless.app.provider.UsageTracker
import com.scrolless.app.provider.UsageTrackerImpl
import com.scrolless.app.services.BlockController
import com.scrolless.app.services.BlockControllerImpl
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
Expand Down Expand Up @@ -52,4 +56,17 @@ class ProviderModule {
fun provideUsageTrackerImpl(
appProvider: AppProvider
): UsageTracker = UsageTrackerImpl(appProvider)

@Provides
@Singleton
fun provideBlockController(
usageTracker: UsageTracker
): BlockController = BlockControllerImpl(usageTracker)

@Provides
@Singleton
fun provideTimerOverlayManager(
usageTracker: UsageTracker,
appProvider: AppProvider
): TimerOverlayManager = TimerOverlayManagerImpl(usageTracker, appProvider)
}
19 changes: 16 additions & 3 deletions app/src/main/java/com/scrolless/app/features/home/HomeFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,8 @@ import com.scrolless.app.provider.AppProvider
import com.scrolless.app.provider.UsageTracker
import com.scrolless.app.services.ScrollessBlockAccessibilityService
import com.scrolless.framework.extensions.*
import com.scrolless.framework.extensions.formatTime
import com.scrolless.framework.extensions.observeFlow
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject

@AndroidEntryPoint
class HomeFragment : BaseFragment<FragmentHomeBinding>() {
companion object {
Expand Down Expand Up @@ -128,6 +125,22 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
if (!isAccessibilityServiceEnabled(requireContext())) {
openAccessibilitySettings(requireContext())
}

setTimerOverlayCheckBoxListener()
}

/**
* Sets up the listener for the timer overlay checkbox.
*
* Initializes the checkbox state from appProvider.timerOverlayEnabled
* and updates it when the checkbox is toggled.
*/
private fun setTimerOverlayCheckBoxListener() {
binding.checkBoxTimerOverlay.isChecked = appProvider.timerOverlayEnabled

binding.checkBoxTimerOverlay.setOnCheckedChangeListener { _, isChecked ->
appProvider.timerOverlayEnabled = isChecked
}
}

override fun onResume() {
Expand Down
13 changes: 13 additions & 0 deletions app/src/main/java/com/scrolless/app/overlay/TimerOverlayManager.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/*
* Copyright (C) 2025, Scrolless
* All rights reserved.
*/
package com.scrolless.app.overlay

import android.content.Context

interface TimerOverlayManager {
fun attachServiceContext(context: Context)
fun show()
fun hide()
}
193 changes: 193 additions & 0 deletions app/src/main/java/com/scrolless/app/overlay/TimerOverlayManagerImpl.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
/*
* Copyright (C) 2025, Scrolless
* All rights reserved.
*/
package com.scrolless.app.overlay

import android.content.Context
import android.graphics.PixelFormat
import android.view.Gravity
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.WindowManager
import android.widget.TextView
import androidx.appcompat.view.ContextThemeWrapper
import com.scrolless.app.R
import com.scrolless.app.provider.AppProvider
import com.scrolless.app.provider.UsageTracker
import com.scrolless.framework.extensions.fadeOutWithBounceAnimation
import com.scrolless.framework.extensions.getReadableTime
import jakarta.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch

/**
* Implementation of TimerOverlayManager that displays a timer overlay on top of the brain rot content.
*/
class TimerOverlayManagerImpl @Inject constructor(
private val usageTracker: UsageTracker,
private val appProvider: AppProvider
) : TimerOverlayManager {

private var overlayView: View? = null
private var timerTextView: TextView? = null

// Job for updating the timer text
private var timerUpdateJob: Job? = null

private lateinit var layoutParams: WindowManager.LayoutParams

// Store the last known overlay position (persisted in appProvider)
private var positionX = appProvider.timerOverlayPositionX
private var positionY = appProvider.timerOverlayPositionY

private val coroutineScope = CoroutineScope(Dispatchers.Main + SupervisorJob())

private lateinit var serviceContext: Context
private var windowManager: WindowManager? = null

/**
* Attaches the service context used to interact with the window manager.
*/
override fun attachServiceContext(context: Context) {
serviceContext = context
windowManager = serviceContext.getSystemService(Context.WINDOW_SERVICE) as WindowManager
}

/**
* Displays the timer overlay on screen, if it's not already visible.
*/
override fun show() {
// If already showing, do nothing
if (overlayView != null) return

// Offset the start time by 1 second to account for delays.
val startTimeMillis = System.currentTimeMillis() - 1000

// Inflate the overlay view using the app's theme
val themedContext = ContextThemeWrapper(serviceContext, R.style.AppTheme)
val inflater = LayoutInflater.from(themedContext)
overlayView = inflater.inflate(R.layout.overlay_timer, null)
timerTextView = overlayView?.findViewById(R.id.timerTextView)

// Set up layout parameters for the overlay window
layoutParams = WindowManager.LayoutParams(
WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY,
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN,
PixelFormat.TRANSLUCENT,
).apply {
gravity = Gravity.TOP or Gravity.END
x = positionX
y = positionY
}

// Start invisible for a quick fade-in effect.
overlayView?.alpha = 0f
windowManager?.addView(overlayView, layoutParams)

overlayView?.animate()
?.alpha(1f)
?.setDuration(200)
?.start()

enableDragging()
startTimer(startTimeMillis)
}

/**
* Hides the timer overlay with an animation, then removes it from the window.
*/
override fun hide() {
stopTimer()

val currentView = overlayView ?: return

// Update the timer text with the total daily usage before hiding.
val totalTime = usageTracker.getDailyUsage()
timerTextView?.text = totalTime.getReadableTime()

currentView.fadeOutWithBounceAnimation {
currentView.visibility = View.GONE
windowManager?.removeView(currentView)
overlayView = null
timerTextView = null
}
}

/**
* Starts the coroutine to update the timer text every second.
*
* @param startTimeMillis The starting point for the timer
*/
private fun startTimer(startTimeMillis: Long) {
// Cancel any existing timer before starting a new one
stopTimer()

timerUpdateJob = coroutineScope.launch(Dispatchers.Main) {
while (isActive) {
val elapsed = System.currentTimeMillis() - startTimeMillis
timerTextView?.text = elapsed.getReadableTime()
delay(1000)
}
}
}

/**
* Cancels any active timer job.
*/
private fun stopTimer() {
timerUpdateJob?.cancel()
timerUpdateJob = null
}

/**
* Enables dragging of the overlay view, updating and persisting position to [appProvider].
*/
private fun enableDragging() {
overlayView?.setOnTouchListener(
object : View.OnTouchListener {
private var initialX = 0
private var initialY = 0
private var initialTouchX = 0f
private var initialTouchY = 0f

override fun onTouch(view: View, motionEvent: MotionEvent): Boolean {
when (motionEvent.action) {
MotionEvent.ACTION_DOWN -> {
initialX = layoutParams.x
initialY = layoutParams.y
initialTouchX = motionEvent.rawX
initialTouchY = motionEvent.rawY
return true
}

MotionEvent.ACTION_MOVE -> {
layoutParams.x = initialX - (motionEvent.rawX - initialTouchX).toInt()
layoutParams.y = initialY + (motionEvent.rawY - initialTouchY).toInt()
windowManager?.updateViewLayout(overlayView, layoutParams)
return true
}

MotionEvent.ACTION_UP -> {
positionX = layoutParams.x
positionY = layoutParams.y

// Persist final position to appProvider
appProvider.timerOverlayPositionX = positionX
appProvider.timerOverlayPositionY = positionY
}
}
return false
}
},
)
}
}
4 changes: 4 additions & 0 deletions app/src/main/java/com/scrolless/app/provider/AppProvider.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,8 @@ interface AppProvider {
*/
var blockConfig: BlockConfig
val blockConfigFlow: StateFlow<BlockConfig>

var timerOverlayEnabled: Boolean
var timerOverlayPositionX: Int
var timerOverlayPositionY: Int
}
17 changes: 17 additions & 0 deletions app/src/main/java/com/scrolless/app/provider/AppProviderImpl.kt
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ class AppProviderImpl(context: Context) : AppProvider {
private const val PREF_INTERVAL_LENGTH = "interval_length"
private const val PREF_LAST_RESET_DAY = "last_reset_day"
private const val PREF_TOTAL_DAILY_USAGE = "total_daily_usage"

// Timer overlay
private const val PREF_TIMER_OVERLAY_ENABLED = "timer_overlay_enabled"
private const val PREF_TIMER_POSITION_X = "timer_overlay_position_x"
private const val PREF_TIMER_POSITION_Y = "timer_overlay_position_y"
}

override val cacheManager = CacheManager(context, PREF_PACKAGE_NAME)
Expand Down Expand Up @@ -61,6 +66,18 @@ class AppProviderImpl(context: Context) : AppProvider {
get() = cacheManager.read(PREF_TOTAL_DAILY_USAGE, 0)
set(value) = cacheManager.write(PREF_TOTAL_DAILY_USAGE, value)

override var timerOverlayEnabled: Boolean
get() = cacheManager.read(PREF_TIMER_OVERLAY_ENABLED, false)
set(value) = cacheManager.write(PREF_TIMER_OVERLAY_ENABLED, value)

override var timerOverlayPositionX: Int
get() = cacheManager.read(PREF_TIMER_POSITION_X, 16)
set(value) = cacheManager.write(PREF_TIMER_POSITION_X, value)

override var timerOverlayPositionY: Int
get() = cacheManager.read(PREF_TIMER_POSITION_Y, 16)
set(value) = cacheManager.write(PREF_TIMER_POSITION_Y, value)

private fun readBlockConfigFromCache(): BlockConfig {
val blockOption = cacheManager.read(PREF_BLOCK_OPTION, BlockOption.NothingSelected)
val timeLimit = cacheManager.read(PREF_TIME_LIMIT, 20000L)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ class UsageTrackerImpl @Inject constructor(
if (currentDay != lastDay) {
dailyUsageInMemory = 0L
appProvider.lastResetDay = currentDay
save()
}
}

Expand Down
Loading