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
28 changes: 28 additions & 0 deletions mobile/src/main/java/net/activitywatch/android/DeviceHostname.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package net.activitywatch.android

import android.content.Context
import android.provider.Settings
import java.util.Locale

/**
* Hostname used for Android buckets, sync, and the Activity webui route.
*
* Sentinel `"unknown"` is a valid stored hostname (stopwatch, unsynced rows) but a
* terrible default: `/#/activity/unknown/` loads desktop visualizations for a host
* that has no `aw-watcher-android_*` buckets.
*/
internal fun sanitizeDeviceHostname(raw: String?): String {
val value = raw?.trim()?.takeIf { it.isNotEmpty() } ?: return "unknown"
return value
.lowercase(Locale.ROOT)
.replace(Regex("[^a-z0-9_-]+"), "_")
.trim('_')
.ifEmpty { "unknown" }
}

internal fun deviceHostname(context: Context): String {
val named = Settings.Global.getString(context.contentResolver, Settings.Global.DEVICE_NAME)
?.trim()
?.takeIf { it.isNotEmpty() }
return sanitizeDeviceHostname(named ?: android.os.Build.DEVICE)
}
21 changes: 14 additions & 7 deletions mobile/src/main/java/net/activitywatch/android/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,18 @@ const val baseURL = "http://127.0.0.1:5600"

// Same destination as the drawer "Activity" item. Notification taps set
// EXTRA_OPEN_ACTIVITY_VIEW so we land here instead of dashboard home.
const val ACTIVITY_VIEW_ROUTE = "/#/activity/unknown/"
// Hostname must match the sanitized device name used for Android buckets —
// `/#/activity/unknown/` is a sentinel, not a host (see DeviceHostname.kt).
const val EXTRA_OPEN_ACTIVITY_VIEW = "net.activitywatch.android.extra.OPEN_ACTIVITY_VIEW"

internal fun activityViewUrl(baseUrl: String = baseURL): String = "$baseUrl$ACTIVITY_VIEW_ROUTE"
internal fun activityViewUrl(hostname: String, baseUrl: String = baseURL): String =
"$baseUrl/#/activity/${sanitizeDeviceHostname(hostname)}/"

internal fun initialWebUiUrl(openActivityView: Boolean, baseUrl: String = baseURL): String =
if (openActivityView) activityViewUrl(baseUrl) else baseUrl
internal fun initialWebUiUrl(
openActivityView: Boolean,
hostname: String,
baseUrl: String = baseURL,
): String = if (openActivityView) activityViewUrl(hostname, baseUrl) else baseUrl

internal fun shouldOpenActivityViewImmediately(openActivityView: Boolean, isResumed: Boolean): Boolean =
openActivityView && isResumed
Expand Down Expand Up @@ -135,7 +140,7 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
// home before onResume. Consume the extra here; onResume is the path
// for reused instances (onNewIntent) and process-death restore.
val openActivityView = intent.getBooleanExtra(EXTRA_OPEN_ACTIVITY_VIEW, false)
showWebUi(initialWebUiUrl(openActivityView), replace = false)
showWebUi(initialWebUiUrl(openActivityView, deviceHostname(this)), replace = false)
if (openActivityView) {
intent.removeExtra(EXTRA_OPEN_ACTIVITY_VIEW)
}
Expand Down Expand Up @@ -176,9 +181,11 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
transaction.commit()
}

private fun activityViewUrlForDevice(): String = activityViewUrl(deviceHostname(this))

private fun openPendingActivityView() {
if (takeOpenActivityView(intent)) {
showWebUi(activityViewUrl(), replace = true)
showWebUi(activityViewUrlForDevice(), replace = true)
}
}

Expand Down Expand Up @@ -232,7 +239,7 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
}
R.id.nav_activity -> {
fragmentClass = WebUIFragment::class.java
url = authenticatedUrl(activityViewUrl())
url = authenticatedUrl(activityViewUrlForDevice())
}
R.id.nav_buckets -> {
fragmentClass = WebUIFragment::class.java
Expand Down
14 changes: 1 addition & 13 deletions mobile/src/main/java/net/activitywatch/android/RustInterface.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import android.content.Context
import android.content.pm.PackageManager
import android.os.Handler
import android.os.Looper
import android.provider.Settings
import android.system.Os
import android.util.Log
import java.util.concurrent.Executors
Expand Down Expand Up @@ -201,17 +200,6 @@ class RustInterface(context: Context? = null) {
}
}

fun getDeviceName(context: Context): String {
val raw = Settings.Global.getString(context.contentResolver, Settings.Global.DEVICE_NAME)
?.trim()
?.takeIf { it.isNotEmpty() }
?: android.os.Build.DEVICE
?: "unknown"
return raw.trim()
.lowercase(java.util.Locale.ROOT)
.replace(Regex("[^a-z0-9_-]+"), "_")
.trim('_')
.ifEmpty { "unknown" }
}
fun getDeviceName(context: Context): String = deviceHostname(context)

}
13 changes: 1 addition & 12 deletions mobile/src/main/java/net/activitywatch/android/SyncInterface.kt
Original file line number Diff line number Diff line change
Expand Up @@ -78,18 +78,7 @@ class SyncInterface(context: Context) {
private external fun syncBoth(port: Int, hostname: String): String
external fun getSyncDir(): String

private fun getDeviceName(): String {
val raw = android.provider.Settings.Global.getString(
appContext.contentResolver,
android.provider.Settings.Global.DEVICE_NAME
)?.trim()?.takeIf { it.isNotEmpty() }
?: android.os.Build.DEVICE ?: "unknown"
return raw.trim()
.lowercase(java.util.Locale.ROOT)
.replace(Regex("[^a-z0-9_-]+"), "_")
.trim('_')
.ifEmpty { "unknown" }
}
private fun getDeviceName(): String = deviceHostname(appContext)

// Async wrapper for syncPullAll
fun syncPullAllAsync(callback: (Boolean, String) -> Unit) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package net.activitywatch.android

import org.junit.Assert.assertEquals
import org.junit.Test

class DeviceHostnameTest {
@Test
fun sanitizeDeviceHostname_fallsBackToUnknown() {
assertEquals("unknown", sanitizeDeviceHostname(null))
assertEquals("unknown", sanitizeDeviceHostname(""))
assertEquals("unknown", sanitizeDeviceHostname(" "))
assertEquals("unknown", sanitizeDeviceHostname("***"))
}

@Test
fun sanitizeDeviceHostname_lowercasesAndCollapses() {
assertEquals("pixel_8", sanitizeDeviceHostname("Pixel 8"))
assertEquals("my-phone_1", sanitizeDeviceHostname("My-Phone_1"))
assertEquals("pixel_8", sanitizeDeviceHostname(" Pixel 8 "))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,37 @@ import org.junit.Test
class MainActivityNavigationTest {
@Test
fun initialWebUiUrl_defaultsToDashboardHome() {
assertEquals(baseURL, initialWebUiUrl(openActivityView = false))
assertEquals(
baseURL,
initialWebUiUrl(openActivityView = false, hostname = "pixel_8"),
)
}

@Test
fun initialWebUiUrl_opensActivityNavDestinationForDeviceHost() {
assertEquals(
"$baseURL/#/activity/pixel_8/",
initialWebUiUrl(openActivityView = true, hostname = "pixel_8"),
)
}

@Test
fun initialWebUiUrl_opensActivityNavDestination() {
fun activityViewUrl_sanitizesRawDeviceName() {
assertEquals(
"$baseURL/#/activity/pixel_8/",
activityViewUrl("Pixel 8"),
)
}

@Test
fun activityViewUrl_unknownIsFallbackNotDefault() {
assertEquals(
"$baseURL/#/activity/unknown/",
activityViewUrl("unknown"),
)
assertEquals(
"$baseURL/#/activity/unknown/",
initialWebUiUrl(openActivityView = true),
activityViewUrl(""),
)
}

Expand Down
Loading