واجهة JavaScript bridge API
جهات الاتصال
القراءة والبحث والاختيار والكتابة - المنتقي لا يحتاج إلى أي إذن.
addContact الجسر (bridge)#
window.WebToApk.addContact(requestId: String, contactJson: String)
منتقي النظام (لا يحتاج إلى إذن) وكتابة جهة اتصال جديدة (WRITE_CONTACTS). جانب القراءة - العرض والبحث - ضمن مجموعة جهات الاتصال أعلاه.
موصوفة ضمن مجموعتها، جهات الاتصال: الاختيار والإضافة، وليس بمفردها.
مثال
يحفظ جهة اتصال جديدة مباشرة في دفتر عناوين الهاتف، دون فتح تطبيق جهات الاتصال.
القيمة المُرجَعة: لا شيء الآن. الإجابة هي الحدث appmint:contacts (وwindow.onAppMintContacts) مع detail = { requestId, ok: true, id: '123' } عند النجاح، أو { requestId, ok: false, id: null } عند فشل الحفظ، أو { requestId, error }. المتطلبات: فعّل تعديل جهات الاتصال في الخطوة 4 (الوصول) عند الإنشاء؛ ويُطلب إذن المستخدم عند أول استدعاء.
احفظ عميلاً. تُمرَّر جهة الاتصال كنص JSON.
function saveCustomer(name, phone, email) {
if (!(window.WebToApk && window.WebToApk.addContact)) return;
var id = 'add-' + Date.now();
window.addEventListener('appmint:contacts', function handler(e) {
if (!e.detail || e.detail.requestId !== id) return;
window.removeEventListener('appmint:contacts', handler);
if (e.detail.error === 'not_enabled') { showMessage('Turn on Edit Contacts when you build'); return; }
if (e.detail.error) { showMessage('Not saved: ' + e.detail.error); return; }
showMessage(e.detail.ok ? 'Saved to contacts' : 'Could not save the contact');
});
WebToApk.addContact(id, JSON.stringify({
displayName: name,
phones: [{ number: phone, type: 'mobile' }], // type: mobile | home | work
emails: [email]
}));
}الصيغة المختصرة. القيم المفردة مقبولة أيضًا: { name: 'Ali', phone: '+911234567890', email: '[email protected]' }.
زر "حفظ في جهات الاتصال" في صفحة بطاقة عمل، باستخدام الصيغة المختصرة وPromise:
function addOne(contact) {
return new Promise(function (resolve, reject) {
var id = 'card-' + Date.now() + '-' + Math.random().toString(36).slice(2);
window.addEventListener('appmint:contacts', function handler(e) {
if (!e.detail || e.detail.requestId !== id) return;
window.removeEventListener('appmint:contacts', handler);
if (e.detail.error) reject(new Error(e.detail.error));
else if (!e.detail.ok) reject(new Error('save_failed'));
else resolve(e.detail.id);
});
WebToApk.addContact(id, JSON.stringify(contact));
});
}
document.getElementById('save-card').addEventListener('click', function () {
if (!(window.WebToApk && window.WebToApk.addContact)) return;
addOne({ name: 'Priya Sharma', phone: '+919876543210', email: '[email protected]' })
.then(function () { showMessage('Priya is in your contacts now'); })
.catch(function (err) {
showMessage(err.message === 'permission_denied' ? 'Allow Contacts to save this card.' : 'Not saved: ' + err.message);
});
});استورد قائمة الفريق شخصًا شخصًا، مع أرقام العمل والمنزل، وأبلغ عن عدد من حُفظوا:
async function importTeam(team) { // [{ name, work, home, email }]
if (!(window.WebToApk && window.WebToApk.addContact)) return;
var saved = 0;
for (var i = 0; i < team.length; i++) {
var m = team[i];
try {
await addOne({ // addOne() from the example above
displayName: m.name,
phones: [{ number: m.work, type: 'work' }, { number: m.home, type: 'home' }],
emails: m.email ? [m.email] : []
});
saved++;
} catch (err) {
if (err.message === 'not_enabled' || err.message === 'permission_denied') break; // the rest would fail too
}
}
showMessage(saved + ' of ' + team.length + ' saved');
}ملاحظات: الأخطاء: not_enabled، permission_denied. قيمة id المُرجَعة هي معرّف raw-contact الجديد. يُحفظ type الهاتف غير home أو work على أنه mobile. تُحفظ جهة الاتصال على الجهاز (بدون حساب). كتابة جهات الاتصال إذن حساس: يجب أن تصرّح به قائمة تطبيقك في Play Store في نموذج أمان البيانات وسياسة الخصوصية.
getContact الجسر (bridge)#
window.WebToApk.getContact(requestId: String, contactId: String)
كلها غير متزامنة: قد تضم قراءات المؤشر آلاف الصفوف، وإرجاع @JavascriptInterface حاجب سيشغّل ذلك على خيط JS مما يسبب ANR. تصل النتائج على هيئة `appmint:contacts` / window.onAppMintContacts({requestId,...}). requestId: مرّر نصًا STRING ('pick-' + Date.now()). كما يعمل رقم NUMBER خام - فالجسر يحوّله عبر double ("1.787491234567E12")، وكل ردّ يعيده إلى صيغته الأصلية (BridgeRequestId)، لذا فإن `res.requestId === Date.now()` تعطي true في الحالتين. WebToApk.listContacts(id, limit, offset) WebToApk.searchContacts(id, query, limit) WebToApk.getContact(id, contactId) WebToApk.pickContact(id) <- لا يحتاج إلى إذن (منتقي النظام) WebToApk.addContact(id, json) <- يحتاج إلى إذن كتابة جهات الاتصال
موصوفة ضمن مجموعتها، جهات الاتصال، وليس بمفردها.
مثال
يقرأ جهة اتصال واحدة مرة أخرى عبر id الخاص بها (قيمة id من listContacts أو searchContacts).
القيمة المُرجَعة: لا شيء الآن. الإجابة هي الحدث appmint:contacts (وwindow.onAppMintContacts) مع detail = { requestId, contact: { id, displayName, photoUri, phones: [{ number, type }], emails: [...] } }. تكون contact بقيمة null عندما لا تعود جهة الاتصال موجودة. عند الفشل: { requestId, error }. المتطلبات: فعّل جهات الاتصال في الخطوة 4 (الوصول) عند الإنشاء.
حدّث جهة اتصال محفوظة باستخدام خطّاف الصفحة بدلاً من مستمع الأحداث.
var pendingId = '';
window.onAppMintContacts = function (detail) {
if (detail.requestId !== pendingId) return;
if (detail.error) { showMessage('Could not read the contact: ' + detail.error); return; }
if (!detail.contact) { showMessage('This contact was deleted.'); return; }
var c = detail.contact;
document.getElementById('name').textContent = c.displayName;
document.getElementById('phones').textContent =
c.phones.map(function (p) { return p.type + ': ' + p.number; }).join(', ');
document.getElementById('emails').textContent = c.emails.join(', ');
};
function openContact(contactId) {
if (!(window.WebToApk && window.WebToApk.getContact)) return;
pendingId = 'one-' + Date.now();
WebToApk.getContact(pendingId, String(contactId));
}حدّث قائمة محفوظة بمعرّفات جهات الاتصال المفضلة واحذف التي حُذفت من الهاتف منذ ذلك الحين:
function getOne(contactId) {
return new Promise(function (resolve, reject) {
var id = 'fav-' + contactId + '-' + Date.now();
window.addEventListener('appmint:contacts', function handler(e) {
if (!e.detail || e.detail.requestId !== id) return;
window.removeEventListener('appmint:contacts', handler);
if (e.detail.error) reject(new Error(e.detail.error)); else resolve(e.detail.contact);
});
WebToApk.getContact(id, String(contactId));
});
}
async function loadFavourites() {
if (!(window.WebToApk && window.WebToApk.getContact)) return;
var ids = JSON.parse(localStorage.getItem('favIds') || '[]');
var people = [];
for (var i = 0; i < ids.length; i++) {
var c = await getOne(ids[i]);
if (c) people.push(c); // null = deleted from the phone
}
localStorage.setItem('favIds', JSON.stringify(people.map(function (c) { return c.id; })));
renderFavourites(people);
}تحقق مما إذا كان الرقم الذي خزّنه تطبيقك لعميل ما لا يزال على بطاقة جهة الاتصال الخاصة به:
function checkStoredNumber(customer) { // { contactId, phone }
if (!(window.WebToApk && window.WebToApk.getContact)) return;
var id = 'check-' + Date.now();
window.addEventListener('appmint:contacts', function handler(e) {
if (!e.detail || e.detail.requestId !== id) return;
window.removeEventListener('appmint:contacts', handler);
if (e.detail.error) { showMessage('Could not check: ' + e.detail.error); return; }
if (!e.detail.contact) { showMessage('This customer is no longer in your contacts.'); return; }
var digits = function (s) { return String(s).replace(/\D/g, ''); };
var still = e.detail.contact.phones.some(function (p) {
return digits(p.number).slice(-10) === digits(customer.phone).slice(-10);
});
if (!still) showMessage('The number for ' + e.detail.contact.displayName + ' has changed.');
});
WebToApk.getContact(id, String(customer.contactId));
}ملاحظات: يتلقى window.onAppMintContacts كل إجابات جهات الاتصال (القائمة والبحث والاختيار والإضافة)، لذا تحقق دائمًا من requestId. يمكنك استخدامه أو addEventListener('appmint:contacts', ...) - كلاهما يُطلق. الأخطاء: not_enabled، permission_denied، foreign_origin. قيمة id من pickContact هي معرّف صف هاتف وليست معرّف جهة اتصال، لذا لا تعمل هنا. جهات الاتصال بيانات شخصية: يجب أن تصرّح بها قائمة تطبيقك في Play Store في نموذج أمان البيانات وسياسة الخصوصية.
listContacts الجسر (bridge)#
window.WebToApk.listContacts(requestId: String, limit: Int, offset: Int)
كلها غير متزامنة: قد تضم قراءات المؤشر آلاف الصفوف، وإرجاع @JavascriptInterface حاجب سيشغّل ذلك على خيط JS مما يسبب ANR. تصل النتائج على هيئة `appmint:contacts` / window.onAppMintContacts({requestId,...}). requestId: مرّر نصًا STRING ('pick-' + Date.now()). كما يعمل رقم NUMBER خام - فالجسر يحوّله عبر double ("1.787491234567E12")، وكل ردّ يعيده إلى صيغته الأصلية (BridgeRequestId)، لذا فإن `res.requestId === Date.now()` تعطي true في الحالتين. WebToApk.listContacts(id, limit, offset) WebToApk.searchContacts(id, query, limit) WebToApk.getContact(id, contactId) WebToApk.pickContact(id) <- لا يحتاج إلى إذن (منتقي النظام) WebToApk.addContact(id, json) <- يحتاج إلى إذن كتابة جهات الاتصال
موصوفة ضمن مجموعتها، جهات الاتصال، وليس بمفردها.
مثال
يقرأ دفتر عناوين الهاتف، صفحة بصفحة، مرتبًا من A إلى Z حسب الاسم.
القيمة المُرجَعة: لا شيء الآن. تصل الإجابة لاحقًا على هيئة الحدث appmint:contacts (وwindow.onAppMintContacts) مع detail = { requestId, contacts: [{ id, displayName, photoUri, phones: [{ number, type }], emails: ['[email protected]'] }] }، أو { requestId, error }. المتطلبات: فعّل جهات الاتصال في الخطوة 4 (الوصول) عند الإنشاء؛ ويُطلب إذن المستخدم عند أول استدعاء.
حمّل أول 100 جهة اتصال. طابق الإجابة على requestId.
function loadContacts() {
if (!(window.WebToApk && window.WebToApk.listContacts)) {
showMessage('Contacts work only inside the app.');
return;
}
var id = 'contacts-' + Date.now();
window.addEventListener('appmint:contacts', function handler(e) {
if (!e.detail || e.detail.requestId !== id) return; // another call's answer
window.removeEventListener('appmint:contacts', handler);
if (e.detail.error) { showMessage('Could not read contacts: ' + e.detail.error); return; }
var list = document.getElementById('list');
list.innerHTML = '';
e.detail.contacts.forEach(function (c) {
var li = document.createElement('li');
var num = c.phones.length ? c.phones[0].number + ' (' + c.phones[0].type + ')' : '';
li.textContent = c.displayName + ' ' + num;
list.appendChild(li);
});
});
WebToApk.listContacts(id, 100, 0); // limit 100, start at 0
}الصفحة التالية. مرّر الرقم الذي لديك بالفعل على أنه offset: WebToApk.listContacts(id, 100, 100).
حمّل دفتر العناوين كاملاً في صفحات من 500 حتى تدل صفحة أقصر على أنك وصلت إلى النهاية:
function listPage(limit, offset) {
return new Promise(function (resolve, reject) {
var id = 'page-' + offset + '-' + Date.now();
window.addEventListener('appmint:contacts', function handler(e) {
if (!e.detail || e.detail.requestId !== id) return;
window.removeEventListener('appmint:contacts', handler);
if (e.detail.error) reject(new Error(e.detail.error));
else resolve(e.detail.contacts);
});
WebToApk.listContacts(id, limit, offset);
});
}
async function loadEveryContact() {
if (!(window.WebToApk && window.WebToApk.listContacts)) return [];
var all = [];
while (true) {
var page = await listPage(500, all.length);
all = all.concat(page);
if (page.length < 500) return all; // last page
}
}أنشئ قائمة "دعوة الأصدقاء" من أرقام الجوال فقط، وأخبر المستخدم بما يفعله عند رفض القراءة:
function showInviteList() {
if (!(window.WebToApk && window.WebToApk.listContacts)) return;
var id = 'invite-' + Date.now();
window.addEventListener('appmint:contacts', function handler(e) {
if (!e.detail || e.detail.requestId !== id) return;
window.removeEventListener('appmint:contacts', handler);
if (e.detail.error === 'permission_denied') { showMessage('Allow Contacts to invite friends.'); return; }
if (e.detail.error) { showMessage('Contacts are not available: ' + e.detail.error); return; }
var mobiles = [];
e.detail.contacts.forEach(function (c) {
c.phones.forEach(function (p) {
if (p.type === 'mobile') mobiles.push({ name: c.displayName, number: p.number });
});
});
renderInviteList(mobiles); // your own list UI
});
WebToApk.listContacts(id, 500, 0);
}ملاحظات: قيمة limit تُحصر بين 1 و2000. الأخطاء: not_enabled (كانت جهات الاتصال متوقفة وقت الإنشاء)، permission_denied (رفض المستخدم)، foreign_origin (استُدعيت من صفحة ليست تابعة لتطبيقك). قيمة type للهاتف هي home أو mobile أو work أو main أو work_fax أو home_fax أو other. photoUri هو عنوان content:// وليس رابط صورة يمكن للصفحة تحميله. لتتيح للمستخدم اختيار شخص واحد فقط، استخدم pickContact بدلاً منها - فهي لا تحتاج إلى إذن. جهات الاتصال بيانات شخصية: يجب أن تصرّح بها قائمة تطبيقك في Play Store في نموذج أمان البيانات وسياسة الخصوصية.
pickContact الجسر (bridge)#
window.WebToApk.pickContact(requestId: String)
منتقي جهات الاتصال في النظام - لا يحتاج إلى أي إذن، وهو الخيار الصحيح كلما أرادت الصفحة فقط أن يختار المستخدم شخصًا واحدًا. يختار من جدول PHONE وليس من جدول جهات الاتصال: فالمنتقي يمنح صلاحية القراءة فقط للصف الذي يعيده، لذا فاختيار صف هاتف هو ما يجعل الرقم قابلًا للقراءة دون READ_CONTACTS. أما اختيار جهة اتصال مجرّدة فسيعيد صفًا فيه اسم بلا رقم، وقراءة الرقم منه ستحتاج إلى الإذن الذي وُجدت هذه الطريقة لتجنّبه.
مثال
يفتح منتقي جهات الاتصال الخاص بـ Android ليختار المستخدم رقم هاتف واحدًا. لا حاجة إلى إذن.
القيمة المُرجَعة: لا شيء الآن. الإجابة هي الحدث appmint:contacts (وwindow.onAppMintContacts) مع detail = { requestId, contact: { id, displayName, photoUri, phones: [{ number, type: 'picked' }], emails: [] } }. إذا أغلق المستخدم المنتقي: { requestId, error: 'cancelled' }. المتطلبات: لا شيء - لا مفتاح إنشاء ولا طلب إذن.
املأ نموذجًا بالشخص المختار.
function chooseContact() {
if (!(window.WebToApk && window.WebToApk.pickContact)) {
showMessage('Open this page in the app to pick a contact.');
return;
}
var id = 'pick-' + Date.now();
window.addEventListener('appmint:contacts', function handler(e) {
if (!e.detail || e.detail.requestId !== id) return;
window.removeEventListener('appmint:contacts', handler);
if (e.detail.error === 'cancelled') return; // user closed the picker
if (e.detail.error) { showMessage('Could not open contacts'); return; }
if (!e.detail.contact) { showMessage('Could not read that contact'); return; }
var c = e.detail.contact;
document.getElementById('name').value = c.displayName;
document.getElementById('phone').value = c.phones.length ? c.phones[0].number : '';
});
WebToApk.pickContact(id);
}احتفظ بقائمة قصيرة لجهات اتصال الطوارئ. احفظ الاسم والرقم، وليس id، حتى لا تعتمد القائمة على دفتر العناوين:
function pickOne() {
return new Promise(function (resolve, reject) {
var id = 'sos-' + Date.now();
window.addEventListener('appmint:contacts', function handler(e) {
if (!e.detail || e.detail.requestId !== id) return;
window.removeEventListener('appmint:contacts', handler);
if (e.detail.error === 'cancelled') resolve(null);
else if (e.detail.error) reject(new Error(e.detail.error));
else resolve(e.detail.contact);
});
WebToApk.pickContact(id);
});
}
document.getElementById('add-sos').addEventListener('click', async function () {
if (!(window.WebToApk && window.WebToApk.pickContact)) return;
var c = await pickOne();
if (!c || !c.phones.length) return; // closed, or nothing readable
var list = JSON.parse(localStorage.getItem('sos') || '[]');
list.push({ name: c.displayName, number: c.phones[0].number });
localStorage.setItem('sos', JSON.stringify(list));
renderSosList(list);
});عطّل الزر أثناء فتح المنتقي، واعرض حقول الكتابة عندما لا يكون في الهاتف تطبيق جهات اتصال:
var pickBtn = document.getElementById('pick');
pickBtn.addEventListener('click', function () {
if (!(window.WebToApk && window.WebToApk.pickContact)) { showManualEntry(); return; }
var id = 'recipient-' + Date.now();
pickBtn.disabled = true; // one picker at a time
window.addEventListener('appmint:contacts', function handler(e) {
if (!e.detail || e.detail.requestId !== id) return;
window.removeEventListener('appmint:contacts', handler);
pickBtn.disabled = false;
if (e.detail.error === 'no_picker') { showManualEntry(); return; }
if (e.detail.error) return; // 'cancelled'
if (e.detail.contact && e.detail.contact.phones.length) {
document.getElementById('to').value = e.detail.contact.phones[0].number;
}
});
WebToApk.pickContact(id);
});ملاحظات: تحصل على الاسم و**رقم واحد** نقر عليه المستخدم - ولا رسائل بريد إلكتروني ولا بقية الدفتر. emails فارغة دائمًا. الأخطاء: cancelled، no_picker (لا يوجد تطبيق جهات اتصال على الهاتف). إذا فشلت القراءة بعد الاختيار، تكون contact بقيمة null. ولأنه لا يُقرأ شيء دون اختيار المستخدم، فهذه هي الطريقة المناسبة لـ Play للحصول على رقم هاتف؛ فضّلها على listContacts عندما تحتاج شخصًا واحدًا فقط. بالنسبة للتطبيقات المبنية بالذكاء الاصطناعي واستيرادات Google AI Studio، يفحص الإنشاء شيفرتك؛ استدعاء pickContact( لا يفعّل جهات الاتصال - فقط listContacts وsearchContacts وgetContact تفعلان ذلك - لذا لا يضيف المنتقي إذن قراءة جهات الاتصال.
searchContacts الجسر (bridge)#
window.WebToApk.searchContacts(requestId: String, query: String, limit: Int)
كلها غير متزامنة: قد تضم قراءات المؤشر آلاف الصفوف، وإرجاع @JavascriptInterface حاجب سيشغّل ذلك على خيط JS مما يسبب ANR. تصل النتائج على هيئة `appmint:contacts` / window.onAppMintContacts({requestId,...}). requestId: مرّر نصًا STRING ('pick-' + Date.now()). كما يعمل رقم NUMBER خام - فالجسر يحوّله عبر double ("1.787491234567E12")، وكل ردّ يعيده إلى صيغته الأصلية (BridgeRequestId)، لذا فإن `res.requestId === Date.now()` تعطي true في الحالتين. WebToApk.listContacts(id, limit, offset) WebToApk.searchContacts(id, query, limit) WebToApk.getContact(id, contactId) WebToApk.pickContact(id) <- لا يحتاج إلى إذن (منتقي النظام) WebToApk.addContact(id, json) <- يحتاج إلى إذن كتابة جهات الاتصال
موصوفة ضمن مجموعتها، جهات الاتصال، وليس بمفردها.
مثال
يعثر على جهات الاتصال التي يطابق اسمها كلمة بحث، دون تحميل دفتر العناوين كاملاً.
القيمة المُرجَعة: لا شيء الآن. الإجابة هي الحدث appmint:contacts (وwindow.onAppMintContacts) مع detail = { requestId, contacts: [...] } - بنفس شكل جهة الاتصال في listContacts - أو { requestId, error }. المتطلبات: فعّل جهات الاتصال في الخطوة 4 (الوصول) عند الإنشاء؛ ويُطلب إذن المستخدم عند أول استدعاء.
ابحث أثناء كتابة المستخدم. تُعرض فقط إجابة آخر بحث.
var lastSearchId = '';
function searchPeople(word) {
if (!(window.WebToApk && window.WebToApk.searchContacts)) return;
lastSearchId = 'search-' + Date.now();
WebToApk.searchContacts(lastSearchId, word, 20); // at most 20 results
}
window.addEventListener('appmint:contacts', function (e) {
if (!e.detail || e.detail.requestId !== lastSearchId) return; // old or other request
if (e.detail.error) { showMessage('Search failed: ' + e.detail.error); return; }
var names = e.detail.contacts.map(function (c) {
return c.displayName + (c.phones.length ? ' - ' + c.phones[0].number : '');
});
document.getElementById('results').textContent = names.join('\n') || 'No match';
});
document.getElementById('q').addEventListener('input', function (e) {
searchPeople(e.target.value);
});غلّف البحث في Promise وانتظر حتى يتوقف المستخدم عن الكتابة، حتى لا تبدأ الكتابة السريعة بحثًا لكل مفتاح:
function searchOnce(word, limit) {
return new Promise(function (resolve, reject) {
var id = 'find-' + Date.now() + '-' + Math.random().toString(36).slice(2);
window.addEventListener('appmint:contacts', function handler(e) {
if (!e.detail || e.detail.requestId !== id) return;
window.removeEventListener('appmint:contacts', handler);
if (e.detail.error) reject(new Error(e.detail.error)); else resolve(e.detail.contacts);
});
WebToApk.searchContacts(id, word, limit);
});
}
var timer = null;
document.getElementById('q').addEventListener('input', function (e) {
clearTimeout(timer);
var word = e.target.value.trim();
timer = setTimeout(function () {
if (!(window.WebToApk && window.WebToApk.searchContacts) || !word) return;
searchOnce(word, 10)
.then(function (people) { renderSuggestions(people); })
.catch(function (err) { showMessage('Search failed: ' + err.message); });
}, 300);
});املأ خانة البريد الإلكتروني من دفتر العناوين عندما يطابق الاسم المكتوب شخصًا واحدًا لديه بريد إلكتروني:
function suggestEmail(name) {
if (!(window.WebToApk && window.WebToApk.searchContacts) || name.length < 2) return;
var id = 'email-' + Date.now();
window.addEventListener('appmint:contacts', function handler(e) {
if (!e.detail || e.detail.requestId !== id) return;
window.removeEventListener('appmint:contacts', handler);
if (e.detail.error) return; // leave the box for the user to type
var withEmail = e.detail.contacts.filter(function (c) { return c.emails.length > 0; });
if (withEmail.length === 1) {
document.getElementById('email').value = withEmail[0].emails[0];
}
});
WebToApk.searchContacts(id, name, 5);
}
document.getElementById('name').addEventListener('change', function (e) {
suggestEmail(e.target.value.trim());
});ملاحظات: كلمة بحث فارغة تُرجع جهات الاتصال من بداية القائمة (مثل listContacts). تُحصر limit بين 1 و2000 ولا يوجد إزاحة. الأخطاء: not_enabled، permission_denied، foreign_origin. جهات الاتصال بيانات شخصية: يجب أن تصرّح بها قائمة تطبيقك في Play Store في نموذج أمان البيانات وسياسة الخصوصية.