ساخت APK رایگان

API پل جاوااسکریپت

اطلاعات دستگاه و اپلیکیشن

این چه گوشی‌ای است، کدام ساخت است، یک شناسه‌ی پایدار برای هر نصب، و API‌های استاندارد موقعیت مکانی و دوربین.

getUserMedia استاندارد وب#

navigator.mediaDevices.getUserMedia — camera and microphone

نمونه

API استاندارد وب برای دوربین و میکروفون داخل اپلیکیشن کار می‌کند. اپلیکیشن درخواست مجوز خود Android را نشان می‌دهد و جریان را به صفحه می‌دهد.

مقدار بازگشتی: یک Promise از MediaStream، یا یک رد (NotAllowedError وقتی کاربر رد کرده یا گزینه هنگام ساخت خاموش بوده، NotFoundError وقتی چنین دستگاهی نیست). پیش‌نیاز: Camera و/یا Mic در مرحله 4 (Access). وقتی از ZIP یا صفحه‌ی HTML می‌سازید و کدتان getUserMedia({ video: … }) یا { audio: … } را فراخوانی می‌کند (یا از <input capture>، BarcodeDetector، SpeechRecognition استفاده می‌کند)، ساخت فراخوانی را پیدا می‌کند و مجوز مناسب را برایتان اضافه می‌کند؛ { video: false } دوربین را اضافه نمی‌کند. برای ساخت از وب‌سایت (URL)، خودتان آن‌ها را تیک بزنید.

دوربین پشت را در یک عنصر video نشان دهید.

<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);
}

نکته‌ها: وقتی دوربین و میکروفون را با هم درخواست می‌کنید، اپلیکیشن هر دو را یک‌جا از کاربر می‌خواهد. اگر گزینه هنگام ساخت خاموش بوده، درخواست بدون هیچ پرسشی رد می‌شود؛ دوباره با گزینه‌ی روشن بسازید. همیشه پس از پایان کار، track‌ها را متوقف کنید، وگرنه چراغ دوربین/میکروفون روشن می‌ماند. ویدیوی DRM (Widevine) بدون هیچ گزینه‌ای مجاز است. دوربین و میکروفون حساس هستند: آن‌ها را در فرم Play Data safety اعلام کنید.

IdleDetector استاندارد وب#

new IdleDetector() — Idle Detection

نمونه

API استاندارد Idle Detection: بفهمید کاربر کی استفاده از گوشی را متوقف می‌کند (مدتی بدون ورودی) یا صفحه را قفل می‌کند و کی برمی‌گردد؛ برای وضعیت «غایب»، توقف یک جلسه‌ی زنده یا قفل‌کردن یک گاوصندوق.

مقدار بازگشتی: 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;
  }
}

نکته‌ها: در اپلیکیشن، «ورودی» یعنی ورودی کاربر به همین اپلیکیشن وقتی در پیش‌زمینه است؛ وقتی اپلیکیشن دیگری در پیش‌زمینه و صفحه روشن است، کاربر فعال حساب می‌شود (یک اپلیکیشن ورودی اپلیکیشن‌های دیگر را نمی‌بیند؛ Chrome در Android هم همین‌طور پاسخ می‌دهد)؛ وقتی صفحه خاموش یا قفل است، زمان بی‌کاری از آخرین لمس حساب می‌شود. مجوز به خاطر سپرده می‌شود و navigator.permissions.query({name:'idle-detection'}) به آن پاسخ می‌دهد. انتقال‌های داخلی: __idleState، __idlePermission؛ تغییرات قفل/بازکردن به‌صورت appmint:idle می‌رسند. اپلیکیشن‌های ساخته‌شده با هوش مصنوعی از watchIdle().

navigator.geolocation استاندارد وب#

navigator.geolocation

نمونه

API استاندارد وب برای موقعیت مکانی داخل اپلیکیشن کار می‌کند. اپلیکیشن درخواست موقعیت خود Android را نشان می‌دهد و پاسخ را به صفحه می‌دهد.

مقدار بازگشتی: callbackهای استاندارد W3C: یک GeolocationPosition (coords.latitude، coords.longitude، coords.accuracy، …) یا یک GeolocationPositionError (code 1 = مجوز رد شد، 2 = در دسترس نیست، 3 = پایان زمان). پیش‌نیاز: GPS (دقیق) و/یا Location (تقریبی) در مرحله 4 (Access). وقتی از 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()

نمونه

API استاندارد related-apps: به صفحه می‌گوید کدام یک از اپلیکیشن‌های Android خودش (فهرست‌شده در مانیفست اپلیکیشن وب) نصب شده‌اند، تا بنر «اپلیکیشن دیگر ما را نصب کنید» را پنهان کند یا مستقیم به آن لینک بدهد.

مقدار بازگشتی: Promise از آرایه‌ای از ورودی‌های related_applications مانیفست که نصب شده‌اند: { 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 })

نمونه

API استاندارد Permissions که از وضعیت واقعی مجوزهای Android گوشی پاسخ می‌دهد؛ بنابراین صفحه می‌تواند «اجازه‌ی دوربین» را فقط وقتی نشان دهد که هنوز می‌شود پرسید و «در تنظیمات روشنش کنید» را وقتی نمی‌شود.

مقدار بازگشتی: یک Promise از PermissionStatus {name, state, onchange}؛ 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 هرگز نمی‌تواند آن را بدهد) و رد با «Don't ask again» را پوشش می‌دهد. screen-wake-lock، clipboard-read و clipboard-write برابر 'granted' هستند (اپلیکیشن آن‌ها را بدون درخواست فراهم می‌کند). نام‌های دیگر (midi، accelerometer، …) مثل قبل توسط WebView پاسخ داده می‌شوند. رویداد پشت آن: appmint:permissions-changed.

TextDetector استاندارد وب#

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

نمونه

متن داخل یک تصویر را روی خود گوشی می‌خواند، مثل مبلغ رسید، شماره‌ی سریال یا کارت ویزیت، با شناساگر ML Kit گوگل. شکل آن همان TextDetector مربوط به Shape Detection API است که 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 را مجاز کنند (بوم آلوده SecurityError را رد می‌کند). انتقال‌های داخلی: __ocrAvailable، __ocrRecognize (به‌صورت appmint:ocr پاسخ داده می‌شود). اپلیکیشن‌های ساخته‌شده با هوش مصنوعی از 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}` از ورودی خود اپلیکیشن و keyguard.

نمونه

انتقال داخلی پشت 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

وضعیت Permissions API برای یک نام مجوز W3C، از مجوزهای واقعی Android: "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".

مقدار بازگشتی: بلافاصله یک رشته. پیش‌نیاز: هیچ.

نسخه را در صفحه‌ی درباره نشان دهید.

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() -> اپراتور/سیم‌کارت، به مجوز 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() -> اپراتور/سیم‌کارت، به مجوز Phone نیاز دارد WebToApk.getInstallId() -> UUID پایدار برای هر نصب

به‌جای توضیح جداگانه، در گروهش توضیح داده شده است: Device / app information.

نمونه

یک شناسه‌ی تصادفی می‌دهد که برای این نصب اپلیکیشن شما ثابت می‌ماند.

مقدار بازگشتی: بلافاصله یک رشته (یک UUID مانند "3f2b…-…"). پیش‌نیاز: هیچ.

از آن به‌عنوان کلید دستگاه استفاده کنید برای سرور خودتان یا بررسی «یک آزمایش رایگان برای هر دستگاه».

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() })
});

شناسه را با هدر به سرور بفرستید و آن را از 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');
}

نکته‌ها: شناسه در اولین استفاده ساخته می‌شود و در حافظه‌ی اپلیکیشن می‌ماند. پس از به‌روزرسانی اپلیکیشن ثابت می‌ماند. وقتی کاربر اپلیکیشن را حذف کند یا داده‌هایش را پاک کند، تغییر می‌کند. این شناسه‌ی سخت‌افزاری نیست؛ Android به اپلیکیشن‌ها IMEI یا سریال نمی‌دهد. همین مقدار در getDeviceInfo() هم به‌صورت installId.

getPhoneIdentity پل#

window.WebToApk.getPhoneIdentity(): String

WebToApk.getDeviceInfo() -> رشته‌ی JSON، بدون نیاز به مجوز WebToApk.getPhoneIdentity() -> اپراتور/سیم‌کارت، به مجوز Phone نیاز دارد WebToApk.getInstallId() -> UUID پایدار برای هر نصب

به‌جای توضیح جداگانه، در گروهش توضیح داده شده است: Device / app information.

نمونه

اطلاعات اپراتور موبایل و سیم‌کارت را می‌دهد: نام اپراتور، کشور سیم‌کارت، کد اپراتور، نوع گوشی و وضعیت سیم‌کارت.

مقدار بازگشتی: بلافاصله یک رشته 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"
}

کد تماس را از کشور سیم‌کارت انتخاب کنید برای فرم ثبت‌نام.

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 آن‌ها را از همه‌ی اپلیکیشن‌ها پنهان می‌کند؛ برای شناسه‌ی پایدار از 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 هم می‌گیرد و روی تلویزیون صفحه حلقه‌ی فوکوس واضح می‌گیرد و لینک‌ها، دکمه‌ها، فیلدها و عناصر متصل‌شده با 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); }

صفحه‌ی اصلی متفاوتی برای تلویزیون انتخاب کنید و چیدمان گوشی را پیش‌فرض نگه دارید.

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 مرحله‌ی جداگانه‌ای است: در Play Console اپلیکیشن را در فرم‌فکتور Android TV ثبت کنید (Release → Advanced settings → Form factors) و یک اسکرین‌شات تلویزیون اضافه کنید.