getreadystack's picture
Upload index.html with huggingface_hub
e1b4c43 verified
Raw History Blame Contribute Delete
48.1 kB
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Android foreground service permission — check manifest declarations</title>
<meta name="description" content="This free browser tool reads an AndroidManifest.xml and identifies lines associated with extra Google Play Console declarations or Android 14+ foreground-service permissions.">
<style>
:root{color-scheme:light}body{margin:0;font:15px/1.55 system-ui,-apple-system,"Segoe UI",sans-serif;background:#faf9f5;color:#1a1512}
.wrap{max-width:960px;margin:0 auto;padding:28px 18px 60px}h1{font-size:26px;margin:0 0 6px}.lede{color:#4a403a;margin:0 0 16px}
textarea{width:100%;min-height:260px;font:13px/1.45 ui-monospace,Menlo,Consolas,monospace;padding:12px;border:1px solid #d9d0c4;border-radius:8px;box-sizing:border-box}
.bar{display:flex;gap:10px;align-items:center;margin:12px 0}button{font:600 15px system-ui;padding:10px 18px;border:0;border-radius:8px;background:#1a1512;color:#fff;cursor:pointer}
input[type=date]{font:14px system-ui;padding:8px;border:1px solid #d9d0c4;border-radius:6px}
table{border-collapse:collapse;width:100%;font-size:14px;margin-top:10px}th,td{text-align:left;padding:8px 10px;border-bottom:1px solid #eee;vertical-align:top}th{font-size:12px;letter-spacing:.06em;text-transform:uppercase;color:#7a6e65}
.sev.error{color:#a8551f;font-weight:700}.sev.warn,.sev.warning{color:#96691f}.ok{color:#2a6446;font-weight:600}.sum{font-weight:600}
.note{font-size:13px;color:#7a6e65;margin-top:18px}#paid{margin-top:22px;padding:14px 16px;border:1px solid #e2d8cb;border-radius:10px;background:#fffdfa}
</style>
<script data-grow-initializer="">!(function(){window.growMe||((window.growMe=function(e){window.growMe._.push(e);}),(window.growMe._=[]));var e=document.createElement("script");(e.type="text/javascript"),(e.src="https://faves.grow.me/main.js"),(e.defer=!0),e.setAttribute("data-grow-faves-site-id","U2l0ZTphZjljMzgyMC0yMmU5LTRhYjgtYTIxNS03OTRhMGY1N2ExYzM=");var t=document.getElementsByTagName("script")[0];t.parentNode.insertBefore(e,t);})();</script>
<!--yj-canon-s101--><link rel="canonical" href="https://getreadystack.com/tools/play-declaration-gap-lint"></head>
<body>
<div class="wrap">
<h1>Android foreground service permission — check manifest declarations</h1>
<p class="lede">Reads AndroidManifest.xml and names every line that forces an extra Google Play Console declaration or an Android 14+ foreground-service permission, with the form Play will ask you to fill. Runs entirely in your browser — nothing is uploaded.</p>
<textarea id="src" spellcheck="false">&lt;?xml version="1.0" encoding="utf-8"?&gt;
&lt;manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.deliverypro"&gt;
&lt;uses-permission android:name="android.permission.INTERNET" /&gt;
&lt;uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" /&gt;
&lt;uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" /&gt;
&lt;uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /&gt;
&lt;uses-permission
android:name="android.permission.ACCESS_BACKGROUND_LOCATION" /&gt;
&lt;uses-permission android:name="android.permission.READ_SMS" /&gt;
&lt;uses-permission android:name="android.permission.READ_CALL_LOG" /&gt;
&lt;uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" /&gt;
&lt;uses-permission android:name="android.permission.READ_MEDIA_IMAGES" /&gt;
&lt;uses-permission android:name="android.permission.READ_MEDIA_VIDEO" /&gt;
&lt;uses-permission android:name="android.permission.health.READ_HEART_RATE" /&gt;
&lt;uses-permission android:name="android.permission.health.READ_STEPS" /&gt;
&lt;uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" /&gt;
&lt;uses-permission android:name="android.permission.USE_EXACT_ALARM" /&gt;
&lt;uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" /&gt;
&lt;uses-permission android:name="com.google.android.gms.permission.AD_ID" /&gt;
&lt;uses-permission android:name="android.permission.FOREGROUND_SERVICE" /&gt;
&lt;application
android:label="Delivery Pro"
android:allowBackup="true"&gt;
&lt;activity android:name=".MainActivity" android:exported="true"&gt;
&lt;intent-filter&gt;
&lt;action android:name="android.intent.action.MAIN" /&gt;
&lt;category android:name="android.intent.category.LAUNCHER" /&gt;
&lt;/intent-filter&gt;
&lt;/activity&gt;
&lt;service
android:name=".tracking.RouteService"
android:exported="false"
android:foregroundServiceType="location|camera" /&gt;
&lt;service
android:name=".upload.NightUploadService"
android:exported="false"
android:foregroundServiceType="specialUse"&gt;
&lt;intent-filter&gt;
&lt;action android:name="android.intent.action.SYNC" /&gt;
&lt;/intent-filter&gt;
&lt;/service&gt;
&lt;service
android:name=".assist.RouteAssistService"
android:exported="true"
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"&gt;
&lt;intent-filter&gt;
&lt;action android:name="android.accessibilityservice.AccessibilityService" /&gt;
&lt;/intent-filter&gt;
&lt;/service&gt;
&lt;/application&gt;
&lt;/manifest&gt;</textarea>
<div class="bar"><button id="go">Check</button><label>as of <input type="date" id="today"></label></div>
<div id="out" aria-live="polite"></div>
<div id="paid"><strong>Free here:</strong> Check the AndroidManifest.xml open in your editor and name every Play Console declaration it triggers, line by line. <strong>In the editor:</strong> Sweep every manifest in the workspace (app plus library and SDK modules) and write a dated declaration worksheet you keep and hand to review — that part asks for a licence key.</div>
<p class="note">Same engine as the VS Code extension, byte for byte.</p>
</div>
<script>var PLAYDECL_RULES = [
{"id": "package_visibility", "sev": "error", "kind": "perm", "perm": "android.permission.QUERY_ALL_PACKAGES",
"form": "App visibility permission declaration",
"msg": "QUERY_ALL_PACKAGES is a restricted permission: Play Console asks for the App visibility declaration and accepts it only for eligible use cases (browsers, launchers, security, device management). Otherwise list the packages you need under \u003cqueries\u003e instead."},
{"id": "all_files_access", "sev": "error", "kind": "perm", "perm": "android.permission.MANAGE_EXTERNAL_STORAGE",
"form": "All files access permission declaration",
"msg": "MANAGE_EXTERNAL_STORAGE (All files access) needs the All files access declaration in Play Console and a core-functionality justification; file pickers and scoped storage are expected instead."},
{"id": "background_location", "sev": "error", "kind": "perm", "perm": "android.permission.ACCESS_BACKGROUND_LOCATION",
"form": "Location permissions declaration",
"msg": "ACCESS_BACKGROUND_LOCATION triggers the Location permissions declaration: Play wants the feature that needs background location, a video of it, and a prominent in-app disclosure before the request."},
{"id": "sms_call_log", "sev": "error", "kind": "perm_any",
"perms": ["android.permission.READ_SMS", "android.permission.RECEIVE_SMS", "android.permission.SEND_SMS", "android.permission.READ_CALL_LOG", "android.permission.WRITE_CALL_LOG", "android.permission.PROCESS_OUTGOING_CALLS"],
"form": "Permissions declaration (SMS and Call Log)",
"msg": "SMS and Call Log permissions are restricted: Play grants them mainly to the app the user has set as default handler, and every other use needs the Permissions declaration with an approved exception."},
{"id": "install_packages", "sev": "error", "kind": "perm", "perm": "android.permission.REQUEST_INSTALL_PACKAGES",
"form": "Install unknown apps declaration",
"msg": "REQUEST_INSTALL_PACKAGES has its own Play declaration: say which files you install and why, or the update is rejected as unapproved app distribution."},
{"id": "photo_video_broad", "sev": "error", "kind": "perm_any",
"perms": ["android.permission.READ_MEDIA_IMAGES", "android.permission.READ_MEDIA_VIDEO"],
"form": "Photo and video permissions declaration",
"msg": "Broad photo and video access needs the Photo and video permissions declaration: Play expects the Android photo picker for one-off or infrequent access, and broad access only where it is core functionality."},
{"id": "health_data", "sev": "error", "kind": "perm_prefix", "prefix": "android.permission.health.",
"form": "Health apps declaration (Health Connect)",
"msg": "Health Connect read or write permissions need the Health apps declaration form approved before release, plus a privacy policy that names the health data you read."},
{"id": "full_screen_intent", "sev": "warn", "kind": "perm", "perm": "android.permission.USE_FULL_SCREEN_INTENT",
"form": "Full-screen intent permission declaration",
"msg": "From Android 14 USE_FULL_SCREEN_INTENT is granted at install only to calling and alarm apps; everything else must justify it to Play and fall back gracefully when the user has turned it off."},
{"id": "use_exact_alarm", "sev": "error", "kind": "perm", "perm": "android.permission.USE_EXACT_ALARM",
"form": "Exact alarm permission declaration",
"msg": "USE_EXACT_ALARM is restricted to alarm clock and calendar apps; if that is not your app Play expects SCHEDULE_EXACT_ALARM with a user-facing setting, or WorkManager."},
{"id": "schedule_exact_alarm", "sev": "warn", "kind": "perm", "perm": "android.permission.SCHEDULE_EXACT_ALARM",
"form": "Exact alarm use case (Play policy answer)",
"msg": "SCHEDULE_EXACT_ALARM is not granted by default on Android 13 and later: check canScheduleExactAlarms() and send the user to the Alarms and reminders setting, and keep the use case ready for review."},
{"id": "ad_id", "sev": "warn", "kind": "perm", "perm": "com.google.android.gms.permission.AD_ID",
"form": "Data safety advertising ID disclosure",
"msg": "Declaring AD_ID means the Data safety form must disclose that the app collects the advertising ID and for what purpose; the two answers are cross-checked at review."},
{"id": "accessibility_service", "sev": "error", "kind": "service_perm", "perm": "android.permission.BIND_ACCESSIBILITY_SERVICE",
"form": "Accessibility API prominent disclosure",
"msg": "An accessibility service is bound here: Play requires a prominent in-app disclosure of what the service reads and does, and rejects accessibility use that is not an accessibility feature."},
{"id": "fgs_use_case", "sev": "warn", "kind": "fgs_any",
"form": "Foreground service use case declaration",
"msg": "Each foregroundServiceType in the manifest needs a matching use case in the Play Console foreground service declaration, with a video showing the service running."},
{"id": "fgs_permission_missing", "sev": "error", "kind": "fgs_permission",
"form": "No form - Android 14 runtime requirement",
"msg": "Android 14 (API 34) throws SecurityException when a foreground service starts with a type whose FOREGROUND_SERVICE_* permission is missing from the manifest."},
{"id": "special_use_subtype", "sev": "error", "kind": "special_use",
"form": "Foreground service declaration (specialUse subtype)",
"msg": "foregroundServiceType=\"specialUse\" needs a \u003cproperty android:name=\"android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE\"\u003e inside the same \u003cservice\u003e, and Play reviews that subtype string by hand."}
]
;</script>
<script>// Play Declaration Gap Lint — the brain. Same file runs in Node (extension) and in the browser (free web check).
'use strict';
var RULES = (typeof module !== 'undefined' && module.exports) ? require('./rules.json') : window.PLAYDECL_RULES;
// foregroundServiceType -> the FOREGROUND_SERVICE_* permission Android 14 (API 34) demands for it.
var FGS_PERM = {
camera: 'FOREGROUND_SERVICE_CAMERA',
connectedDevice: 'FOREGROUND_SERVICE_CONNECTED_DEVICE',
dataSync: 'FOREGROUND_SERVICE_DATA_SYNC',
health: 'FOREGROUND_SERVICE_HEALTH',
location: 'FOREGROUND_SERVICE_LOCATION',
mediaPlayback: 'FOREGROUND_SERVICE_MEDIA_PLAYBACK',
mediaProcessing: 'FOREGROUND_SERVICE_MEDIA_PROCESSING',
mediaProjection: 'FOREGROUND_SERVICE_MEDIA_PROJECTION',
microphone: 'FOREGROUND_SERVICE_MICROPHONE',
phoneCall: 'FOREGROUND_SERVICE_PHONE_CALL',
remoteMessaging: 'FOREGROUND_SERVICE_REMOTE_MESSAGING',
specialUse: 'FOREGROUND_SERVICE_SPECIAL_USE',
systemExempted: 'FOREGROUND_SERVICE_SYSTEM_EXEMPTED'
};
function lineOf(text, idx) { return text.slice(0, idx).split('\n').length; }
function flat(s) { return String(s == null ? '' : s).replace(/\s+/g, ' ').trim(); } // attributes wrap over lines in real manifests
function attr(tag, name) {
var m = new RegExp('android:' + name + '\\s*=\\s*"([^"]*)"').exec(tag);
return m ? m[1] : '';
}
// Commented-out permissions must not count. Keep the line count intact.
function stripComments(text) {
return String(text).replace(/<!--[\s\S]*?-->/g, function (c) { return c.replace(/[^\n]/g, ' '); });
}
function permissions(text) {
var out = [], re = /<uses-permission(?:-sdk-\d+)?\b([^>]*)>/g, m;
while ((m = re.exec(text))) {
var a = flat(m[1]), n = attr(a, 'name');
if (n) out.push({ name: n, line: lineOf(text, m.index) });
}
return out;
}
function services(text) {
var out = [], re = /<service\b([^>]*)>/g, m;
while ((m = re.exec(text))) {
var open = flat(m[1]);
var body = '';
if (!/\/$/.test(open)) {
var end = text.indexOf('</service>', re.lastIndex);
body = flat(text.slice(re.lastIndex, end === -1 ? text.length : end));
}
out.push({
line: lineOf(text, m.index),
name: attr(open, 'name'),
perm: attr(open, 'permission'),
fgs: attr(open, 'foregroundServiceType'),
body: body
});
}
return out;
}
function check(text, opts) {
opts = opts || {};
var src = stripComments(String(text || ''));
var perms = permissions(src), svcs = services(src);
var have = {};
perms.forEach(function (p) { have[p.name] = p.line; });
var findings = [];
function push(rule, line, extra) {
findings.push({
check: rule.id, sev: rule.sev, line: line,
msg: (extra ? extra + ' ' : '') + rule.msg + ' [Play surface: ' + rule.form + ']'
});
}
(RULES || []).forEach(function (rule) {
if (rule.kind === 'perm') {
if (have[rule.perm] !== undefined) push(rule, have[rule.perm], rule.perm.split('.').pop() + ' is declared.');
} else if (rule.kind === 'perm_any') {
(rule.perms || []).forEach(function (p) {
if (have[p] !== undefined) push(rule, have[p], p.split('.').pop() + ' is declared.');
});
} else if (rule.kind === 'perm_prefix') {
var hit = perms.filter(function (p) { return p.name.indexOf(rule.prefix) === 0; });
if (hit.length) push(rule, hit[0].line, hit.length + ' Health Connect permission' + (hit.length === 1 ? '' : 's') + ' declared (' + hit[0].name + (hit.length > 1 ? ', ...' : '') + ').');
} else if (rule.kind === 'service_perm') {
svcs.forEach(function (s) { if (s.perm === rule.perm) push(rule, s.line, (s.name || 'a service') + ':'); });
} else if (rule.kind === 'fgs_any') {
var types = {};
svcs.forEach(function (s) { if (s.fgs) s.fgs.split('|').forEach(function (t) { if (t) types[t.trim()] = s.line; }); });
var names = Object.keys(types);
if (names.length) push(rule, types[names[0]], names.length + ' foreground service type' + (names.length === 1 ? '' : 's') + ' in this manifest (' + names.join(', ') + ').');
} else if (rule.kind === 'fgs_permission') {
svcs.forEach(function (s) {
if (!s.fgs) return;
s.fgs.split('|').forEach(function (t) {
t = t.trim(); if (!t) return;
var need = FGS_PERM[t];
if (need && have['android.permission.' + need] === undefined) {
push(rule, s.line, 'foregroundServiceType="' + t + '" without android.permission.' + need + '.');
}
});
});
} else if (rule.kind === 'special_use') {
svcs.forEach(function (s) {
if (s.fgs && s.fgs.split('|').map(function (t) { return t.trim(); }).indexOf('specialUse') !== -1
&& s.body.indexOf('android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE') === -1) {
push(rule, s.line, (s.name || 'this service') + ':');
}
});
}
});
findings.sort(function (a, b) { return (a.line || 0) - (b.line || 0); });
return { findings: findings, permissions: perms.length, services: svcs.length };
}
var API = { engine: { check: check }, RULES: RULES, RULE_COUNT: (RULES || []).length, FGS_PERM: FGS_PERM };
if (typeof module !== 'undefined' && module.exports) module.exports = API;
else window.PLAYDECLENGINE = API;
</script>
<script>// ⛔뼈대 — kit/scaffold.py 가 찍는다 (s143). findings [{check,sev,msg,line?}] → 글/HTML. 같은 파일이 Node 와 브라우저에서 산다.
'use strict';
function escapeHtml(s) { return String(s == null ? '' : s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;'); }
function summaryLine(res) {
const f = (res && res.findings) || []; const e = f.filter(function (x) { return String(x.sev || 'error').toLowerCase().startsWith('err'); }).length;
return f.length ? (f.length + ' finding' + (f.length === 1 ? '' : 's') + ' (' + e + ' error' + (e === 1 ? '' : 's') + ')') : 'Clean — no findings.';
}
function toText(res, label) {
const f = (res && res.findings) || [];
const lines = ['== ' + (label || '') + ' — ' + summaryLine(res)];
f.forEach(function (x) { lines.push(' ' + String(x.sev || 'error').toUpperCase().slice(0, 4) + ' ' + (x.line ? 'L' + x.line + ' ' : '') + (x.check || x.id || '') + ' ' + (x.msg || x.message || '')); });
return lines.join('\n');
}
function toHtml(res) {
const f = (res && res.findings) || [];
if (!f.length) return '<p class="ok">' + escapeHtml(summaryLine(res)) + '</p>';
const rows = f.map(function (x) { return '<tr><td class="sev ' + escapeHtml(String(x.sev || 'error')) + '">' + escapeHtml(String(x.sev || 'error')) + '</td><td>' + (x.line ? 'L' + escapeHtml(x.line) : '') + '</td><td><code>' + escapeHtml(x.check || x.id || '') + '</code></td><td>' + escapeHtml(x.msg || x.message || '') + '</td></tr>'; }).join('');
return '<p class="sum">' + escapeHtml(summaryLine(res)) + '</p><table><thead><tr><th>Level</th><th>Line</th><th>Check</th><th>What</th></tr></thead><tbody>' + rows + '</tbody></table>';
}
var PLAYDECLREPORT = { escapeHtml: escapeHtml, summaryLine: summaryLine, toText: toText, toHtml: toHtml };
if (typeof module !== 'undefined' && module.exports) module.exports = PLAYDECLREPORT;
else window.PLAYDECLREPORT = PLAYDECLREPORT;
</script>
<script>
(function(){
var src=document.getElementById('src'), out=document.getElementById('out'), go=document.getElementById('go'), today=document.getElementById('today');
today.value=new Date().toISOString().slice(0,10);
function run(){ try { var res=window.PLAYDECLENGINE.engine.check(src.value,{today:today.value}); out.innerHTML=window.PLAYDECLREPORT.toHtml(res); } catch(e){ out.innerHTML='<p class="sev error">Could not run: '+String(e.message||e)+'</p>'; } }
go.onclick=run; if (src.value.trim()) run();
})();
</script>
<section data-yj-install="free" style="margin:2rem auto 1rem;max-width:46rem;padding:1.25rem 1.25rem;border:1px solid #cfe3d6;border-radius:12px;background:#f6fbf8;font:16px/1.6 system-ui,-apple-system,Segoe UI,sans-serif"><h2 style="margin:0 0 .4rem;font-size:1.2rem">Install free</h2><p style="margin:0 0 .8rem;color:#41505f">Free for the file open in your editor - no key, no limit. The workspace sweep and the report ask for a key.</p><a href="https://marketplace.visualstudio.com/items?itemName=ReadyStack.play-declaration-gap-lint" rel="noopener" data-yj-install-door="mp" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'play-declaration-gap-lint',src:'mp',path:location.pathname})],{type:'application/json'}))}catch(e){}" style="display:inline-block;margin:0 .5rem .5rem 0;padding:.6rem 1rem;border-radius:8px;background:#0f7b3f;color:#fff;text-decoration:none;font-weight:600">VS Code Marketplace</a><a href="https://www.npmjs.com/package/@readystack/play-declaration-gap-lint" rel="noopener" data-yj-install-door="npm" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'play-declaration-gap-lint',src:'npm',path:location.pathname})],{type:'application/json'}))}catch(e){}" style="display:inline-block;margin:0 .5rem .5rem 0;padding:.6rem 1rem;border-radius:8px;background:#0f7b3f;color:#fff;text-decoration:none;font-weight:600">npm</a><a href="https://www.npmjs.com/package/@readystack/play-declaration-gap-lint" rel="noopener" data-yj-install-door="mcp" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'play-declaration-gap-lint',src:'mcp',path:location.pathname})],{type:'application/json'}))}catch(e){}" style="display:inline-block;margin:0 .5rem .5rem 0;padding:.6rem 1rem;border-radius:8px;background:#0f7b3f;color:#fff;text-decoration:none;font-weight:600">MCP server (npx)</a><a href="https://hub.docker.com/r/getreadystack/play-declaration-gap-lint" rel="noopener" data-yj-install-door="docker" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'play-declaration-gap-lint',src:'docker',path:location.pathname})],{type:'application/json'}))}catch(e){}" style="display:inline-block;margin:0 .5rem .5rem 0;padding:.6rem 1rem;border-radius:8px;background:#0f7b3f;color:#fff;text-decoration:none;font-weight:600">Docker Hub</a><pre style="margin:.5rem 0 0;padding:.6rem .8rem;background:#f3f5f8;border-radius:8px;font-size:.9rem;overflow:auto">ext install ReadyStack.play-declaration-gap-lint
npx @readystack/play-declaration-gap-lint &lt;file&gt;
npx @readystack/play-declaration-gap-lint --mcp</pre><p style="margin:.6rem 0 0;font-size:.92rem;color:#41505f"><a href="https://getreadystack.com/api/buy/team" rel="nofollow" style="color:#0f7b3f;text-decoration:underline">Team? One key for every ReadyStack linter, 5 seats, $149 once</a></p></section>
<section data-yj-alert="rule" style="margin:1rem auto 1.5rem;max-width:46rem;padding:1.1rem 1.25rem;border:1px solid #d9dee6;border-radius:12px;background:#fbfcfd;font:15px/1.55 system-ui,-apple-system,Segoe UI,sans-serif"><b style="font-size:1.05rem">Get the full rule table as a PDF</b><div style="opacity:.78;margin:.35rem 0 .8rem">All 15 rules Play Declaration Gap Lint (AndroidManifest) checks, each with its source and date, on printable pages you can hand to your team or auditor. We email it now, and once more only if a rule changes. No newsletter.</div><div style="display:flex;gap:8px;flex-wrap:wrap"><input id="yjae" type="email" placeholder="you@example.com" style="flex:1;min-width:210px;padding:11px 13px;border:1px solid rgba(140,150,165,.34);border-radius:8px;background:#fff;color:inherit"><button id="yjab" style="padding:11px 18px;border-radius:8px;border:0;cursor:pointer;background:#0f7b3f;color:#fff;font:600 15px system-ui,sans-serif">Email me the PDF</button></div><div id="yjao" hidden style="margin-top:.6rem;color:#0f7b3f">Sent. It arrives within the hour - check spam too.</div><script>(function(){var b=document.getElementById("yjab"),e=document.getElementById("yjae");if(!b||!e)return;b.addEventListener("click",function(){var v=e.value.trim();if(!/.+@.+\..+/.test(v))return;try{localStorage.setItem("yj_em","1");}catch(_){}fetch(location.origin+"/api/subscribe",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({email:v,slug:"play-declaration-gap-lint",lang:"en",source:"rule_table"+((b.closest&&b.closest("section")&&b.closest("section").getAttribute("data-yj-moved"))?"_at_result":"")})}).then(function(r){var o=document.getElementById("yjao");o.hidden=false;if(!r.ok){o.textContent="Could not save - please try again.";}}).catch(function(){var o=document.getElementById("yjao");o.hidden=false;o.textContent="Could not save - please try again.";});});})();</script><script>(function(){var e=document.getElementById("yjae"),s=e&&e.closest?e.closest("section"):null,o=document.getElementById("out");if(!s||!o)return;var d=false;function mv(ev){if(d)return;if(ev&&ev.target&&s.contains(ev.target))return;d=true;var b=o;while(b.parentNode&&b.parentNode!==document.body&&b.parentNode.tagName!=="MAIN"&&b.parentNode.tagName!=="ARTICLE")b=b.parentNode;if(!b.parentNode||b===s||b.contains(s))return;try{b.parentNode.insertBefore(s,b.nextSibling);s.setAttribute("data-yj-moved","1");}catch(_){}}document.addEventListener("input",mv,true);document.addEventListener("change",mv,true);})();</script></section>
<section data-yj-cta="paid" style="margin:3rem auto;max-width:46rem;padding:1.5rem 1.25rem;border:1px solid #d8dee9;border-radius:12px;background:#fbfcfe;font:16px/1.6 system-ui,-apple-system,Segoe UI,sans-serif"><h2 style="margin:0 0 .5rem;font-size:1.25rem">Get the complete version <span class="yj-price">$29</span></h2><p style="margin:.25rem 0 1rem;color:#41505f">This page is the working piece. The full pack has everything below.</p><p style="margin:0 0 1rem;color:#41505f">15 checks that name every Play Console declaration your manifest opens</p><p style="margin:0 0 1rem;color:#5b6b7c;font-size:.94rem">A freelance Android release engineer bills $60-$120 an hour; reading a multi-module app's manifests against every current Play declaration is most of a working day</p><a href="https://getreadystack.com/api/buy/cl/polar_cl_9pjxdAKOI372jyFZtBN3s5u8ajfGhvBhh1rwf3TGsGE" rel="nofollow" style="display:inline-block;padding:.7rem 1.3rem;border-radius:8px;background:#1b64f2;color:#fff;text-decoration:none;font-weight:600">Buy the full version — $29</a></section>
<!--yj-share-s101--><div style="margin:28px 0 8px;text-align:center"><button id="yjcp" type="button" style="font:inherit;padding:10px 18px;border:1px solid #8884;border-radius:10px;background:transparent;cursor:pointer">Copy link</button><button id="yjimg" type="button" style="font:inherit;padding:10px 18px;margin-left:8px;border:1px solid #8884;border-radius:10px;background:transparent;cursor:pointer">Save result as image</button></div><script>(function(){var b=document.getElementById("yjimg");if(!b)return;b.addEventListener("click",function(){var o=document.getElementById("out");var txt=(o&&(o.innerText||o.textContent)||"").trim();if(!txt){b.textContent="(no result yet)";setTimeout(function(){b.textContent="Save result as image";},1500);return;}var lines=txt.split(String.fromCharCode(10)).filter(function(x){return x.trim();}).slice(0,10);var W=1200,P=56,c=document.createElement("canvas");c.width=W;var lh=38,H=P*2+70+lines.length*lh+60;c.height=H;var x=c.getContext("2d");x.fillStyle="#1a1512";x.fillRect(0,0,W,H);x.fillStyle="#f3e9d2";x.font="600 34px system-ui,Segoe UI,Arial";var t=(document.title||"").split("|")[0].trim();x.fillText(t.slice(0,54),P,P+34);x.font="22px ui-monospace,Menlo,Consolas,monospace";x.fillStyle="#e8dcc4";for(var i=0;i<lines.length;i++){x.fillText(lines[i].slice(0,88),P,P+70+i*lh+22);}x.font="600 22px system-ui,Segoe UI,Arial";x.fillStyle="#c9a227";var d=location.hostname.replace("www.","");x.fillText(d,W-P-x.measureText(d).width,H-P+6);c.toBlob(function(bl){var u=URL.createObjectURL(bl),a=document.createElement("a");a.href=u;a.download=(t||"result").replace(/[^a-z0-9]+/gi,"-").toLowerCase()+"-"+d+".png";document.body.appendChild(a);a.click();setTimeout(function(){URL.revokeObjectURL(u);a.remove();},2000);},"image/png");});})();</script><script>(function(){var b=document.getElementById("yjcp");if(!b)return;var t=b.textContent;b.addEventListener("click",function(){var u=location.href.split("#")[0];function d(){b.textContent="Link copied";setTimeout(function(){b.textContent=t;},1800);}if(navigator.clipboard&&navigator.clipboard.writeText){navigator.clipboard.writeText(u).then(d,function(){prompt("",u);});}else{prompt("",u);}});})();</script>
<script data-yj="yj-view-beacon">(function(){try{var p=location.pathname;var k='yjv:'+p;if(sessionStorage.getItem(k))return;sessionStorage.setItem(k,'1');fetch('/api/ev',{method:'POST',keepalive:true,headers:{'Content-Type':'application/json'},body:JSON.stringify({path:p,lang:"en",ref:(document.referrer||''),src:(function(){try{return (new URLSearchParams(location.search).get('ref')||'').replace(/[^a-z_]/g,'').slice(0,12)}catch(e){return ''}})()})}).catch(function(){});}catch(e){}})();</script>
<script data-yj="yj-use-beacon">(function(){try{var o=document.getElementById('out');if(!o)return;var p=location.pathname,k='yju:'+p,sent=false;function fire(){if(sent||sessionStorage.getItem(k))return;if(!(o.textContent||'').replace(/\s+/g,'').length)return;sent=true;sessionStorage.setItem(k,'1');fetch('/api/ev',{method:'POST',keepalive:true,headers:{'Content-Type':'application/json'},body:JSON.stringify({path:p,lang:"en",t:'use',src:(function(){try{return (new URLSearchParams(location.search).get('ref')||'').replace(/[^a-z_]/g,'').slice(0,12)}catch(e){return ''}})()})}).catch(function(){});}var mo=new MutationObserver(function(){fire();if(sent)mo.disconnect();});mo.observe(o,{childList:true,characterData:true,subtree:true});}catch(e){}})();</script><div class="yj-home-find"><div class="yj-lgbar"><a href="/" class="yj-lg on">EN</a><a href="/de/" class="yj-lg">DE</a><a href="/ja/" class="yj-lg">JA</a><a href="/es/" class="yj-lg">ES</a><a href="/pt/" class="yj-lg">PT</a></div><section class="yj-find"><h2>Find a tool</h2><input id="yjq" type="search" autocomplete="off" placeholder="Type a product name, format or task"><div id="yjr"></div></section><style>.yj-lgbar{position:absolute;top:14px;right:18px;display:flex;gap:6px;z-index:60}.yj-lg{font:600 12px/1 system-ui,sans-serif;padding:6px 9px;border-radius:4px;text-decoration:none;color:#8b96a3;border:1px solid rgba(140,150,165,.28)}.yj-lg:hover{color:#e8eaed;border-color:rgba(160,175,195,.6)}.yj-lg.on{color:#0e1116;background:#cfd6de;border-color:#cfd6de}.yj-find{max-width:760px;margin:34px auto 8px;padding:0 20px}.yj-find h2{font:600 20px/1.3 system-ui,sans-serif;margin:0 0 10px}#yjq{width:100%;box-sizing:border-box;padding:13px 15px;font:400 15.5px system-ui,sans-serif;border:1px solid rgba(140,150,165,.34);border-radius:8px;background:rgba(255,255,255,.04);color:inherit}#yjq:focus{outline:none;border-color:#7FD1C4}#yjr{margin-top:10px;display:flex;flex-direction:column;gap:6px}#yjr a{display:flex;justify-content:space-between;gap:12px;padding:11px 14px;border:1px solid rgba(140,150,165,.22);border-radius:6px;text-decoration:none;font:400 14.5px/1.4 system-ui,sans-serif;color:inherit}#yjr a:hover{border-color:#7FD1C4}#yjr .pr{font:600 13px system-ui,sans-serif;color:#5fbf9a;white-space:nowrap}</style><script>(function(){var q=document.getElementById("yjq"),r=document.getElementById("yjr"),D=null;function load(){if(D)return Promise.resolve(D);return fetch("/search-index.json").then(function(x){return x.json();}).then(function(j){D=j;return j;}).catch(function(){D=[];return D;});}function esc(t){return String(t).replace(/[&<>"]/g,function(c){return {"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;"}[c];});}function run(){var v=q.value.trim().toLowerCase();if(v.length<2){r.innerHTML="";return;}load().then(function(j){var w=v.split(/\s+/),o=[];for(var i=0;i<j.length&&o.length<12;i++){var t=j[i].t.toLowerCase();var ok=true;for(var k=0;k<w.length;k++){if(t.indexOf(w[k])<0){ok=false;break;}}if(ok)o.push(j[i]);}r.innerHTML=o.length?o.map(function(x){return "<a href=\""+esc(x.u)+"\">"+"<span>"+esc(x.t)+"</span>"+(x.p?"<span class=\"pr\">$"+x.p+"</span>":"")+"</a>";}).join(""):"";});}q.addEventListener("input",run);q.addEventListener("focus",load);})();</script></div><section data-yj="yj-facts-v1" style="max-width:920px;margin:48px auto 0;padding:0 20px;font-family:system-ui,sans-serif"><style>.yj-article p{font:400 15.5px/1.7 system-ui,sans-serif;color:#26313b;margin:0 0 12px;max-width:72ch}.yj-article h2,.yj-article h3,.yj-article h4{font:600 17px/1.35 system-ui,sans-serif;color:#1b1f24;margin:22px 0 8px}.yj-article table{border-collapse:collapse;margin:10px 0 16px;font:14px/1.45 system-ui,sans-serif}.yj-article th,.yj-article td{border:1px solid #e2e6ea;padding:6px 10px;text-align:left}.yj-article th{background:#f4f6f8}.yj-article pre{background:#0f1720;color:#e6edf3;padding:12px 14px;border-radius:8px;overflow:auto;font:13px/1.5 ui-monospace,Menlo,monospace}.yj-article code{font:13px ui-monospace,Menlo,monospace;background:#f1f3f5;padding:1px 4px;border-radius:3px}.yj-article pre code{background:none;padding:0}.yj-article ul,.yj-article ol{font:15px/1.6 system-ui,sans-serif;color:#26313b}</style><p style="font:400 13px/1.5 system-ui,sans-serif;color:#6b7680;margin:0 0 14px"><time datetime="2026-09-20">Updated 2026-09-20</time> · ReadyStack</p><script type="application/ld+json">{"@context":"https://schema.org","@type":"WebPage","dateModified":"2026-09-20","author":{"@type":"Organization","name":"ReadyStack"}}</script><h2 style="font:600 21px/1.3 system-ui,sans-serif;margin:0 0 6px;color:#1b1f24">Worked example</h2><p style="font:400 14.5px/1.5 system-ui,sans-serif;margin:0 0 16px;color:#5b6570">Real numbers from this tool, line by line.</p><div class="yj-article"><p>A 58-line AndroidManifest.xml comes back with 19 findings — 15 of them errors — the moment an Android developer measures it against what Google Play asks to be declared, rather than against the Android SDK.</p>
<p>That is what the 15 checks in Play Declaration Gap Lint return on the sample manifest shipped with the extension: 17 permissions, 3 services, 58 lines, 19 findings, 15 errors, 4 warnings. All 15 checks fire on that file, naming 14 Play Console surfaces plus one thing that is not a form at all.</p>
<p>Android Lint measures your manifest against the SDK: is the attribute spelled right, is the permission known, is the component exported. It says nothing about the second life a manifest line has — some permissions open a form in Play Console that a human reviewer reads before your update ships. QUERY_ALL_PACKAGES opens the App visibility declaration; MANAGE_EXTERNAL_STORAGE opens All files access; ACCESS_BACKGROUND_LOCATION opens the Location permissions declaration and wants a video of the feature. READ_SMS, READ_MEDIA_IMAGES, the android.permission.health.* family, exact alarms and the advertising ID each have their own answer waiting in the console. None of them stop the build.</p>
<p>One check is different. Since Android 14 (API 34) a foreground service must declare its type, and each type needs its own permission: foregroundServiceType=&quot;location&quot; needs FOREGROUND_SERVICE_LOCATION, &quot;camera&quot; needs FOREGROUND_SERVICE_CAMERA, &quot;specialUse&quot; needs FOREGROUND_SERVICE_SPECIAL_USE and a PROPERTY_SPECIAL_USE_FGS_SUBTYPE property inside the same service element. When the permission is missing the app compiles, installs, and throws a SecurityException the first time the service starts. In the sample manifest three services trip exactly that, which is why the run returns 15 errors rather than 12.</p>
<p>This is the failure mode of generated code: an assistant writes the permission the feature needs, not the declaration behind it, and it cannot see which service Play will ask about. The manifest looks finished. The upload is where it is not.</p>
<p>Timing matters this year: Google Play&#x27;s yearly target API requirement lands on 2026-08-31, when updates have to target API 36. An app sitting on an older target crosses the API 34 and API 35 foreground service rules on the way there, all at once.</p>
<p>The free part is the whole job on one file: open an AndroidManifest.xml, run &quot;Play Declaration Gap: Check this file&quot;, and every triggering line is marked in the Problems panel with its line number and the Play surface it belongs to. No key, no limit, no watermark. The identical engine runs in the browser on the product page, so a reviewer without VS Code can paste a manifest; nothing is uploaded. On the clean sample manifest — 44 lines, a &lt;queries&gt; block, a dataSync service with its permission in place — the run returns 0 errors and 1 warning: that service still needs a use case in the foreground service declaration.</p>
<p>The licensed part changes the scope, not the depth: it sweeps every manifest the workspace holds — app module, library modules, the merged manifests SDKs bring in — into a dated playDecl-report.md, one block per manifest, so whoever fills in Play Console reads a file instead of a screenshot. That sweep is free for seven days from first use; after that it asks for a licence key, $29 once, one key per person or CI seat.</p>
<p>For comparison: a freelance Android release engineer bills $60-$120 an hour, and walking a multi-module app&#x27;s manifests against every current Play declaration is most of a working day.</p></div><h2 style="font:600 21px/1.3 system-ui,sans-serif;margin:34px 0 6px;color:#1b1f24">Questions people ask</h2><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">What does Play Declaration Gap Lint actually do?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">It reads an AndroidManifest.xml and marks every line that Google Play treats as a declaration, not just a permission. The 15 checks cover app visibility, all files access, background location, SMS and call log, install unknown apps, photo and video, Health Connect, full-screen intent, exact alarms, the advertising ID, accessibility bindings, and the three foreground service rules.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">Who is it for?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">Android developers and release engineers who upload to Google Play, and the person who reviews a manifest before a release branch is cut. It is most useful on manifests assembled by an assistant or merged in from an SDK, where a permission arrives without anyone deciding who will answer the Play Console form behind it.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">Why is a free tool or a chatbot not enough?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">Android Lint checks the manifest against the SDK, not against Play policy, so it stays quiet about declaration forms. A chatbot cannot see your file and will not know that your specialUse service is missing its subtype property. This runs on the file itself, offline, and prints a line number for each of the 15 checks.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">What is free and what needs a licence key?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">The file open in your editor is free forever, with no key, no limit and no watermark: all 15 checks, marked in the Problems panel and listed in the output channel. The licensed part is scope and ownership - sweeping every manifest in the workspace, library modules included, into a dated playDecl-report.md you keep.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">What would this cost if a person did it?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">A freelance Android release engineer bills roughly $60-$120 an hour, and walking a multi-module app&#x27;s manifests against every current Play declaration takes most of a working day. A single policy rejection costs more than the hours: the release waits for a re-review while the fix sits in a build queue.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">Why not just ask ChatGPT or another AI chat?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">A general AI chat answers from training data with a cutoff date, cannot read your repository and names no rule version. Play Declaration Gap Lint (AndroidManifest) checks the file you open against 15 rules from a rule set dated 2026-09-20, and points at the exact line. For a filing, an audit or a client you need that dated result on your own files.</p></details><script type="application/ld+json" data-yj="yj-faq-ld">{"@context": "https://schema.org", "@type": "FAQPage", "mainEntity": [{"@type": "Question", "name": "What does Play Declaration Gap Lint actually do?", "acceptedAnswer": {"@type": "Answer", "text": "It reads an AndroidManifest.xml and marks every line that Google Play treats as a declaration, not just a permission. The 15 checks cover app visibility, all files access, background location, SMS and call log, install unknown apps, photo and video, Health Connect, full-screen intent, exact alarms, the advertising ID, accessibility bindings, and the three foreground service rules."}}, {"@type": "Question", "name": "Who is it for?", "acceptedAnswer": {"@type": "Answer", "text": "Android developers and release engineers who upload to Google Play, and the person who reviews a manifest before a release branch is cut. It is most useful on manifests assembled by an assistant or merged in from an SDK, where a permission arrives without anyone deciding who will answer the Play Console form behind it."}}, {"@type": "Question", "name": "Why is a free tool or a chatbot not enough?", "acceptedAnswer": {"@type": "Answer", "text": "Android Lint checks the manifest against the SDK, not against Play policy, so it stays quiet about declaration forms. A chatbot cannot see your file and will not know that your specialUse service is missing its subtype property. This runs on the file itself, offline, and prints a line number for each of the 15 checks."}}, {"@type": "Question", "name": "What is free and what needs a licence key?", "acceptedAnswer": {"@type": "Answer", "text": "The file open in your editor is free forever, with no key, no limit and no watermark: all 15 checks, marked in the Problems panel and listed in the output channel. The licensed part is scope and ownership - sweeping every manifest in the workspace, library modules included, into a dated playDecl-report.md you keep."}}, {"@type": "Question", "name": "What would this cost if a person did it?", "acceptedAnswer": {"@type": "Answer", "text": "A freelance Android release engineer bills roughly $60-$120 an hour, and walking a multi-module app's manifests against every current Play declaration takes most of a working day. A single policy rejection costs more than the hours: the release waits for a re-review while the fix sits in a build queue."}}, {"@type": "Question", "name": "Why not just ask ChatGPT or another AI chat?", "acceptedAnswer": {"@type": "Answer", "text": "A general AI chat answers from training data with a cutoff date, cannot read your repository and names no rule version. Play Declaration Gap Lint (AndroidManifest) checks the file you open against 15 rules from a rule set dated 2026-09-20, and points at the exact line. For a filing, an audit or a client you need that dated result on your own files."}}]}</script><div id="yj-ask" style="margin:36px 0 0;padding:18px 18px 16px;border:1px solid #e2e6ea;border-radius:10px;background:#fbfbfc"><h2 style="font:600 19px/1.3 system-ui,sans-serif;margin:0 0 4px;color:#1b1f24">Ask about this tool</h2><p style="font:400 13.5px/1.5 system-ui,sans-serif;margin:0 0 12px;color:#5b6570">One question, answered by the person who built it. Your email only if you want the answer sent.</p><form onsubmit="return yjAsk(event)" style="display:grid;gap:8px"><textarea name="question" required minlength="10" maxlength="2000" rows="3" placeholder="Your question" style="font:14px/1.5 system-ui,sans-serif;padding:10px;border:1px solid #cfd6dd;border-radius:6px"></textarea><div style="display:flex;gap:8px;flex-wrap:wrap"><input type="email" name="email" placeholder="Email (optional)" style="flex:1;min-width:200px;font:14px system-ui,sans-serif;padding:9px 10px;border:1px solid #cfd6dd;border-radius:6px"><button type="submit" style="font:600 14px system-ui,sans-serif;padding:9px 18px;border:0;border-radius:6px;background:#1b1f24;color:#fff;cursor:pointer">Send</button></div><p id="yj-ask-msg" style="font:13px system-ui,sans-serif;margin:2px 0 0;color:#0a6238;min-height:1em"></p></form><script>function yjAsk(e){e.preventDefault();var f=e.target,m=document.getElementById("yj-ask-msg");m.style.color="#0a6238";fetch(location.origin+"/api/ask",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({sku:143180,slug:"play-declaration-gap-lint",lang:"en",question:f.question.value,email:f.email.value,page:location.pathname})}).then(function(r){if(!r.ok)throw 0;m.textContent="Thanks \u2014 noted. Answers land on this page and, if you left an email, in your inbox.";f.question.value="";}).catch(function(){m.style.color="#a8551f";m.textContent="Could not send. Please try again in a minute.";});return false;}</script></div></section><section class="yj-related" style="max-width:920px;margin:56px auto 40px;padding:0 20px"><h2 style="font:600 21px/1.3 system-ui,sans-serif;margin:0 0 6px;color:#1b1f24">Same job, next tool</h2><p style="font:400 14.5px/1.5 system-ui,sans-serif;margin:0 0 20px;color:#5b6570">Free in the browser. The full version is a one-time licence.</p><div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(250px,1fr));gap:14px"><a class="yj-rel-c" href="https://getreadystack.com/tools/play-release-blocker-lint"><span class="yj-rel-t">Google Play Release Blocker Lint - target API 36, Billing 8, 16 KB</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/mobile-app-ui-kit-account-deletion"><span class="yj-rel-t">Mobile App UI Kit — Account Deletion Flow for App Store 5.1.1(v) &amp; Google Play</span><span class="yj-rel-p">$168</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/vat-relief-declaration-gap-uk"><span class="yj-rel-t">VAT Relief Declaration Gap - UK Notice 701/7 exposure</span><span class="yj-rel-p">$60</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/robots-txt-ai-crawler-audit"><span class="yj-rel-t">AI Crawler Rules - robots.txt Audit for AI Search</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/autohotkey-toml-devicetree-snippets"><span class="yj-rel-t">AutoHotkey v2, TOML and DeviceTree checks for VS Code</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/consent-mode-v2-lint"><span class="yj-rel-t">Consent Mode v2 Lint for GA4 &amp; Google Ads</span><span class="yj-rel-p">$29</span></a></div><style>.yj-rel-c{display:flex;flex-direction:column;gap:8px;padding:15px 16px;border:1px solid #e2e6ea;border-radius:6px;text-decoration:none;background:#fff}.yj-rel-c:hover{border-color:#b9c2cb;background:#fafbfc}.yj-rel-t{font:500 14.5px/1.42 system-ui,sans-serif;color:#1b1f24}.yj-rel-p{font:600 13px system-ui,sans-serif;color:#0a6238}</style></section></body>
</html>