Skip to content

Make Timeline live-update and give it a guiding empty state#572

Merged
kasnder merged 8 commits intomasterfrom
claude/improve-onboarding-home-YgyE8
Apr 27, 2026
Merged

Make Timeline live-update and give it a guiding empty state#572
kasnder merged 8 commits intomasterfrom
claude/improve-onboarding-home-YgyE8

Conversation

@kasnder
Copy link
Copy Markdown
Member

@kasnder kasnder commented Apr 24, 2026

New users landed on an empty Timeline with no feedback: the screen only
refreshed on onResume, and the empty text said "Enable TrackerControl"
even when it was already on. They had no way to know they needed to
open an app to generate traffic.

  • ActivityTimeline subscribes to DatabaseHelper.AccessChangedListener
    (same pattern as ActivityLog) with a 500ms debounce so entries stream
    in while the screen is open.
  • Wrap the RecyclerView in a SwipeRefreshLayout as a manual fallback.
  • Replace the single tvEmpty label with a context-aware empty state:
    "TrackerControl is off" when disabled, or "Watching for trackers…"
    plus an "Open an app" button (launches the system launcher) when
    enabled.
  • Drop the obsolete msg_private_dns Toast in ActivityMain — private
    DNS is blocked by default now, so the nudge no longer applies.
  • Remove the dead llUsage / hint_usage banner plumbing (the layout
    was already visibility="gone"; the flag was only gating other hints).

claude added 8 commits April 24, 2026 20:31
New users landed on an empty Timeline with no feedback: the screen only
refreshed on onResume, and the empty text said "Enable TrackerControl"
even when it was already on. They had no way to know they needed to
open an app to generate traffic.

- ActivityTimeline subscribes to DatabaseHelper.AccessChangedListener
  (same pattern as ActivityLog) with a 500ms debounce so entries stream
  in while the screen is open.
- Wrap the RecyclerView in a SwipeRefreshLayout as a manual fallback.
- Replace the single tvEmpty label with a context-aware empty state:
  "TrackerControl is off" when disabled, or "Watching for trackers…"
  plus an "Open an app" button (launches the system launcher) when
  enabled.
- Drop the obsolete msg_private_dns Toast in ActivityMain — private
  DNS is blocked by default now, so the nudge no longer applies.
- Remove the dead llUsage / hint_usage banner plumbing (the layout
  was already visibility="gone"; the flag was only gating other hints).
Onboarding dropped new users on the app list with no guidance. With the
Timeline now live-updating and owning a guiding empty state, promote it
to a peer destination via a bottom navigation bar, landing users there
by default while keeping the filterable app list and SearchView a tap
away.

- Extract TimelineFragment from ActivityTimeline (same live refresh,
  swipe refresh, and context-aware empty state with "Open an app" CTA)
  and reuse it in both ActivityMain and ActivityTimeline.
- Add BottomNavigationView to main.xml with Timeline and Apps items;
  ActivityMain swaps visibility of the Apps content vs. the Timeline
  fragment container and adjusts the toolbar (custom on/off switch
  visible only on the Apps tab; screen title shown on Timeline).
- onPrepareOptionsMenu hides search/filter/sort on the Timeline tab.
- Remove the now-redundant "Tracker activity" overflow menu item and
  the ActivityMain->ActivityTimeline launcher intent. The standalone
  ActivityTimeline still exists as a thin fragment host so the
  Insights hero card's "timeline" shortcut keeps working (it now
  switches tabs in-place when launched from ActivityMain).
- Back from Timeline returns to Apps instead of exiting.
The insights card (total trackers blocked, companies, block %) used to
sit above the app list on the Apps tab. With Timeline as the default
home, it makes more sense as the top card there — an at-a-glance
dashboard that frames whatever tracker activity follows.

- TimelineFragment now owns a ConcatAdapter of
  (InsightsHeaderAdapter, TimelineEmptyAdapter, TimelineAdapter). The
  insights card is always present; the empty-state card appears only
  when there are no timeline entries so the hero stays visible.
- Move loadInsightsData() into TimelineFragment so insights refresh
  on the same debounced access-change callback as the timeline.
- Strip InsightsHeaderAdapter, ConcatAdapter, and loadInsightsData
  from ActivityMain. The Apps tab is now a plain AdapterRule list.
- Hide the now-redundant "open timeline" shortcut inside the insights
  card (the card already lives on Timeline).
- Delete ActivityTimeline and its layout/manifest entry — no longer
  reachable now that the tab + in-card action route traffic directly.
- Replace the overlay-style empty view in fragment_timeline.xml with
  an in-list TimelineEmptyAdapter so the insights card remains visible
  alongside the empty state.
Two fixes from on-device feedback:

- The insights hero card was being clipped under the AppBar. The cause
  was wrapping the tab contents in an extra FrameLayout that broke the
  CoordinatorLayout's appbar_scrolling_view_behavior offset. Drop the
  wrapper and put llApps and timelineContainer back as direct children
  of the CoordinatorLayout (the original llApps pattern), each with
  the scrolling behavior + dodgeInsetEdges so they sit below the
  AppBar and above the bottom nav.
- The toolbar was swapping its custom view (on/off switch) and title
  between tabs, which felt jarring. Keep the on/off switch always
  visible — the global VPN toggle now works on Timeline too — and
  drop the per-tab title swap. Filter and sort still hide on Timeline
  since they're meaningless without the app list, but Search stays
  visible: tapping it on Timeline jumps to the Apps tab and expands
  the search field.
The clipping was real and caused by app:layout_dodgeInsetEdges="bottom"
on the content child interacting with the BottomNavigationView's
app:layout_insetEdge="bottom". CoordinatorLayout's dodge translates the
dodging child upward by the insetting sibling's height, and combined
with appbar_scrolling_view_behavior the result was the content's top
ending up above the AppBar — so the insights card's top rows
(Last 7 days header + first ~30dp of the big numbers) rendered behind
the toolbar.

We don't need any of CoordinatorLayout's behavior magic here: the
AppBar is pinned (liftOnScroll="false"), the bottom nav is always
visible, and there are no scroll-dependent transitions. Switch the
root to a vertical LinearLayout — AppBar / content FrameLayout
(weight=1) / BottomNavigationView — which gives a predictable static
layout. Also swap timelineContainer from FragmentContainerView to a
plain FrameLayout to avoid FragmentContainerView's window-insets
interception, which was a second potential source of layout drift.
Latent bug from commit 7e6bccb (the M3 edge-to-edge migration). Both
activity_settings.xml and activity_details.xml have:

  CoordinatorLayout android:fitsSystemWindows="true"
    AppBarLayout
      MaterialToolbar

Combined with the OnApplyWindowInsetsListener in ActivitySettings.java
and DetailsActivity.java that pads the AppBar by sysBars.top, the
status-bar inset was applied twice: once by the CoordinatorLayout
(fitsSystemWindows pads the parent) and once by the listener (pads the
AppBar) — yielding 2 × statusBarHeight of red space above the toolbar.

ActivityMain's CoordinatorLayout never had fitsSystemWindows, which is
why this branch's screenshots only showed it after looking at Settings.
Drop fitsSystemWindows from both layouts so the listener is the single
source of truth, matching how main.xml has always behaved.
Two follow-ups from the bottom-nav move:

Insights card: the white horizontal divider and the "View recent tracker
activity" row used to separate the stats from the link to the Timeline
activity. With the card now living on the Timeline tab itself the
forwarding row was already hidden — drop the divider and the row from
item_insights_header.xml entirely, and remove the matching field/wiring
in InsightsHeaderAdapter.

Search from Timeline: tapping the toolbar search icon already jumped to
the Apps tab, but the SearchView never received focus (so the keyboard
did not pop up and the user could not type). Two fixes:

  1. selectTab no longer calls invalidateOptionsMenu. Filter/sort live
     in the overflow, so they re-prepare lazily next time it opens; we
     avoid rebuilding the SearchView mid-expand and dropping its focus.
     Also only collapse the SearchView on Apps→Timeline, not in the
     Timeline→Apps direction triggered by expanding it.
  2. The bottom-nav switch in onMenuItemActionExpand is posted so the
     SearchView finishes expanding and the IME shows on the current
     frame; the layout toggle then runs without pre-empting focus.
Two issues on the Timeline tab:

1. Opening the app went to "Watching for trackers…" even when there
   was recent activity; only pulling to refresh populated the list.
   Root cause: TimelineFragment.buildTimeline() calls the static
   TrackerList.findTracker(daddr), which reads a static
   hostname → Tracker map populated lazily by
   TrackerList.getInstance(context). InsightsDataProvider initializes
   it (kt:50, "Ensure TrackerList is initialized") but the timeline
   path never did. loadTimeline() and loadInsights() race on
   different executors; if the timeline ran first the map was empty,
   every entry was dropped, and the empty state showed. Pull-to-
   refresh worked because by then insights had won the race. Call
   TrackerList.getInstance() at the top of buildTimeline.

2. The screen relied solely on AccessChangedListener for updates,
   so relative timestamps drifted and any missed callback left the
   list stale. Add a 30-second periodic tick (Handler.postDelayed
   loop) that re-runs refreshAll while the fragment is resumed, and
   cancel it in onPause.
@kasnder kasnder merged commit 58b61cf into master Apr 27, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants