Skip to content

feat(kids flavor): opt-in parental controls for family Signal use#747

Open
avacar wants to merge 10 commits into
mollyim:mainfrom
avacar:pr/parental-controls
Open

feat(kids flavor): opt-in parental controls for family Signal use#747
avacar wants to merge 10 commits into
mollyim:mainfrom
avacar:pr/parental-controls

Conversation

@avacar

@avacar avacar commented May 4, 2026

Copy link
Copy Markdown

Summary

This PR adds an opt-in kids Gradle build flavor to Molly that enables parental controls
for families who want to use Signal-compatible messaging with young children.

All changes are behind the kids flavor flag. The existing prod and staging flavors are
completely unaffected — this is purely additive.


Motivation

Families with young children often want a secure, private messaging channel with grandparents,
relatives, and family friends. Signal is an excellent technical foundation for this, but it has
no concept of a "restricted" child account. The child can message anyone, see all contacts, and
change account settings.

This fork adds a PIN-gated parental layer so parents can:

  • Allow only specific group chats to be visible to the child
  • Block the child from starting new conversations or making calls
  • Prevent access to account settings, Stories, and linked device management
  • Manage the allow-list from a parent settings panel on the child's device

Registration uses free Google Voice numbers so children never expose personal phone numbers.
Communication happens via Signal usernames, requiring no app changes.


What's in this PR

Phase Feature
0 kids Gradle build flavor + parentalModeEnabled flag
1 ParentalControlValues.kt — PIN storage (SHA-256 + random salt) and allowed-thread list
2 Conversation list filtering — show only allowed threads
3 & 4 Block new outbound conversations/calls; gate group invites behind PIN
5 Suppress notifications from non-allowed threads
6 Hide Stories tab and lock account/linked-device settings
7 ParentalControlActivity — PIN-gated parent settings panel on the child's device

62 unit tests cover the parental-control layer. All pass on the current build.


Architecture — why this is safe to merge

New files only for parental logic. All parental UI and data live in:

  • app/src/main/java/.../keyvalue/ParentalControlValues.kt
  • app/src/main/java/.../parental/ (Activity, ViewModel, dialogs)

Minimal touch of existing files. Guard clauses are added at a handful of call sites:

if (SignalStore.parental().parentalModeEnabled) { /* restrict */ }

No Signal protocol, crypto, database schema, or network code is modified.

Zero-conflict upstream tracking. This branch is based on Molly's current main and has
been kept in sync through multiple upstream releases with zero merge conflicts.

Opt-in at build time. A standard ./gradlew assembleProdRelease produces the normal Molly
APK. The parental controls only activate in a kidsRelease build.


Public fork

The full fork with history and documentation is at:
https://github.com/avacar/mollykids-android


Notes for reviewers

  • If this scope is too large to consider for mainline Molly, I'm happy to discuss a narrower
    contribution (e.g. just the kids flavor scaffolding in Phase 0, with the rest maintained
    downstream).
  • The ParentalControlActivity is intentionally a standalone AppCompatActivity rather than
    a destination in AppSettingsActivity's NavGraph, to avoid upstream merge conflicts in the
    settings navigation graph.
  • The PIN is stored as SHA-256(salt + PIN) with a random 16-byte salt. No plaintext PIN is
    ever persisted.

@avacar
avacar force-pushed the pr/parental-controls branch from ec38f08 to cf48d44 Compare May 5, 2026 00:32
avacar and others added 10 commits May 4, 2026 20:35
Adds a 'kids' flavor to the distribution dimension. The flavor enables
a parental mode build that restricts children to pre-approved contacts.
No changes to the standard prod/staging flavors.
- Create ParentalControlValues.kt extending SignalStoreValues to store:
  * parentalModeEnabled (defaults to true)
  * parentPinHash (empty string = no PIN set)
  * PIN salt (auto-generated random 16-byte salt, stored locally)
  * allowedThreadIds (Set of conversation thread IDs)
- computePinHash() helper using SHA-256(salt + pin)
- Register in SignalStore.kt (init, onFirstEverAppLaunch, accessor)
- Create comprehensive unit tests covering defaults, PIN hashing, salt generation, thread ID storage
- No backup inclusion (parental config is device-local)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fixed ParentalControlValuesTest: wrap KeyValueDataSet in an
  in-memory KeyValuePersistentStorage (upstream API change)
- Fixed ParentalControlValuesTest: replace containsExactly with
  isEqualTo(setOf(...)) for Set<Long> assertion
- Added JDK path properties to gradle.properties
When parental mode is enabled, ConversationListViewModel.conversationsState
now filters out any THREAD-type conversation not in the parent-approved
allowedThreadIds set. Non-THREAD items (headers, footers) always pass
through to preserve list structure.

ParentalControlValues gains a settingsChanges PublishSubject that emits
whenever parentalModeEnabled or allowedThreadIds changes; the ViewModel
subscribes and invalidates the paging controller so the filtered list
updates reactively without touching any database queries.

14 unit tests added across ParentalControlValuesTest and the new
ConversationListParentalFilterTest (10 tests for filter logic,
2 for dynamic settingsChanges emission, 2 previously existing).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ind PIN

Phase 3 — Block New Conversations & Calls:
- Hide compose and call FABs in parental mode (MainFloatingActionButtons)
- Safety-net finish() in NewConversationActivity and NewCallActivity
- Suppress incoming 1:1 calls from non-allowed threads (IncomingCallActionProcessor)
- Suppress incoming group call rings from non-allowed threads (IncomingGroupCallActionProcessor)
- isThreadCallAllowed() helper in ParentalControlValues
- 7 tests in ParentalCallGuardTest + 5 more in ParentalControlValuesTest (27 total)

Phase 4 — Group Invite PIN Gate:
- Invites hidden from children; parents manage via PIN-gated overflow menu item
- verifyPin() and addAllowedThreadId() added to ParentalControlValues
- GROUP_V2_INVITE accept UI suppressed in ConversationFragment
- Group-link join blocked in CommunicationActions (toast shown)
- ParentalPinDialog: reusable PIN entry dialog
- PendingGroupInvitesViewModel + PendingGroupInvitesFragment (BottomSheet)
- "Pending group invites" item in MainToolbar, wired in MainActivity
- Accepting invite auto-adds thread to allowedThreadIds → Phase 2 filter shows it
- 7 tests in ParentalInviteGuardTest + 5 more in ParentalControlValuesTest (32 total)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- NotificationState.filterThreads(allowedThreadIds): filters conversations list
  using data class copy(); preserves mute/profile filtered message side-lists
- DefaultMessageNotifier.updateNotification(): applies filter immediately after
  constructNotificationState(); existing isEmpty early-exit silences all
  notifications when no threads are allowed
- NotificationStateParentalFilterTest: 5 pure unit tests (no Robolectric needed)
  covering allowed retained, disallowed removed, mixed, empty set, side-lists
- 37 total parental-control tests green; assembleProdKidsDebug BUILD SUCCESSFUL

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Extract buildNavEntries() helper in MainNavigation.kt; both MainNavigationBar
  and MainNavigationRail now filter STORIES when parentalModeEnabled is true
- AppSettingsFragment: hide Account, Linked Devices, Donate to Signal rows
  (and their trailing divider) behind !parentalModeEnabled guard
- PrivacySettingsFragment: hide Privacy > Advanced clickPref behind
  !parentalModeEnabled guard
- Add ParentalNavigationFilterTest (4 tests); 41 parental tests total, all green

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a "Parent Controls" entry to the main overflow menu (visible when
parental mode is enabled). Tapping it requires PIN entry, then opens
ParentalControlActivity with three sections:
- Master toggle for parental mode
- Per-thread switch list to manage allowedThreadIds
- Change PIN (two-field dialog with length validation)

Fresh-install path: if no PIN is set, the activity opens directly with
a PIN-setup dialog instead of the verification dialog.

New files: ParentalControlActivity, ParentalControlViewModel,
activity_parental_control.xml, item_parental_thread.xml,
ParentalControlViewModelTest (6 tests; 62 total parental tests green).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ntal guards

- Remove applicationIdSuffix from kids flavor (caused install conflicts)
- Fix New Group menu item visibility in parental mode
- Fix Create a call link row appearing in Calls tab via paging consistency
- Add parental guards to ConversationSettingsActivity and ConversationOptionsMenu
- Tighten MainToolbar/MainNavigationRepository parental filtering
- Update project_status.md with 2026-05-03 session notes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@avacar
avacar force-pushed the pr/parental-controls branch from cf48d44 to d02c862 Compare May 5, 2026 00:36
@valldrac
valldrac force-pushed the main branch 7 times, most recently from 469424f to bf57ae5 Compare July 15, 2026 00:13
@bluecat21

bluecat21 commented Jul 16, 2026

Copy link
Copy Markdown

This is great!

I happen to be aware of parents who will not let their children use anything other than a landline or dumb phone without images if that exists because they are concerned about their child seeing genitalia, gore, etc. from their school friends who don't have a locked down device.

I propose these remedies: allow disabling receiving images and videos, video calls, and eventually use an AI model to detect nudity and gore and block it. This is provided as part of AICore (app included on Google Mobile Services devices) and does function locally, but an open source alternative could be provided in the future and since it will likely use significant storage it should be provided as an addon pack via another app (in the same way as icon packs) which can be updated by Molly to avoid dynamic code loading or wasting storage for users who don't want this.

Separately, allow disabling sending images and videos to prevent them from sending inappropriate images.

It probably makes sense for the settings for sending/receiving images/videos and video calls to be per-contact.

@bluecat21

bluecat21 commented Jul 16, 2026

Copy link
Copy Markdown

And possibly somewhat controversially, deleted messages should be able to be sent to a quarantine so that parents can review them later.

I know this might upset some people but I dare deeply for children and this would not impact the privacy, security, freedom or anything else of adults and would allow concerned parents to choose a safe option where if their children make a mistake, their messages are not stored in a central database, contrary to mainstream "kid friendly" messengers or SMS monitoring tools.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants