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
84 changes: 74 additions & 10 deletions mobile/src/main/java/net/activitywatch/android/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,33 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.view.GravityCompat
import androidx.fragment.app.Fragment
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import com.google.android.material.navigation.NavigationView
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.launch
import net.activitywatch.android.databinding.ActivityMainBinding
import net.activitywatch.android.fragments.TestFragment
import net.activitywatch.android.fragments.WebUIFragment
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.launch
import net.activitywatch.android.watcher.UsageStatsWatcher

private const val TAG = "MainActivity"

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/"
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 initialWebUiUrl(openActivityView: Boolean, baseUrl: String = baseURL): String =
if (openActivityView) activityViewUrl(baseUrl) else baseUrl

internal fun shouldOpenActivityViewImmediately(openActivityView: Boolean, isResumed: Boolean): Boolean =
openActivityView && isResumed


class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelectedListener, WebUIFragment.OnFragmentInteractionListener {

Expand Down Expand Up @@ -104,13 +118,6 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
val serviceIntent = Intent(this, BackgroundService::class.java)
startForegroundService(serviceIntent)

if (savedInstanceState != null) {
return
}
val firstFragment = WebUIFragment.newInstance(authenticatedUrl())
supportFragmentManager.beginTransaction()
.add(R.id.fragment_container, firstFragment).commit()

onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
if (binding.drawerLayout.isDrawerOpen(GravityCompat.START)) {
Expand All @@ -121,11 +128,68 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
}
})

if (savedInstanceState != null) {
return
}
// Cold start: pick the right first fragment so we don't flash dashboard
// 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)
if (openActivityView) {
intent.removeExtra(EXTRA_OPEN_ACTIVITY_VIEW)
}
}

override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent)

val isResumed = lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED)
if (shouldOpenActivityViewImmediately(
intent.getBooleanExtra(EXTRA_OPEN_ACTIVITY_VIEW, false),
isResumed,
)
) {
openPendingActivityView()
}
// A stopped activity cannot safely commit here because its fragment
// state may already be saved. onResume consumes the intent instead.
}
Comment thread
TimeToBuildBob marked this conversation as resolved.

private fun takeOpenActivityView(intent: Intent): Boolean {
val open = intent.getBooleanExtra(EXTRA_OPEN_ACTIVITY_VIEW, false)
if (open) {
intent.removeExtra(EXTRA_OPEN_ACTIVITY_VIEW)
}
return open
}

private fun showWebUi(url: String, replace: Boolean) {
val fragment = WebUIFragment.newInstance(authenticatedUrl(url))
val transaction = supportFragmentManager.beginTransaction()
if (replace) {
transaction.replace(R.id.fragment_container, fragment)
} else {
transaction.add(R.id.fragment_container, fragment)
}
transaction.commit()
}

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

override fun onResume() {
super.onResume()

// Notification tap on a stopped/restored instance: replace after the
// FragmentManager is ready. Cold start and resumed delivery already
// consumed the extra, so this is a no-op in those cases.
openPendingActivityView()

// Ensures data is always fresh when app is opened,
// even if it was up to an hour since the last logging-alarm was triggered.
val usw = UsageStatsWatcher(this)
Expand Down Expand Up @@ -168,7 +232,7 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
}
R.id.nav_activity -> {
fragmentClass = WebUIFragment::class.java
url = authenticatedUrl("$baseURL/#/activity/unknown/")
url = authenticatedUrl(activityViewUrl())
}
R.id.nav_buckets -> {
fragmentClass = WebUIFragment::class.java
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import androidx.core.app.NotificationCompat
import androidx.work.Worker
import androidx.work.WorkerParameters
import com.jakewharton.threetenabp.AndroidThreeTen
import net.activitywatch.android.EXTRA_OPEN_ACTIVITY_VIEW
import net.activitywatch.android.MainActivity
import net.activitywatch.android.R
import net.activitywatch.android.RustInterface
Expand Down Expand Up @@ -209,9 +210,14 @@ class NotifyWorker(context: Context, params: WorkerParameters) : Worker(context,
val body = "${alert.label}: $thresholdStr" +
if (thresholdStr != actualStr) " ($actualStr)" else ""

// Open the activity/timeline view in MainActivity when the notification is tapped.
// Open the activity/timeline view (same destination as the drawer
// "Activity" item) when the notification is tapped. SINGLE_TOP lets an
// already-running MainActivity receive onNewIntent instead of stacking.
val openIntent = Intent(applicationContext, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
flags = Intent.FLAG_ACTIVITY_NEW_TASK or
Intent.FLAG_ACTIVITY_CLEAR_TOP or
Intent.FLAG_ACTIVITY_SINGLE_TOP
putExtra(EXTRA_OPEN_ACTIVITY_VIEW, true)
}
val pendingIntent = PendingIntent.getActivity(
applicationContext,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package net.activitywatch.android

import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test

class MainActivityNavigationTest {
@Test
fun initialWebUiUrl_defaultsToDashboardHome() {
assertEquals(baseURL, initialWebUiUrl(openActivityView = false))
}

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

@Test
fun notificationIntent_navigatesImmediatelyWhenActivityIsResumed() {
assertTrue(
shouldOpenActivityViewImmediately(openActivityView = true, isResumed = true),
)
}

@Test
fun notificationIntent_defersNavigationWhenActivityIsStopped() {
assertFalse(
shouldOpenActivityViewImmediately(openActivityView = true, isResumed = false),
)
}
}
Loading