Skip to content
Open
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
39 changes: 39 additions & 0 deletions PR-DESCRIPTION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
## Add configurable network access setting

I needed to access the ActivityWatch API from other devices on my network (via Tailscale) to sync data to a central server, so I figured I'd contribute a proper implementation rather than just hardcoding `0.0.0.0`.

### What this does

Adds a "Allow network access" toggle in a new native Settings screen, accessible from the action bar settings button (which previously showed a "not yet implemented" snackbar).

- **Off by default** — server binds to `127.0.0.1` (unchanged behavior)
- **When enabled** — server binds to `0.0.0.0`, allowing connections from other devices on the local network
- Enabling shows a **security warning dialog** explaining that the API has no authentication and activity data will be exposed to the network
- Displays a toast noting the change takes effect on app restart

### Changes

**aw-android:**
- `AWPreferences.kt` — new `isNetworkAccessEnabled` / `setNetworkAccessEnabled` preference
- `RustInterface.kt` — `startServer()` and `startServerTask()` now accept a `host` parameter
- `MainActivity.kt` — reads the preference and passes the host to the server; wires the Settings action bar button to the new `SettingsActivity`
- `SettingsActivity.kt` (new) — simple settings screen with the network access toggle + confirmation dialog
- `activity_settings.xml` (new) — layout for the settings screen
- `strings.xml` — added setting labels and warning text
- `AndroidManifest.xml` — registered `SettingsActivity`

**aw-server-rust** (submodule, [see commit](https://github.com/lucletoffe/aw-server-rust/commit/7a5c14841ee7b33b29576d4b2cc2003cacd52ac2)):
- `android/mod.rs` — `startServer` JNI function now accepts a `host` string parameter and passes it to `AWConfig` instead of relying on the default

### Security considerations

The toggle is opt-in with a clear warning. The dialog text mentions:
- No authentication on the API
- Anyone on the same network can read/modify data
- Recommendation to only enable on trusted networks (e.g. VPN)

### Screenshots

_Not available yet — will add after building the APK._

Closes #121, closes #107
2 changes: 1 addition & 1 deletion aw-server-rust
6 changes: 6 additions & 0 deletions mobile/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,12 @@
android:theme="@style/AppTheme.NoActionBar"
android:exported="true"/>

<activity
android:name=".SettingsActivity"
android:screenOrientation="portrait"
android:label="@string/settings_title"
android:exported="false"/>

<receiver
android:name=".watcher.AlarmReceiver"
android:enabled="true"
Expand Down
13 changes: 13 additions & 0 deletions mobile/src/main/java/net/activitywatch/android/AWPreferences.kt
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,17 @@ class AWPreferences(context: Context) {
editor.putBoolean("isFirstTime", true)
editor.apply()
}

// Whether the server should bind to all network interfaces (0.0.0.0)
// instead of localhost only (127.0.0.1).
// Off by default for security.
fun isNetworkAccessEnabled(): Boolean {
return sharedPreferences.getBoolean("networkAccessEnabled", false)
}

fun setNetworkAccessEnabled(enabled: Boolean) {
val editor = sharedPreferences.edit()
editor.putBoolean("networkAccessEnabled", enabled)
editor.apply()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
binding.navView.setNavigationItemSelectedListener(this)

val ri = RustInterface(this)
ri.startServerTask(this)
val host = if (prefs.isNetworkAccessEnabled()) "0.0.0.0" else "127.0.0.1"
ri.startServerTask(this, host)

if (savedInstanceState != null) {
return
Expand Down Expand Up @@ -96,8 +97,8 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
// as you specify a parent activity in AndroidManifest.xml.
return when (item.itemId) {
R.id.action_settings -> {
Snackbar.make(binding.coordinatorLayout, "The settings button was clicked, but it's not yet implemented!", Snackbar.LENGTH_LONG)
.setAction("Action", null).show()
val intent = Intent(this, SettingsActivity::class.java)
startActivity(intent)
true
}
else -> super.onOptionsItemSelected(item)
Expand Down
10 changes: 5 additions & 5 deletions mobile/src/main/java/net/activitywatch/android/RustInterface.kt
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ class RustInterface constructor(context: Context? = null) {

private external fun initialize(): String
private external fun greeting(pattern: String): String
private external fun startServer()
private external fun startServer(host: String)
private external fun setDataDir(path: String)
external fun getBuckets(): String
external fun createBucket(bucket: String): String
Expand All @@ -51,7 +51,7 @@ class RustInterface constructor(context: Context? = null) {
return greeting(to)
}

fun startServerTask(context: Context) {
fun startServerTask(context: Context, host: String = "127.0.0.1") {
if(!serverStarted) {
// check if port 5600 is already in use
try {
Expand All @@ -69,16 +69,16 @@ class RustInterface constructor(context: Context? = null) {
// will not block the UI thread

// Start server
Log.w(TAG, "Starting server...")
startServer()
Log.w(TAG, "Starting server on $host...")
startServer(host)

handler.post {
// will run on UI thread after the task is done
Log.i(TAG, "Server finished")
serverStarted = false
}
}
Log.w(TAG, "Server started")
Log.w(TAG, "Server started on $host")
}
}

Expand Down
54 changes: 54 additions & 0 deletions mobile/src/main/java/net/activitywatch/android/SettingsActivity.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package net.activitywatch.android

import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import com.google.android.material.switchmaterial.SwitchMaterial

class SettingsActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_settings)

supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.title = getString(R.string.settings_title)

val prefs = AWPreferences(this)
val switchNetworkAccess = findViewById<SwitchMaterial>(R.id.switchNetworkAccess)
switchNetworkAccess.isChecked = prefs.isNetworkAccessEnabled()

switchNetworkAccess.setOnCheckedChangeListener { _, isChecked ->
if (isChecked) {
// Show security warning before enabling
AlertDialog.Builder(this)
.setTitle(R.string.network_access_warning_title)
.setMessage(R.string.network_access_warning_message)
.setPositiveButton(R.string.network_access_warning_enable) { _, _ ->
prefs.setNetworkAccessEnabled(true)
showRestartNotice()
}
.setNegativeButton(android.R.string.cancel) { _, _ ->
switchNetworkAccess.isChecked = false
}
.setOnCancelListener {
switchNetworkAccess.isChecked = false
}
.show()
} else {
prefs.setNetworkAccessEnabled(false)
showRestartNotice()
}
}
}

private fun showRestartNotice() {
Toast.makeText(this, R.string.network_access_restart_notice, Toast.LENGTH_LONG).show()
}

override fun onSupportNavigateUp(): Boolean {
finish()
return true
}
}
24 changes: 24 additions & 0 deletions mobile/src/main/res/layout/activity_settings.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp">

<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/switchNetworkAccess"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/network_access_title"
android:padding="8dp" />

<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/network_access_summary"
android:textAppearance="?android:attr/textAppearanceSmall"
android:paddingStart="8dp"
android:paddingEnd="8dp"
android:textColor="?android:attr/textColorSecondary" />

</LinearLayout>
9 changes: 9 additions & 0 deletions mobile/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,13 @@

<!-- TODO: Remove or change this placeholder text -->
<string name="hello_blank_fragment">Hello blank fragment</string>

<!-- Settings -->
<string name="settings_title">Settings</string>
<string name="network_access_title">Allow network access</string>
<string name="network_access_summary">Allow other devices on the local network to connect to the ActivityWatch server. When disabled, only this device can access the server.</string>
<string name="network_access_warning_title">Security warning</string>
<string name="network_access_warning_message">Enabling network access will allow any device on the same network to access your ActivityWatch data.\n\nThe ActivityWatch API does not have authentication, so anyone on your network will be able to read and modify your activity data.\n\nOnly enable this if you trust your network (e.g. a VPN like Tailscale).</string>
<string name="network_access_warning_enable">Enable</string>
<string name="network_access_restart_notice">Restart the app for this change to take effect.</string>
</resources>