Skip to content

Commit 0898df0

Browse files
v1.5.3+304: add Prominent Disclosure for location access (Play Store compliance)
Google Play reviewer rejected v1.5.2+303 Android submission with "Inadequate Prominent Disclosure" under the User Data policy: the in-app disclosure did not adequately explain how location data is accessed, used, or handled. Changes: - Rewrite PermissionsPage location card as a 4-section Prominent Disclosure: data accessed, how used, background access, where data goes. Explicitly states location stays on device, is only shared with directly-connected peers over encrypted BLE, and is never uploaded to any server or third party. - Guard LocationService.initialize() so it never silently requests location permission. Permission may only be requested from the onboarding card (which shows the disclosure first). - Bump version to 1.5.3+304 (and fix AppConstants.appVersion drift from 1.5.1 to 1.5.3). - Update google.releaseNotes in store.config.json. - Add V1.5.3 roadmap bullet to docs/roadmap.html.
1 parent dc579ec commit 0898df0

6 files changed

Lines changed: 179 additions & 16 deletions

File tree

docs/roadmap.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ <h1>Product <span class="accent">Roadmap</span></h1>
158158
<li>V1.5.1 patch: repaired Emergency Beacon wiring and Field Link scan/join flow</li>
159159
<li>V1.5.2 patch: fixed BLE peer sync, MGRS grid accuracy, mode-specific callouts, PIN flow</li>
160160
<li>V1.5.2 Android production submission to Google Play — full iOS feature parity, first production build since v1.3.1 closed beta</li>
161+
<li>V1.5.3 patch: expanded in-app location disclosure to clearly explain what location data is accessed, how it is used, and that it stays on your device</li>
161162
</ul>
162163
</div>
163164

lib/core/constants/app_constants.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ class AppConstants {
33
AppConstants._();
44

55
static const String appName = 'Red Grid Link';
6-
static const String appVersion = '1.5.1';
6+
static const String appVersion = '1.5.3';
77

88
// Device limits
99
static const int maxDevices = 8;

lib/services/location/location_service.dart

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -207,13 +207,24 @@ class LocationService {
207207

208208
/// Initialize the location service.
209209
///
210-
/// Checks permissions and starts the GPS position stream if
211-
/// permission is granted. If permission is denied, the stream
212-
/// will remain empty until [requestPermission] is called and
213-
/// [initialize] is re-invoked.
210+
/// Checks current permission status and starts the GPS position
211+
/// stream only if permission is already granted.
212+
///
213+
/// IMPORTANT: This method deliberately does NOT request permission.
214+
/// Permission may only be requested from UI surfaces that first
215+
/// display an in-app Prominent Disclosure explaining what location
216+
/// data is accessed, how it is used, and how it is handled (e.g.
217+
/// the onboarding PermissionsPage). Requesting permission silently
218+
/// on app launch — without a prior disclosure — violates the Google
219+
/// Play "Prominent Disclosure and Consent" policy and has been the
220+
/// cause of prior review rejections.
221+
///
222+
/// If permission is not yet granted, the stream stays empty. The
223+
/// user can grant it explicitly from onboarding or device Settings,
224+
/// and [initialize] will start the stream on next invocation.
214225
Future<void> initialize() async {
215-
final hasPermission = await requestPermission();
216-
if (!hasPermission) return;
226+
final status = await _permissionHandler.checkStatus();
227+
if (status != LocationPermissionStatus.granted) return;
217228

218229
_startStream();
219230
}

lib/ui/screens/onboarding/widgets/permissions_page.dart

Lines changed: 158 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -258,13 +258,9 @@ class _PermissionsPageState extends State<PermissionsPage>
258258
),
259259
const SizedBox(height: 24),
260260

261-
// Location permission
262-
_PermissionCard(
263-
icon: Icons.my_location,
264-
title: 'LOCATION ACCESS',
265-
description:
266-
'Required for MGRS grid display, map positioning, '
267-
'and sharing your location with nearby team members.',
261+
// Location permission — Prominent Disclosure
262+
// (Google Play User Data policy: Prominent Disclosure and Consent)
263+
_LocationDisclosureCard(
268264
isGranted: _locationGranted,
269265
isChecked: _locationChecked,
270266
isRequesting: _locationRequesting,
@@ -318,6 +314,161 @@ class _PermissionsPageState extends State<PermissionsPage>
318314
}
319315
}
320316

317+
/// Prominent Disclosure card for location access.
318+
///
319+
/// This card satisfies Google Play's User Data policy "Prominent
320+
/// Disclosure and Consent Requirement" for apps that access location
321+
/// (including background location). It spells out, before the OS
322+
/// runtime permission prompt fires, exactly what data the app
323+
/// accesses, how it is used, what happens in the background, and
324+
/// where the data goes (or does not go).
325+
///
326+
/// Do NOT request location permission without showing this card first.
327+
class _LocationDisclosureCard extends StatelessWidget {
328+
const _LocationDisclosureCard({
329+
required this.isGranted,
330+
required this.isChecked,
331+
required this.isRequesting,
332+
required this.colors,
333+
required this.onRequest,
334+
});
335+
336+
final bool isGranted;
337+
final bool isChecked;
338+
final bool isRequesting;
339+
final TacticalColorScheme colors;
340+
final VoidCallback onRequest;
341+
342+
@override
343+
Widget build(BuildContext context) {
344+
return TacticalCard(
345+
colors: colors,
346+
padding: const EdgeInsets.all(16),
347+
child: Column(
348+
crossAxisAlignment: CrossAxisAlignment.start,
349+
children: [
350+
// Header row
351+
Row(
352+
children: [
353+
Icon(Icons.my_location, size: 24, color: colors.accent),
354+
const SizedBox(width: 12),
355+
Expanded(
356+
child: Text(
357+
'LOCATION ACCESS',
358+
style: TacticalTextStyles.subheading(colors),
359+
),
360+
),
361+
if (isChecked && isGranted)
362+
Icon(Icons.check_circle, size: 24, color: colors.accent),
363+
],
364+
),
365+
const SizedBox(height: 12),
366+
367+
// Prominent disclosure — four labelled sections.
368+
_DisclosureSection(
369+
label: 'DATA ACCESSED',
370+
body:
371+
'Your precise location from the device\u2019s GPS — '
372+
'latitude, longitude, altitude, speed, and heading.',
373+
colors: colors,
374+
),
375+
_DisclosureSection(
376+
label: 'HOW RED GRID LINK USES IT',
377+
body:
378+
'Displays your position on the map, computes your MGRS '
379+
'grid, records GPS tracks during sessions, shares your '
380+
'position with connected teammates over a direct '
381+
'Bluetooth peer-to-peer mesh, and powers the emergency '
382+
'beacon and boundary alerts.',
383+
colors: colors,
384+
),
385+
_DisclosureSection(
386+
label: 'BACKGROUND ACCESS',
387+
body:
388+
'If you separately allow "Allow all the time" in your '
389+
'device Settings, the app continues to read and share '
390+
'your position with connected teammates while the '
391+
'screen is off or another app is in the foreground. '
392+
'This is what lets the team keep seeing you during an '
393+
'active search-and-rescue mission. You can revoke this '
394+
'at any time in Settings.',
395+
colors: colors,
396+
),
397+
_DisclosureSection(
398+
label: 'WHERE YOUR DATA GOES',
399+
body:
400+
'Your location data stays on your device and is shared '
401+
'only with teammates you are directly connected to over '
402+
'encrypted Bluetooth Low Energy. Red Grid Link has no '
403+
'user accounts, no cloud sync, and no analytics. Your '
404+
'location is never uploaded to our servers, any third '
405+
'party, or any advertising network.',
406+
colors: colors,
407+
),
408+
const SizedBox(height: 4),
409+
Text(
410+
'See Settings \u2192 Privacy Policy for full details.',
411+
style: TacticalTextStyles.dim(colors),
412+
),
413+
if (isChecked && !isGranted) ...[
414+
const SizedBox(height: 12),
415+
if (isRequesting)
416+
SizedBox(
417+
height: 44,
418+
child: Center(
419+
child: SizedBox(
420+
width: 24,
421+
height: 24,
422+
child: CircularProgressIndicator(
423+
strokeWidth: 2,
424+
color: colors.accent,
425+
),
426+
),
427+
),
428+
)
429+
else
430+
TacticalButton(
431+
label: 'Grant Location Access',
432+
icon: Icons.shield,
433+
colors: colors,
434+
isCompact: true,
435+
onPressed: onRequest,
436+
),
437+
],
438+
],
439+
),
440+
);
441+
}
442+
}
443+
444+
/// A single labelled paragraph inside the location Prominent Disclosure.
445+
class _DisclosureSection extends StatelessWidget {
446+
const _DisclosureSection({
447+
required this.label,
448+
required this.body,
449+
required this.colors,
450+
});
451+
452+
final String label;
453+
final String body;
454+
final TacticalColorScheme colors;
455+
456+
@override
457+
Widget build(BuildContext context) {
458+
return Padding(
459+
padding: const EdgeInsets.only(bottom: 10),
460+
child: Column(
461+
crossAxisAlignment: CrossAxisAlignment.start,
462+
children: [
463+
Text(label, style: TacticalTextStyles.label(colors)),
464+
const SizedBox(height: 2),
465+
Text(body, style: TacticalTextStyles.caption(colors)),
466+
],
467+
),
468+
);
469+
}
470+
}
471+
321472
class _PermissionCard extends StatelessWidget {
322473
const _PermissionCard({
323474
required this.icon,

pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name: red_grid_link
22
description: Offline-first MGRS-native proximity coordination platform
33
publish_to: 'none'
4-
version: 1.5.2+303
4+
version: 1.5.3+304
55

66
environment:
77
sdk: '>=3.2.0 <4.0.0'

store.config.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@
6969
"title": "Red Grid Link",
7070
"shortDescription": "ATAK alternative: encrypted Bluetooth team tracking + offline MGRS maps.",
7171
"fullDescription": "Your team. Your grid. No cell towers required.\n\nInspired by military blue force tracking systems like ATAK, Red Grid Link brings encrypted team coordination to civilian and professional teams -- without cell towers, accounts, or servers. Track your team's positions in real time over Bluetooth, navigate with military-grade MGRS precision, and operate completely offline.\n\nRed Grid Link is an offline-first, MGRS-native coordination platform for small teams (2-8 people) operating beyond reliable cell service. It combines precision land navigation with Field Link -- zero-config proximity sync over Bluetooth and peer-to-peer Wi-Fi. Lightweight where ATAK is complex. Cross-platform where ATAK is Android-only. Zero infrastructure where ATAK requires TAK Server.\n\nFIELD LINK -- TEAM SYNC WITHOUT INFRASTRUCTURE:\nField Link is what sets Red Grid Link apart. Devices within proximity automatically discover each other and share encrypted position and marker data -- no internet, no configuration, no pairing codes. Just turn it on and your team appears on the map.\n- BLE + peer-to-peer Wi-Fi proximity transport (cross-platform)\n- AES-256-GCM encrypted sync with ECDH ephemeral session keys\n- Compact delta payloads (under 200 bytes per position update)\n- Tiered session security: Open (auto-join), PIN (4-digit), or QR code\n- Ghost markers: see last-known positions when teammates move out of range\n- Time-decay visualization: full opacity fades to outline over 30 minutes\n- Velocity vectors project teammate movement direction at disconnect\n- Snap-to-live animation on reconnect\n- Expedition Mode: under 3% battery per hour (BLE-only, 30-second updates)\n- Ultra Expedition Mode: under 2% battery per hour (BLE-only, 60-second updates)\n- Auto-reconnect with exponential backoff on disconnect\n\nMGRS-NATIVE NAVIGATION:\nBuilt on the proven MGRS engine from Red Grid MGRS -- the same coordinate system used by NATO forces worldwide.\n- Live MGRS coordinates (4/6/8/10-digit precision)\n- MGRS grid overlay on offline maps (GZD to 100m resolution)\n- Bearing, distance, and dead reckoning tools\n- Magnetic declination (WMM model)\n- NATO phonetic voice readout for hands-free grid calls\n\n11 TACTICAL TOOLS:\n- Dead Reckoning plotter\n- Two-point Resection\n- Pace Count tracker\n- Bearing calculator with back azimuth\n- Coordinate Converter (MGRS, lat/lon, DMS, UTM)\n- Range Estimation (mil-relation formula)\n- Slope Calculator (percentage and angle)\n- ETA / Speed Calculator\n- Magnetic Declination converter\n- Celestial Navigation (sun/moon bearing reference)\n- MGRS Precision Reference\n\nOFFLINE MAP SYSTEM:\n- Downloadable map packs from USGS Topo and OpenTopoMap\n- Full offline operation -- maps cached locally as MBTiles\n- MGRS grid lines rendered as a dynamic overlay at all zoom levels\n\n4 OPERATIONAL MODES:\nOne engine, four presentation layers. Terminology, icons, and quick actions adapt to your mission:\n- Search & Rescue -- sector assignments, clue markers, search patterns\n- Backcountry -- camp, waypoint, and trail navigation\n- Hunting -- stand locations, game sightings, property boundaries\n- Training -- exercise objectives, rally points, phase lines\n\nAFTER-ACTION REPORTS:\nOne-tap PDF export containing map snapshot, mission timeline, track data, timestamps, team roster, markers, and session log.\n\n4 TACTICAL DISPLAY THEMES:\n- Red Light: preserve night-adapted vision (free)\n- NVG Green: night observation device compatibility (Pro)\n- Day White: high-contrast for daylight (Pro)\n- Blue Force: tactical blue display (Pro)\n\nBUILT FOR THE FIELD:\n- Glove-friendly UI with 44px+ minimum touch targets\n- 3-tap maximum to any primary action\n- Haptic feedback on key interactions\n- Accelerometer-based step detection for hands-free pace counting\n- Landscape and portrait support\n- Background location updates with battery drain projection\n\nZERO FOOTPRINT PRIVACY:\n- No accounts. No sign-up. No login.\n- No cloud sync. No analytics. No tracking.\n- No ads. No third-party data SDKs.\n- Location data stays on your device.\n- Field Link data is ephemeral -- nothing persists after the session ends.\n- All Field Link communication encrypted with AES-256-GCM.\n- In-app purchases processed by Apple/Google only.",
72-
"releaseNotes": "Now available on Android!\n\nRed Grid Link v1.5.2 brings encrypted Bluetooth team coordination to Android:\n\n- Field Link peer sync: teammates appear on the map in real time over Bluetooth.\n- MGRS precision: corrected grid square calculation for all UTM zones.\n- 11 tactical tools: dead reckoning, resection, celestial navigation, and more.\n- 4 display themes: Red Light, NVG Green, Day White, Blue Force.\n- Mode-specific callouts: SAR, Backcountry, Hunting, and Training.\n- Zero footprint: no accounts, no tracking, no servers. AES-256-GCM encrypted.",
72+
"releaseNotes": "Red Grid Link v1.5.3:\n\n- Clearer in-app explanation of how your location is used, where it is shared, and that it stays on your device.\n- Streamlined initial location permission flow.\n\nAll v1.5.2 features for Android:\n- Field Link peer sync: teammates appear on the map in real time over Bluetooth.\n- MGRS precision: corrected grid square calculation for all UTM zones.\n- 11 tactical tools, 4 display themes, 4 operational modes.\n- Zero footprint: no accounts, no tracking, no servers. AES-256-GCM encrypted.",
7373
"category": "NAVIGATION",
7474
"contentRating": "Everyone",
7575
"privacyPolicyUrl": "https://github.com/RedGridTactical/RedGridLink/blob/master/PRIVACY.md",

0 commit comments

Comments
 (0)