Make Timeline live-update and give it a guiding empty state#572
Merged
Make Timeline live-update and give it a guiding empty state#572
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.
(same pattern as ActivityLog) with a 500ms debounce so entries stream
in while the screen is open.
"TrackerControl is off" when disabled, or "Watching for trackers…"
plus an "Open an app" button (launches the system launcher) when
enabled.
DNS is blocked by default now, so the nudge no longer applies.
was already visibility="gone"; the flag was only gating other hints).