1. قبل البدء
في هذا الدرس التطبيقي حول الترميز، ستتعرّف على كيفية تنفيذ ميزة "تسجيل الدخول باستخدام Google" على Android باستخدام "مدير بيانات الاعتماد".
المتطلبات الأساسية
- فهم أساسي لاستخدام Kotlin لتطوير تطبيقات Android
- فهم أساسي لـ Jetpack Compose (يمكنك الاطّلاع على مزيد من المعلومات هنا)
أهداف الدورة التعليمية
- كيفية إنشاء مشروع على Google Cloud
- كيفية إنشاء برامج تعتمد على بروتوكول OAuth في Google Cloud Console
- كيفية تنفيذ ميزة "تسجيل الدخول باستخدام حساب Google" باستخدام مسار "ورقة أسفل الشاشة"
- كيفية تنفيذ ميزة "تسجيل الدخول باستخدام حساب Google" من خلال مسار الزر
ما تحتاج إليه
- استوديو Android (يمكنك تنزيله هنا)
- جهاز كمبيوتر يستوفي متطلبات النظام في Android Studio
- جهاز كمبيوتر يستوفي متطلبات النظام الخاصة بـ "محاكي Android"
2. إنشاء مشروع في "استوديو Android"
المدة 3:00 - 5:00
للبدء، علينا إنشاء مشروع جديد في Android Studio:
- فتح "استوديو Android"
- انقر على مشروع جديد
.
- اختَر الهاتف والجهاز اللوحي ونشاط فارغ
- انقر على Next (التالي).
- حان الآن وقت إعداد بعض أجزاء المشروع:
- الاسم: هذا هو اسم مشروعك
- اسم الحزمة: ستتم تعبئة هذا الحقل تلقائيًا استنادًا إلى اسم مشروعك
- موقع الحفظ: يجب أن يكون هذا الموقع هو المجلد التلقائي الذي يحفظ فيه "استوديو Android" مشاريعك. يمكنك تغيير ذلك إلى أي مكان تريده.
- الحد الأدنى لإصدار حزمة تطوير البرامج (SDK): هو أدنى إصدار من حزمة تطوير البرامج لنظام التشغيل Android تم تصميم تطبيقك ليعمل عليه. في هذا الدرس التطبيقي، سنستخدم الإصدار 36 من واجهة برمجة التطبيقات (Baklava).
- انقر على إنهاء.
- سينشئ "استوديو Android" المشروع وينزّل أي تبعيات ضرورية للتطبيق الأساسي، وقد يستغرق ذلك عدة دقائق. للاطّلاع على ذلك، ما عليك سوى النقر على رمز الإنشاء:
- بعد اكتمال ذلك، من المفترض أن يظهر Android Studio على النحو التالي:
3- إعداد مشروعك على Google Cloud
إنشاء مشروع على Google Cloud
- انتقِل إلى Google Cloud Console.
- افتح مشروعك أو أنشئ مشروعًا جديدًا
- انقر على واجهات برمجة التطبيقات والخدمات
- انتقِل إلى شاشة طلب الموافقة المتعلّقة ببروتوكول OAuth
- عليك ملء الحقول في نظرة عامة للمتابعة. انقر على البدء لبدء ملء هذه المعلومات:
- اسم التطبيق: اسم هذا التطبيق، ويجب أن يكون هو نفسه الاسم الذي استخدمته عند إنشاء المشروع في "استوديو Android"
- البريد الإلكتروني لدعم المستخدمين: سيتم عرض حساب Google الذي تسجّل الدخول باستخدامه وأي مجموعات Google تديرها.
- الجمهور:
- داخلي لتطبيق يُستخدم داخل مؤسستك فقط إذا لم تكن لديك مؤسسة مرتبطة بمشروع Google Cloud، لن تتمكّن من اختيار هذا المشروع.
- سنستخدم الخيار "خارجي".
- معلومات الاتصال: يمكن أن يتضمّن هذا الحقل أي عنوان بريد إلكتروني تريد استخدامه كجهة اتصال للتطبيق.
- راجِع خدمات Google API: سياسة بيانات المستخدمين.
- بعد مراجعة سياسة بيانات المستخدمين والموافقة عليها، انقر على إنشاء
إعداد عملاء OAuth
بعد إعداد مشروع على Google Cloud، علينا إضافة عميل ويب وعميل Android حتى نتمكّن من إجراء طلبات إلى خادم الخلفية OAuth باستخدام معرّفات العملاء.
بالنسبة إلى "عميل الويب" على Android، ستحتاج إلى ما يلي:
- اسم حزمة تطبيقك (مثلاً com.example.example)
- توقيع SHA-1 لتطبيقك
- ما هو توقيع SHA-1؟
- الملف المرجعي SHA-1 هو تجزئة تشفير يتم إنشاؤها من مفتاح توقيع تطبيقك. ويعمل كمعرّف فريد لشهادة توقيع تطبيقك المحدّد. يمكنك اعتبارها بمثابة "توقيع" رقمي لتطبيقك.
- لماذا نحتاج إلى توقيع SHA-1؟
- تضمن بصمة SHA-1 ألا يتمكّن سوى تطبيقك، الذي تم توقيعه باستخدام مفتاح التوقيع المحدّد، من طلب رموز الدخول باستخدام معرّف عميل OAuth 2.0، ما يمنع التطبيقات الأخرى (حتى تلك التي تحمل اسم الحزمة نفسه) من الوصول إلى موارد مشروعك وبيانات المستخدمين.
- يمكنك التفكير في الأمر على النحو التالي:
- مفتاح توقيع التطبيق يشبه المفتاح الفعلي "لباب" تطبيقك. وهو ما يتيح الوصول إلى العمليات الداخلية للتطبيق.
- الملف المرجعي لشهادة SHA-1 هو بمثابة رقم تعريف فريد لبطاقة المفتاح المرتبط بمفتاحك الفعلي. وهو رمز محدّد يعرّف هذا المفتاح.
- معرّف عميل OAuth 2.0 هو بمثابة رمز دخول إلى خدمة أو مورد معيّنَين من Google (مثل "تسجيل الدخول باستخدام حساب Google").
- عند تقديم بصمة SHA-1 أثناء إعداد عميل OAuth، فإنّك في الأساس تخبر Google بما يلي: "لا يمكن إلا لبطاقة المفتاح التي تحمل هذا المعرّف المحدّد (SHA-1) فتح رمز الوصول هذا (معرّف العميل)". ويضمن ذلك ألا يتمكّن سوى تطبيقك من الوصول إلى خدمات Google المرتبطة برمز الدخول هذا".
- ما هو توقيع SHA-1؟
بالنسبة إلى "عميل الويب"، كل ما نحتاج إليه هو الاسم الذي تريد استخدامه لتحديد العميل في وحدة التحكّم.
إنشاء عميل OAuth 2.0 لنظام التشغيل Android
- انتقِل إلى صفحة العملاء
- انقر على إنشاء عميل
- اختَر Android لنوع التطبيق.
- عليك تحديد اسم حزمة تطبيقك
- من "استوديو Android"، علينا الحصول على توقيع SHA-1 لتطبيقنا ونسخه ولصقه هنا:
- الانتقال إلى Android Studio وفتح الوحدة الطرفية
- نفِّذ الأمر التالي:
تم تصميم هذا الأمر لإدراج تفاصيل إدخال (اسم مستعار) معيّن في ملف تخزين المفاتيح.keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -keypass android
-
-list
: يطلب هذا الخيار من أداة keytool إدراج محتويات ملف تخزين المفاتيح. -v
: يتيح هذا الخيار إخراجًا مفصّلاً، ما يوفّر معلومات أكثر تفصيلاً حول الإدخال.-keystore ~/.android/debug.keystore
: يحدّد هذا الخيار مسار ملف تخزين المفاتيح.-alias androiddebugkey
: يحدّد هذا الخيار الاسم المستعار (اسم الإدخال) للمفتاح الذي تريد فحصه.-storepass android
: يوفّر هذا الحقل كلمة المرور لملف تخزين المفاتيح.-
-keypass android
: يوفّر هذا الخيار كلمة المرور للمفتاح الخاص للاسم المستعار المحدّد.
-
- انسخ قيمة توقيع SHA-1:
- ارجع إلى نافذة Google Cloud والصِق قيمة توقيع SHA-1:
- يُفترض أن تبدو شاشتك الآن على النحو التالي، ويمكنك النقر على إنشاء:
إنشاء عميل OAuth 2.0 على الويب
- لإنشاء معرّف عميل لتطبيق ويب، كرِّر الخطوتَين 1 و2 من قسم إنشاء عميل Android واختَر تطبيق ويب في "نوع التطبيق".
- امنح العميل اسمًا (سيكون هذا هو عميل OAuth):
- انقر على إنشاء
- انسخ معرّف العميل من النافذة المنبثقة، ستحتاج إليه لاحقًا
بعد إعداد جميع عملاء OAuth، يمكننا الرجوع إلى Android Studio لإنشاء تطبيق Android الذي يتيح تسجيل الدخول باستخدام Google.
4. إعداد "الجهاز الافتراضي المتوافق مع Android"
لإجراء اختبار سريع لتطبيقك بدون جهاز Android فعلي، عليك إنشاء جهاز Android افتراضي يمكنك إنشاء تطبيقك وتشغيله عليه على الفور من Android Studio. إذا كنت تريد إجراء الاختبار باستخدام جهاز Android فعلي، يمكنك اتّباع التعليمات الواردة في مستندات مطوّري تطبيقات Android.
إنشاء جهاز Android افتراضي
- في Android Studio، افتح "أداة إدارة الأجهزة"
- انقر على الزر + > إنشاء جهاز افتراضي
- من هنا، يمكنك إضافة أي جهاز تحتاجه لمشروعك. لأغراض هذا الدرس التطبيقي العملي، اختَر هاتف متوسط، ثم انقر على التالي
- يمكنك الآن ضبط الجهاز لمشروعك من خلال منح الجهاز اسمًا فريدًا واختيار إصدار Android الذي سيتم تشغيله على الجهاز وغير ذلك. تأكَّد من ضبط واجهة برمجة التطبيقات على API 36 "Baklava"؛ Android 16، ثم انقر على إنهاء
- من المفترض أن يظهر الجهاز الجديد في "إدارة الأجهزة". للتأكّد من أنّ الجهاز يعمل، انقر على
بجانب الجهاز الذي أنشأته للتو
.
- من المفترض أن يكون الجهاز قيد التشغيل الآن.
تسجيل الدخول إلى "جهاز Android الافتراضي"
الجهاز الذي أنشأته للتو يعمل، والآن لمنع حدوث أخطاء عند اختبار ميزة "تسجيل الدخول باستخدام حساب Google"، عليك تسجيل الدخول إلى الجهاز باستخدام حساب Google.
- انتقِل إلى "الإعدادات" باتّباع الخطوات التالية:
- انقر على وسط الشاشة على الجهاز الافتراضي ومرِّر سريعًا للأعلى
- ابحث عن تطبيق "الإعدادات" وانقر عليه
- انقر على Google في "الإعدادات"
- انقر على تسجيل الدخول واتّبِع التعليمات لتسجيل الدخول إلى حسابك على Google
- من المفترض أن تكون قد سجّلت الدخول الآن على الجهاز
أصبح "جهاز Android الافتراضي" جاهزًا الآن للاختبار.
5- إضافة عناصر تابعة
المدة 5:00
لإجراء طلبات إلى واجهة برمجة تطبيقات OAuth، علينا أولاً دمج المكتبات اللازمة التي تتيح لنا إجراء طلبات المصادقة واستخدام معرّفات Google لإجراء هذه الطلبات:
- libs.googleid
- libs.play.services.auth
- انتقِل إلى "ملف" (File) > "بنية المشروع" (Project Structure):
- بعد ذلك، انتقِل إلى التبعيات > التطبيق > '+' > تبعية المكتبة
- الآن، علينا إضافة مكتباتنا:
- في مربّع حوار البحث، اكتب googleid وانقر على بحث.
- يجب أن يكون هناك إدخال واحد فقط، لذا اختَر الإدخال وأحدث إصدار متاح (في وقت إنشاء هذا الدرس العملي، كان الإصدار 1.1.1).
- انقر على حسنًا
- كرِّر الخطوات من 1 إلى 3، ولكن ابحث بدلاً من ذلك عن "play-services-auth" واختَر السطر الذي يتضمّن "com.google.android.gms" كـ معرّف المجموعة و "play-services-auth" كـ اسم العنصر
- انقر على حسنًا
6. سير عمل البطاقة السفلية
يستفيد مسار ورقة البيانات السفلية من واجهة برمجة التطبيقات Credential Manager API لتوفير طريقة مبسطة تتيح للمستخدمين تسجيل الدخول إلى تطبيقك باستخدام حساباتهم على Google على أجهزة Android. تم تصميمها لتكون سريعة ومريحة، خاصةً للمستخدمين المتكررين. يجب أن يتم تشغيل هذا المسار عند تشغيل التطبيق.
إنشاء طلب تسجيل الدخول
- للبدء، عليك إزالة الدالتَين
Greeting()
وGreetingPreview()
منMainActivity.kt
، فلن نحتاج إليهما. - الآن، علينا التأكّد من استيراد الحِزم التي نحتاجها لهذا المشروع. واصِل العملية وأضِف عبارات
import
التالية بعد العبارات الحالية بدءًا من السطر 3::import android.content.ContentValues.TAG import android.content.Context import android.credentials.GetCredentialException import android.os.Build import android.util.Log import android.widget.Toast import androidx.annotation.RequiresApi import androidx.compose.foundation.clickable import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.credentials.CredentialManager import androidx.credentials.exceptions.GetCredentialCancellationException import androidx.credentials.exceptions.GetCredentialCustomException import androidx.credentials.exceptions.NoCredentialException import androidx.credentials.GetCredentialRequest import com.google.android.libraries.identity.googleid.GetGoogleIdOption import com.google.android.libraries.identity.googleid.GetSignInWithGoogleOption import com.google.android.libraries.identity.googleid.GoogleIdTokenParsingException import java.security.SecureRandom import java.util.Base64 import kotlinx.coroutines.CoroutineScope import androidx.compose.runtime.LaunchedEffect import kotlinx.coroutines.delay import kotlinx.coroutines.launch
- بعد ذلك، علينا إنشاء الدالة الخاصة بنا لإنشاء طلب "ورقة البيانات السفلية". ألصِق هذا الرمز أسفل فئة MainActivity
//This line is not needed for the project to build, but you will see errors if it is not present.
//This code will not work on Android versions < UpsideDownCake
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
@Composable
fun BottomSheet(webClientId: String) {
val context = LocalContext.current
// LaunchedEffect is used to run a suspend function when the composable is first launched.
LaunchedEffect(Unit) {
// Create a Google ID option with filtering by authorized accounts enabled.
val googleIdOption: GetGoogleIdOption = GetGoogleIdOption.Builder()
.setFilterByAuthorizedAccounts(true)
.setServerClientId(webClientId)
.setNonce(generateSecureRandomNonce())
.build()
// Create a credential request with the Google ID option.
val request: GetCredentialRequest = GetCredentialRequest.Builder()
.addCredentialOption(googleIdOption)
.build()
// Attempt to sign in with the created request using an authorized account
val e = signIn(request, context)
// If the sign-in fails with NoCredentialException, there are no authorized accounts.
// In this case, we attempt to sign in again with filtering disabled.
if (e is NoCredentialException) {
val googleIdOptionFalse: GetGoogleIdOption = GetGoogleIdOption.Builder()
.setFilterByAuthorizedAccounts(false)
.setServerClientId(webClientId)
.setNonce(generateSecureRandomNonce())
.build()
val requestFalse: GetCredentialRequest = GetCredentialRequest.Builder()
.addCredentialOption(googleIdOptionFalse)
.build()
//We will build out this function in a moment
signIn(requestFalse, context)
}
}
}
//This function is used to generate a secure nonce to pass in with our request
fun generateSecureRandomNonce(byteLength: Int = 32): String {
val randomBytes = ByteArray(byteLength)
SecureRandom.getInstanceStrong().nextBytes(randomBytes)
return Base64.getUrlEncoder().withoutPadding().encodeToString(randomBytes)
}
لنحلّل ما تفعله هذه التعليمات البرمجية:
fun BottomSheet(webClientId: String) {...}
: تنشئ دالة باسم BottomSheet تأخذ وسيطة سلسلة واحدة باسم webClientid
-
val context = LocalContext.current
: يستردّ سياق Android الحالي. وهو مطلوب لإجراء عمليات مختلفة، بما في ذلك تشغيل مكوّنات واجهة المستخدِم. -
LaunchedEffect(Unit) { ... }
:LaunchedEffect
هي عنصر قابل للإنشاء في Jetpack Compose يتيح لك تنفيذ دالة تعليق (دالة يمكنها إيقاف التنفيذ مؤقتًا واستئنافه) ضمن دورة حياة العنصر القابل للإنشاء. يعني استخدام الوحدة كمفتاح أنّ هذا التأثير لن يتم تنفيذه إلا مرة واحدة عند تشغيل العنصر القابل للإنشاء لأول مرة.-
val googleIdOption: GetGoogleIdOption = ...
: تنشئ عنصرGetGoogleIdOption
. يضبط هذا العنصر نوع بيانات الاعتماد المطلوب الحصول عليها من Google.-
.Builder()
: يتم استخدام نمط أداة الإنشاء لضبط الخيارات. -
.setFilterByAuthorizedAccounts(true)
: تحدّد ما إذا كان سيُسمح للمستخدم بالاختيار من بين جميع حسابات Google أو الحسابات التي سبق لها منح الإذن للتطبيق. في هذه الحالة، تم ضبطها على "صحيح"، ما يعني أنّه سيتم تقديم الطلب باستخدام بيانات الاعتماد التي سبق أن منح المستخدم الإذن باستخدامها مع هذا التطبيق، إذا كانت أي بيانات متاحة. .setServerClientId(webClientId)
: يضبط معرّف العميل للخادم، وهو معرّف فريد لخادم الخلفية في تطبيقك. هذا الإجراء مطلوب للحصول على رمز مميّز للمعرّف..setNonce(generateSecureRandomNonce())
: تضبط قيمة عشوائية (nonce) لمنع هجمات إعادة الإرسال والتأكّد من أنّ رمز التعريف المميّز مرتبط بالطلب المحدّد.-
.build()
: تنشئ هذه الطريقة العنصرGetGoogleIdOption
باستخدام الإعدادات المحدّدة.
-
-
val request: GetCredentialRequest = ...
: تنشئ عنصرGetCredentialRequest
. يحتوي هذا العنصر على طلب بيانات الاعتماد بالكامل.-
.Builder()
: يبدأ نمط أداة الإنشاء لضبط الطلب. .addCredentialOption(googleIdOption)
: يضيف googleIdOption إلى الطلب، مع تحديد أنّنا نريد طلب رمز مميّز لمعرّف Google.-
.build()
: لإنشاء العنصرGetCredentialRequest
-
-
val e = signIn(request, context)
: تحاول هذه الطريقة تسجيل دخول المستخدم باستخدام الطلب الذي تم إنشاؤه والسياق الحالي. يتم تخزين نتيجة الدالة signIn في e. سيحتوي هذا المتغير إما على النتيجة الناجحة أو على استثناء. -
if (e is NoCredentialException) { ... }
: هذا هو رمز التحقّق الشرطي. إذا تعذّر تنفيذ الدالة signIn بسبب ظهور الخطأ NoCredentialException، يعني ذلك أنّه لا تتوفّر أي حسابات تم منحها الإذن سابقًا.val googleIdOptionFalse: GetGoogleIdOption = ...
: في حال تعذّر تنفيذsignIn
السابق، ينشئ هذا الجزءGetGoogleIdOption
جديدًا..setFilterByAuthorizedAccounts(false)
: هذا هو الاختلاف الأساسي عن الخيار الأول. يؤدي ذلك إلى إيقاف فلترة الحسابات المصرّح بها، ما يعني أنّه يمكن استخدام أي حساب على Google على الجهاز لتسجيل الدخول.val requestFalse: GetCredentialRequest = ...
: يتم إنشاءGetCredentialRequest
جديد باستخدامgoogleIdOptionFalse
.-
signIn(requestFalse, context)
: تحاول هذه الطريقة تسجيل دخول المستخدم باستخدام الطلب الجديد الذي يسمح باستخدام أي حساب.
-
بشكل أساسي، يُعدّ هذا الرمز طلبًا إلى Credential Manager API لاسترداد رمز مميّز لمعرّف Google للمستخدم، وذلك باستخدام الإعدادات المقدَّمة. يمكن بعد ذلك استخدام GetCredentialRequest لتشغيل واجهة مستخدم "مدير بيانات الاعتماد"، حيث يمكن للمستخدم اختيار حسابه على Google ومنح الأذونات اللازمة.
fun generateSecureRandomNonce(byteLength: Int = 32): String
: يحدّد هذا الرمز دالة باسم generateSecureRandomNonce
. تقبل هذه الطريقة وسيطة عدد صحيح byteLength (بقيمة تلقائية تبلغ 32) تحدد الطول المطلوب للرقم العشوائي بالبايت. تعرض هذه الطريقة سلسلة نصية تمثّل وحدات البايت العشوائية بترميز Base64.
-
val randomBytes = ByteArray(byteLength)
: لإنشاء مصفوفة بايتات من byteLength المحدّد لاحتواء البايتات العشوائية SecureRandom.getInstanceStrong().nextBytes(randomBytes)
:SecureRandom.getInstanceStrong()
: يؤدي ذلك إلى الحصول على مولّد أرقام عشوائية قوي من الناحية التشفيرية. وهذا أمر بالغ الأهمية للأمان، لأنّه يضمن أنّ الأرقام التي يتم إنشاؤها عشوائية حقًا ولا يمكن توقّعها. يستخدم هذا المولد أقوى مصدر إنتروبيا متاح على النظام.-
.nextBytes(randomBytes)
: يؤدي هذا الإجراء إلى ملء مصفوفة randomBytes ببايتات عشوائية تم إنشاؤها بواسطة مثيل SecureRandom.
return Base64.getUrlEncoder().withoutPadding().encodeToString(randomBytes)
:-
Base64.getUrlEncoder()
: يحصل هذا الرمز على أداة ترميز Base64 تستخدم مجموعة أحرف آمنة لعناوين URL (باستخدام - و_ بدلاً من + و /). وهذا مهم لأنه يضمن إمكانية استخدام السلسلة الناتجة بأمان في عناوين URL بدون الحاجة إلى ترميز إضافي. .withoutPadding()
: يؤدي ذلك إلى إزالة أي أحرف حشو من سلسلة Base64 المرمّزة. وغالبًا ما يكون من المستحسن أن يكون الرقم العشوائي أقصر وأكثر إيجازًا..encodeToString(randomBytes)
: يرمّز هذا الرمز randomBytes إلى سلسلة Base64 ويعرضها.
-
باختصار، تنشئ هذه الدالة عددًا عشوائيًا قويًا من الناحية التشفيرية بطول محدّد، وترمّزه باستخدام Base64 الآمن لعناوين URL، وتعرض السلسلة الناتجة. هذه ممارسة معتادة لإنشاء أرقام عشوائية صالحة للاستخدام في السياقات الحسّاسة للأمان.
إرسال طلب تسجيل الدخول
بعد أن أصبح بإمكاننا إنشاء طلب تسجيل الدخول، يمكننا استخدام "مدير بيانات الاعتماد" لتسجيل الدخول. لإجراء ذلك، علينا إنشاء دالة تعالج تمرير طلبات تسجيل الدخول باستخدام Credential Manager، مع معالجة الاستثناءات الشائعة التي قد نواجهها.
يمكنك لصق هذه الدالة أسفل الدالة BottomSheet()
لإجراء ذلك.
//This code will not work on Android versions < UPSIDE_DOWN_CAKE when GetCredentialException is
//is thrown.
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
suspend fun signIn(request: GetCredentialRequest, context: Context): Exception? {
val credentialManager = CredentialManager.create(context)
val failureMessage = "Sign in failed!"
var e: Exception? = null
//using delay() here helps prevent NoCredentialException when the BottomSheet Flow is triggered
//on the initial running of our app
delay(250)
try {
// The getCredential is called to request a credential from Credential Manager.
val result = credentialManager.getCredential(
request = request,
context = context,
)
Log.i(TAG, result.toString())
Toast.makeText(context, "Sign in successful!", Toast.LENGTH_SHORT).show()
Log.i(TAG, "(☞゚ヮ゚)☞ Sign in Successful! ☜(゚ヮ゚☜)")
} catch (e: GetCredentialException) {
Toast.makeText(context, failureMessage, Toast.LENGTH_SHORT).show()
Log.e(TAG, failureMessage + ": Failure getting credentials", e)
} catch (e: GoogleIdTokenParsingException) {
Toast.makeText(context, failureMessage, Toast.LENGTH_SHORT).show()
Log.e(TAG, failureMessage + ": Issue with parsing received GoogleIdToken", e)
} catch (e: NoCredentialException) {
Toast.makeText(context, failureMessage, Toast.LENGTH_SHORT).show()
Log.e(TAG, failureMessage + ": No credentials found", e)
return e
} catch (e: GetCredentialCustomException) {
Toast.makeText(context, failureMessage, Toast.LENGTH_SHORT).show()
Log.e(TAG, failureMessage + ": Issue with custom credential request", e)
} catch (e: GetCredentialCancellationException) {
Toast.makeText(context, ": Sign-in cancelled", Toast.LENGTH_SHORT).show()
Log.e(TAG, failureMessage + ": Sign-in was cancelled", e)
}
return e
}
في ما يلي تفصيل لما تفعله التعليمات البرمجية هنا:
suspend fun signIn(request: GetCredentialRequest, context: Context): Exception?
: يحدّد هذا الرمز دالة تعليق باسم signIn. وهذا يعني أنّه يمكن إيقافه مؤقتًا واستئنافه بدون حظر سلسلة التعليمات الرئيسية، كما أنّه يعرض Exception?
ستكون قيمته فارغة إذا تم تسجيل الدخول بنجاح أو يعرض الاستثناء المحدّد في حال تعذُّر تسجيل الدخول.
تتضمّن هذه الدالة مَعلمتَين:
-
request
: عنصرGetCredentialRequest
يحتوي على إعدادات نوع بيانات الاعتماد المطلوب استردادها (مثل معرّف Google). -
context
: سياق Android المطلوب للتفاعل مع النظام.
بالنسبة إلى نص الدالة:
-
val credentialManager = CredentialManager.create(context)
: تنشئ هذه السمة مثيلاً من CredentialManager، وهي الواجهة الرئيسية للتفاعل مع Credential Manager API. هذه هي الطريقة التي سيبدأ بها التطبيق عملية تسجيل الدخول. -
val failureMessage = "Sign in failed!"
: تحدّد سلسلة (failureMessage) سيتم عرضها في إشعار مؤقت عند تعذُّر تسجيل الدخول. -
var e: Exception? = null
: يهيئ هذا السطر المتغير e لتخزين أي استثناء قد يحدث أثناء العملية، بدءًا بالقيمة null. delay(250)
: يؤدي إلى تأخير بمقدار 250 ملي ثانية. هذا حلّ بديل لمشكلة محتملة قد يتم فيها عرض NoCredentialException فور بدء تشغيل التطبيق، خاصةً عند استخدام مسار BottomSheet. يمنح ذلك النظام الوقت الكافي لتهيئة "مدير بيانات الاعتماد".try { ... } catch (e: Exception) { ... }
:يتم استخدام كتلة try-catch للتعامل مع الأخطاء بشكل فعّال. يضمن ذلك أنّه في حال حدوث أي خطأ أثناء عملية تسجيل الدخول، لن يتعطّل التطبيق، ويمكنه التعامل مع الاستثناء بسلاسة.-
val result = credentialManager.getCredential(request = request, context = context)
: هذا هو المكان الذي يتم فيه إجراء طلب البيانات الفعلي من واجهة برمجة تطبيقات "إدارة بيانات الاعتماد" وبدء عملية استرداد بيانات الاعتماد. تتلقّى هذه الواجهة الطلب والسياق كمدخلات، ثم تعرض واجهة مستخدم للمستخدم لاختيار بيانات اعتماد. في حال نجاح العملية، سيتم عرض نتيجة تحتوي على بيانات الاعتماد المحدّدة. يتم تخزين نتيجة هذه العملية،GetCredentialResponse
، في المتغيرresult
. -
Toast.makeText(context, "Sign in successful!", Toast.LENGTH_SHORT).show()
:تعرض رسالة قصيرة تشير إلى نجاح عملية تسجيل الدخول. -
Log.i(TAG, "Sign in Successful!")
: يسجّل رسالة ممتعة وناجحة في logcat. -
catch (e: GetCredentialException)
: تعالج الاستثناءات من النوعGetCredentialException
. هذه فئة رئيسية لعدة استثناءات محددة يمكن أن تحدث أثناء عملية استرداد بيانات الاعتماد. -
catch (e: GoogleIdTokenParsingException)
: يتعامل مع الاستثناءات التي تحدث عند حدوث خطأ في تحليل رمز التعريف المميّز من Google. -
catch (e: NoCredentialException)
: تعالج الخطأNoCredentialException
الذي يتم عرضه عندما لا تتوفّر بيانات اعتماد للمستخدم (مثلاً، لم يحفظ المستخدم أي بيانات اعتماد أو ليس لديه حساب على Google).- الأهم من ذلك أنّ هذه الدالة تعرض الاستثناء المخزّن في
e
،NoCredentialException
، ما يسمح للمستدعي بمعالجة الحالة المحدّدة في حال عدم توفّر بيانات الاعتماد.
- الأهم من ذلك أنّ هذه الدالة تعرض الاستثناء المخزّن في
-
catch (e: GetCredentialCustomException)
: يتعامل مع الاستثناءات المخصّصة التي قد يطرحها موفّر بيانات الاعتماد. -
catch (e: GetCredentialCancellationException)
: تعالج الخطأGetCredentialCancellationException
الذي يتم عرضه عندما يلغي المستخدم عملية تسجيل الدخول. -
Toast.makeText(context, failureMessage, Toast.LENGTH_SHORT).show()
: يعرض رسالة إشعار مؤقت تشير إلى تعذُّر تسجيل الدخول باستخدام failureMessage. -
Log.e(TAG, "", e)
: يسجّل الاستثناء في أداة Android logcat باستخدام Log.e، والتي تُستخدَم في تسجيل الأخطاء. سيشمل ذلك تتبُّع تسلسل استدعاء الدوال البرمجية للاستثناء للمساعدة في تصحيح الأخطاء. ويتضمّن أيضًا رمزًا تعبيريًا غاضبًا للمرح.
-
return e
: تعرض الدالة الاستثناء إذا تم رصده، أو تعرض قيمة فارغة إذا تم تسجيل الدخول بنجاح.
باختصار، يوفّر هذا الرمز طريقة للتعامل مع تسجيل دخول المستخدم باستخدام Credential Manager API، ويدير العملية غير المتزامنة، ويتعامل مع الأخطاء المحتملة، ويقدّم ملاحظات للمستخدم من خلال الإشعارات المنبثقة والسجلات، مع إضافة لمسة من الفكاهة إلى معالجة الأخطاء.
تنفيذ مسار البطاقة السفلية في التطبيق
يمكننا الآن إعداد طلب لتفعيل مسار BottomSheet في فئة MainActivity
باستخدام الرمز التالي ومعرّف عميل تطبيق الويب الذي نسخناه من Google Cloud Console سابقًا:
class MainActivity : ComponentActivity() {
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//replace with your own web client ID from Google Cloud Console
val webClientId = "YOUR_CLIENT_ID_HERE"
setContent {
//ExampleTheme - this is derived from the name of the project not any added library
//e.g. if this project was named "Testing" it would be generated as TestingTheme
ExampleTheme {
Surface(
modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background,
) {
//This will trigger on launch
BottomSheet(webClientId)
}
}
}
}
}
يمكننا الآن حفظ مشروعنا (ملف > حفظ) وتشغيله:
- انقر على زر التشغيل:
- بعد تشغيل تطبيقك على المحاكي، من المفترض أن تظهر النافذة المنبثقة BottomSheet لتسجيل الدخول. انقر على متابعة لاختبار تسجيل الدخول
- من المفترض أن تظهر لك رسالة Toast تشير إلى نجاح عملية تسجيل الدخول.
7. مسار الزر
تسهّل "عملية الزر" في ميزة "تسجيل الدخول باستخدام حساب Google" على المستخدمين الاشتراك في تطبيق Android أو تسجيل الدخول إليه باستخدام حسابهم الحالي على Google. سيتم عرضها إذا أغلق المستخدم ورقة البيانات السفلية أو إذا كان يفضّل استخدام حسابه على Google لتسجيل الدخول أو الاشتراك. بالنسبة إلى المطوّرين، يعني ذلك عملية إعداد أكثر سلاسة وتقليل المشاكل أثناء الاشتراك.
على الرغم من إمكانية تنفيذ ذلك باستخدام زر Jetpack Compose جاهز للاستخدام، سنستخدم رمزًا تجاريًا معتمدًا مسبقًا من صفحة إرشادات تصميم ميزة "تسجيل الدخول باستخدام حساب Google".
إضافة رمز العلامة التجارية إلى المشروع
- يمكنك تنزيل ملف ZIP الذي يحتوي على رموز العلامات التجارية الموافَق عليها مسبقًا هنا.
- فكّ ضغط الملف signin-assest.zip من مجلد التنزيلات (سيختلف هذا الإجراء استنادًا إلى نظام التشغيل على جهاز الكمبيوتر). يمكنك الآن فتح مجلد signin-assets والاطّلاع على الرموز المتاحة. في هذا الدرس التطبيقي حول الترميز، سنستخدم
signin-assets/Android/png@2x/neutral/android_neutral_sq_SI@2x.png
. - نسخ الملف
- الصق الملف في المشروع ضمن res > drawable في "استوديو Android" من خلال النقر بزر الماوس الأيمن على مجلد drawable ثم النقر على لصق (قد تحتاج إلى توسيع مجلد res لعرضه)
- سيظهر مربّع حوار يطلب منك إعادة تسمية الملف وتأكيد الدليل الذي ستتم إضافته إليه. أعِد تسمية مادة العرض إلى siwg_button.png، ثم انقر على موافق
رمز تدفّق الزر
سيستخدم هذا الرمز الدالة signIn()
نفسها المستخدَمة في BottomSheet()
، ولكنّه سيستخدم GetSignInWithGoogleOption
بدلاً من GetGoogleIdOption
لأنّ هذا المسار لا يستفيد من بيانات الاعتماد ومفاتيح المرور المخزَّنة على الجهاز لعرض خيارات تسجيل الدخول. إليك الرمز الذي يمكنك لصقه أسفل الدالة BottomSheet()
:
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
@Composable
fun ButtonUI(webClientId: String) {
val context = LocalContext.current
val coroutineScope = rememberCoroutineScope()
val onClick: () -> Unit = {
val signInWithGoogleOption: GetSignInWithGoogleOption = GetSignInWithGoogleOption
.Builder(serverClientId = webClientId)
.setNonce(generateSecureRandomNonce())
.build()
val request: GetCredentialRequest = GetCredentialRequest.Builder()
.addCredentialOption(signInWithGoogleOption)
.build()
coroutineScope.launch {
signIn(request, context)
}
}
Image(
painter = painterResource(id = R.drawable.siwg_button),
contentDescription = "",
modifier = Modifier
.fillMaxSize()
.clickable(enabled = true, onClick = onClick)
)
}
في ما يلي تفصيل لما يفعله الرمز:
fun ButtonUI(webClientId: String)
: تعرّف هذه السمة دالة باسم ButtonUI
تقبل webClientId
(رقم تعريف العميل لمشروعك على Google Cloud) كمعلَمة.
val context = LocalContext.current
: يستردّ سياق Android الحالي. وهو مطلوب لإجراء عمليات مختلفة، بما في ذلك تشغيل مكوّنات واجهة المستخدِم.
val coroutineScope = rememberCoroutineScope()
: لإنشاء نطاق كوروتين يُستخدَم هذا الإجراء لإدارة المهام غير المتزامنة، ما يسمح بتشغيل الرمز بدون حظر سلسلة التعليمات الرئيسية. rememberCoroutineScope
() هي دالة قابلة للإنشاء من Jetpack Compose توفّر نطاقًا مرتبطًا بدورة حياة العنصر القابل للإنشاء.
val onClick: () -> Unit = { ... }
: يؤدي ذلك إلى إنشاء دالة lambda سيتم تنفيذها عند النقر على الزر. ستنفّذ دالة lambda ما يلي:
-
val signInWithGoogleOption: GetSignInWithGoogleOption = GetSignInWithGoogleOption.Builder(serverClientId = webClientId).setNonce(generateSecureRandomNonce()).build()
: ينشئ هذا الجزء عنصرGetSignInWithGoogleOption
. يُستخدَم هذا العنصر لتحديد مَعلمات عملية "تسجيل الدخول من خلال Google"، ويتطلّبwebClientId
وnonce (سلسلة عشوائية تُستخدَم لأغراض الأمان). -
val request: GetCredentialRequest = GetCredentialRequest.Builder().addCredentialOption(signInWithGoogleOption).build()
: يؤدي ذلك إلى إنشاء عنصرGetCredentialRequest
. سيتم استخدام هذا الطلب للحصول على بيانات اعتماد المستخدم باستخدام "مدير الاعتماد". يضيفGetCredentialRequest
GetSignInWithGoogleOption
الذي تم إنشاؤه سابقًا كخيار، وذلك لطلب بيانات اعتماد "تسجيل الدخول باستخدام حساب Google".
-
coroutineScope.launch { ... }
:CoroutineScope
لإدارة العمليات غير المتزامنة (باستخدام الروتينات الفرعية).-
signIn(request, context)
: تستدعي الدالةsignIn
() التي تم تحديدها سابقًا
-
Image(...)
: يعرض هذا الرمز صورة باستخدام painterResource
الذي يحمّل الصورة R.drawable.siwg_button
Modifier.fillMaxSize().clickable(enabled = true, onClick = onClick)
:fillMaxSize()
: لجعل الصورة تملأ المساحة المتاحةclickable(enabled = true, onClick = onClick)
: تجعل الصورة قابلة للنقر، وعند النقر عليها، يتم تنفيذ دالة lambda onClick التي تم تحديدها سابقًا.
باختصار، يضبط هذا الرمز زر "تسجيل الدخول باستخدام Google" في واجهة مستخدم Jetpack Compose. عند النقر على الزر، يتم إعداد طلب بيانات اعتماد لتشغيل "مدير بيانات الاعتماد" والسماح للمستخدم بتسجيل الدخول باستخدام حسابه على Google.
الآن، يجب تعديل فئة MainActivity لتشغيل الدالة ButtonUI()
:
class MainActivity : ComponentActivity() {
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//replace with your own web client ID from Google Cloud Console
val webClientId = "YOUR_CLIENT_ID_HERE"
setContent {
//ExampleTheme - this is derived from the name of the project not any added library
//e.g. if this project was named "Testing" it would be generated as TestingTheme
ExampleTheme {
Surface(
modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background,
) {
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
//This will trigger on launch
BottomSheet(webClientId)
//This requires the user to press the button
ButtonUI(webClientId)
}
}
}
}
}
}
يمكننا الآن حفظ مشروعنا (ملف > حفظ) وتشغيله:
- انقر على زر التشغيل:
- بعد تشغيل التطبيقات على المحاكي، من المفترض أن يظهر BottomSheet. انقر خارجها لإغلاقها.
- من المفترض أن يظهر الآن الزر الذي أنشأناه في التطبيق. انقر عليه لمشاهدة مربّع حوار تسجيل الدخول
- انقر على حسابك لتسجيل الدخول.
8. الخاتمة
لقد أكملت هذا الدرس التطبيقي حول الترميز. لمزيد من المعلومات أو المساعدة بشأن ميزة "تسجيل الدخول باستخدام Google" على Android، يُرجى الاطّلاع على قسم "الأسئلة الشائعة" أدناه:
الأسئلة الشائعة
- Stackoverflow
- دليل تحديد المشاكل وحلّها في "إدارة بيانات الاعتماد" على Android
- الأسئلة الشائعة حول "إدارة بيانات الاعتماد على Android"
- مركز مساعدة التحقّق من تطبيقات OAuth
الرمز الكامل لملف MainActivity.kt
في ما يلي الرمز الكامل لملف MainActivity.kt كمرجع:
package com.example.example
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.example.example.ui.theme.ExampleTheme
import android.content.ContentValues.TAG
import android.content.Context
import android.credentials.GetCredentialException
import android.os.Build
import android.util.Log
import android.widget.Toast
import androidx.annotation.RequiresApi
import androidx.compose.foundation.clickable
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.credentials.CredentialManager
import androidx.credentials.exceptions.GetCredentialCancellationException
import androidx.credentials.exceptions.GetCredentialCustomException
import androidx.credentials.exceptions.NoCredentialException
import androidx.credentials.GetCredentialRequest
import com.google.android.libraries.identity.googleid.GetGoogleIdOption
import com.google.android.libraries.identity.googleid.GetSignInWithGoogleOption
import com.google.android.libraries.identity.googleid.GoogleIdTokenParsingException
import java.security.SecureRandom
import java.util.Base64
import kotlinx.coroutines.CoroutineScope
import androidx.compose.runtime.LaunchedEffect
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
class MainActivity : ComponentActivity() {
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//replace with your own web client ID from Google Cloud Console
val webClientId = "YOUR_CLIENT_ID_HERE"
setContent {
//ExampleTheme - this is derived from the name of the project not any added library
//e.g. if this project was named "Testing" it would be generated as TestingTheme
ExampleTheme {
Surface(
modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background,
) {
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
//This will trigger on launch
BottomSheet(webClientId)
//This requires the user to press the button
ButtonUI(webClientId)
}
}
}
}
}
}
//This line is not needed for the project to build, but you will see errors if it is not present.
//This code will not work on Android versions < UpsideDownCake
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
@Composable
fun BottomSheet(webClientId: String) {
val context = LocalContext.current
// LaunchedEffect is used to run a suspend function when the composable is first launched.
LaunchedEffect(Unit) {
// Create a Google ID option with filtering by authorized accounts enabled.
val googleIdOption: GetGoogleIdOption = GetGoogleIdOption.Builder()
.setFilterByAuthorizedAccounts(true)
.setServerClientId(webClientId)
.setNonce(generateSecureRandomNonce())
.build()
// Create a credential request with the Google ID option.
val request: GetCredentialRequest = GetCredentialRequest.Builder()
.addCredentialOption(googleIdOption)
.build()
// Attempt to sign in with the created request using an authorized account
val e = signIn(request, context)
// If the sign-in fails with NoCredentialException, there are no authorized accounts.
// In this case, we attempt to sign in again with filtering disabled.
if (e is NoCredentialException) {
val googleIdOptionFalse: GetGoogleIdOption = GetGoogleIdOption.Builder()
.setFilterByAuthorizedAccounts(false)
.setServerClientId(webClientId)
.setNonce(generateSecureRandomNonce())
.build()
val requestFalse: GetCredentialRequest = GetCredentialRequest.Builder()
.addCredentialOption(googleIdOptionFalse)
.build()
signIn(requestFalse, context)
}
}
}
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
@Composable
fun ButtonUI(webClientId: String) {
val context = LocalContext.current
val coroutineScope = rememberCoroutineScope()
val onClick: () -> Unit = {
val signInWithGoogleOption: GetSignInWithGoogleOption = GetSignInWithGoogleOption
.Builder(serverClientId = webClientId)
.setNonce(generateSecureRandomNonce())
.build()
val request: GetCredentialRequest = GetCredentialRequest.Builder()
.addCredentialOption(signInWithGoogleOption)
.build()
signIn(coroutineScope, request, context)
}
Image(
painter = painterResource(id = R.drawable.siwg_button),
contentDescription = "",
modifier = Modifier
.fillMaxSize()
.clickable(enabled = true, onClick = onClick)
)
}
fun generateSecureRandomNonce(byteLength: Int = 32): String {
val randomBytes = ByteArray(byteLength)
SecureRandom.getInstanceStrong().nextBytes(randomBytes)
return Base64.getUrlEncoder().withoutPadding().encodeToString(randomBytes)
}
//This code will not work on Android versions < UPSIDE_DOWN_CAKE when GetCredentialException is
//is thrown.
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
suspend fun signIn(request: GetCredentialRequest, context: Context): Exception? {
val credentialManager = CredentialManager.create(context)
val failureMessage = "Sign in failed!"
var e: Exception? = null
//using delay() here helps prevent NoCredentialException when the BottomSheet Flow is triggered
//on the initial running of our app
delay(250)
try {
// The getCredential is called to request a credential from Credential Manager.
val result = credentialManager.getCredential(
request = request,
context = context,
)
Log.i(TAG, result.toString())
Toast.makeText(context, "Sign in successful!", Toast.LENGTH_SHORT).show()
Log.i(TAG, "(☞゚ヮ゚)☞ Sign in Successful! ☜(゚ヮ゚☜)")
} catch (e: GetCredentialException) {
Toast.makeText(context, failureMessage, Toast.LENGTH_SHORT).show()
Log.e(TAG, failureMessage + ": Failure getting credentials", e)
} catch (e: GoogleIdTokenParsingException) {
Toast.makeText(context, failureMessage, Toast.LENGTH_SHORT).show()
Log.e(TAG, failureMessage + ": Issue with parsing received GoogleIdToken", e)
} catch (e: NoCredentialException) {
Toast.makeText(context, failureMessage, Toast.LENGTH_SHORT).show()
Log.e(TAG, failureMessage + ": No credentials found", e)
return e
} catch (e: GetCredentialCustomException) {
Toast.makeText(context, failureMessage, Toast.LENGTH_SHORT).show()
Log.e(TAG, failureMessage + ": Issue with custom credential request", e)
} catch (e: GetCredentialCancellationException) {
Toast.makeText(context, ": Sign-in cancelled", Toast.LENGTH_SHORT).show()
Log.e(TAG, failureMessage + ": Sign-in was cancelled", e)
}
return e
}