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
62 changes: 53 additions & 9 deletions .github/agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ This document provides guidance for AI assistants and LLM models working on the

- **License**: Mozilla Public License 2.0 (MPL-2.0)
- **Language**: Kotlin (Android)
- **Minimum SDK**: 24 (Android 7.0)
- **Target SDK**: 34 (Android 14)
- **Minimum SDK**: 27 (Android 8.1)
- **Target SDK**: 37
- **Mozilla Components**: 153.0 (`mozComponentsVersion` in `app/build.gradle`)
- **Architecture**: MVVM with Android Components and Jetpack Compose

## Related Mozilla Projects
Expand Down Expand Up @@ -49,8 +50,9 @@ nira-browser/
│ │ │ ├── UnifiedTabGroupManager.kt # Group management
│ │ │ ├── TabGroup.kt # Database entities
│ │ │ └── TabGroupDatabase.kt # Room database
│ │ ├── profiles/ # Multi-profile system
│ │ ├── profile/ # Multi-profile system (ProfileManager, BrowserProfile)
│ │ └── ...
│ ├── search/ # Address-bar search dialog + AwesomeBar
│ ├── components/ # UI components
│ │ ├── toolbar/ # Browser toolbar (modern/classic)
│ │ │ └── modern/ # Modern Compose toolbar (PRIMARY)
Expand Down Expand Up @@ -88,10 +90,31 @@ nira-browser/
- `browser/tabs/modern/` - Old tab management system

### Multi-Profile System
- `browser/profiles/ProfileManager.kt` - Profile CRUD
- `browser/profiles/Profile.kt` - Database entities
- `browser/profile/ProfileManager.kt` - Profile CRUD
- `browser/profile/BrowserProfile.kt` - Profile model (`id = "default"` for the built-in profile)
- `components/toolbar/modern/ComposeTabBarWithProfileSwitcher.kt` - Profile UI

### Search (address bar vs unified search)
These are **two different UIs**. Do not merge them unless asked.

| UI | Entry | Role |
|----|--------|------|
| Address-bar search | `search/SearchDialogFragment.kt` | URL / search-engine suggestions while typing in the toolbar |
| Unified search | `browser/tabs/TabSearchFragment.kt` + `TabSearchAdapter.kt` | Grouped cards for tabs / bookmarks / history (profile + date chips) |

Address-bar stack:
- `BrowserActivity.load()` / `openToBrowserAndLoad()` — URL vs search decision
- `search/SearchDialogController.kt` — commit / suggestion tap
- `search/SearchFragmentStore.kt` — selected engine for the dialog
- `search/awesomebar/AwesomeBarView.kt` — provider wiring
- `search/awesomebar/AwesomeBarWrapper.kt` — Compose host; **must invoke** click / remove callbacks
- `search/awesomebar/NiraAwesomeBar.kt` — grouped rounded-card list (title only, no edit arrow)
- `search/awesomebar/SearchForQueryProvider.kt` — standalone `Search for "query"` row
- `search/awesomebar/NiraHistorySuggestionProvider.kt` — history rows + favicons
- `browser/SearchEngineList.kt` — Nira's default engines and `{searchTerms}` templates

History page (separate from suggestions): `history/HistoryActivity.kt` + `HistoryItemRecyclerViewAdapter.kt`. Use `FaviconLoader.loadFavicon()`, not cache-only.

### Progressive Web Apps (PWAs)
- `webapp/WebAppManager.kt` - PWA management
- `webapp/InstalledWebApp.kt` - Database entities
Expand Down Expand Up @@ -127,10 +150,20 @@ nira-browser/
- Look for color/theme issues in `theme/ColorConstants.kt`

### Profile-Related Issues
- `browser/profiles/ProfileManager.kt` - Profile management
- `browser/profile/ProfileManager.kt` - Profile management
- `components/toolbar/modern/` - Profile switching UI
- Check for contextId filtering in tab/group queries

### Search / "everything became a URL"
1. `BrowserActivity.load()`: if `engine == null`, input is treated as a URL (`toNormalizedUrl()`). Always resolve an engine first (`store.selectedOrDefaultSearchEngine` or `SearchEngineList.getSelectedEngine()`).
2. `String.isUrl()` is Mozilla's **lenient** `URLStringUtils.isURLLike` (no spaces + `.` / `:` / `://` counts as a URL). Do not replace it unless asked.
3. `SearchMiddleware` + `RegionMiddleware` load bundled engines **asynchronously**. `selectedOrDefaultSearchEngine` is often null on first search. Seed from `UserPreferences` / `SearchEngineList`.
4. `setupSearchEngines()` is deferred with `view.post` — do not assume engines are selected at Activity `onCreate`.
5. Engine `suggestUrl` must be a real OpenSearch template containing `{searchTerms}` (e.g. Google `complete/search?client=firefox&q={searchTerms}`). Homepage URLs produce zero suggestions.
6. `SearchSuggestionProvider` with `filterExactMatch = true` excludes the typed query. The typed query belongs in `SearchForQueryProvider` (`Search for "%s"`), **above** the suggestions group, not inside it.
7. Address-bar history/tab grouping by profile or date is **slow** (`getDetailedVisits`). Keep that only in unified search (`TabSearchFragment`).
8. History X-delete: `historyStorage.deleteVisitsFor(url)` using `suggestion.description` (URL). Hide the row via `hiddenSuggestions`.

### PWA Issues
- `webapp/WebAppManager.kt` - Installation, uninstallation
- Database schema in `webapp/InstalledWebApp.kt`
Expand Down Expand Up @@ -208,6 +241,14 @@ suspend fun getData() = withContext(Dispatchers.IO) {
- Prefer Compose for new features
- `modern/` packages indicate Compose implementations
- Legacy code often in root package or `modern/` may indicate old implementation
- **Never toggle `Modifier.animateItem()` after first composition** (e.g. after a delay). That crashes with `ArrayIndexOutOfBoundsException` in `LazyLayoutItemAnimator`. Keep the modifier stable or omit it.
- Lazy list keys must be unique across types: prefix `"tab-"` / `"group-"` / section id. Duplicate `item.id` values crash the same animator.
- `NiraTheme` must **not** cast `view.context as Activity`. Search dialogs wrap context in `ContextThemeWrapper`. Unwrap with a `ContextWrapper` loop (`findActivity()`).
- `AwesomeBarWrapper` used to pass empty `{ }` click lambdas — Compose AwesomeBar does not call `Suggestion.onSuggestionClicked` unless the wrapper does.

### Favicons
- Suggestions / history page: `FaviconLoader.loadFavicon(context, url)` (memory → disk → `BrowserIcons`).
- `FaviconCache.loadFavicon()` is cache-only and will miss most history icons.

## Testing

Expand Down Expand Up @@ -274,13 +315,16 @@ Ask the user for guidance when:
- `UnifiedTabGroupManager.kt` - Single source of truth for groups
- `TabViewModel.kt` - Tab UI state management
- `TabOrderManager.kt` - Tab/group ordering persistence
- `ProfileManager.kt` - Profile lifecycle management
- `ProfileManager.kt` - Profile lifecycle management (`browser/profile/`)
- `WebAppManager.kt` - PWA lifecycle management
- `ColorConstants.kt` - Color definitions and conversions
- `BrowserActivity.load()` - URL vs search; never treat missing engine as URL
- `SearchEngineList.kt` - Default engines + suggest URL templates
- `AwesomeBarWrapper.kt` / `NiraAwesomeBar.kt` - Address-bar suggestion UI

### Data Models
- `TabGroup.kt` - Room entity for groups
- `Profile.kt` - Room entity for profiles
- `BrowserProfile.kt` - Profile model (`browser/profile/`)
- `InstalledWebApp.kt` - Room entity for PWAs
- `UnifiedTabOrder.kt` - DataStore model for tab order

Expand Down Expand Up @@ -312,6 +356,6 @@ Common Mozilla components used:

---

**Last Updated**: March 2026
**Last Updated**: August 2026

For questions or updates to this guide, please open an issue or discussion on GitHub.
27 changes: 24 additions & 3 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ Database (Room) + DataStore
| `Components.kt` | Manual DI container — initializes all managers |
| `ColorConstants.kt` | Color palette definitions and string→Int conversion |
| `BrowserFragment.kt` | Main browser screen entry point |
| `BrowserActivity.load()` | URL vs search. `engine == null` must **not** fall through to `toNormalizedUrl()` |
| `SearchEngineList.kt` | Nira default engines; `suggestUrl` must include `{searchTerms}` |
| `AwesomeBarWrapper.kt` / `NiraAwesomeBar.kt` | Address-bar suggestion UI; wrapper must invoke click/remove callbacks |

### Package layout

Expand All @@ -56,12 +59,14 @@ app/src/main/java/com/prirai/android/nira/
│ ├── tabs/compose/ ← PRIMARY tab UI (Compose)
│ ├── tabs/modern/ ← LEGACY tab system (avoid modifying)
│ ├── tabgroups/ ← Group CRUD + Room DB
│ └── profiles/ ← Multi-profile system
│ └── profile/ ← Multi-profile (ProfileManager, BrowserProfile)
├── search/ ← Address-bar search dialog + AwesomeBar
├── components/
│ └── toolbar/modern/ ← PRIMARY toolbar (Compose)
├── settings/ ← Settings screens
├── webapp/ ← PWA support
├── theme/ ← Material 3 + color constants
├── history/ ← Full history page (not the AwesomeBar)
├── theme/ ← Material 3 + color constants (`ui/theme/Theme.kt` = NiraTheme)
├── addons/ ← WebExtension support
└── ext/ ← Kotlin extension functions
```
Expand Down Expand Up @@ -116,10 +121,26 @@ Every data entity (tab, group, PWA) is scoped to a `contextId`:

Always filter queries by `contextId`. The default profile must accept both `null` and `"profile_default"` for backward compatibility.

### Search vs URL
- Address-bar search (`SearchDialogFragment`) ≠ unified search (`TabSearchFragment`). Different UIs.
- `String.isUrl()` is Mozilla's **lenient** `isURLLike` (spaces absent + `.` / `:` / `://` ⇒ URL).
- `SearchMiddleware` loads engines asynchronously. `selectedOrDefaultSearchEngine` is often null at first type. Fall back to `SearchEngineList.getSelectedEngine(UserPreferences)`.
- `setupSearchEngines()` is deferred (`view.post`). Do not assume a selected engine in `onCreate`.
- Typed query belongs in `SearchForQueryProvider` (`Search for "term"`), **not** inside the suggestions group. `SearchSuggestionProvider(filterExactMatch = true)` already excludes the exact query.
- Do **not** group address-bar history/tabs by profile or date — `getDetailedVisits` is too slow. That grouping lives only in unified search.
- History delete from suggestions: `historyStorage.deleteVisitsFor(url)` using `suggestion.description`.

### UI: Compose vs Views
- `modern/` subdirectories → Compose implementations (prefer these)
- Legacy XML Views exist in some places — migrate to Compose for new work
- `TabSheetStateManager` is a singleton `object` that signals tab sheet dismissal events to the tab bar via a `StateFlow<Long>` timestamp; call `notifyTabSheetDismissed()` after closing the sheet
- Never toggle `Modifier.animateItem()` after first composition. Causes `ArrayIndexOutOfBoundsException` in `LazyLayoutItemAnimator`.
- Lazy keys must be unique across item types (`"tab-$id"` vs `"group-$id"`).
- `NiraTheme` must unwrap `ContextThemeWrapper` to find the Activity. `view.context as Activity` crashes in the search dialog.
- `AwesomeBarWrapper` must call `suggestion.onSuggestionClicked` / remove listeners itself.

### Favicons
- Use `FaviconLoader.loadFavicon(context, url)` (cache + BrowserIcons). `FaviconCache.loadFavicon()` is cache-only and misses most history icons.

### Database schema changes
Room migrations are required for any schema change. Ask before modifying entity classes.
Expand All @@ -141,4 +162,4 @@ For GeckoView and Mozilla Android Components questions, refer to:
- [Reference Browser](https://github.com/mozilla-mobile/reference-browser) — simpler implementation examples
- [GeckoView Docs](https://mozilla.github.io/geckoview/)

Current Mozilla Components version: **148.0** (defined in `build.gradle`).
Current Mozilla Components version: **153.0** (`mozComponentsVersion` in `app/build.gradle`). After AC bumps, re-check `SearchEngine` constructors, `SearchSuggestionProvider` headers, and `compose-awesomebar` click APIs — they change between 148 and 153.
13 changes: 13 additions & 0 deletions app/src/main/java/com/prirai/android/nira/components/Components.kt
Original file line number Diff line number Diff line change
Expand Up @@ -174,8 +174,21 @@ open class Components(private val applicationContext: Context) {
}
preferredColorScheme = darkEnabled()
javascriptEnabled = UserPreferences(applicationContext).javaScriptEnabled
val prefs = UserPreferences(applicationContext)
httpsOnlyMode = prefs.getHttpsOnlyMode()
dohSettingsMode = prefs.getDohSettingsMode()
dohProviderUrl = prefs.dohProviderUrl
globalPrivacyControlEnabled = prefs.globalPrivacyControl
}

fun applyPrivacyEngineSettings() {
val prefs = UserPreferences(applicationContext)
engine.settings.httpsOnlyMode = prefs.getHttpsOnlyMode()
engine.settings.dohSettingsMode = prefs.getDohSettingsMode()
engine.settings.dohProviderUrl = prefs.dohProviderUrl
engine.settings.globalPrivacyControlEnabled = prefs.globalPrivacyControl
}

private val notificationManagerCompat = NotificationManagerCompat.from(applicationContext)

val notificationsDelegate: NotificationsDelegate by lazy {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,6 @@ object UserJsPreferences {
private val securityPrefs = mapOf<String, Any>(
// HTTPS-first: try HTTPS before falling back to HTTP
"dom.security.https_first" to true,
// HTTPS-only: block all plain HTTP requests
"dom.security.https_only_mode" to true,
// Do not probe HTTP while in HTTPS-only mode
"dom.security.https_only_mode_send_http_background_request" to false,
// Block pop-up windows opened while a page is loading
Expand Down Expand Up @@ -146,8 +144,6 @@ object UserJsPreferences {
* they set the live runtime preference value directly, not just the Gecko default.
*/
fun applyTypedSettings(settings: GeckoRuntimeSettings) {
// Global Privacy Control (privacy.globalprivacycontrol.enabled)
settings.setGlobalPrivacyControl(true)
// Enhanced fingerprinting protection for both normal and private browsing
settings.setFingerprintingProtection(true)
settings.setFingerprintingProtectionPrivateBrowsing(true)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import com.prirai.android.nira.settings.HomepageBackgroundChoice
import com.prirai.android.nira.settings.HomepageChoice
import com.prirai.android.nira.settings.ThemeChoice
import com.prirai.android.nira.components.toolbar.ToolbarPosition
import mozilla.components.concept.engine.Engine
import mozilla.components.support.ktx.android.content.booleanPreference
import mozilla.components.support.ktx.android.content.floatPreference
import mozilla.components.support.ktx.android.content.intPreference
Expand Down Expand Up @@ -101,6 +102,28 @@ class UserPreferences(appContext: Context) : mozilla.components.support.ktx.andr
var etpSocialTracking by booleanPreference(ETP_SOCIAL_TRACKING, true)
var etpEmailTracking by booleanPreference(ETP_EMAIL_TRACKING, true)

// HTTPS-Only: 0=Off, 1=Private tabs only, 2=All tabs
var httpsOnlyMode by intPreference(HTTPS_ONLY_MODE, HTTPS_ONLY_ALL)
// DNS over HTTPS: 0=Default, 1=Increased, 2=Max, 3=Off
var dohMode by intPreference(DOH_MODE, DOH_INCREASED)
var dohProviderUrl by stringPreference(DOH_PROVIDER_URL, CLOUDFLARE_DOH_URI)
var globalPrivacyControl by booleanPreference(GLOBAL_PRIVACY_CONTROL, true)

fun getHttpsOnlyMode(): Engine.HttpsOnlyMode = when (httpsOnlyMode) {
HTTPS_ONLY_PRIVATE -> Engine.HttpsOnlyMode.ENABLED_PRIVATE_ONLY
HTTPS_ONLY_ALL -> Engine.HttpsOnlyMode.ENABLED
else -> Engine.HttpsOnlyMode.DISABLED
}

fun getDohSettingsMode(): Engine.DohSettingsMode = when (dohMode) {
DOH_INCREASED -> Engine.DohSettingsMode.INCREASED
DOH_MAX -> Engine.DohSettingsMode.MAX
DOH_OFF -> Engine.DohSettingsMode.OFF
else -> Engine.DohSettingsMode.DEFAULT
}

fun isDohProviderSelectable(): Boolean = dohMode == DOH_INCREASED || dohMode == DOH_MAX

// SECURITY: Third-party certificate trust disabled for security
// var trustThirdPartyCerts by booleanPreference(TRUST_THIRD_PARTY_CERTS, false)
var barAddonsList by stringPreference(BAR_ADDONS_LIST, "")
Expand Down Expand Up @@ -200,5 +223,21 @@ class UserPreferences(appContext: Context) : mozilla.components.support.ktx.andr
const val ETP_TRACKING_ADS = "etp_tracking_ads"
const val ETP_SOCIAL_TRACKING = "etp_social_tracking"
const val ETP_EMAIL_TRACKING = "etp_email_tracking"
const val HTTPS_ONLY_MODE = "https_only_mode"
const val DOH_MODE = "doh_mode"
const val DOH_PROVIDER_URL = "doh_provider_url"
const val GLOBAL_PRIVACY_CONTROL = "global_privacy_control"

const val HTTPS_ONLY_OFF = 0
const val HTTPS_ONLY_PRIVATE = 1
const val HTTPS_ONLY_ALL = 2

const val DOH_DEFAULT = 0
const val DOH_INCREASED = 1
const val DOH_MAX = 2
const val DOH_OFF = 3

const val CLOUDFLARE_DOH_URI = "https://mozilla.cloudflare-dns.com/dns-query"
const val NEXTDNS_DOH_URI = "https://firefox.dns.nextdns.io/"
}
}
Loading
Loading