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
14 changes: 14 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,17 @@ Add JVM tests alongside production packages in `app/src/test` using JUnit4, nami

## Commit & Pull Request Guidelines
The current snapshot lacks Git history, so adopt Conventional Commit prefixes (`feat:`, `fix:`, `chore:`) for clarity. Keep the first line under 72 characters in the imperative mood and link issues using `Refs #123` in the body. Pull requests should summarize functional changes, call out affected components (e.g., tile service, resources), include screenshots or screen recordings when UI changes occur, and list verification steps such as emulator/API levels tested.

## Agent skills

### Issue tracker

Issues and specs live in `StoneHub/screen-timeout-tile` GitHub Issues. Read `docs/agents/issue-tracker.md` before issue or planning work.

### Triage labels

Use the default Matt triage roles. Read `docs/agents/triage-labels.md` before triage work.

### Domain docs

This is a single-context project. Read `docs/agents/domain.md` before domain-modeling or architecture work.
2 changes: 1 addition & 1 deletion PROJECT_GOAL.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,4 +91,4 @@ implementation sample here.
* [ ] Test on API 24-32 devices for manual tile placement.
* [ ] Test on API 33+ devices for direct tile placement.
* [x] Handle permission flow.
* [ ] Build release APK/AAB.
* [x] Build and verify the release APK/AAB for version 1.1.0.
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ It is built for the common Android workflow where you want the screen to stay aw

## Download

Download the signed APK from the [latest GitHub release](https://github.com/StoneHub/screen-timeout-tile/releases/latest). Android 7.0 or newer is required.
Download the signed APK for free from the [latest GitHub release](https://github.com/StoneHub/screen-timeout-tile/releases/latest). Android 7.0 or newer is required. The free APK will also remain available from the project website when that page is published.

A paid Google Play listing is planned at a one-time price between $1 and $3, with no ads or subscription. Purchases should be eligible for [Google Play Family Library](https://support.google.com/googleplay/answer/7007852). Play Store publication has not happened yet.

Because this app is distributed outside Google Play, Android may ask you to allow installs from the browser or file manager you used to download it. That permission can be turned off again immediately after installation.

Expand Down Expand Up @@ -132,7 +134,7 @@ app/src/androidTest/java/ Instrumentation smoke tests

## Current Status

Version 1.1.0 is the first signed GitHub release. Play Store publishing remains a separate future distribution path.
Version 1.1.0 is published as the first signed GitHub release. The next distribution lane is a paid Google Play listing, while the signed GitHub and website APK stays free.

## Contributing

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ class MainActivityInstrumentedTest {
fun shouldUseExpectedPackageName_whenAppContextIsLoaded() {
val appContext = InstrumentationRegistry.getInstrumentation().targetContext

assertEquals("com.stonecode.screentimeouttile", appContext.packageName)
assertEquals("com.stonecode.screentimeouttile.debug", appContext.packageName)
}

@Test
Expand All @@ -28,6 +28,10 @@ class MainActivityInstrumentedTest {
onView(withId(R.id.permissionRationale)).check(matches(isDisplayed()))
onView(withId(R.id.tileButton)).check(matches(isDisplayed()))
onView(withId(R.id.nextAction)).check(matches(isDisplayed()))
onView(withId(R.id.setupProgress)).check(matches(isDisplayed()))
onView(withId(R.id.permissionStepBadge)).check(matches(isDisplayed()))
onView(withId(R.id.tileStepBadge)).check(matches(isDisplayed()))
onView(withId(R.id.useStepBadge)).check(matches(isDisplayed()))
}
}
}
110 changes: 90 additions & 20 deletions app/src/main/java/com/stonecode/screentimeouttile/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,25 +7,33 @@ import android.graphics.drawable.Icon
import android.os.Build
import android.os.Bundle
import android.provider.Settings
import android.view.View
import android.widget.Button
import android.widget.TextView
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.net.toUri
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updatePadding
import com.google.android.material.progressindicator.LinearProgressIndicator

class MainActivity : AppCompatActivity() {

private lateinit var controller: ScreenTimeoutController
private lateinit var statusView: TextView
private lateinit var timeoutView: TextView
private lateinit var permissionRationaleView: TextView
private lateinit var nextActionView: TextView
private lateinit var actionButton: Button
private lateinit var tileButton: Button
private lateinit var tileRequestStatusView: TextView
private lateinit var permissionStepBadge: TextView
private lateinit var tileStepBadge: TextView
private lateinit var useStepBadge: TextView
private lateinit var setupProgress: LinearProgressIndicator

private val permissionLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
Expand All @@ -43,19 +51,24 @@ class MainActivity : AppCompatActivity() {
controller = ScreenTimeoutController(this)
statusView = findViewById(R.id.permissionStatus)
timeoutView = findViewById(R.id.timeoutValue)
permissionRationaleView = findViewById(R.id.permissionRationale)
nextActionView = findViewById(R.id.nextAction)
actionButton = findViewById(R.id.permissionButton)
tileButton = findViewById(R.id.tileButton)
tileRequestStatusView = findViewById(R.id.tileRequestStatus)
permissionStepBadge = findViewById(R.id.permissionStepBadge)
tileStepBadge = findViewById(R.id.tileStepBadge)
useStepBadge = findViewById(R.id.useStepBadge)
setupProgress = findViewById(R.id.setupProgress)

actionButton.setOnClickListener { openWriteSettings() }
tileButton.setOnClickListener { requestTilePlacement() }
}

private fun configureSystemBars() {
WindowCompat.getInsetsController(window, window.decorView).apply {
isAppearanceLightStatusBars = false
isAppearanceLightNavigationBars = false
isAppearanceLightStatusBars = true
isAppearanceLightNavigationBars = true
}
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.screenContent)) { view, insets ->
val safeDrawing = insets.getInsets(
Expand Down Expand Up @@ -99,31 +112,74 @@ class MainActivity : AppCompatActivity() {
canRequestTilePlacement = canRequestTilePlacement(),
)

statusView.text = if (hasPermission) {
getString(R.string.permission_granted_status)
} else {
getString(R.string.permission_needed_status)
}

actionButton.isEnabled = state.primaryAction == SetupAction.GRANT_PERMISSION
tileButton.isEnabled = state.canRequestTilePlacement
nextActionView.text = when (state.primaryAction) {
SetupAction.GRANT_PERMISSION -> getString(R.string.next_action_grant_permission)
SetupAction.ADD_TILE -> getString(R.string.next_action_add_tile)
SetupAction.MANUAL_ADD_TILE -> getString(R.string.next_action_manual_add_tile)
}

if (!state.canRequestTilePlacement && hasPermission) {
tileRequestStatusView.text = getString(R.string.tile_request_status_unsupported)
}

timeoutView.text = when (val status = state.timeoutStatus) {
is TimeoutStatus.Available -> getString(
R.string.current_timeout_value,
TimeoutLabelFormatter.format(this, status.label),
)
TimeoutStatus.Unavailable -> getString(R.string.current_timeout_unknown)
}

renderSetupState(state)
}

private fun renderSetupState(state: SetupUiState) {
val needsPermission = state.primaryAction == SetupAction.GRANT_PERMISSION

if (needsPermission) {
setupProgress.setProgressCompat(PROGRESS_PERMISSION, true)
nextActionView.text = getString(R.string.setup_progress_step_one)
statusView.text = getString(R.string.setup_permission_title)
permissionRationaleView.visibility = View.VISIBLE
timeoutView.visibility = View.GONE
actionButton.visibility = View.VISIBLE
actionButton.isEnabled = true
tileButton.visibility = View.VISIBLE
tileButton.isEnabled = false
tileRequestStatusView.text = getString(R.string.setup_tile_locked_description)
renderBadge(permissionStepBadge, "1", R.drawable.setup_step_badge_current, R.color.white)
renderBadge(tileStepBadge, "2", R.drawable.setup_step_badge_pending, R.color.setup_text_muted)
renderBadge(useStepBadge, "3", R.drawable.setup_step_badge_pending, R.color.setup_text_muted)
return
}

setupProgress.setProgressCompat(PROGRESS_TILE, true)
nextActionView.text = getString(R.string.setup_progress_step_two)
statusView.text = getString(R.string.setup_permission_complete_title)
permissionRationaleView.visibility = View.GONE
timeoutView.visibility = View.VISIBLE
actionButton.visibility = View.GONE
renderBadge(permissionStepBadge, CHECK_MARK, R.drawable.setup_step_badge_done, R.color.white)
renderBadge(tileStepBadge, "2", R.drawable.setup_step_badge_current, R.color.white)
renderBadge(useStepBadge, "3", R.drawable.setup_step_badge_pending, R.color.setup_text_muted)

if (state.canRequestTilePlacement) {
tileButton.visibility = View.VISIBLE
tileButton.isEnabled = true
tileRequestStatusView.text = getString(R.string.setup_tile_ready_description)
} else {
tileButton.visibility = View.GONE
tileRequestStatusView.text = getString(R.string.setup_tile_manual_description)
}
}

private fun renderBadge(
badge: TextView,
label: String,
backgroundResource: Int,
textColorResource: Int,
) {
badge.text = label
badge.setBackgroundResource(backgroundResource)
badge.setTextColor(ContextCompat.getColor(this, textColorResource))
}

private fun renderSetupComplete() {
setupProgress.setProgressCompat(PROGRESS_COMPLETE, true)
nextActionView.text = getString(R.string.setup_progress_complete)
tileButton.isEnabled = false
renderBadge(tileStepBadge, CHECK_MARK, R.drawable.setup_step_badge_done, R.color.white)
renderBadge(useStepBadge, CHECK_MARK, R.drawable.setup_step_badge_done, R.color.white)
}

private fun canRequestTilePlacement(): Boolean = Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
Expand All @@ -145,6 +201,9 @@ class MainActivity : AppCompatActivity() {
mainExecutor,
) { result ->
tileRequestStatusView.text = tileRequestStatusText(result)
if (isTilePlacementComplete(result)) {
renderSetupComplete()
}
}
}.onFailure {
tileRequestStatusView.text = getString(R.string.tile_request_status_error)
Expand All @@ -167,4 +226,15 @@ class MainActivity : AppCompatActivity() {
} else {
getString(R.string.tile_request_status_unsupported)
}

private fun isTilePlacementComplete(result: Int): Boolean =
result == StatusBarManager.TILE_ADD_REQUEST_RESULT_TILE_ADDED ||
result == StatusBarManager.TILE_ADD_REQUEST_RESULT_TILE_ALREADY_ADDED

companion object {
private const val CHECK_MARK = "✓"
private const val PROGRESS_PERMISSION = 33
private const val PROGRESS_TILE = 67
private const val PROGRESS_COMPLETE = 100
}
}
5 changes: 5 additions & 0 deletions app/src/main/res/drawable/setup_step_badge_current.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="@color/brand_primary" />
</shape>
5 changes: 5 additions & 0 deletions app/src/main/res/drawable/setup_step_badge_done.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="@color/setup_success" />
</shape>
8 changes: 8 additions & 0 deletions app/src/main/res/drawable/setup_step_badge_pending.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="@color/setup_surface" />
<stroke
android:width="1dp"
android:color="@color/setup_outline" />
</shape>
5 changes: 5 additions & 0 deletions app/src/main/res/drawable/setup_timeout_background.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/setup_timeout_surface" />
<corners android:radius="12dp" />
</shape>
Loading
Loading