واجهة JavaScript bridge API
معلومات الجهاز والتطبيق
أي هاتف هذا، وأي إنشاء، ومعرّف ثابت لكل تثبيت، وواجهات الموقع والكاميرا القياسية.
getUserMedia معيار الويب#
navigator.mediaDevices.getUserMedia — camera and microphone
مثال
تعمل واجهة كاميرا الويب والميكروفون القياسية داخل التطبيق. يعرض التطبيق نافذة إذن Android نفسها ويعطي البثّ للصفحة.
القيمة المُرجَعة: Promise بـ MediaStream، أو رفض (NotAllowedError عندما رفض المستخدم أو كان المفتاح متوقفًا عند الإنشاء، NotFoundError عندما لا يوجد جهاز كهذا). المتطلبات: الكاميرا و/أو الميكروفون في الخطوة 4 (الوصول). عند الإنشاء من ZIP أو صفحة HTML وكانت شيفرتك تستدعي getUserMedia({ video: … }) أو { audio: … } (أو تستخدم <input capture> أو BarcodeDetector أو SpeechRecognition)، يجد الإنشاء الاستدعاء ويضيف الإذن المناسب لك؛ أما { video: false } فلا تضيف الكاميرا. أما في إنشاء موقع (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.
IdleDetector معيار الويب#
new IdleDetector() — Idle Detection
مثال
واجهة Idle Detection API القياسية: اعرف متى يتوقف المستخدم عن استخدام الهاتف (لا إدخال لفترة) أو يقفل الشاشة، ومتى يعود - لحالة "بعيد" أو لإيقاف جلسة حية مؤقتًا أو لقفل خزنة.
القيمة المُرجَعة: IdleDetector.requestPermission() ← 'granted' أو 'denied' (يحتاج إلى نقرة)؛ detector.start({threshold, signal}) ← Promise (يرفض بـ NotAllowedError دون الإذن، وبـ TypeError إذا كان أقل من 60000 مللي ثانية)؛ ثم 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().
TextDetector معيار الويب#
new TextDetector().detect(image) — text recognition (OCR)
مثال
يقرأ النص في صورة على الهاتف نفسه - مجموع إيصال أو رقم تسلسلي أو بطاقة عمل - باستخدام معرّف ML Kit من Google. وشكله هو TextDetector من Shape Detection API، التي يشحنها Chrome خلف علَم.
القيمة المُرجَعة: Promise بمصفوفة، مدخل لكل سطر: { rawValue, boundingBox, cornerPoints } بوحدات بكسلات الصورة. يرفض بـ NotSupportedError دون خدمات Google Play وبـ 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)، واستخرج تفاصيل الاتصال من بطاقة عمل:
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: يطلبه الإنشاء عند التثبيت إذا كانت شيفرتك تستخدمه، وإلا فإن أول استدعاء على هاتف جديد ينتظر بضع ثوانٍ أثناء تنزيله. يجب أن تسمح الصور من موقع آخر بـ CORS (اللوحة الملوثة ترفض SecurityError). قنوات النقل الداخلية: __ocrAvailable و__ocrRecognize (يُجاب عنها على هيئة appmint:ocr). تستخدم التطبيقات المبنية بالذكاء الاصطناعي recognizeText().
__idlePermission الجسر (bridge)#
window.WebToApk.__idlePermission(request: Boolean): String
IdleDetector.requestPermission() ([request] true، يُستدعى بإجراء من المستخدم) يسجّل المنح؛ وfalse يقرأه فقط. "granted" | "prompt".
مثال
قناة نقل داخلية وراء IdleDetector.requestPermission() وحالة إذن 'idle-detection'. تستخدم الصفحات الواجهة القياسية؛ لا تستدعِ هذه مباشرة - فقد تتغير دوال __ دون إشعار.
القيمة المُرجَعة: 'granted' أو 'prompt'. المتطلبات: إجراء من المستخدم للطلب.
استخدم الواجهة العامة:
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 الجسر (bridge)#
window.WebToApk.__idleState(): String
IdleDetector: `{idleMs, locked}` من إدخال التطبيق نفسه ومن keyguard.
مثال
قناة نقل داخلية وراء IdleDetector: كم مضى منذ آخر إدخال من المستخدم، وهل الشاشة مقفلة. تستخدم الصفحات الواجهة القياسية؛ لا تستدعِ هذه مباشرة - فقد تتغير دوال __ دون إشعار.
القيمة المُرجَعة: {idleMs, locked} بصيغة JSON. تُدفع تغييرات الشاشة والتطبيق على هيئة الحدث appmint:idle بالشكل نفسه. المتطلبات: لا شيء.
استخدم الواجهة العامة:
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 الجسر (bridge)#
window.WebToApk.__permissionState(name: String): String
حالة Permissions API لاسم إذن W3C، من منح Android الحقيقية: "granted" | "denied" | "prompt"، أو "" عندما لا يجيب الغلاف عن هذا الاسم (ينتقل استعلام الصفحة حينها إلى تنفيذ WebView نفسه).
مثال
ناقل داخلي يقف خلف navigator.permissions.query({ name }). تستخدم الصفحات الواجهة القياسية؛ لا تستدعِ هذا مباشرةً أبدًا - فقد تتغير دوال __ دون إشعار.
القيمة المُرجَعة: 'granted' أو 'denied' أو 'prompt' أو '' لاسم لا يجيب عنه التطبيق (ينتقل الاستعلام حينها إلى WebView نفسه). المتطلبات: لا شيء لتفعيله.
استخدم الواجهة العامة:
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. أما القدرة التي تركها الإنشاء متوقفة (أو التي لا يوجد إذنها في manifest) فهي denied. تُعاد قراءة الحالات الحية عند appmint:permissions-changed (يُطلق بعد كل نافذة إذن) وعندما تصبح الصفحة مرئية مجددًا.
getAppName الجسر (bridge)#
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 الجسر (bridge)#
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();ملاحظات: هذا هو **اسم** الإصدار الذي حددته عند الإنشاء، وليس رمز الإصدار. إذا تعذرت قراءة الحزمة يُرجع "1.0"، فلا يكون فارغًا أبدًا. رمز الإصدار وتواريخ التثبيت في getDeviceInfo() ضمن app.
getDeviceInfo الجسر (bridge)#
window.WebToApk.getDeviceInfo(): String
WebToApk.getDeviceInfo() -> نص JSON، لا يلزم إذن WebToApk.getPhoneIdentity() -> شركة الاتصالات/SIM، يحتاج إذن الهاتف WebToApk.getInstallId() -> UUID ثابت لكل تثبيت
موصوفة ضمن مجموعتها، معلومات الجهاز / التطبيق، وليس بمفردها.
مثال
يعطي معلومات عن الهاتف و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 هو com.android.vending للتثبيت من Play وفارغ للتثبيت اليدوي. تُقرأ القيم لحظة الاستدعاء - استدعِ مجددًا لتحديث البطارية أو الشبكة. لا يوجد IMEI أو رقم تسلسلي: يحجبهما Android عن كل التطبيقات؛ استخدم getInstallId().
getInstallId الجسر (bridge)#
window.WebToApk.getInstallId(): String
WebToApk.getDeviceInfo() -> نص JSON، لا يلزم إذن WebToApk.getPhoneIdentity() -> شركة الاتصالات/SIM، يحتاج إذن الهاتف WebToApk.getInstallId() -> UUID ثابت لكل تثبيت
موصوفة ضمن مجموعتها، معلومات الجهاز / التطبيق، وليس بمفردها.
مثال
يعطي معرّفًا عشوائيًا يبقى ثابتًا لهذا التثبيت من تطبيقك.
القيمة المُرجَعة: نص (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() })
});أرسل المعرّف إلى خادمك في ترويسة وأبقِه خارج الرابط.
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 الجسر (bridge)#
window.WebToApk.getPhoneIdentity(): String
WebToApk.getDeviceInfo() -> نص JSON، لا يلزم إذن WebToApk.getPhoneIdentity() -> شركة الاتصالات/SIM، يحتاج إذن الهاتف WebToApk.getInstallId() -> UUID ثابت لكل تثبيت
موصوفة ضمن مجموعتها، معلومات الجهاز / التطبيق، وليس بمفردها.
مثال
يعطي معلومات شركة الاتصالات وSIM: اسم الشركة، وبلد SIM، ورمز المشغّل، ونوع الهاتف، وحالة SIM.
القيمة المُرجَعة: نص JSON فورًا - استخدم JSON.parse: { carrierName, simCountryIso, networkCountryIso, simOperator, phoneType, simState, hardwareIdAvailable: false }، أو { error }. المتطلبات: فعّل الهاتف في الخطوة 4 (الوصول) عند الإنشاء؛ وإلا يُرجع { "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 يخفيها عن كل التطبيقات منذ Android 10؛ استخدم getInstallId() لمعرّف ثابت. مفتاح الهاتف يضيف إذنًا حساسًا إلى قائمتك في Play؛ صرّح بما تستخدمه له في نموذج أمان البيانات.
isTv الجسر (bridge)#
window.WebToApk.isTv(): Boolean
تكون صحيحة عندما يعمل التطبيق على تلفزيون (Android TV أو Google TV أو Fire TV).
مثال
يخبر الصفحة هل يعمل التطبيق على تلفزيون (Android TV أو Google TV أو Fire TV)، لتعرض تخطيطًا مناسبًا للريموت.
القيمة المُرجَعة: true أو false (بشكل متزامن). المتطلبات: لا شيء. مع تفعيل دعم Android 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 (الإصدار ← الإعدادات المتقدمة ← أشكال الأجهزة) وأضف لقطة شاشة للتلفزيون.