@capgo/capacitor-mute 8.0.34 → 8.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -11,7 +11,7 @@ import com.getcapacitor.annotation.CapacitorPlugin;
|
|
|
11
11
|
@CapacitorPlugin(name = "Mute")
|
|
12
12
|
public class MutePlugin extends Plugin {
|
|
13
13
|
|
|
14
|
-
private final String pluginVersion = "8.0
|
|
14
|
+
private final String pluginVersion = "8.1.0";
|
|
15
15
|
|
|
16
16
|
@PluginMethod
|
|
17
17
|
public void isMuted(PluginCall call) {
|
|
@@ -7,7 +7,7 @@ import Capacitor
|
|
|
7
7
|
*/
|
|
8
8
|
@objc(MutePlugin)
|
|
9
9
|
public class MutePlugin: CAPPlugin, CAPBridgedPlugin {
|
|
10
|
-
private let pluginVersion: String = "8.0
|
|
10
|
+
private let pluginVersion: String = "8.1.0"
|
|
11
11
|
public let identifier = "MutePlugin"
|
|
12
12
|
public let jsName = "Mute"
|
|
13
13
|
public let pluginMethods: [CAPPluginMethod] = [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@capgo/capacitor-mute",
|
|
3
|
-
"version": "8.0
|
|
3
|
+
"version": "8.1.0",
|
|
4
4
|
"description": "Detect if the mute switch is enabled/disabled on a device",
|
|
5
5
|
"main": "dist/plugin.cjs.js",
|
|
6
6
|
"module": "dist/esm/index.js",
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
"ios/Sources",
|
|
14
14
|
"ios/Tests",
|
|
15
15
|
"Package.swift",
|
|
16
|
-
"CapgoCapacitorMute.podspec"
|
|
16
|
+
"CapgoCapacitorMute.podspec",
|
|
17
|
+
"scripts/capgo-postsync-hook.mjs"
|
|
17
18
|
],
|
|
18
19
|
"author": "Martin Donadieu <martin@capgo.app>",
|
|
19
20
|
"license": "MPL-2.0",
|
|
@@ -54,9 +55,9 @@
|
|
|
54
55
|
"watch": "tsc --watch",
|
|
55
56
|
"prepublishOnly": "npm run build",
|
|
56
57
|
"check:wiring": "node scripts/check-capacitor-plugin-wiring.mjs",
|
|
58
|
+
"capacitor:sync:after": "node scripts/capgo-postsync-hook.mjs",
|
|
57
59
|
"example:install": "cd example-app && bun install --frozen-lockfile",
|
|
58
|
-
"example:build": "bun run build && cd example-app && bun install --frozen-lockfile && bun run build"
|
|
59
|
-
"example:capgo:deploy": "bun run example:build && bun scripts/deploy-example-capgo.mjs"
|
|
60
|
+
"example:build": "bun run build && cd example-app && bun install --frozen-lockfile && bun run build"
|
|
60
61
|
},
|
|
61
62
|
"devDependencies": {
|
|
62
63
|
"@capacitor/android": "^8.0.0",
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Capgo `capacitor:sync:after` hook.
|
|
4
|
+
*
|
|
5
|
+
* Wired in package.json so it runs at the very end of `npx cap sync`
|
|
6
|
+
* (see https://capacitorjs.com/docs/plugins/plugin-hooks). Fetches a single
|
|
7
|
+
* postsync message from https://a0.capgo.app/v0 and prints it to the console.
|
|
8
|
+
*
|
|
9
|
+
* Placement:
|
|
10
|
+
* - Capacitor invokes sync hooks once per platform (android, ios, web).
|
|
11
|
+
* We want to print exactly ONCE, at the very end of the user's sync.
|
|
12
|
+
* We walk up the process tree to find the parent `cap` command, parse
|
|
13
|
+
* its argv, and figure out which platforms were actually requested:
|
|
14
|
+
* `cap sync` → android, ios, web → last = web
|
|
15
|
+
* `cap sync ios` → ios only → last = ios
|
|
16
|
+
* `cap sync android`→ android only → last = android
|
|
17
|
+
* We only print when CAPACITOR_PLATFORM_NAME matches the last one.
|
|
18
|
+
* If we can't read the parent (weird shell, sandboxed CI), we fall back
|
|
19
|
+
* to "print on web" and let the cooldown handle anything weird.
|
|
20
|
+
*
|
|
21
|
+
* Throttling:
|
|
22
|
+
* - In addition to the platform gate, we use a marker file in /tmp with a
|
|
23
|
+
* 5-second cooldown so back-to-back syncs (or unexpected re-invocations)
|
|
24
|
+
* don't spam the user.
|
|
25
|
+
*
|
|
26
|
+
* Reporting (not skipping):
|
|
27
|
+
* - CI / non-TTY are NOT skipped. Instead we forward `ci` and `tty` as
|
|
28
|
+
* query params so the postsync endpoint can decide what to do.
|
|
29
|
+
*
|
|
30
|
+
* Safety:
|
|
31
|
+
* - Completely silent on any failure (network, parse, fs, …).
|
|
32
|
+
* - Never exits non-zero — must never break a consumer's `cap sync`.
|
|
33
|
+
* - 1.5s fetch timeout so it never hangs the build.
|
|
34
|
+
* - Honors hard opt-outs: CAPGO_NO_AD=1, ADBLOCK=1, DISABLE_OPENCOLLECTIVE=1.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
import { tmpdir, platform as osPlatform } from "node:os";
|
|
38
|
+
import { join } from "node:path";
|
|
39
|
+
import {
|
|
40
|
+
statSync,
|
|
41
|
+
utimesSync,
|
|
42
|
+
closeSync,
|
|
43
|
+
openSync,
|
|
44
|
+
readFileSync,
|
|
45
|
+
readdirSync,
|
|
46
|
+
} from "node:fs";
|
|
47
|
+
import { execFileSync } from "node:child_process";
|
|
48
|
+
|
|
49
|
+
const PLATFORM_ORDER = ["android", "ios", "web"];
|
|
50
|
+
const CAP_TOKEN = /\b(cap|capacitor)\b/;
|
|
51
|
+
const SYNC_VERB = /\bsync\b/;
|
|
52
|
+
|
|
53
|
+
const POSTSYNC_ENDPOINT = "https://a0.capgo.app/v0";
|
|
54
|
+
const MARKER_FILE = join(tmpdir(), "capgo-capacitor-mute-postsync-shown");
|
|
55
|
+
const COOLDOWN_MS = 5_000;
|
|
56
|
+
const FETCH_TIMEOUT_MS = 1_500;
|
|
57
|
+
|
|
58
|
+
function isOptedOut() {
|
|
59
|
+
return (
|
|
60
|
+
process.env.CAPGO_NO_AD === "1" ||
|
|
61
|
+
process.env.ADBLOCK === "1" ||
|
|
62
|
+
process.env.DISABLE_OPENCOLLECTIVE === "1"
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function withinCooldown() {
|
|
67
|
+
try {
|
|
68
|
+
const { mtimeMs } = statSync(MARKER_FILE);
|
|
69
|
+
return Date.now() - mtimeMs < COOLDOWN_MS;
|
|
70
|
+
} catch {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function touchMarker() {
|
|
76
|
+
try {
|
|
77
|
+
const fd = openSync(MARKER_FILE, "a");
|
|
78
|
+
closeSync(fd);
|
|
79
|
+
const now = new Date();
|
|
80
|
+
utimesSync(MARKER_FILE, now, now);
|
|
81
|
+
} catch {
|
|
82
|
+
// ignore — worst case we show the message one extra time
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Take a single snapshot of every process (pid, ppid, args) and return it as
|
|
88
|
+
* a Map<pid, {ppid, args}>. We do this in ONE call so we never have to spawn
|
|
89
|
+
* `ps` repeatedly while walking the tree, and we use width flags so the args
|
|
90
|
+
* column is never truncated.
|
|
91
|
+
*
|
|
92
|
+
* - linux: read /proc directly, no fork
|
|
93
|
+
* - darwin / *bsd: `ps -axww -o pid=,ppid=,args=` (-ww = unlimited width)
|
|
94
|
+
* - win32: `wmic process get ProcessId,ParentProcessId,CommandLine`
|
|
95
|
+
*
|
|
96
|
+
* Returns an empty Map on any failure.
|
|
97
|
+
*/
|
|
98
|
+
function snapshotProcesses() {
|
|
99
|
+
const plat = osPlatform();
|
|
100
|
+
const map = new Map();
|
|
101
|
+
try {
|
|
102
|
+
if (plat === "linux") {
|
|
103
|
+
for (const entry of readdirSync("/proc")) {
|
|
104
|
+
if (!/^\d+$/.test(entry)) continue;
|
|
105
|
+
const pid = Number(entry);
|
|
106
|
+
try {
|
|
107
|
+
const cmdline = readFileSync(`/proc/${entry}/cmdline`)
|
|
108
|
+
.toString("utf8")
|
|
109
|
+
.replace(/\0/g, " ")
|
|
110
|
+
.trim();
|
|
111
|
+
const status = readFileSync(`/proc/${entry}/status`, "utf8");
|
|
112
|
+
const m = status.match(/^PPid:\s+(\d+)/m);
|
|
113
|
+
map.set(pid, { ppid: m ? Number(m[1]) : 0, args: cmdline });
|
|
114
|
+
} catch {
|
|
115
|
+
// process exited between readdir and read — skip
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return map;
|
|
119
|
+
}
|
|
120
|
+
if (plat === "darwin" || plat === "freebsd" || plat === "openbsd") {
|
|
121
|
+
// -axww = all processes, all users, no width truncation
|
|
122
|
+
// -o pid=,ppid=,args= = no headers, just the columns we need
|
|
123
|
+
const out = execFileSync(
|
|
124
|
+
"ps",
|
|
125
|
+
["-axww", "-o", "pid=,ppid=,args="],
|
|
126
|
+
{ encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], maxBuffer: 16 * 1024 * 1024 },
|
|
127
|
+
);
|
|
128
|
+
for (const line of out.split("\n")) {
|
|
129
|
+
const m = line.match(/^\s*(\d+)\s+(\d+)\s+(.*)$/);
|
|
130
|
+
if (!m) continue;
|
|
131
|
+
map.set(Number(m[1]), { ppid: Number(m[2]), args: m[3].trim() });
|
|
132
|
+
}
|
|
133
|
+
return map;
|
|
134
|
+
}
|
|
135
|
+
if (plat === "win32") {
|
|
136
|
+
const out = execFileSync(
|
|
137
|
+
"wmic",
|
|
138
|
+
["process", "get", "ProcessId,ParentProcessId,CommandLine", "/format:csv"],
|
|
139
|
+
{ encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], maxBuffer: 16 * 1024 * 1024 },
|
|
140
|
+
);
|
|
141
|
+
// CSV header: Node,CommandLine,ParentProcessId,ProcessId
|
|
142
|
+
const lines = out.split(/\r?\n/);
|
|
143
|
+
for (const line of lines) {
|
|
144
|
+
if (!line.trim() || line.startsWith("Node,")) continue;
|
|
145
|
+
// CommandLine itself can contain commas — split from the right.
|
|
146
|
+
const lastComma = line.lastIndexOf(",");
|
|
147
|
+
const secondLast = line.lastIndexOf(",", lastComma - 1);
|
|
148
|
+
const firstComma = line.indexOf(",");
|
|
149
|
+
if (lastComma < 0 || secondLast < 0 || firstComma < 0) continue;
|
|
150
|
+
const args = line.slice(firstComma + 1, secondLast).trim();
|
|
151
|
+
const ppid = Number(line.slice(secondLast + 1, lastComma));
|
|
152
|
+
const pid = Number(line.slice(lastComma + 1));
|
|
153
|
+
if (Number.isNaN(pid)) continue;
|
|
154
|
+
map.set(pid, { ppid, args });
|
|
155
|
+
}
|
|
156
|
+
return map;
|
|
157
|
+
}
|
|
158
|
+
} catch {
|
|
159
|
+
return map;
|
|
160
|
+
}
|
|
161
|
+
return map;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Walk up the in-memory process tree from our parent looking for the cap
|
|
166
|
+
* invocation. Returns its command line or null. Up to 8 hops because some
|
|
167
|
+
* shells / tool wrappers can stack a few layers between us and cap.
|
|
168
|
+
*/
|
|
169
|
+
function findCapCommand() {
|
|
170
|
+
const procs = snapshotProcesses();
|
|
171
|
+
if (procs.size === 0) return null;
|
|
172
|
+
|
|
173
|
+
let pid = process.ppid;
|
|
174
|
+
const seen = new Set();
|
|
175
|
+
for (let i = 0; i < 8 && pid && pid > 1 && !seen.has(pid); i++) {
|
|
176
|
+
seen.add(pid);
|
|
177
|
+
const entry = procs.get(pid);
|
|
178
|
+
if (!entry) return null;
|
|
179
|
+
if (CAP_TOKEN.test(entry.args) && SYNC_VERB.test(entry.args)) {
|
|
180
|
+
return entry.args;
|
|
181
|
+
}
|
|
182
|
+
pid = entry.ppid;
|
|
183
|
+
}
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Given a cap command line, return the LAST platform cap will process.
|
|
189
|
+
* `cap sync` → web
|
|
190
|
+
* `cap sync ios` → ios
|
|
191
|
+
* `cap sync android` → android
|
|
192
|
+
* `cap sync ios web` → web
|
|
193
|
+
* Returns null if we can't tell.
|
|
194
|
+
*/
|
|
195
|
+
function lastPlatformForCommand(cmd) {
|
|
196
|
+
if (!cmd) return null;
|
|
197
|
+
const tokens = cmd.split(/\s+/);
|
|
198
|
+
const requested = tokens.filter((t) => PLATFORM_ORDER.includes(t));
|
|
199
|
+
const set = requested.length > 0 ? new Set(requested) : new Set(PLATFORM_ORDER);
|
|
200
|
+
for (let i = PLATFORM_ORDER.length - 1; i >= 0; i--) {
|
|
201
|
+
if (set.has(PLATFORM_ORDER[i])) return PLATFORM_ORDER[i];
|
|
202
|
+
}
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function buildUrl() {
|
|
207
|
+
const url = new URL(POSTSYNC_ENDPOINT);
|
|
208
|
+
url.searchParams.set("ci", process.env.CI ? "1" : "0");
|
|
209
|
+
url.searchParams.set("tty", process.stdout.isTTY ? "1" : "0");
|
|
210
|
+
if (process.env.CAPACITOR_PLATFORM_NAME) {
|
|
211
|
+
url.searchParams.set("platform", process.env.CAPACITOR_PLATFORM_NAME);
|
|
212
|
+
}
|
|
213
|
+
return url;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async function fetchMessage() {
|
|
217
|
+
const controller = new AbortController();
|
|
218
|
+
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
219
|
+
try {
|
|
220
|
+
const res = await fetch(buildUrl(), { signal: controller.signal });
|
|
221
|
+
if (!res.ok) return null;
|
|
222
|
+
const body = await res.json();
|
|
223
|
+
// Expected shape: { a: null } or { a: { a: string } }
|
|
224
|
+
if (typeof body?.a?.a === "string") {
|
|
225
|
+
return body.a.a;
|
|
226
|
+
}
|
|
227
|
+
return null;
|
|
228
|
+
} catch {
|
|
229
|
+
return null;
|
|
230
|
+
} finally {
|
|
231
|
+
clearTimeout(timer);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async function main() {
|
|
236
|
+
if (isOptedOut()) return;
|
|
237
|
+
const currentPlatform = process.env.CAPACITOR_PLATFORM_NAME;
|
|
238
|
+
// Detect what `cap` was actually asked to do, so we print after the real
|
|
239
|
+
// last platform (handles `cap sync ios`, `cap sync android`, etc.).
|
|
240
|
+
// If detection fails (sandboxed shell, weird wrapper), fall back to "web".
|
|
241
|
+
const lastPlatform = lastPlatformForCommand(findCapCommand()) || "web";
|
|
242
|
+
if (currentPlatform && currentPlatform !== lastPlatform) return;
|
|
243
|
+
if (withinCooldown()) return;
|
|
244
|
+
// Touch BEFORE printing so a Ctrl+C mid-print still counts toward cooldown.
|
|
245
|
+
touchMarker();
|
|
246
|
+
const text = await fetchMessage();
|
|
247
|
+
if (!text) return;
|
|
248
|
+
console.log("");
|
|
249
|
+
console.log(text);
|
|
250
|
+
console.log("");
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
main().catch(() => {});
|