मुफ़्त APK बनाएँ

JavaScript ब्रिज API

डिवाइस और ऐप की जानकारी

यह कौन-सा फ़ोन है, कौन-सा बिल्ड है, हर इंस्टॉल की स्थिर पहचान, और मानक लोकेशन व कैमरा API।

getUserMedia वेब स्टैंडर्ड#

navigator.mediaDevices.getUserMedia — camera and microphone

उदाहरण

मानक वेब कैमरा और माइक्रोफ़ोन API ऐप के अंदर काम करती है। ऐप Android का अपना अनुमति प्रॉम्प्ट दिखाता है और स्ट्रीम पेज को दे देता है।

लौटाता है: एक MediaStream का Promise, या रिजेक्शन (NotAllowedError जब उपयोगकर्ता ने मना किया या बिल्ड के समय स्विच बंद था, NotFoundError जब ऐसा कोई डिवाइस न हो)। ज़रूरत: चरण 4 (Access) में Camera और/या Mic। जब आप ZIP या HTML पेज से बिल्ड करते हैं और आपका कोड getUserMedia({ video: … }) या { audio: … } कॉल करता है (या <input capture>, BarcodeDetector, SpeechRecognition इस्तेमाल करता है), तो बिल्ड वह कॉल ढूँढकर आपके लिए सही अनुमति जोड़ देता है; { video: false } Camera नहीं जोड़ता। वेबसाइट (URL) बिल्ड में उन्हें खुद टिक करें।

वीडियो एलिमेंट में पीछे का कैमरा दिखाएँ।

<video id="preview" autoplay playsinline muted></video>
<button id="startCam">Start camera</button>
<script>
document.getElementById('startCam').addEventListener('click', async function () {
  if (!(navigator.mediaDevices && navigator.mediaDevices.getUserMedia)) {
    showMessage('Camera is not available here.');
    return;
  }
  try {
    var stream = await navigator.mediaDevices.getUserMedia({
      video: { facingMode: 'environment' },   // 'user' = front camera
      audio: false
    });
    document.getElementById('preview').srcObject = stream;
  } catch (err) {
    if (err.name === 'NotAllowedError') showMessage('Camera permission was refused.');
    else showMessage('Camera error: ' + err.name);
  }
});
</script>

सिर्फ़ माइक्रोफ़ोन से वॉइस नोट रिकॉर्ड करें।

async function recordVoice(seconds) {
  var stream;
  try {
    stream = await navigator.mediaDevices.getUserMedia({ audio: true, video: false });
  } catch (err) {
    showMessage('Microphone permission was refused.');
    return;
  }
  var chunks = [];
  var rec = new MediaRecorder(stream);
  rec.ondataavailable = function (e) { chunks.push(e.data); };
  rec.onstop = function () {
    stream.getTracks().forEach(function (t) { t.stop(); });   // turn the mic off
    var blob = new Blob(chunks, { type: rec.mimeType });
    document.getElementById('player').src = URL.createObjectURL(blob);
  };
  rec.start();
  setTimeout(function () { rec.stop(); }, seconds * 1000);
}

कैमरा स्ट्रीम से एक फ़ोटो लें और कैमरा छोड़ दें।

async function snapshot() {
  if (!(navigator.mediaDevices && navigator.mediaDevices.getUserMedia)) return null;
  let stream;
  try {
    stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: 'user' } });
  } catch (err) {
    showMessage(err.name === 'NotFoundError' ? 'No camera on this device.' : 'Camera permission was refused.');
    return null;
  }
  const video = document.createElement('video');
  video.srcObject = stream;
  await video.play();
  const canvas = document.createElement('canvas');
  canvas.width = video.videoWidth; canvas.height = video.videoHeight;
  canvas.getContext('2d').drawImage(video, 0, 0);
  stream.getTracks().forEach(function (t) { t.stop(); });
  return canvas.toDataURL('image/jpeg', 0.9);
}

ध्यान दें: जब आप कैमरा और माइक साथ में माँगते हैं, तो ऐप यूज़र से दोनों एक साथ माँगता है। अगर बिल्ड के समय स्विच बंद था, तो रिक्वेस्ट बिना कोई प्रॉम्प्ट दिखाए मना हो जाती है - स्विच चालू करके दोबारा बिल्ड करें। काम पूरा होने पर हमेशा ट्रैक बंद करें, वरना कैमरा/माइक की लाइट जलती रहती है। DRM वीडियो (Widevine) बिना किसी स्विच के अनुमत है। कैमरा और माइक्रोफ़ोन संवेदनशील हैं: इन्हें अपने Play Data safety फ़ॉर्म में बताएँ।

IdleDetector वेब स्टैंडर्ड#

new IdleDetector() — Idle Detection

उदाहरण

मानक Idle Detection API: जानें कि उपयोगकर्ता ने फ़ोन इस्तेमाल करना कब बंद किया (कुछ देर कोई इनपुट नहीं) या स्क्रीन कब लॉक की, और वह कब लौटा - "दूर" स्थिति दिखाने, लाइव सेशन रोकने या वॉल्ट लॉक करने के लिए।

लौटाता है: IdleDetector.requestPermission() → 'granted' या 'denied' (टैप चाहिए); detector.start({threshold, signal}) → Promise (अनुमति के बिना NotAllowedError से रिजेक्ट, 60000 ms से कम पर TypeError से); फिर userState ('active'/'idle'), screenState ('locked'/'unlocked') और एक change इवेंट। ज़रूरत: चालू करने के लिए कुछ नहीं और कोई Android अनुमति नहीं।

document.getElementById('away').addEventListener('click', async () => {
  if (!('IdleDetector' in window)) return;
  if ((await IdleDetector.requestPermission()) !== 'granted') return;
  const ac = new AbortController();
  const idle = new IdleDetector();
  idle.addEventListener('change', () => {
    setStatus(idle.userState === 'idle' || idle.screenState === 'locked' ? 'Away' : 'Online');
  });
  await idle.start({ threshold: 5 * 60000, signal: ac.signal });
  stopButton.onclick = () => ac.abort();
});

अगले लॉन्च पर नए टैप के बिना फिर से देखना शुरू करें, जब उपयोगकर्ता पहले ही हाँ कह चुका हो:

async function watchAwayIfAllowed() {
  if (!('IdleDetector' in window)) return;
  const perm = await navigator.permissions.query({ name: 'idle-detection' });
  if (perm.state !== 'granted') return;          // 'prompt' until requestPermission() ran from a tap
  const idle = new IdleDetector();
  idle.onchange = () => {
    if (idle.screenState === 'locked') pauseLiveSession();
    else if (idle.userState === 'active') resumeLiveSession();
  };
  await idle.start({ threshold: 60000 });         // 1 minute is the smallest threshold
}
watchAwayIfAllowed();

उपयोगकर्ता को ऑटो-लॉक की देरी बदलने दें: चल रहे डिटेक्टर को रोकें, नया शुरू करें, और असली त्रुटियाँ बताएँ:

let vaultWatch = null;
async function setAutoLock(minutes) {
  if (vaultWatch) vaultWatch.abort();             // aborting stops the old detector
  vaultWatch = new AbortController();
  const idle = new IdleDetector();
  idle.addEventListener('change', () => {
    if (idle.userState === 'idle' || idle.screenState === 'locked') lockVault();
  });
  try {
    await idle.start({ threshold: minutes * 60000, signal: vaultWatch.signal });
  } catch (err) {
    if (err.name === 'NotAllowedError') showMessage('Tap "Enable auto-lock" first.');
    else if (err instanceof TypeError) showMessage('Pick at least 1 minute.');
    else throw err;
  }
}

ध्यान दें: ऐप में "इनपुट" का मतलब है इसी ऐप के सामने रहते हुए उपयोगकर्ता का इनपुट; जब स्क्रीन चालू हो और कोई दूसरा ऐप सामने हो, तो उपयोगकर्ता सक्रिय माना जाता है (कोई ऐप दूसरे ऐप्स का इनपुट नहीं देख सकता - Android पर Chrome भी यही जवाब देता है); स्क्रीन बंद या लॉक होने के बाद, निष्क्रिय समय आख़िरी टच से गिना जाता है। अनुमति याद रखी जाती है, और navigator.permissions.query({name:'idle-detection'}) उसका जवाब देता है। अंदरूनी ट्रांसपोर्ट: __idleState, __idlePermission; लॉक/अनलॉक के बदलाव appmint:idle के रूप में आते हैं। AI से बने ऐप इस्तेमाल करते हैं watchIdle().

navigator.geolocation वेब स्टैंडर्ड#

navigator.geolocation

उदाहरण

मानक वेब लोकेशन API ऐप के अंदर काम करती है। ऐप Android का अपना लोकेशन प्रॉम्प्ट दिखाता है और जवाब पेज को दे देता है।

लौटाता है: सामान्य W3C कॉलबैक: एक GeolocationPosition (coords.latitude, coords.longitude, coords.accuracy, …) या एक GeolocationPositionError (code 1 = अनुमति नहीं मिली, 2 = उपलब्ध नहीं, 3 = समय सीमा पूरी)। ज़रूरत: चरण 4 (Access) में GPS (सटीक) और/या Location (अनुमानित)। जब आप ZIP या HTML पेज से बिल्ड करते हैं और आपका कोड navigator.geolocation, getCurrentPosition या watchPosition को कॉल करता है, तो बिल्ड वह कॉल ढूँढकर आपके लिए लोकेशन जोड़ देता है। वेबसाइट (URL) बिल्ड में इसे खुद टिक करें।

लोकेशन एक बार पाएँ।

function whereAmI() {
  if (!('geolocation' in navigator)) { showMessage('Location is not available here.'); return; }

  navigator.geolocation.getCurrentPosition(
    function (pos) {
      showMessage('You are at ' + pos.coords.latitude.toFixed(5) + ', ' +
                  pos.coords.longitude.toFixed(5) + ' (±' + Math.round(pos.coords.accuracy) + ' m)');
    },
    function (err) {
      if (err.code === 1) showMessage('Location permission was refused.');
      else if (err.code === 3) showMessage('Location timed out. Try again outside.');
      else showMessage('Location is unavailable. Is location turned on in phone settings?');
    },
    { enableHighAccuracy: true, timeout: 15000, maximumAge: 0 }
  );
}

उपयोगकर्ता के चलने के साथ उसे फ़ॉलो करें (रन ट्रैकर, डिलीवरी मैप)। स्क्रीन बंद होने पर देखना बंद कर दें।

var watchId = null;

function startTracking() {
  if (!('geolocation' in navigator)) return;
  watchId = navigator.geolocation.watchPosition(
    function (pos) { addPointToMap(pos.coords.latitude, pos.coords.longitude); },
    function (err) { showMessage('Tracking stopped: ' + err.message); },
    { enableHighAccuracy: true }
  );
}

function stopTracking() {
  if (watchId !== null) { navigator.geolocation.clearWatch(watchId); watchId = null; }
}

पहले अनुमति जाँचें, फिर लोकेशन तभी माँगें जब जवाब मिल सकता हो:

async function locateIfAllowed() {
  if (!('geolocation' in navigator)) return;
  if (navigator.permissions) {
    const s = await navigator.permissions.query({ name: 'geolocation' });
    if (s.state === 'denied') { showMessage('Location is off for this app. Turn it on in Settings.'); return; }
  }
  navigator.geolocation.getCurrentPosition(
    (pos) => centerMap(pos.coords.latitude, pos.coords.longitude),
    (err) => showMessage('Location error ' + err.code + ': ' + err.message),
    { timeout: 10000 }
  );
}

ध्यान दें: GPS चालू होने पर ऐप सटीक और अनुमानित लोकेशन एक साथ माँगता है; Android 12+ पर उपयोगकर्ता "Approximate" चुन सकता है, जो फिर भी काम करता है (सटीकता कम होती है)। सिर्फ़ Location चालू हो तो सिर्फ़ अनुमानित लोकेशन माँगी जाती है। दोनों बंद हों तो हर कॉल कोड 1 के साथ फ़ेल होती है और कोई प्रॉम्प्ट नहीं आता। उपयोगकर्ता एक बार अनुमति दे दे, तो बाद की कॉल बिना प्रॉम्प्ट के जवाब पाती हैं। फ़ोन का अपना लोकेशन स्विच भी चालू होना चाहिए। लोकेशन निजी डेटा है: इसे अपने Play Data safety फ़ॉर्म में बताएँ; बैकग्राउंड लोकेशन उपलब्ध नहीं है।

navigator.getInstalledRelatedApps वेब स्टैंडर्ड#

navigator.getInstalledRelatedApps()

उदाहरण

मानक related-apps API: पेज को बताती है कि उसके अपने Android ऐप्स (जो उसके वेब ऐप मैनिफ़ेस्ट में लिखे हैं) में से कौन-से इंस्टॉल हैं - ताकि "हमारा दूसरा ऐप इंस्टॉल करें" बैनर छिपाया जा सके, या सीधे उसमें लिंक किया जा सके।

लौटाता है: मैनिफ़ेस्ट की उन related_applications एंट्री की ऐरे का Promise जो इंस्टॉल हैं: { platform: 'play', id, url?, version? }। ज़रूरत: एक <link rel="manifest"> जिसके मैनिफ़ेस्ट में ऐप "platform": "play" और "id": "<package name>" के साथ लिखे हों, और जो ऐप के साथ बंडल हो ताकि बिल्ड उन्हें घोषित कर सके (Android ऐप को सिर्फ़ वही पैकेज देखने देता है जिनके नाम उसने बताए हों)।

// manifest.webmanifest: { "related_applications": [{ "platform": "play", "id": "com.example.scanner" }] }
const apps = await navigator.getInstalledRelatedApps();
const hasScanner = apps.some((a) => a.id === 'com.example.scanner');
document.getElementById('get-scanner').hidden = hasScanner;

साथी ऐप इंस्टॉल हो तो उसे खोलें (एंट्री का url आपके मैनिफ़ेस्ट से आता है), वरना उपयोगकर्ता को उसके स्टोर पेज पर भेजें:

// manifest: { "platform": "play", "id": "com.example.scanner", "url": "https://example.com/scan" }
document.getElementById('open-scanner').addEventListener('click', async () => {
  const apps = typeof navigator.getInstalledRelatedApps === 'function'
    ? await navigator.getInstalledRelatedApps() : [];
  const scanner = apps.find((a) => a.id === 'com.example.scanner');
  location.href = scanner && scanner.url
    ? scanner.url
    : 'https://play.google.com/store/apps/details?id=com.example.scanner';
});

"हमारी और ऐप्स" स्क्रीन पर दिखाएँ कि आपके कौन-से ऐप फ़ोन में पहले से मौजूद हैं:

async function renderMoreApps(catalog) {         // [{ id, name }] matching your manifest entries
  if (typeof navigator.getInstalledRelatedApps !== 'function') return; // e.g. inside an iframe
  const installed = new Set((await navigator.getInstalledRelatedApps()).map((a) => a.id));
  document.getElementById('more-apps').innerHTML = catalog.map((app) =>
    `<li>${app.name} — ${installed.has(app.id) ? 'Installed' : '<a href="https://play.google.com/store/apps/details?id=' + app.id + '">Get it</a>'}</li>`
  ).join('');
}

ध्यान दें: बिल्ड आपके बंडल का मैनिफ़ेस्ट पढ़ता है और लिखे हर पैकेज को घोषित करता है; जो ऐप वहाँ नहीं लिखा, वह दिख नहीं सकता और कभी रिपोर्ट नहीं होता। वेबसाइट-मोड वाले ऐप अपना मैनिफ़ेस्ट साइट पर रखते हैं, इसलिए कुछ घोषित नहीं होता और सूची खाली रहती है। अंदरूनी ट्रांसपोर्ट: __relatedAppsInstalled.

navigator.permissions वेब स्टैंडर्ड#

navigator.permissions.query({ name })

उदाहरण

मानक Permissions API, जिसका जवाब फ़ोन की असली Android अनुमति स्थिति से आता है - ताकि पेज "कैमरा की अनुमति दें" तभी दिखाए जब अब भी माँगा जा सकता हो, और "इसे सेटिंग्स में चालू करें" तब दिखाए जब न माँगा जा सके।

लौटाता है: PermissionStatus {name, state, onchange} का Promise; state 'granted', 'denied' या 'prompt' होता है, और change अनुमति डायलॉग बंद होते ही या उपयोगकर्ता के सेटिंग्स से लौटने पर चलता है। ज़रूरत: चालू करने के लिए कुछ नहीं।

async function cameraButton() {
  const btn = document.getElementById('scan');
  const status = await navigator.permissions.query({ name: 'camera' });
  function render() {
    if (status.state === 'denied') {
      btn.textContent = 'Camera is off — enable it in Settings';
      btn.disabled = true;
    } else {
      btn.textContent = status.state === 'granted' ? 'Scan' : 'Allow camera and scan';
      btn.disabled = false;
    }
  }
  render();
  status.onchange = render;   // e.g. after the Android dialog, or back from Settings
}
cameraButton();

// The other names the app answers the same way:
for (const name of ['microphone', 'geolocation', 'notifications']) {
  navigator.permissions.query({ name }).then((s) => console.log(name, s.state));
}

तभी माँगें जब पेज अब भी माँग सकता हो, वरना उपयोगकर्ता को सेटिंग्स में भेजें:

async function ensureMic() {
  if (!(navigator.permissions && navigator.permissions.query)) return true;   // a browser without it
  const s = await navigator.permissions.query({ name: 'microphone' });
  if (s.state === 'denied') {
    showMessage('Microphone is off for this app. Turn it on in Settings.');
    return false;
  }
  return true;   // 'granted', or 'prompt' (Android asks on first use)
}

document.getElementById('record').onclick = async () => {
  if (await ensureMic()) startRecording();
};

जब उपयोगकर्ता सेटिंग्स में कोई अनुमति बदले तो प्रतिक्रिया दें, और उसे असली अनुरोध के साथ जोड़ें:

async function watchLocationSwitch() {
  const status = await navigator.permissions.query({ name: 'geolocation' });
  status.addEventListener('change', () => {
    if (status.state === 'granted') {
      navigator.geolocation.getCurrentPosition((p) => showNearby(p.coords.latitude, p.coords.longitude));
    } else {
      hideNearby();
    }
  });
}
watchLocationSwitch();

ध्यान दें: denied उस क्षमता को भी कवर करता है जिसके बिना ऐप बना था (बिल्ड में उसका स्विच बंद था, इसलिए Android उसे कभी नहीं दे सकता) और "फिर न पूछें" वाले इनकार को भी। screen-wake-lock, clipboard-read और clipboard-write 'granted' हैं (ऐप इन्हें बिना प्रॉम्प्ट के देता है)। दूसरे नामों (midi, accelerometer, …) का जवाब पहले की तरह WebView देता है। इसके पीछे का इवेंट: appmint:permissions-changed.

TextDetector वेब स्टैंडर्ड#

new TextDetector().detect(image) — text recognition (OCR)

उदाहरण

फ़ोन पर ही किसी तस्वीर का टेक्स्ट पढ़ता है - रसीद का कुल, सीरियल नंबर, बिज़नेस कार्ड - Google के ML Kit रिकग्नाइज़र से। इसका रूप Shape Detection API का TextDetector है, जिसे Chrome एक फ़्लैग के पीछे देता है।

लौटाता है: ऐरे का Promise, हर लाइन के लिए एक एंट्री: इमेज के पिक्सेल में { rawValue, boundingBox, cornerPoints }। Google Play services के बिना NotSupportedError से रिजेक्ट होता है और पहचान फ़ेल होने पर OperationError से। ज़रूरत: चालू करने के लिए कुछ नहीं और कोई अनुमति नहीं (कैमरा से पढ़ने के लिए <input type="file" accept="image/*" capture> या कैमरा स्ट्रीम से फ़ोटो लें)।

document.getElementById('photo').addEventListener('change', async (e) => {
  const file = e.target.files[0];
  if (!file || !('TextDetector' in window)) return;
  status.textContent = 'Reading…';
  try {
    const lines = await new TextDetector().detect(file);   // a Blob, <img>, <canvas>, <video> or ImageBitmap
    const text = lines.map((l) => l.rawValue).join('\n');
    const total = /total\s*[:$€£]?\s*([\d.,]+)/i.exec(text);
    status.textContent = total ? 'Total: ' + total[1] : text;
  } catch (err) {
    status.textContent = 'Could not read the text (' + err.name + ')';
  }
});

लाइव कैमरा प्रीव्यू से सीरियल नंबर पढ़ें (चल रहा <video> जैसा है वैसा दिया जाता है, उसका मौजूदा फ़्रेम पढ़ा जाता है):

const video = document.querySelector('video');
async function startPreview() {
  video.srcObject = await navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment' } });
  await video.play();
}

document.getElementById('read-serial').addEventListener('click', async () => {
  if (!('TextDetector' in window)) return;
  const lines = await new TextDetector().detect(video);
  const hit = lines.map((l) => l.rawValue).find((t) => /S\/?N[:\s]*[A-Z0-9-]{6,}/i.test(t));
  serialField.value = hit ? hit.replace(/^.*S\/?N[:\s]*/i, '') : '';
  if (!hit) showMessage('No serial number in view — move closer and try again.');
});

boundingBox (इमेज पिक्सेल) की मदद से फ़ोटो के ऊपर पहचानी गई हर लाइन की आउटलाइन बनाएँ:

async function outlineText(img, canvas) {
  await img.decode();                             // detect() rejects InvalidStateError before it has loaded
  canvas.width = img.naturalWidth;
  canvas.height = img.naturalHeight;
  const g = canvas.getContext('2d');
  g.drawImage(img, 0, 0);
  const lines = await new TextDetector().detect(img);
  g.strokeStyle = '#ff3b30';
  g.lineWidth = 3;
  for (const { boundingBox: b, rawValue } of lines) {
    g.strokeRect(b.x, b.y, b.width, b.height);
    console.log(rawValue);
  }
  return lines.length;
}

जहाँ टेक्स्ट पहचान उपलब्ध नहीं (Google Play services नहीं हैं) वहाँ स्कैन बटन छिपाएँ, और बिज़नेस कार्ड से संपर्क की जानकारी निकालें:

const scanBtn = document.getElementById('scan-card');
scanBtn.hidden = !('TextDetector' in window);
document.getElementById('card-photo').addEventListener('change', async (e) => {
  const file = e.target.files[0];
  if (!file) return;
  const text = (await new TextDetector().detect(file)).map((l) => l.rawValue).join('\n');
  fillContact({
    email: (text.match(/[\w.+-]+@[\w-]+\.[\w.]+/) || [''])[0],
    phone: (text.match(/\+?\d[\d\s().-]{7,}\d/) || [''])[0],
  });
});

ध्यान दें: सिर्फ़ लैटिन लिपियाँ (अंग्रेज़ी, स्पेनिश, फ़्रेंच, जर्मन, इंडोनेशियाई, वियतनामी, तुर्की और अन्य); चीनी, देवनागरी, जापानी, कोरियाई और अरबी पहचानी नहीं जातीं। मॉडल Google Play services से आता है: आपका कोड इसे इस्तेमाल करे तो बिल्ड इंस्टॉल के समय इसे माँगता है, वरना नए फ़ोन पर पहली कॉल कुछ सेकंड रुकती है जब तक यह डाउनलोड होता है। दूसरी साइट की इमेज में CORS की अनुमति होनी चाहिए (दूषित canvas SecurityError को रिजेक्ट कर देता है)। अंदरूनी ट्रांसपोर्ट: __ocrAvailable, __ocrRecognize (जवाब appmint:ocr के रूप में आता है)। AI से बने ऐप इस्तेमाल करते हैं recognizeText().

__idlePermission ब्रिज#

window.WebToApk.__idlePermission(request: Boolean): String

IdleDetector.requestPermission() ([request] true, उपयोगकर्ता के जेस्चर के दौरान कॉल किया गया) अनुमति दर्ज करता है; false सिर्फ़ उसे पढ़ता है। "granted" | "prompt".

उदाहरण

IdleDetector.requestPermission() और 'idle-detection' अनुमति की स्थिति के पीछे का अंदरूनी ट्रांसपोर्ट। पेज मानक API इस्तेमाल करते हैं; इसे कभी सीधे कॉल न करें - __ वाले तरीके बिना सूचना के बदल सकते हैं।

लौटाता है: 'granted' या 'prompt'। ज़रूरत: अनुरोध के लिए उपयोगकर्ता का जेस्चर।

पब्लिक API इस्तेमाल करें:

button.onclick = async () => {
  const state = await IdleDetector.requestPermission();
  console.log(state); // 'granted'
};

पहले जाँच लें कि फ़ीचर मौजूद है, और मना किए जाने को "अनुमति नहीं है" मानें:

async function askIdlePermission() {
  if (!('IdleDetector' in window)) return false;
  try {
    return (await IdleDetector.requestPermission()) === 'granted';
  } catch (err) {
    return false;
  }
}

टैप पर अनुमति माँगें, फिर पहचान शुरू करें:

startBtn.addEventListener('click', async () => {
  if (!(await askIdlePermission())) { showMessage('Idle detection was not allowed.'); return; }
  const idle = new IdleDetector();
  idle.addEventListener('change', () => console.log(idle.userState));
  await idle.start({ threshold: 60000 });
});

ध्यान दें: ऐप शेल (IdleAndRelatedApps.kt) में लागू किया गया है।

__idleState ब्रिज#

window.WebToApk.__idleState(): String

IdleDetector: ऐप के अपने इनपुट और कीगार्ड से `{idleMs, locked}`।

उदाहरण

IdleDetector के पीछे का अंदरूनी ट्रांसपोर्ट: उपयोगकर्ता ने आख़िरी बार इनपुट कब दिया और स्क्रीन लॉक है या नहीं। पेज मानक API इस्तेमाल करते हैं; इसे कभी सीधे कॉल न करें - __ वाले तरीके बिना सूचना के बदल सकते हैं।

लौटाता है: JSON {idleMs, locked}। स्क्रीन और ऐप के बदलाव उसी रूप में appmint:idle इवेंट के रूप में भेजे जाते हैं। ज़रूरत: कुछ नहीं।

पब्लिक API इस्तेमाल करें:

const idle = new IdleDetector();
idle.onchange = () => console.log(idle.userState, idle.screenState);
await idle.start({ threshold: 60000 });

स्क्रीन बंद होने पर AbortController से देखना बंद करें:

const ctl = new AbortController();
const idle = new IdleDetector();
idle.addEventListener('change', () => {
  if (idle.userState === 'idle') showAwayBanner();
});
await idle.start({ threshold: 60000, signal: ctl.signal });
// later: ctl.abort();

स्क्रीन लॉक होने पर ऐप को लॉक करें:

if ('IdleDetector' in window && (await IdleDetector.requestPermission()) === 'granted') {
  const d = new IdleDetector();
  d.onchange = () => { if (d.screenState === 'locked') showPinScreen(); };
  await d.start({ threshold: 60000 });
}

ध्यान दें: ऐप शेल (IdleAndRelatedApps.kt) में लागू किया गया है।

__permissionState ब्रिज#

window.WebToApk.__permissionState(name: String): String

Android के असली अनुमति-स्वीकृतियों से, किसी W3C अनुमति नाम की Permissions API स्थिति: "granted" | "denied" | "prompt", या "" जब शेल उस नाम का जवाब नहीं देता (तब पेज की क्वेरी WebView के अपने इम्प्लीमेंटेशन के पास जाती है)।

उदाहरण

navigator.permissions.query({ name }) के पीछे का आंतरिक ट्रांसपोर्ट। पेज स्टैंडर्ड API इस्तेमाल करते हैं; इसे कभी सीधे कॉल न करें - __ मेथड बिना सूचना के बदल सकते हैं।

लौटाता है: 'granted', 'denied', 'prompt', या '' उस नाम के लिए जिसका जवाब ऐप नहीं देता (तब क्वेरी WebView की अपनी जगह जाती है)। ज़रूरत: चालू करने के लिए कुछ नहीं।

पब्लिक API इस्तेमाल करें:

const status = await navigator.permissions.query({ name: 'camera' });
showCameraHint(status.state);                       // 'granted' | 'denied' | 'prompt'
status.addEventListener('change', () => showCameraHint(status.state));

एक साथ कई अनुमतियाँ जाँचें:

const names = ['camera', 'microphone', 'geolocation', 'notifications'];
const states = await Promise.all(names.map((name) =>
  navigator.permissions.query({ name }).then((s) => s.state, () => 'unknown')));
names.forEach((name, i) => console.log(name, states[i]));

तभी माँगें जब स्थिति denied न हो, और जब हो तब बताएँ कि क्या करना है:

async function ensureLocation() {
  const s = await navigator.permissions.query({ name: 'geolocation' });
  if (s.state === 'denied') { showMessage('Turn location on in the phone settings.'); return null; }
  return new Promise((ok, fail) => navigator.geolocation.getCurrentPosition(ok, fail));
}

ध्यान दें: इनका जवाब देता है: camera, microphone, geolocation, notifications, screen-wake-lock, clipboard-read, clipboard-write। जिस क्षमता को बिल्ड ने बंद रखा (या जिसकी अनुमति मैनिफ़ेस्ट में नहीं है) वह denied होती है। लाइव स्थितियाँ appmint:permissions-changed (हर अनुमति डायलॉग के बाद भेजा जाता है) पर और पेज दोबारा दिखने पर फिर से पढ़ी जाती हैं।

__relatedAppsInstalled ब्रिज#

window.WebToApk.__relatedAppsInstalled(idsJson: String): String

navigator.getInstalledRelatedApps(): [idsJson] (पेज के मैनिफ़ेस्ट के पैकेज नाम) में से कौन-से इंस्टॉल हैं और इस ऐप को दिखते हैं।

उदाहरण

navigator.getInstalledRelatedApps() के पीछे का आंतरिक ट्रांसपोर्ट। पेज स्टैंडर्ड API इस्तेमाल करते हैं; इसे कभी सीधे कॉल न करें - __ मेथड बिना सूचना के बदल सकते हैं।

लौटाता है: दिए गए उन पैकेज नामों की JSON ऐरे जो इंस्टॉल हैं और इस ऐप को दिखते हैं। ज़रूरत: बिल्ड के समय घोषित पैकेज (बंडल किए गए वेब ऐप मैनिफ़ेस्ट से)।

पब्लिक API इस्तेमाल करें:

const apps = await navigator.getInstalledRelatedApps();
console.log(apps.map((a) => a.id));

"साथी ऐप पाएँ" बटन तभी दिखाएँ जब वह इंस्टॉल न हो:

async function showCompanionLink() {
  if (!navigator.getInstalledRelatedApps) return;
  const apps = await navigator.getInstalledRelatedApps();
  document.getElementById('get-companion').hidden = apps.length > 0;
}

पैकेज उस वेब ऐप मैनिफ़ेस्ट से आते हैं जिससे पेज जुड़ा है:

<link rel="manifest" href="/manifest.webmanifest">
<!-- manifest.webmanifest: "related_applications": [{ "platform": "play", "id": "com.example.companion" }] -->

ध्यान दें: ऐप शेल (IdleAndRelatedApps.kt) में लागू किया गया है।

getAppName ब्रिज#

window.WebToApk.getAppName(): String

क्रिएटर ने विज़ार्ड में जो ऐप का नाम चुना - लॉन्चर जो दिखाता है।

उदाहरण

बिल्ड विज़ार्ड में आपका चुना हुआ ऐप का नाम बताता है - लॉन्चर आइकन के नीचे वाला नाम।

लौटाता है: तुरंत एक स्ट्रिंग। ज़रूरत: कुछ नहीं।

टाइटल और शेयर टेक्स्ट में ऐप का असली नाम इस्तेमाल करें।

var appName = (window.WebToApk && window.WebToApk.getAppName)
  ? WebToApk.getAppName()
  : document.title;                       // in a browser: the page title

document.getElementById('welcome').textContent = 'Welcome to ' + appName;
document.getElementById('shareBtn').addEventListener('click', function () {
  if (navigator.share) navigator.share({ text: 'Try ' + appName + '!' });
});

ऐप का नाम पेज टाइटल और हेडिंग में रखें:

if (window.WebToApk && WebToApk.getAppName) {
  const name = WebToApk.getAppName();
  document.title = name;
  document.querySelector('h1').textContent = name;
}

ऐसा सपोर्ट ईमेल बनाएँ जिसमें ऐप और उसका वर्ज़न लिखा हो:

function supportMailto() {
  const b = window.WebToApk;
  const name = b && b.getAppName ? b.getAppName() : 'our app';
  const version = b && b.getAppVersion ? b.getAppVersion() : 'web';
  const subject = encodeURIComponent('Help with ' + name + ' ' + version);
  return 'mailto:[email protected]?subject=' + subject;
}
document.getElementById('contact').href = supportMailto();

ध्यान दें: यह विज़ार्ड वाला नाम है, पेज का <title> नहीं। यह चलते समय नहीं बदलता। वर्ज़न के लिए इसका इस्तेमाल करें getAppVersion().

getAppVersion ब्रिज#

window.WebToApk.getAppVersion(): String

इंस्टॉल किए गए पैकेज का versionName, जैसे "1.4.2" - वह स्ट्रिंग जो क्रिएटर बिल्ड करते समय तय करता है, versionCode नहीं। अगर पैकेज पढ़ा न जा सके तो "1.0" लौटाता है, ताकि पेज बिना null जाँच के इसे प्रिंट कर सके।

उदाहरण

इंस्टॉल किए गए ऐप का वर्ज़न नाम बताता है, उदाहरण के लिए "1.4.2".

लौटाता है: तुरंत एक स्ट्रिंग। ज़रूरत: कुछ नहीं।

About स्क्रीन में वर्ज़न दिखाएँ।

var version = (window.WebToApk && window.WebToApk.getAppVersion)
  ? WebToApk.getAppVersion()
  : 'web';

document.getElementById('about').textContent = 'Version ' + version;

इंस्टॉल किया वर्ज़न पुराना हो तो अपडेट के लिए कहें:

function isOlder(a, b) {            // "1.4.2" < "1.10.0"
  const x = a.split('.').map(Number), y = b.split('.').map(Number);
  for (let i = 0; i < Math.max(x.length, y.length); i++) {
    if ((x[i] || 0) !== (y[i] || 0)) return (x[i] || 0) < (y[i] || 0);
  }
  return false;
}

if (window.WebToApk && WebToApk.getAppVersion && isOlder(WebToApk.getAppVersion(), '1.5.0')) {
  document.getElementById('update-banner').hidden = false;
}

बग रिपोर्ट में getDeviceInfo() से वर्ज़न और वर्ज़न कोड जोड़ें:

function reportInfo() {
  const b = window.WebToApk;
  if (!(b && b.getAppVersion)) return 'web';
  let text = 'v' + b.getAppVersion();
  if (b.getDeviceInfo) {
    const info = JSON.parse(b.getDeviceInfo());
    text += ' (build ' + info.app.versionCode + ')';
  }
  return text;
}
document.getElementById('report').value = reportInfo();

ध्यान दें: यह वर्ज़न का NAME है जो आपने बिल्ड करते समय तय किया, वर्ज़न कोड नहीं। अगर पैकेज पढ़ा न जा सके तो यह "1.0" लौटाता है, इसलिए कभी खाली नहीं होता। वर्ज़न कोड और इंस्टॉल की तारीखें getDeviceInfo() में हैं, इसके तहत app.

getDeviceInfo ब्रिज#

window.WebToApk.getDeviceInfo(): String

WebToApk.getDeviceInfo() -> JSON स्ट्रिंग, कोई अनुमति ज़रूरी नहीं WebToApk.getPhoneIdentity() -> कैरियर/SIM, Phone अनुमति चाहिए WebToApk.getInstallId() -> हर इंस्टॉल का स्थिर UUID

अलग से नहीं, बल्कि अपने समूह Device / app information के साथ बताया गया है।

उदाहरण

फ़ोन, Android, स्क्रीन, आपके ऐप और मौजूदा स्थिति (बैटरी, मेमोरी, नेटवर्क) की जानकारी एक ही कॉल में देता है।

लौटाता है: तुरंत एक JSON स्ट्रिंग - इस्तेमाल करें JSON.parse। टॉप-लेवल कुंजियाँ: android, hardware, screen, app, runtime, installId। ज़रूरत: कुछ नहीं - कोई अनुमति नहीं।

फ़ोन की जानकारी दिखाएँ।

function showPhoneInfo() {
  if (!(window.WebToApk && window.WebToApk.getDeviceInfo)) {
    document.getElementById('out').textContent = navigator.userAgent;   // browser: only the user agent
    return;
  }
  var info = JSON.parse(WebToApk.getDeviceInfo());

  document.getElementById('out').innerHTML =
    'Android: ' + info.android.release + ' (SDK ' + info.android.sdkInt + ')<br>' +
    'Version ID: ' + info.android.buildId + '<br>' +
    'Build number: ' + info.android.incremental + '<br>' +
    'Phone: ' + info.hardware.manufacturer + ' ' + info.hardware.model + '<br>' +
    'Screen: ' + info.screen.widthPx + 'x' + info.screen.heightPx + ' @' + info.screen.refreshRate + 'Hz<br>' +
    'Battery: ' + info.runtime.batteryLevel + '%' + (info.runtime.charging ? ' (charging)' : '') + '<br>' +
    'Network: ' + info.runtime.networkType + '<br>' +
    'App: ' + info.app.versionName + ' (' + info.app.versionCode + ')<br>' +
    'WebView: ' + info.app.webViewPackage;
}

सारी कुंजियाँ:

{
  "android":  { "release": "14", "sdkInt": 34, "codename": "REL", "buildId": "UP1A.231005.007", "incremental": "R.1a2b3c", "display": "…", "fingerprint": "…", "securityPatch": "2026-08-05" },
  "hardware": { "manufacturer": "OPPO", "brand": "OPPO", "model": "CPH2269", "device": "…", "product": "…", "board": "…", "hardware": "…", "abis": ["arm64-v8a"], "socManufacturer": "…", "socModel": "…", "isEmulator": false },
  "screen":   { "widthPx": 720, "heightPx": 1600, "density": 2, "densityDpi": 320, "refreshRate": 90, "nightMode": true },
  "app":      { "packageName": "com.example.app", "versionName": "1.0", "versionCode": 1, "targetSdk": 35, "firstInstallTime": 1737000000000, "lastUpdateTime": 1737000000000, "installer": "com.android.vending", "webViewPackage": "com.google.android.webview 131.0" },
  "runtime":  { "totalMemoryBytes": 4000000000, "availableMemoryBytes": 1500000000, "lowMemory": false, "storageFreeBytes": 20000000000, "storageTotalBytes": 64000000000, "batteryLevel": 80, "charging": false, "locale": "en-IN", "timezone": "Asia/Kolkata", "networkType": "wifi" },
  "installId": "3f2b8c1e-…"
}

कम स्टोरेज या कम मेमोरी वाले फ़ोन पर चेतावनी दें, बड़े डाउनलोड से पहले।

function canDownload(bytesNeeded) {
  if (!(window.WebToApk && WebToApk.getDeviceInfo)) return true;   // browser: let the download try
  const r = JSON.parse(WebToApk.getDeviceInfo()).runtime;
  if (r.storageFreeBytes < bytesNeeded * 2) { showMessage('Not enough free storage.'); return false; }
  if (r.lowMemory) showMessage('The phone is low on memory; this may be slow.');
  return true;
}

ध्यान दें: socManufacturer / socModel सिर्फ़ Android 12+ पर मिलते हैं। networkType इनमें से एक है: wifi, cellular, ethernet, other, none या unknown। installer Play इंस्टॉल के लिए com.android.vending है और साइड-लोड के लिए खाली। मान उसी पल पढ़े जाते हैं जब आप कॉल करते हैं - बैटरी या नेटवर्क ताज़ा करने के लिए दोबारा कॉल करें। IMEI या सीरियल नंबर नहीं मिलता: Android सभी ऐप्स के लिए इन्हें रोकता है; इस्तेमाल करें getInstallId().

getInstallId ब्रिज#

window.WebToApk.getInstallId(): String

WebToApk.getDeviceInfo() -> JSON स्ट्रिंग, कोई अनुमति ज़रूरी नहीं WebToApk.getPhoneIdentity() -> कैरियर/SIM, Phone अनुमति चाहिए WebToApk.getInstallId() -> हर इंस्टॉल का स्थिर UUID

अलग से नहीं, बल्कि अपने समूह Device / app information के साथ बताया गया है।

उदाहरण

एक रैंडम ID देता है जो आपके ऐप के इस इंस्टॉल के लिए वही रहती है।

लौटाता है: तुरंत एक स्ट्रिंग (जैसे "3f2b…-…" जैसा UUID)। ज़रूरत: कुछ नहीं।

इसे डिवाइस कुंजी की तरह इस्तेमाल करें अपने सर्वर के लिए या "हर डिवाइस पर एक मुफ़्त ट्रायल" की जाँच के लिए।

function deviceKey() {
  if (window.WebToApk && window.WebToApk.getInstallId) {
    return WebToApk.getInstallId();
  }
  // In a browser: keep your own random ID in localStorage (per browser, cleared with site data).
  var k = localStorage.getItem('installId');
  if (!k) {
    k = 'w-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 10);
    localStorage.setItem('installId', k);
  }
  return k;
}

fetch('https://api.example.com/register', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ device: deviceKey() })
});

ID को हेडर के साथ अपने सर्वर पर भेजें और उसे URL से बाहर रखें।

async function apiGet(path) {
  const id = (window.WebToApk && WebToApk.getInstallId) ? WebToApk.getInstallId() : null;
  const res = await fetch('https://api.example.com' + path, {
    headers: id ? { 'X-Install-Id': id } : {}
  });
  return res.json();
}

इसे क्रैश रिपोर्ट के साथ जोड़ें ताकि एक इंस्टॉल की समस्याएँ खोजी जा सकें।

if (window.WebToApk && WebToApk.setCrashUserId && WebToApk.getInstallId) {
  // returns false when Crash reports are off or the id has other characters
  const ok = WebToApk.setCrashUserId(WebToApk.getInstallId());
  if (!ok) console.log('crash user id not set');
}

ध्यान दें: ID पहली बार इस्तेमाल पर बनती है और ऐप के स्टोरेज में रखी जाती है। ऐप अपडेट के बाद भी यह वही रहती है। यूज़र के ऐप अनइंस्टॉल करने या उसका डेटा साफ़ करने पर यह बदल जाती है। यह हार्डवेयर ID नहीं है - Android ऐप को IMEI या सीरियल नहीं देता। यही वैल्यू getDeviceInfo() में भी है installId.

getPhoneIdentity ब्रिज#

window.WebToApk.getPhoneIdentity(): String

WebToApk.getDeviceInfo() -> JSON स्ट्रिंग, कोई अनुमति ज़रूरी नहीं WebToApk.getPhoneIdentity() -> कैरियर/SIM, Phone अनुमति चाहिए WebToApk.getInstallId() -> हर इंस्टॉल का स्थिर UUID

अलग से नहीं, बल्कि अपने समूह Device / app information के साथ बताया गया है।

उदाहरण

मोबाइल कैरियर और SIM की जानकारी देता है: कैरियर का नाम, SIM का देश, ऑपरेटर कोड, फ़ोन का प्रकार और SIM की स्थिति।

लौटाता है: तुरंत एक JSON स्ट्रिंग - इस्तेमाल करें JSON.parse: { carrierName, simCountryIso, networkCountryIso, simOperator, phoneType, simState, hardwareIdAvailable: false }, या { error }। ज़रूरत: बिल्ड करते समय चरण 4 (Access) में Phone चालू करें; वरना यह लौटाता है { "error": "not_enabled" }.

कैरियर दिखाएँ और देश कोड चुनें।

function showCarrier() {
  if (!(window.WebToApk && window.WebToApk.getPhoneIdentity)) return;
  var p = JSON.parse(WebToApk.getPhoneIdentity());

  if (p.error === 'not_enabled') { showMessage('Turn on Phone when you build the app'); return; }
  if (p.error) { showMessage('Could not read SIM info: ' + p.error); return; }

  if (p.simState !== 'ready') {
    showMessage('No SIM ready (' + p.simState + ')');   // absent | pin_required | network_locked | unknown
    return;
  }
  document.getElementById('carrier').textContent = p.carrierName;                 // e.g. "Jio 4G"
  document.getElementById('country').value = p.simCountryIso.toUpperCase();        // e.g. "IN"
}

साइन-अप फ़ॉर्म के लिए SIM वाले देश का कॉलिंग कोड चुनें।

const DIAL = { IN: '+91', US: '+1', GB: '+44', DE: '+49' };
function defaultDialCode() {
  if (!(window.WebToApk && WebToApk.getPhoneIdentity)) return '+1';
  const p = JSON.parse(WebToApk.getPhoneIdentity());
  if (p.error || p.simState !== 'ready') return '+1';
  return DIAL[p.simCountryIso.toUpperCase()] || '+1';
}
document.getElementById('dial').value = defaultDialCode();

सिर्फ़ Wi-Fi वाले टैबलेट और फ़ोन में फ़र्क करें इससे phoneType.

if (window.WebToApk && WebToApk.getPhoneIdentity) {
  const p = JSON.parse(WebToApk.getPhoneIdentity());
  if (!p.error && p.phoneType === 'none') {
    document.getElementById('sms-login').hidden = true;   // no cellular radio: hide SMS sign-in
  }
}

ध्यान दें: phoneType है gsm, cdma, sip या none (सिर्फ़ Wi-Fi वाले टैबलेट)। simOperator MCC+MNC कोड है (जैसे "40445")। कोई IMEI, MEID या सीरियल नहीं है - hardwareIdAvailable हमेशा false होता है, क्योंकि Android 10 से Android इन्हें हर ऐप से छिपाता है; स्थिर id के लिए getInstallId() इस्तेमाल करें। Phone स्विच आपकी Play लिस्टिंग में संवेदनशील अनुमति जोड़ता है; Data safety फ़ॉर्म में बताएँ कि आप इसे किसलिए इस्तेमाल करते हैं।

isTv ब्रिज#

window.WebToApk.isTv(): Boolean

जब ऐप टेलीविज़न (Android TV, Google TV, Fire TV) पर चलता है तो True।

उदाहरण

पेज को बताता है कि ऐप टेलीविज़न (Android TV, Google TV, Fire TV) पर चल रहा है या नहीं, ताकि वह रिमोट के अनुकूल लेआउट दिखा सके।

लौटाता है: true या false (तुरंत)। ज़रूरत: कुछ नहीं। विज़ार्ड में Android TV support चालू होने पर ऐप को TV लॉन्चर में TV बैनर भी मिलता है, और TV पर पेज को साफ़ फ़ोकस रिंग मिलती है और लिंक, बटन, फ़ील्ड तथा onclick या बटन रोल वाले एलिमेंट रिमोट के ऐरो से पहुँचने योग्य हो जाते हैं; OK फ़ोकस वाले एलिमेंट को क्लिक करता है और Back फ़ोन की तरह काम करता है।

var onTv = !!(window.WebToApk && typeof window.WebToApk.isTv === 'function' && window.WebToApk.isTv());
if (onTv) {
  document.documentElement.classList.add('tv');     /* bigger text, no hover-only menus */
  document.querySelector('#play').focus();           /* start the remote on the main action */
}
html.tv body { font-size: 22px; }
html.tv :focus-visible { transform: scale(1.05); }

TV के लिए अलग होम स्क्रीन चुनें और फ़ोन वाला लेआउट डिफ़ॉल्ट रखें।

function isTv() {
  return !!(window.WebToApk && typeof WebToApk.isTv === 'function' && WebToApk.isTv());
}
history.replaceState({}, '', isTv() ? '/tv-home' : '/home');
if (isTv()) document.querySelectorAll('.touch-only').forEach(function (el) { el.hidden = true; });

ध्यान दें: Android TV एमुलेटर और उसके D-pad से टेस्ट करें। Google Play पर TV में प्रकाशित करना एक अलग चरण है: Play Console में ऐप को Android TV फ़ॉर्म फ़ैक्टर में शामिल करें (Release → Advanced settings → Form factors) और TV का एक स्क्रीनशॉट जोड़ें।

हर डॉक्स बिल्ड पर ऐप रनटाइम और उसकी उदाहरण फ़ाइलों से बनाया जाता है। Markdown में पढ़ें · सभी फ़ैमिली.

2026-10-03 को शिप किए गए ब्रिज से मिलाकर जाँचा गया।