الصفحة الرئيسية/ التقنية / برنامج دردشة : خطوات بناء تطبيق محادثة كود جافا

برنامج دردشة : خطوات بناء تطبيق محادثة كود جافا


for Large تمت الكتابة بواسطة : محمد



برنامج دردشة هو منطقة من عالم الإنترنت تتيح للمستخدمين إبداء التفاعل فيما بينهم. وذلك من خلال مشاركة المحادثات والصور والفيديو وكافة الوسائط الممكنة. حيث يمتلك برنامج دردشه الهاتف حصة واهتمام كبيرين من قبل متجر جوجل بلاي وسامسونج وهواوي.

بالتالي سنحاول توفير كافة مصادر الشيفرة التي يعجز عنها كل من يريد بناء تطبيقات محادثة. والسبب في ذلك تحديثات جوجل المستمرة. والتي تعيق المبرمجين من العثور على شيفرة صالحة.

فغالبا ما يقوم المطورون بشراء الكثير من المشاريع المفتوحة دون إدراك منهم وقدرة على التطوير. في هذه الدورة ستتمكن من بناء تطبيقك الأول بكامل الاحترافية عبر أنظمة أندرويد.

بالتالي سوف نعطيك كافة المستندات والكود البرمجي الذي يصعب الحصول عليه. لا تتقاضى هذه الدورة رسوما جراء بناء تطبيق دردشة. وإنما تتطلب منك قدرات عالية من التركيز. و سنضمن لك العثور على تطبيق يعمل في حال قمت بتطبيق الشيفرة الموجودة في المرفقات.  

تحتاج إلى إلغاء المساحات والفراغات التي أجريت على العديد من وظائف استدعاء المكتبات والمصادر الخارجية وأيضا المتغيرات والوظائف , وذلك لكي تعمل الشيفرة.

 

 

جدول المحتويات

  1. متطلبات برنامج دردشه
  2.  
  3. بناء برنامج دردشة Android
    1. 1- تجهيز صفحة المشروع
    2. 2- تفعيل قواعد البيانات Firebase
      1. إضافة قواعد البيانات
      2. تصدير ملفات JSON
  4. 3-إجراء تكامل المشروع
  5. 4- بناء صفحات التطبيق الأولى
    1. صفحة تسجيل الدخول
      1. تحديث المصادر
      2. حل مشكلة Multidex
      3. التعديل في صفحة gradle.properties
    2. الصفحة الرئيسية
    3. صفحة انشاء حساب جديد
      1. إصلاح مشكلة الحسابات
    4. عمل Sign-in و Sign-out
      1. عمل قائمة
      2. إبقاء الحساب متصل
      3. إنشاء Package مع User Class
      4. إظهار الحساب في الشريط العلوي
    5. عمل Fragment وتقسيم الصفحات
      1. آلية عمل الـ Fragments
      2. عمل صفحات Fragment
      3. تحضير أيقونات الأقسام
      4. إضافة وحدة التحكم الخاصة بـ Fragments
    6. جلب الحسابات من قواعد البيانات
    7. بناء واجهات الرسائل
      1. توجيه إلى صفحات المستخدمين
      2.  
      3. تحديث صفحة الألوان
    8. إضافة Drawable
    9. عرض الرسائل في الحسابات
    10.  
      1. تعديلات على Message Activity
      2.  
    11. عرض المحادثات التي تمت
    12.  
    13. تحضير صفحة الحساب
    14.  
      1. تمكين صلاحيات التخزين
      2. تغيير صورة الحساب
      3.  
    15. تفعيل الظهور كمتصل
    16.  
      1. التعديل في user item
      2.  
      3. إضافة عنصر حالة الإتصال
      4. إجراء تعديل على شيفرة User Adapter
      5.  
    17. تفعيل خاصية مشاهدة الرسائل
    18.  
    19. نسيت كلمة المرور
    20.  
      1. التوجيه لصفحة الحماية
      2.  
      3. تحديث صفحة الحماية مع قواعد البيانات
    21. إدارة مركز الإشعارات
    22.  
    23. ما هو FCM؟
      1. اختبار محاولة الإتصال
      2. تمكين شيفرة الإتصال
      3. إرفاق FCM عند تفعيل الحساب
      4. تفعيل مكتبة Retrofit
      5. إضافة المكونات
      6. إضافة صفحة Constant
      7. الوراثة من FirebaseMessagingService
      8. طلب الدوال والكائنات في صفحة Message Activity
      9.  
      10. التعديل في Manifest
  6. كيف بنيت هذا التطبيق؟
    1. المراجع

 

متطلبات برنامج دردشه

  1. الإلمام بأساسيات أنظمة الهواتف الذكية.
  2. إتقان لغة جافا ومحررات XML.
  3. امتلاك حاسوب قوي لتشغيل الأجهزة الافتراضية.
  4. مهارة في تصحيح الكود وإجراء التعديلات.
  5. الإلمام بقواعد البيانات وجلبها.
  6. محرر تشغيل أندرويد ستوديو.
  7. مهارات أساسية في  Firebase database.

 

في حال وجدت صعوبة في تكوين الأجهزة الإفتراضية في Android Studio. فيمكنك توصيل جهازين أندرويد في حاسوبك عبر وصلات USB الخاصة بها. حيث أن ذلك يأخذ عن عاتق الذاكرة الكثير من المهام. نبدأ على بركة الله.  

ملاحظة : لا يقدم مقرر برنامج دردشه أساسيات في أنظمة Android. يمكنك الإستمرار في القراءة وسنوفر لك دورات أخرى في وقت لاحق.

 

بناء برنامج دردشة Android

نبدأ ببناء البرنامج بعد تحقيق كافة المتطلبات السابقة. وهو برنامج دردشة بلغة جافا يحتوي على صفحات تسجيل الدخول وتفعيل الحساب. وأيضا ثلاثة تصنيفات. ستحتوي التصنيفات على آخر المحادثات وقائمة المستخدمين الغرباء وصفحة شخصية للمستخدم.  

1- تجهيز صفحة المشروع

بعد التأكد من نسخة android studio. سنعمل على إضافة مشروع جديد واختيار Empty Project كما في الصورة التالية. بعد ذلك نقوم بتسمية المشروع بالإسم الذي نريده وليكن Free Chat App.

نلاحظ أن المشروع يتوافق مع API 21 كحد أدنى. على سبيل المثال , فإن هذا الإصدار من التطبيق سيعمل على 98.8% من أجهزة أندرويد حول العالم.

بالتالي سنترك الخيارات كما هي ونضغط على Finish ليتم تكوين ملفات المشروع لأول مرة. قد يستغرق ذلك بعض الوقت. حيث يعتمد على مدى سرعة حاسوبك. وفي حال وجدت بطئ شديد في تكوين المشروع ننصحك بتغيير القرص الصلب إلى تقنية SSD واستئناف الدورة في وقت لاحق.

  من الطبيعي ظهور الصفحة الأولى بهذا الشكل في المشروع. بالتالي فإن كائن AppCompatActivity هو أحد الكائنات الرئيسية في عمل التطبيقات. لذلك لا تكترث للأمر في حال لم تشاهد هذه الصفحة من قبل.    

2- تفعيل قواعد البيانات Firebase

نعتبر أن قواعد البيانات هي المطلب الرئيسي عند بناء برنامج دردشة. فلا يهم نوعها بقدر ما توفره من جمل الاستعلام واضافة وحذف . البعض يقدم على استخدام Mysql أو حتى mongodb. وفي حالتنا سنستخدم Firebase database.

حيث بذلك توحيد قياسي للشيفرة الخاصة بنا.   يجب الدخول إلى Firebase Console. وهي احدى منتجات جوجل العريقة.

 بالتالي نقم بالضغط على Get started ومن ثم Add project. تظهر لنا الصورة التالية. ونقوم بتسمية المشروع بنفس الإسم وهو Free Chat App. ومن ثم الضغط على Continue.   قم بالضغط على Continue عند ظهور الصفحة التالية.

ومن ثم نبقي على احصائيات Google Analytics بوضعية التفعيل. وفي حال ظهور اختيار الحساب. نقم باختيار حساب جوجل الأساسي الذي يمكننا من رؤية هذه المنتجات. بالتالي تظهر لنا صفحة استكمال المشروع بهذا الشكل.  

إضافة قواعد البيانات

ستأخذنا الصفحة إلى قائمة المهام المتواجدة في Firebase. على سبيل المثال , فإن لكل برنامج دردشه قاعدة بيانات يتم تخزينها لحين الرجوع لها من المستخدمين. ولذلك من قائمة Build نختار Realtime Database. الآن وبعد أن عثرنا على قاعدة البيانات.

انقر بالفأرة على Realtime Database ومن خلالها يتم فتح صفحة على الجزء الأيمن. سنجد في داخلها Create Database وسنقوم بالضغط عليها. نحن على مشارف الانتهاء من إضافة قاعدة البيانات.

بالتالي نقم بالضغط على Create Database. على سبيل المثال ستظهر صفحات مرادفة الآن ومنها تحديد مكان مراكز البيانات وهي أمريكا أو بلجيكا أو سنغافورة.

بالتالي نبقى على الخيار الافتراضي ونضغط على Next.   على سبيل المثال. فإن Firebase بمثابة خدمة سحابية للإحتفاظ بالبيانات كما لو أنك قمت باستئجار خادم لتلك المهمة. في هذه الحالة نحن نقوم بضبط إعدادات الخادم الخاصة بنا.

بالتالي يشمل ذلك جوانب عديدة منها مركز البيانات وطبيعة المهام في برنامج دردشة. ستظهر لنا شاشة تفيد بقفل أو فتح قواعد البيانات. وبذلك نختار منها وضع الفحص. حيث يسمح للمطور بالقراءة والكتابة في قواعد البيانات.

الصورة التالي تبين ذلك جيدا. بعد تمكين قواعد البيانات في المشروع. يتعين علينا استخراج ملفات Json.  

تصدير ملفات JSON

ملفات Json هي تخويل للمشروع الحقيقي والمستخدمين. فهي ترسم الخريطة اللازمة في جلب البيانات. بالتالي هي عملية ربط عبر Package name في ملفات المشروع. ومن الصفحة الرئيسية في الكونسول نقم باختيار الأيقونة .  

وعند ظهور شاشة التفعيل نقم بلصق Package Name الخاصة بمشروع Free App Chat في Android studio. وهي com.example.free chat app.

نضغط على Register App . وبعدها يتم تحميل ملفات المشروع كما في الصورة التالية. لن ينجح تكامل المشروع دون وجود هذا الملف. بالتالي وعند تحميله في حاسوبك ستقوم بعمل لصق ونسخ في مشروع Free Chat App.

ولكن حذاري من نسخه في مكان خاطئ. تبين الصورة التالية مكان نسخ الملف. كما تلاحظ. فإننا نشير إلى مكان تواجد الملف في اللون الأصفر. وتستطيع فتح دليل الملفات من خلال اختيار Project بدلا من Android في الأعلى. بالتالي يتبقى لدينا الآن عمل تكامل للمشروع مع قواعد البيانات السحابية.  

3-إجراء تكامل المشروع

الآن وبعد أن انتهينا من نسخ ملف JSON. يتبقى لنا إضافة بعض المصادر والمكتبات التي ستحتاجها في الشيفرة. وهي ملفات Firebase التي من خلالها تتيح الاتصال السحابي مع جوجل. لدى جوجل الكثير من عمليات الربط. لكنها تخضع للكثير من التغييرات المستمرة.

ولذلك يتعين علينا اجراء الاتصال بطريقة مختلفة وخاصة للمشاريع التي تدعم SDK 31. بالتالي نقم بالذهاب إلى صفحة build.gradle(module)ومن ثم جعل دالة Plugin تبدو كما في الشيفرة التالية.  

plugins {
    id 'com.android.application'
    //Firebase
    id 'com.google.gms.google-services'
}

  وفي ملف build.gradle (project) نقم باضافة الملفات التالية:

buildscript {
    dependencies {
        classpath 'com.google.gms:google-services:4.3.13'
    }
}

وفي الاسفل نقم باضافة السطور التالية في قائمة dependencies:

//Firebase
   implementation 'com.google.firebase:firebase-auth:21.0.6'
   implementation 'com.google.firebase:firebase-database:20.0.5'
   implementation 'com.google.firebase:firebase-core:21.1.0'
   implementation 'com.google.firebase:firebase-storage:20.0.1'
   implementation 'com.google.firebase:firebase-messaging:23.0.6'

  ومن ثم نقم بعمل Sync للمشروع لإجراء التعديل. بالتالي تظهر عملية بناء ناجحة كما في الصورة التالية:   نحن على مشارف الانتهاء من عملية التكامل والبدء في وضع شيفرة التطبيق. لكن يتبقى لدينا مرحلة أخيرة وهي إتمام عملية الربط.

وذلك من خلال الذهاب إلى Tools و اختيار Firebase في برنامج Android Studio. ستظهر لائحة على اليمين. بالتالي سنبحث على Realtime Database ونقوم باختيار Get started with realtime database.

  على سبيل المثال , وعند ظهور لائحة التفعيل. فإن البرنامج سيبدأ بمزامنة عملية الاتصال. وعند الضغط على Build ستبدأ عملية التوثيق.   قد تستغرق عملية المزامنة بعض الوقت.

بالتالي بعد الانتهاء من البناء يمكنك التأكد من نجاح العملية بإعادة تشغيل المشروع. وذلك عن طريق File->Invalidate Cache.

وذلك لتنظيف ذاكرة البرنامج جيدًا. وعند التحقق من جديد ستجد أن المشروع تم مزامنته بنجاح. كما يظهر في الصورة التالية:   قد يطلب برنامج دردشة العديد من عمليات المزامنة. بالتالي سيتم التطرق لها من خلال نفس المشروع لاحقًا.  

4- بناء صفحات التطبيق الأولى

بعد أن تحققنا من نجاح مزامنة المشروع. يتوجب علينا الآن التفكير مليًا في صفحات التطبيق لكي تسهل عملية البناء. حيث أن كل برنامج دردشة يحتوي على صفحة تسجيل الدخول.

صفحة إنشاء حساب وصفحة التمرير بين كل Activity. يخضع برنامج دردشة الهاتف إلى توافق وانسجام مع أنظمته الداخلية. بالتالي تختلف عن صفحات الويب. لكنها تتفق ببعض الأساسيات مثل القوائم والصفحات المنسدلة ونظامي الإشعارات والمحادثات الوقتية.

سنركز في هذه الدورة على أساسيات البناء ومن ثم سيسهل على كل مطور الإضافة والتعديل في الشيفرة كما يحلو له.   وبذلك , سنعتمد على الطرق الأساسية في بناء الصفحات. بالتالي يسهل إنشاء الصفحات الفارغة. وبهذا الدور نستطيع تشكيل التطبيق كما يحلو لنا.

سنعمل على تكرار الشيفرة مع التحديثات الجديدة في كل قسم. على سبيل المثال , سيتم ذكر صفحة Main Activity أكثر من مرة وفي كل تحديث يتم تطبيقه عليها.  

صفحة تسجيل الدخول

عند بناء برنامج دردشة , لا بد من وجود صفحة تسجيل الدخول. فهي بدورها تمكن المستخدم من الوصول إلى بياناته في قواعد البيانات ليتم معرفة من هو المرسل والمستقبل. والآن نقوم بإضافة Empty Activity باسم LoginActivity.

بعد إضافتها ستجد أن كل Activity يتم اضافتها يتم ربطها بصفحة XML. وذلك طبيعي جدا. وذلك ليتم الوصول إلى عناصر الصفحة وإدارتها من قبل الشيفرة. ويتم الوصول لصفحات XML من خلال res->layout.   LoginActivity.java

package com.example.free chat app;
import androidx.annotation.NonNull;

import android x . appcompat . widget .Toolbar;
import android.content.Intent;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;
import com.google.android.gms.tasks.OnCompleteListener;
import com.google.android.gms.tasks.Task;
import com.google.firebase.auth.AuthResult;
import com.google.firebase.auth.FirebaseAuth;
import com. reng wuxian .material edittext.Material EditText;
public class LoginActivity extends AppCompatActivity {
    MaterialEditText email , password;
    Button btn_login;
    FirebaseAuth auth;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_login);
        Toolbar toolbar = findViewById(R.id.toolBar);
        //setSupportActionBar(toolbar);
        getSupportActionBar().setTitle("Login");
        getSupportActionBar().setDisplayHomeAsUpEnabled(true);
        auth = FirebaseAuth.getInstance();
        email = findViewById(R.id.email);
        password = findViewById(R.id.password);
        btn_login = findViewById(R.id.btn_login);
        btn_login.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                String txt_email = email.getText().toString();
                String txt_password = password.getText().toString();
                if(TextUtils.isEmpty(txt_email) || TextUtils.isEmpty(txt_password)){
                    Toast.makeText(LoginActivity.this, "All Fields Are Required!", Toast.LENGTH_SHORT).show();
                }else{
                    auth.signInWithEmailAndPassword(txt_email , txt_password)
                            .addOnCompleteListener(new OnCompleteListener<AuthResult>() {
                                @Override
                                public void onComplete(@NonNull Task<AuthResult> task) {
                                    if(task.isSuccessful()){
                                        Intent intent = new Intent(LoginActivity.this , MainActivity.class);
                                        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
                                        startActivity(intent);
                                        finish();
                                    }else{
                                        Toast.makeText(LoginActivity.this, "task: " + task.getException(), Toast.LENGTH_SHORT).show();
                                    }
                                }
                            });
                }
            }
        });
    }
}

  activity login.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".LoginActivity">
    <include
        android:id="@+id/toolBar"
        layout="@layout/bar_layout"/>
    <LinearLayout
        android:layout_width="match_parent"
        android:orientation="vertical"
        android:gravity="center_horizontal"
        android:layout_below="@+id/toolBar"
        android:padding="16dp"
        android:layout_height="wrap_content">
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Login"
            android:textSize="20sp"
            android:textStyle="bold"/>
        <com. reng wuxian.material edit text.Material EditText
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:id="@+id/email"
            android:inputType="textEmailAddress"
            android:layout_marginTop="10dp"
            app:met_floatingLabel="normal"
            android:hint="@string/email"/>
        <com. reng wuxian .material edit text.Material EditText
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:id="@+id/password"
            android:inputType="textPassword"
            android:layout_marginTop="10dp"
            app:met_floatingLabel="normal"
            android:hint="@string/password"/>
        <Button
            android:id="@+id/btn_login"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@string/login"
            android:layout_marginTop="10dp"
            android:background="@color/colorPrimaryDark"
            android:textColor="#FFF"/>
    </LinearLayout>
</RelativeLayout>

في صفحة XML. تستطيع تجاوز الأخطاء من خلال الصورة التالية:

تحديث المصادر

ربما تظهر مجموعة من الأخطاء , مثل فقدان بعض المكتبات المتعلقة بحقول الإدخال. ولتجاوز ذلك قم بإضافة المصادر التالية في قسم build.gradle(module). ليصبح شكل dependencies كالتالي:

implementation ' android x .appcompat:appcompat:1.4.2'
implementation 'com.google.android.material:material:1.6.1'
implementation ' android x .constraintlayout:constraintlayout:2.1.4'
//firebase
implementation ' android x .legacy:legacy-support-v4:1.0.0'
implementation 'com.google.android.material:material:1.0.0'
implementation 'com.google.firebase:firebase-auth:21.0.6'
implementation 'com.google.firebase:firebase-database:20.0.5'
implementation 'com.google.firebase:firebase-core:21.1.0'
implementation ' android x .cardview:cardview:1.0.0'
implementation 'com. reng wuxian.material edit text:library:2.1.4'
implementation ' android x.annotation:annotation:1.4.0'
implementation 'android x.lifecycle:lifecycle-livedata-ktx:2.3.1'
implementation 'android x.lifecycle:lifecycle-viewmodel-ktx:2.3.1'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation ' android x.test.ext:junit:1.1.3'
androidTestImplementation ' android x.test.espresso:espresso-core:3.4.0'

  القسم الخاص في Layout. نعمل على إضافة Layout Resource File بعنوان bar layout. وذلك لتضمينه في صفحات xml.

<?xml version="1.0" encoding="utf-8"?>
<android x .appcompat.widget.Toolbar xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:id="@+id/toolBar"
    android:background="@color/colorPrimaryDark">
</android x .appcompat.widget.Toolbar >

في حال استمرار ظهور أخطاء في التصميم. قم بالذهاب إلى string.xml وانسخ الشيفرة التالية:

<resources>
    <string name="app_name">Free Chat App</string>
    <string name="email">Email</string>
    <string name="password">Password</string>
    <string name="login">Login</string>
</resources>

على سبيل المثال , وفي حال وجدت مشاكل في عرض الألوان والخلفيات , من المستحسن إجراء تخصيص يدوي على قائمة الألوان color.xml. او تستطيع نسخ الشيفرة التالية بها لتصبح:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="purple_200">#FFBB86FC</color>
    <color name="purple_500">#726C7C</color>
    <color name="purple_700">#D1CAE3</color>
    <color name="teal_200">#FF03DAC5</color>
    <color name="teal_700">#FF018786</color>
    <color name="black">#FF000000</color>
    <color name="white">#FFFFFFFF</color>
    <color name="colorPrimaryDark">#226e6e</color>
    <color name="colorPrimary"></color>
    <color name="complexRed"></color>
</resources>

وبعد أن انتهينا من موالفة المشروع. تستطيع القيام بتنظيفه من خلال Build->Clean Project ومن ثم بنائه. ولكن تأكد في حال العثور على أخطاء تستطيع التتبع من صفحة log بالأسفل.

بالتالي فإن أي برنامج دردشة قد لا يخلو من الصفحة التالية: ستظهر لك العديد من الأخطاء أثناء عملية تشغيل البرنامج. على سبيل المثال , مسائل تتعلق بـ Multidex. ويجب التعديل في صفحتين من بناء المشروع. الأولى هي gradle.build(module) والثانية هي gradle.properties.

حل مشكلة Multidex

نستطيع الذهاب إلى صفحة gradle.build(module) واضافة :

defaultConfig {
   ....
    multiDexEnabled true
   ....
}

وفي خانة dependencies نقوم بإضافة:

dependencies {
   .....
    implementation 'com.android.support:multidex:2.0.1'
}

 

التعديل في صفحة gradle.properties

نقم بإضافة التالي:

....
android.enableJetifier=true

  وفي حال قمت ببناء المشروع فإنه سيعمل ولكن سيذهب بك إلى صفحة Main Activity. وذلك بسبب تعيينها صفحة أساسية في Android Manifest. ولكي نتجاوز تلك المشكلة سنجري بعض التعديلات فيها.

على سبيل المثال, فإن أي برنامج دردشة يتولى مهام أكثر من صفحة. حيث يقوم المبرمج بتعيين واحدة منها بشكل أساسي خاصة عند لحظات الدخول الأولى. نلاحظ هنا وجود صفحتين في برنامج دردشة.

وتحتوي كل واحدة منها على إعدادات في هذا الملف. بالتالي تستطيع جعل احداها أساسية بمجرد عكس القيم من true إلى false مع إضافة العنصر android:parentActivityName=".LoginActivity".

وفي حال قد قررنا جعل Login Activity أساسية سنقوم بذلك الأمر ونحاول تشغيل البرنامج. ولكن ندعك تلق نظرة على Manifest قبل عرض صورة التطبيق.  

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    package="com.example.free chat app">
    <application
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.FreeChatApp"
        tools:targetApi="31">
        <activity
            android:name=".LoginActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity
            android:name=".MainActivity"
            android:exported="false"
            android:parentActivityName=".LoginActivity"/>
    </application>
</manifest>

  نلاحظ أن الوصول المباشر لصفحة تسجيل الدخول ليس منطقيا على الإطلاق. على سبيل المثال , نريد إحاطة المستخدم بخيارات التسجيل أو نسيان كلمة المرور أو غيرها. لذا سنعمل على إضافة صفحة StartActivity.java. وجعلها أساسية من Manifest.  

الصفحة الرئيسية

بالحديث عن صفحة Main Activity. فهي تعتبر أساسية لغالبية التطبيقات. لكننا نخصصها لأمور أخرى في هذه الدورة. ونستبدلها بصفحة رئيسية يتم من خلالها جمع الكثير من العناصر في تطبيقنا برنامج دردشه. StartActivity.java

package com.example.free chat app;
import  android x .appcompat.app.AppCompatActivity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
public class StartActivity extends AppCompatActivity {
    Button login , register;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_start);
        login = findViewById(R.id.login);
        register = findViewById(R.id.register);
        login.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                startActivity(new Intent(StartActivity.this , LoginActivity.class));
            }
        });
        register.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
               // startActivity(new Intent(StartActivity.this , RegisterActivity.class));
            }
        });
    }
}

  activity_start.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:gravity="center"
    android:orientation="vertical"
    android:layout_height="match_parent"
    tools:context=".StartActivity">
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/app_name"
        android:textSize="25sp"
        android:textStyle="bold"/>
    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="100dp"
        android:id="@+id/login"
        android:text="Login"/>
    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/register"
        android:text="Register"/>
</LinearLayout>

ولا داعي للقلق في صفحة AndroidManifest.xml. حيث سنقوم بتعديل الشيفرة لتبدو بالشكل التالي:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    package="com.example.free chat app">
    <application
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.FreeChatApp"
        tools:targetApi="31">
        <activity
            android:name=".StartActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity
            android:name=".LoginActivity"
            android:exported="false"
            android:parentActivityName=".LoginActivity"/>
        <activity
            android:name=".MainActivity"
            android:exported="false"
            android:parentActivityName=".LoginActivity" />
    </application>
</manifest>

بالتالي وعند تشغيل برنامج دردشة ستظهر الصفحة الأولى. على سبيل المثال , فقد تدعم هذه الشاشة خيارات. عملية توجيه لصفحتين رئيسيتين في التطبيق. وهما صفحة تسجيل الدخول لصفحة انشاء حساب جديد. ولذلك سنعمل على إضافة صفحة انشاء حساب جديد.  

صفحة انشاء حساب جديد

تحتوي صفحة انشاء حساب على عمليات عدة, مثل اسم المستخدم وكلمة المرور. وتستطيع إضافة العديد من الخيارات الأخرى. على سبيل المثال , ستتمكن من إضافة الدولة ورقم الهاتف والصورة الشخصية. وفي دورتنا سنعمل على البعض منها. RegisterActivity.java

package com.example.free chat app;
import androidx.annotation.NonNull;
import android x .appcompat.app.AppCompatActivity;
import android x .appcompat.widget.Toolbar;
import android.content.Intent;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;
import com.google.android.gms.tasks.OnCompleteListener;
import com.google.android.gms.tasks.Task;
import com.google.firebase.auth.AuthResult;
import com.google.firebase.auth.FirebaseAuth;
import com.google.firebase.auth.FirebaseUser;
import com.google.firebase.database.DatabaseReference;
import com.google.firebase.database.FirebaseDatabase;
import com. reng wuxian.material edit text.Material Edit Text;
import java.util.HashMap;
public class RegisterActivity extends AppCompatActivity {
    MaterialEditText username , email , password;
    Button btn_register;
    FirebaseAuth auth;
    DatabaseReference reference;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_register);
        Toolbar toolbar = findViewById(R.id.toolBar);
        //setSupportActionBar(toolbar);
        getSupportActionBar().setTitle("Register");
        getSupportActionBar().setDisplayHomeAsUpEnabled(true);
        username = findViewById(R.id.username);
        email = findViewById(R.id.email);
        password = findViewById(R.id.password);
        btn_register = findViewById(R.id.btn_register);
        auth = FirebaseAuth.getInstance();
        btn_register.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                String txt_username = username.getText().toString();
                String txt_email = email.getText().toString();
                String txt_password = password.getText().toString();
                //Check if values is empty
                if(TextUtils.isEmpty(txt_username) || TextUtils.isEmpty(txt_email) || TextUtils.isEmpty(txt_password)){
                    Toast.makeText(RegisterActivity.this, "All field are required!", Toast.LENGTH_SHORT).show();
                }else if(txt_password.length() < 6){
                    Toast.makeText(RegisterActivity.this, "Password must be longer!", Toast.LENGTH_SHORT).show();
                }
                else{
                    //Here we will register new account
                    register(txt_username , txt_email , txt_password);
                }
            }
        });
    }
    //Connect to firebase database and register new account
    private void register(String username , String email , String password){
        auth.createUserWithEmailAndPassword(email , password)
                .addOnCompleteListener(new OnCompleteListener<AuthResult>() {
                    @Override
                    public void onComplete(@NonNull Task<AuthResult> task) {
                        if(task.isSuccessful()){
                            FirebaseUser firebaseUser = auth.getCurrentUser();
                            assert firebaseUser != null;
                            String userId = firebaseUser.getUid();
                            reference = FirebaseDatabase.getInstance().getReference("Users").child(userId);
                            HashMap<String , String> hashMap = new HashMap<>();
                            hashMap.put("id" , userId);
                            hashMap.put("username" , username);
                            hashMap.put("imageUrl" , "default");
                            reference.setValue(hashMap).addOnCompleteListener(new OnCompleteListener<Void>() {
                                @Override
                                public void onComplete(@NonNull Task<Void> task) {
                                    if(task.isSuccessful()){
                                        Intent intent = new Intent(RegisterActivity.this , MainActivity.class);
                                        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
                                        startActivity(intent);
                                        finish();
                                    }
                                }
                            });
                        } else{
                            Toast.makeText(RegisterActivity.this, "You can't register with this mail", Toast.LENGTH_SHORT).show();
                        }
                    }
                });
    }
}

عند الضغط على btn register. سنجبر المستخدم على ملئ الحقول كاملة وذلك من خلال الصف TextUtils. وهي أداة مخصصة لتعقب القيم داخل الحقول. بالتالي وعند عدم ملئ الحقول بشكل صحيح سيتم رفض الاتصال وإظهار رسالة خطأ.

وتقوم الدالة register على تسجيل معلومات المستخدم الجديد وذلك من خلال FirebaseAuth. وهي متوفر من مكتبة Firebase. ولا شك أن من إحدى كائناتها الدالة createUserWithEmailAndPassword. وهي التي تقوم بتفعيل عمليات الاتصال بقواعد البيانات. وتختلف لغة التواصل مع الخادم في لغة جافا عن الكثير من اللغات.

على سبيل المثال , فإن لغة PHP لتطوير الويب تبدأ عمليات الاستعلام مباشرة من خلال mysql. بينما في حالتنا سنحتاج إلى ارفاق البيانات بواسطة Hashmap. ناهيك عن بعض أجزاء قادمة في الدورة والتي تحضر القيم باستخدام الصفوف Classes.

وبعد قبول عملية التسجيل سيتم التحويل تلقائيا إلى صفحة MainActivity.class. بالتالي ستجد فيها عبارة Hello World. حيث أننا بذلك لم نجري عليها أية تغييرات بعد.

نريد التنويه أن MainActivity ستكن الصفحة الأولى عند تسجيل الدخول في تطبيق برنامج دردشة.   activity_register.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".RegisterActivity">
    <include
        android:id="@+id/toolBar"
        layout="@layout/bar_layout"/>
    <LinearLayout
        android:layout_width="match_parent"
        android:orientation="vertical"
        android:gravity="center_horizontal"
        android:layout_below="@+id/toolBar"
        android:padding="16dp"
        android:layout_height="wrap_content">
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/create_a_new_account"
            android:textSize="20sp"
            android:textStyle="bold"/>
        <com. reng wuxian .material edit text.Material Edit Text
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:id="@+id/username"
            android:layout_marginTop="10dp"
            app:met_floatingLabel="normal"
            android:hint="@string/username"/>
        <com. reng wuxian .material edit text.Material Edit Text
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:id="@+id/email"
            android:inputType="textEmailAddress"
            android:layout_marginTop="10dp"
            app:met_floatingLabel="normal"
            android:hint="@string/email"/>
        <com. reng wuxian .material edit text.Material Edit Text
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:id="@+id/password"
            android:inputType="textPassword"
            android:layout_marginTop="10dp"
            app:met_floatingLabel="normal"
            android:hint="@string/password"/>
        <Button
            android:id="@+id/btn_register"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@string/register"
            android:layout_marginTop="10dp"
            android:background="@color/colorPrimaryDark"
            android:textColor="#FFF"/>
    </LinearLayout>
</RelativeLayout>

تتكون صفحة تسجيل الدخول من الإسم والبريد الإلكتروني وكلمة المرور. وهي إحدى أساسيات بناء الحسابات. وفي صفحة StartActivity.java. سنعمل على تفعيل رابط التوجيه إلى صفحة Register بدلاً من تعليقه. ليصبح الكود بالشكل التالي.   StartActivity.java

package com.example.free chat app;
import android x .appcompat.app.AppCompatActivity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
public class StartActivity extends AppCompatActivity {
    Button login , register;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_start);
        login = findViewById(R.id.login);
        register = findViewById(R.id.register);
        login.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                startActivity(new Intent(StartActivity.this , LoginActivity.class));
            }
        });
        register.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                startActivity(new Intent(StartActivity.this , RegisterActivity.class));
            }
        });
    }
}

عند تشغيل وفحص البرنامج ستجد أن رسالة وهي : You can't register with this email. وهي مشكلة لها سببين:

  • وجود حساب سابق في Firebase.
  • عدم تفعيل خدمة Authentication في لوحة مهام الكونسول.

إصلاح مشكلة الحسابات

ولذلك نعاود الرجوع إلى صفحة console.firebase.google.com.  ونقوم باختيار Build->Authentication. ومن الصفحة في الوسط نضغط على Set Up Sign-in method. ومن ثم نختار منها Email/Password. تماما كما في الصورة التالية.   بعد ذلك قم بتمكين الخيار بوضعية Enable. ومن ثم الضغط على عملية حفظ Save. بالتالي ستتمكن من تفعيل حسابك الخاص لتنجح عملية الدخول .

  لقد نجحت عملية إدخال إسم المستخدم. وهي إحدى أهم ما يمكن توثيقه عند بناء تطبيق برنامج دردشة. ونستكمل في هذه الدورة إضافة قائمة صغيرة مع إمكانية عمل Sign-in و Sign-out.  

عمل Sign-in و Sign-out

لدينا بعض الأعمال المهمة للقيام بها في هذا القسم. بالتالي ستتمكن الإلمام بجلسات الدخول لدى الهواتف المحمولة. وهي بيانات مؤقتة يتم تخزينها في ذاكرة الهاتف , وذلك في حال بقي المستخدم على تواصل مع إستخدام التطبيق.

لا تستخدم جلسات الذاكرة في تطبيقات برنامج دردشه فحسب. حيث أنها تساعد في برامج أخرى مثل محررات الصور والفيديو وحفظ الإعدادت في الذاكرة المؤقتة لحين الرجوع لها.  

عمل قائمة

يتعين علينا بداية عمل قائمة باسم menu. ويتم ذلك من خلال اضافة Android Resource File. ومن ثم اختيار نوع الملف menu كما في الصورة التالية.   بعد الانتهاء من إضافة القائمة.

تستطيع بناء الكثير من العناصر داخلها. وسنكتفي الآن ببناء عنصر واحد فقط وهو sign out. ليظهر الكود بالشكل التالي: menu.xml

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <item
        android:id="@+id/logout"
        android:title="@string/signout"
        app:showAsAction="never"/>
</menu>

على سبيل المثال , وبعد إتمام بناء تطبيق دردشة. تستطيع زيادة حجم القائمة مثل صفحات اتصل بنا أو معلومات حول البرنامج أو حتى الإعدادات. بالتالي فإن ذلك من شأنه قابلية التوسع في بناء المشاريع لاحقًا. إليك كود القائمة. والذي يجب نسخه في MainActivity.java:  

//****************************************************Menu Items Here************************************************************
   //This source for menu activation
   @Override
   public boolean onCreateOptionsMenu(Menu menu) {
       getMenuInflater().inflate(R.menu.menu , menu);
       return true;
   }
   @Override
   public boolean onOptionsItemSelected(@NonNull MenuItem item) {
       switch (item.getItemId()){
           case R.id.logout:
               FirebaseAuth.getInstance().signOut();  //Call database to create sighout task
               startActivity(new Intent(MainActivity.this , StartActivity.class));
               finish();
               return true;
       }
       return false;
   }
   //***********************************************************************************************************************************

سأبدأ في شرحها ثم سأترك لك صف Activity Main عند الانتهاء. تم الاتصال بالقائمة عبر متغير Boolean. ليخبر برنامج دردشه بوجود قائمة. وتم الوصول لها من خلال AppCompatActivity.

ومن خلال عملية switch. قمنا بإضافة حدث القائمة وهو sign out.

بالتالي تم الاتصال بمتغير ديناميكي ومباشر من قبل FirebaseAuth. ليتم بعدها التوقف عن عملية الاتصال والخروج من الجلسة. بالتالي وعند وضع الشيفرة في MainActivity.java. فإنها ستبدو تماما كالآتي:  

MainActivity.java

package com.example.free chat app;
import androidx.annotation.NonNull;
import android x .appcompat.app.AppCompatActivity;
import android.content.Intent;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import com.google.firebase.auth.FirebaseAuth;
public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
    //****************************************************Menu Items Here************************************************************
    //This source for menu activation
    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.menu , menu);
        return true;
    }
    @Override
    public boolean onOptionsItemSelected(@NonNull MenuItem item) {
        switch (item.getItemId()){
            case R.id.logout:
                FirebaseAuth.getInstance().signOut();  //Call database to create sighout task
                startActivity(new Intent(MainActivity.this , StartActivity.class));
                finish();
                return true;
        }
        return false;
    }
    //***********************************************************************************************************************************
}

 

إبقاء الحساب متصل

بعد أن قمنا بعمل إجراء sign-in و sign-out. يتعين علينا إبقاء الحساب متصلا. وذلك تجنبا لفرض إدخال اسم المستخدم وكلمة المرور في كل عملية دخول.

ولأن التطبيق هو برنامج دردشة فإن من أهم أولويات تلك البرامج هي الاحتفاظ بالجلسات مفتوحة. عند الذهاب إلى شيفرة StartActivity.java. قم بلصق الكود التالي مع تعريف المتغير FirebaseUser.

FirebaseUser firebaseUser;
    @Override
    protected void onStart() {
        super.onStart();
        firebaseUser = FirebaseAuth.getInstance().getCurrentUser();
        //Check if user is null or not / that mean this code is for autologin
        if(firebaseUser != null) {
            Intent intent = new Intent(StartActivity.this , MainActivity.class);
            startActivity(intent);
            finish();
        }
    }

لقد قمنا بتعريف FirebaseUser في الأعلى. ومن ثم اتصلنا بدالة ثابتة في كلاس AppCompatActivity. وهي تتحقق فيما لو أبقى المستخدم على فتح تطبيق برنامج دردشة أو أنه قام بإغلاقه. بالتالي يصبح الكود كامل على النحو التالي: StartActivity.java

package com.example.free chat app;
import android x .appcompat.app.AppCompatActivity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import com.google.firebase.auth.FirebaseAuth;
import com.google.firebase.auth.FirebaseUser;
public class StartActivity extends AppCompatActivity {
    Button login , register;
    FirebaseUser firebaseUser;
    @Override
    protected void onStart() {
        super.onStart();
        firebaseUser = FirebaseAuth.getInstance().getCurrentUser();
        //Check if user is null or not / that mean this code is for autologin
        if(firebaseUser != null) {
            Intent intent = new Intent(StartActivity.this , MainActivity.class);
            startActivity(intent);
            finish();
        }
    }
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_start);
        login = findViewById(R.id.login);
        register = findViewById(R.id.register);
        login.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                startActivity(new Intent(StartActivity.this , LoginActivity.class));
            }
        });
        register.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                startActivity(new Intent(StartActivity.this , RegisterActivity.class));
            }
        });
    }
}

نلاحظ أنه حتى لو تم اغلاق التطبيق بالكامل. فإن الجلسة ستبقى بوضع الإتصال. ويفيد ذلك كثيرا. خاصة في حال الحاجة إلى استلام الإشعارات والتي سنقوم بشرحها في هذه الدورة بإذن الله.  

إنشاء Package مع User Class

سنقوم الآن ببعض التعديلات على الشريط العلوي. ليظهر فيه صورة الحساب وإسم المستخدم. على سبيل المثال , سيتم ظهور إسم الحساب الذي قمت بعمل مزامنة له في Firebase. سنقوم بإضافة Package. وذلك لإضفاء المزيد من التنظيم على الشيفرة.

والسبب في ذلك أننا نريد الوصول إلى قواعد البيانات وجلب القيم على طريقة الكائنات الموجهة. على سبيل المثال , تخيل أن معلومات الحساب العامة تتضمن الاسم والعنوان والدولة. في هذه الحالة سيتم الوصول إلى حقول قواعد البيانات.

ومناداة كل عمود بإسمه الحقيقي.  ولا ننسى أننا قمنا بوضع رابط الصورة واسم المستخدم وعناوين id الفريدة لكل شخص. سأقوم ببناء User Class في الـ package التي قمت بتسميتها Model. User.java

public class User {
    private String id;
    private String username;
    private String imageUrl;
    public User(String id, String username, String imageUrl) {
        this.id = id;
        this.username = username;
        this.imageUrl = imageUrl;
    }
    public User(){
    }
    public String getId() {
        return id;
    }
    public String getUsername() {
        return username;
    }
    public String getImageUrl() {
        return imageUrl;
    }
}

 

بالتالي سيتم الوصول إلى أعمدة قواعد البيانات عبر الــ getters في الكائنات. ما يعني أن متغيرات الكلاس User. هي عناصر حقيقية في قواعد البيانات.

تحذير : قم بكتابة أسماء المتغيرات. تماما مثل الأعمدة التي تم بناؤها في قواعد البيانات , وذلك تجنبا لتعذر عملية الجلب.

 

إظهار الحساب في الشريط العلوي

لم يتبقى لنا الكثير في إظهار الحساب في الشريط العلوي لتطبيق برنامج دردشه. حيث أن الشيفرة أصبحت قوية لدينا ونستطيع دعمها بالمزيد من الخصائص الرائعة. ولكن يجب أن ندرك بأن الحساب يحتوي على صورة شخصية و اسم المستخدم.

بالتالي نحتاج إلى مشغل صور في هذه المرحلة. ونستطيع التغاضي عن هذه المشكلة بإضافة مكتبة Glide إلى المصادر. فهي مكتبة شهيرة ويتم استخدامها بنطاق واسع في تطبيقات التواصل الإجتماعي.

من فضلك قم بنسخ مصدر المكتبة في قائمة dependencies الموجودة في gradle.build(module).

dependencies {
   .....
    implementation  'de.hdodenhof:circleimageview:2.2.0'
    implementation  'com.github.bumptech.glide:glide:4.8.0'
   .....
}

قد نلاحظ وجود مكتبة أخرى وهي hdodenhof المسؤولة عن عرض الصورة بشكل دائري. على سبيل المثال , فقد تستطيع الاستغناء عنها والاعتماد على مصادرك الخاصة. سنقوم الآن بتغيير هيكل التصميم في activity_main.

وذلك من خلال التبديل إلى وضعية Linear Layout. والتي تعمل بنظام الأعمدة بدلا من الصفوف.   activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    tools:context=".MainActivity">
    <com.google.android.material.appbar.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
        <android x .appcompat.widget.Toolbar
            android:id="@+id/toolBar"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="@color/red_complex"
            android:theme="@style/Base.ThemeOverlay.AppCompat.Dark.ActionBar"
            app:popupTheme="@style/MenuStyle">
            <de.hdodenhof.circleimageview.CircleImageView
                android:id="@+id/profileImage"
                android:layout_width="30dp"
                android:layout_height="30dp"/>
            <TextView
                android:id="@+id/username"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginStart="25sp"
                android:text="@string/username"
                android:textColor="#FFF"
                android:textStyle="bold"
                />
        </android x .appcompat.widget.Toolbar>
    </com.google.android.material.appbar.AppBarLayout>
</LinearLayout>

  وفي حال وجدت أخطاء في الألوان. قم بتخصيصها كما شئت. والآن نقوم بإضافة المتغيرات في رأس الـ Class في صفحة MainActivity.java.

CircleImageView profile_image;
   TextView userName;
   FirebaseUser firebaseUser;
   DatabaseReference reference;

ثم بعد ذلك نضيف عملية الإستعلام وجلب المستخدم من قواعد البيانات.

//Prepare database connection
      firebaseUser = FirebaseAuth.getInstance().getCurrentUser();
      reference = FirebaseDatabase.getInstance().getReference("Users").child(firebaseUser.getUid());
      reference.addValueEventListener(new ValueEventListener() {
          @Override
          public void onDataChange(@NonNull DataSnapshot snapshot) {
            User user = snapshot.getValue(User.class);  //Send requirement to database
            userName.setText(user.getUsername());
            if(user.getImageUrl().equals("default")){
                profile_image.setImageResource(R.mipmap.ic_launcher);
            } else{
                //Loading image  / Check Glide library if crashed
                Glide.with(MainActivity.this).load(user.getImageUrl()).into(profile_image);
            }
          }
          @Override
          public void onCancelled(@NonNull DatabaseError error) {
          }
      });

  والآن نعرض لكم كود MainActivity. كيف أنه سيبدو. MainActivity.java

package com.example.chat application basics;
import androidx.annotation.NonNull;
import android x .appcompat.app.AppCompatActivity;
import android x .appcompat.widget.Toolbar;
import android.content.Intent;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.TextView;
import com.bumptech.glide.Glide;
import com.example.chat application basics.Model.User;
import com.google.firebase.auth.FirebaseAuth;
import com.google.firebase.auth.FirebaseUser;
import com.google.firebase.database.DataSnapshot;
import com.google.firebase.database.DatabaseError;
import com.google.firebase.database.DatabaseReference;
import com.google.firebase.database.FirebaseDatabase;
import com.google.firebase.database.ValueEventListener;
import de.hdodenhof.circleimageview.CircleImageView;
public class MainActivity extends AppCompatActivity {
    CircleImageView profile_image;
    TextView userName;
    FirebaseUser firebaseUser;
    DatabaseReference reference;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Toolbar toolbar = findViewById(R.id.toolBar);
        setSupportActionBar(toolbar);
        getSupportActionBar().setTitle("");
        profile_image = findViewById(R.id.profileImage);
        userName = findViewById(R.id.username);
        //Prepare database connection
        firebaseUser = FirebaseAuth.getInstance().getCurrentUser();
        reference = FirebaseDatabase.getInstance().getReference("Users").child(firebaseUser.getUid());
        reference.addValueEventListener(new ValueEventListener() {
            @Override
            public void onDataChange(@NonNull DataSnapshot snapshot) {
              User user = snapshot.getValue(User.class);  //Send requirement to database
              userName.setText(user.getUsername());
              if(user.getImageUrl().equals("default")){
                  profile_image.setImageResource(R.mipmap.ic_launcher);
              } else{
                  //Loading image  / Check Glide library if crashed
                  Glide.with(MainActivity.this).load(user.getImageUrl()).into(profile_image);
              }
            }
            @Override
            public void onCancelled(@NonNull DatabaseError error) {
            }
        });
    }
    //****************************************************Menu Items Here************************************************************
    //This source for menu activation
    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.menu , menu);
        return true;
    }
    @Override
    public boolean onOptionsItemSelected(@NonNull MenuItem item) {
        switch (item.getItemId()){
            case R.id.logout:
                FirebaseAuth.getInstance().signOut();  //Call database to create sighout task
                startActivity(new Intent(MainActivity.this , StartActivity.class));
                finish();
                return true;
        }
        return false;
    }
    //***********************************************************************************************************************************
}

  وعند بناء الكود ستظهر المخرجات كما في الصورة التالية. تستطيع إضفاء المزيد من التحسينات. وذلك من خلال الاستعانة بأيقونات مكتبة Apache. وذلك عن طريق Vector Asset.  

عمل Fragment وتقسيم الصفحات

تقسيم الصفحات هو الجزء الذي يستطيع من خلالها مستخدم التطبيق التنقل بين الصفحات. فهو محرك بسيط لا يتصف بالتعقيدات. متاح للاستخدام بشكل مجاني. بالتالي سنحتاج له خاصة عند التنقل بين عناصر قواعد البيانات.

غالبا ما نحتاج إلى ثلاث صفحات الآن. وهي صفحة البروفايل الشخصية. وصفحة المستخدمين العامة والمحادثات التي تمت. و لإضفاء المزيد من التنظيم سنقوم بإضافة Package باسم Fragments.

آلية عمل الـ Fragments

تمتلك كل Fragment صفات وبيانات مختلفة عن غيرها. ويتم إضافتها بطريقة مشابهة لــ Activity. نشاهد الصورة التالية كيف يتم إضافتها: تعتمد ال Fragment على العمل عبر عنصرين وهما:

  1. الوراثة من Fragment State Adapter.
  2. وأيضًا وراثة من Fragment.

تشير الأولى إلى تنظيم حركة التنقل عند سحب شاشة الهاتف يمينًا أو يسارًا. بينما الثانية إلى وراثة صفات Fragment وخصائصه. على سبيل المثال , العنوان في الأعلى هو من أحد صفاتها. بينما التبديل بينها يعود إلى وحدة التحكم.

عمل صفحات Fragment

يتطلب عمل الصفحات الطريقة التي عرضناها في الصورة السابقة. لذلك سنقوم ببناء ثلاث صفحات. الأولى Chats Fragment. والثانية Users Fragment. بينما الثالثة ستكون Profile Fragments.

ونظرا لعدم وجود Empty Fragment. سنقوم باستدعاء Blank. بالتالي نقوم بمسح كافة الشيفرة بعد التسمية ليتبقى الكائن بهذا الشكل:

public class ChatsFragment extends Fragment {
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        // Inflate the layout for this fragment
        return inflater.inflate(R.layout.fragment_chats, container, false);
    }
}

والحالة تنطبق على بقية الأقسام التي ذكرناها. بالتالي تأكد من أن الكود المقبل سيتم بداخل هذه الأقسام, لذا ندعوك بالإعداد لتلك المهمة جيدًا.   الآن وبعد أن قمنا بإضافة الأقسام. فلو ألقينا لمحة على شكل المشروع كيف سيبدو لكان كما في الصورة التالية:  

تحضير أيقونات الأقسام

نعتبر بأن أيقونات الأقسام واحدة من أهم المزايا التي تحسن من ظهور تطبيق برنامج دردشة أمام المستخدمين. فقد وفرت لنا مكتبة Apache أيقونات مرخصة وتستطيع استخدامها عند عملية النشر في المتاجر. بالتالي يتعين علينا تحضير بعض الأيقونات.

على سبيل المثال , فإننا نقوم بتخصيص أيقونة قسم الحساب الشخصي من خلال الصورة التالية:     من فضلك , قم بتجهيز الأيقونات في المشروع قبل الانتقال إلى إعداد الأقسام. وذلك لتجنب ظهور الأخطاء.  

إضافة وحدة التحكم الخاصة بـ Fragments

لا بد من وجود محول يقوم بإدارة التحريك والتنقل بين هذه الأقسام. بالتالي سنحضر Class جديد ونسميه Fragment Adapter. وذلك ليبدو بالشكل التالي:   FragmentAdapter.java

package com.example. free chat app .Fragments;
import androidx.annotation.NonNull;
import android x .fragment.app.Fragment;
import android x .fragment.app.FragmentActivity;
import androidx.viewpager2.adapter.FragmentStateAdapter;
public class FragmentAdapter extends FragmentStateAdapter {
    public FragmentAdapter(@NonNull FragmentActivity fragmentActivity) {
        super(fragmentActivity);
    }
    @NonNull
    @Override
    public Fragment createFragment(int position) {
        switch (position){
            case 0:
                return new UsersFragment();
            case 1:
                return new ChatsFragment();
            case 2:
                return new ProfileFragment();    
        }
        return new UsersFragment();
    }
    @Override
    public int get ItemCount() {
        return 3;
    }
}

تحتاج الأقسام إلى عناصر يتم إضافتها خلال التصميم. ولتجاوز تلك المسألة. سنقوم بوضع عنصرين داخل main_activity وهما tabLayout و viewPager2.

<com.google.android.material.tabs.TabLayout
    android:id="@+id/tab_layout"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="#FFFFF0"
    app:tabIndicatorColor="@color/white"
    app:tabSelectedTextColor="@color/red_complex"
    app:tabTextColor="@color/red_light">
</com.google.android.material.tabs.TabLayout>
<android x .viewpager2.widget.ViewPager2
    android:id="@+id/view_pager"
    android:layout_width="match_parent"
    android:layout_height="match_parent"/>

  بالتالي يصبح الشكل العام لصفحة activity_main: activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/white">
<android x .appcompat.widget.Toolbar
android:id="@+id/toolBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/purple_500"
android:theme="@style/Base.ThemeOverlay.AppCompat.Dark.ActionBar"
app:popupTheme="@style/MenuStyle">
<de.hdodenhof.circleimageview.CircleImageView
android:id="@+id/profileImage"
android:layout_width="30dp"
android:layout_height="30dp"/>
<TextView
android:id="@+id/username"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="25sp"
android:text="@string/username"
android:textColor="#FFF"
android:textStyle="bold"
/>
</android x .appcompat.widget.Toolbar>
<com.google.android.material.tabs.TabLayout
android:id="@+id/tab_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/complexYellow"
app:tabIndicatorColor="@color/white"
app:tabSelectedTextColor="@color/complexRed"
app:tabTextColor="@color/purple_500">
</com.google.android.material.tabs.TabLayout>
<android x .viewpager2.widget.ViewPager2
android:id="@+id/view_pager"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</com.google.android.material.appbar.AppBarLayout>
</LinearLayout>

  إعداد الأقسام Fragments في MainActivity يتم تفعيل الأقسام في MainActivity عبر كتلة واحدة من الكود وهي :

TabLayout tabLayout = findViewById(R.id.tab_layout);
       ViewPager2 viewPager = findViewById(R.id.view_pager);
       /***********************************Preparing Pager Fragments************************************************************/
       viewPager.setAdapter(new FragmentAdapter(this));
       tabLe layout tabLe layout Mediator= new tabLe layout Mediator (tabLayout, viewPager, new tabLe layout Mediator .TabConfigurationStrategy() {
           @Override
           public void onConfigureTab(@NonNull TabLayout.Tab tab, int position) {
               switch (position){
                   case 0:{
                       tab.setText("Users");
                       tab.setIcon(getResources().getDrawable(R.drawable.ic_users));
                       break;
                   }
                   case 1:{
                       tab.setText("Chat");
                       tab.setIcon(getResources().getDrawable(R.drawable.ic_chat));
                       break;
                   }
                   case 2:{
                       tab.setText("Profile");
                       tab.setIcon(getResources().getDrawable(R.drawable.ic_profile));
                       break;
                   }
               }
           }
       });
       tab LayoutMediator.attach();

بينما تصبح شيفرة MainActivity على نحو كامل. MainActivity.java

package com.example.free chat app;
import androidx.annotation.NonNull;
import android x .appcompat.app.AppCompatActivity;
import android x .appcompat.widget.Toolbar;
import android x .viewpager2.widget.ViewPager2;
import android.content.Intent;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.TextView;
import com.bumptech.glide.Glide;
import com.example.free chat app.Fragments.FragmentAdapter;
import com.example.free chat app.Model.User;
import com.google.android.material.tabs.TabLayout;
import com.google.android.material.tabs.tabLe layout Mediator;
import com.google.firebase.auth.FirebaseAuth;
import com.google.firebase.auth.FirebaseUser;
import com.google.firebase.database.DataSnapshot;
import com.google.firebase.database.DatabaseError;
import com.google.firebase.database.DatabaseReference;
import com.google.firebase.database.FirebaseDatabase;
import com.google.firebase.database.ValueEventListener;
import de.hdodenhof.circleimageview.CircleImageView;
public class MainActivity extends AppCompatActivity {
    CircleImageView profile_image;
    TextView userName;
    FirebaseUser firebaseUser;
    DatabaseReference reference;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Toolbar toolbar = findViewById(R.id.toolBar);
        //setSupportActionBar(toolbar);
        getSupportActionBar().setTitle("");
        profile_image = findViewById(R.id.profileImage);
        userName = findViewById(R.id.username);
        //Prepare database connection
        firebaseUser = FirebaseAuth.getInstance().getCurrentUser();
        reference = FirebaseDatabase.getInstance().getReference("Users").child(firebaseUser.getUid());
        reference.addValueEventListener(new ValueEventListener() {
            @Override
            public void onDataChange(@NonNull DataSnapshot snapshot) {
                User user = snapshot.getValue(User.class);  //Send requirement to database
                userName.setText(user.getUsername());
                if(user.getImageUrl().equals("default")){
                    profile_image.setImageResource(R.mipmap.ic_launcher);
                } else{
                    //Loading image  / Check Glide library if crashed
                    Glide.with(MainActivity.this).load(user.getImageUrl()).into(profile_image);
                }
            }
            @Override
            public void onCancelled(@NonNull DatabaseError error) {
            }
        });
        TabLayout tabLayout = findViewById(R.id.tab_layout);
        ViewPager2 viewPager = findViewById(R.id.view_pager);
        /***********************************Preparing Pager Fragments************************************************************/
        viewPager.setAdapter(new FragmentAdapter(this));
        tabLe layout Mediator tabLe layout Mediator= new tabLe layout Mediator(tabLayout, viewPager, new tabLe layout Mediator.TabConfigurationStrategy() {
            @Override
            public void onConfigureTab(@NonNull TabLayout.Tab tab, int position) {
                switch (position){
                    case 0:{
                        tab.setText("Users");
                        tab.setIcon(getResources().getDrawable(R.drawable.ic_users));
                        break;
                    }
                    case 1:{
                        tab.setText("Chat");
                        tab.setIcon(getResources().getDrawable(R.drawable.ic_chat));
                        break;
                    }
                    case 2:{
                        tab.setText("Profile");
                        tab.setIcon(getResources().getDrawable(R.drawable.ic_profile));
                        break;
                    }
                }
            }
        });
        tab LayoutMediator.attach();
    }
    //****************************************************Menu Items Here************************************************************
    //This source for menu activation
    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.menu , menu);
        return true;
    }
    @Override
    public boolean onOptionsItemSelected(@NonNull MenuItem item) {
        switch (item.getItemId()){
            case R.id.logout:
                FirebaseAuth.getInstance().signOut();  //Call database to create sighout task
                startActivity(new Intent(MainActivity.this , StartActivity.class));
                finish();
                return true;
        }
        return false;
    }
    //***********************************************************************************************************************************
}

يمكنك تهيئة وإضافة بعض الخصائص في كل صفحة من الأقسام. ولقد قمنا باستبدال Frame Layout بالعنصر Relative في جميع صفحات تصميم الأقسام. بالتالي كانت النتيجة منسجمة وجميلة كما كنا نتوقع!.

لقد تم بناء حجر الأساس في البرنامج مثل الصفحات وعملية الدخول والخروج. بالتالي بات من الممكن القدوم بالمستخدمين من قواعد البيانات. وذلك لكي يحقق تطبيق برنامج دردشه متطلباته الأساسية.  

جلب الحسابات من قواعد البيانات

لكي يتم جلب المستخدمين من قواعد البيانات. يتعين علينا إنشاء Adapter. بالتالي سيعمل على وضع الأعمدة على شكل عناصر في قسم Users. والآن سنحاول إضافة Package باسم Adapter. وذلك لإضفاء طابع التنظيم إلى الكود.

سنضع بداخل الـ Package كلاس باسم UserAdapter. بحيث يتزامن العمل مع صفحة user item.   UserAdapter.java

package com.example.free chat app.Adapter;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.Glide;
import com.example.free chat app.Model.User;
import com.example.free chat app.R;
import java.util.List;
public class UserAdapter extends RecyclerView.Adapter<UserAdapter.ViewHolder> {
    private Context mContext;
    private List<User> mUser;
    public UserAdapter(Context mContext , List<User> mUsers){
        this.mContext = mContext;
        this.mUser = mUsers;
    }
    @NonNull
    @Override
    public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(mContext).inflate(R.layout.user_item , parent , false);
        return new UserAdapter.ViewHolder(view);
    }
    @Override
    public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
        //When call database by using this function we will get all data of user for views
        User user = mUser.get(position);
        holder.username.setText(user.getUsername());
        if(user.getImageUrl().equals("default")){
            holder.profileImage.setImageResource(R.mipmap.ic_launcher);
        } else{
            Glide.with(mContext).load(user.getImageUrl()).into(holder.profileImage);
        }
    }
    @Override
    public int get ItemCount() {
        return mUser.size();
    }
    public class ViewHolder extends RecyclerView.ViewHolder{
        public TextView username;
        public ImageView profileImage;
        public ViewHolder(@NonNull View itemView) {
            super(itemView);
            username = itemView.findViewById(R.id.username);
            profileImage = itemView.findViewById(R.id.profileImage);
        }
    }
}

الآن نقم بإضافة صفحة xml جديدة ومنفردة ولتكن بعنوان user_item. user item.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="10dp">
    <de.hdodenhof.circleimageview.CircleImageView
        android:id="@+id/profileImage"
        android:layout_width="50dp"
        android:layout_height="50dp"
        android:src="@mipmap/ic_launcher"/>
    <TextView
        android:id="@+id/username"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/username"
        android:layout_toEndOf="@+id/profileImage"
        android:layout_marginStart="10dp"
        android:layout_centerVertical="true"
        android:textSize="18sp"
        />
</RelativeLayout>

ولأننا نقوم بعرض المستخدمين في قسم User Fragment والذي قمنا ببنائه للتو. فإنها فكرة جيدة لتطبيق الشيفرة التالية بكل من UsersFragment.java و fragment_user.xml. UsersFragment.java

package com.example.free chat app.Fragments;
import android.os.Bundle;
import androidx.annotation.NonNull;
import android x .fragment.app.Fragment;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import com.example.free chat app.Adapter.UserAdapter;
import com.example.free chat app.Model.User;
import com.example.free chat app.R;
import com.google.firebase.auth.FirebaseAuth;
import com.google.firebase.auth.FirebaseUser;
import com.google.firebase.database.DataSnapshot;
import com.google.firebase.database.DatabaseError;
import com.google.firebase.database.DatabaseReference;
import com.google.firebase.database.FirebaseDatabase;
import com.google.firebase.database.ValueEventListener;
import java.util.ArrayList;
import java.util.List;
public class UsersFragment extends Fragment {
    private RecyclerView recyclerView;
    private UserAdapter userAdapter;
    private List<User> mUsers;
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        // Inflate the layout for this fragment
        View view = inflater.inflate(R.layout.fragment_users, container, false);
        recyclerView = view.findViewById(R.id.recycler_view);
        recyclerView.setHasFixedSize(true);
        recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
        mUsers = new ArrayList<>();
        read Users();
        return view;
    }
    private void read Users() {
        FirebaseUser firebaseUser = FirebaseAuth.getInstance().getCurrentUser();
        DatabaseReference reference;
        reference = FirebaseDatabase.getInstance().getReference("Users");
        reference.addValueEventListener(new ValueEventListener() {
            @Override
            public void onDataChange(@NonNull DataSnapshot snapshot) {
                mUsers.clear();
                for(DataSnapshot snapshot1 :  snapshot.getChildren()){
                    User user = snapshot1.getValue(User.class);
                    assert user != null;
                    assert firebaseUser != null;
                    //Skip current user when fetch users from database
                    if(!user.getId().equals(firebaseUser.getUid())){
                        mUsers.add(user);
                    }
                }
                userAdapter = new UserAdapter(getContext() , mUsers);
                recyclerView.setAdapter(userAdapter);
            }
            @Override
            public void onCancelled(@NonNull DatabaseError error) {
            }
        });
    }
}

fragment_user.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/white"
    tools:context=".Fragments.UsersFragment">
    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recycler_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</RelativeLayout>

قد تلاحظ أثناء بناء برنامج دردشه بعدم وجود مستخدمين في قسم User Fragment. وذلك لعدم وجود سوى مستخدم واحد في Firebase. بالتالي نحن قمنا بحجب ظهور المستخدم الذي سجلنا الدخول به في القائمة.

ومع ذلك لكي تلاحظ وجود نتائج , تحتاج إلى إضافة حساب آخر ومن المستحسن توصيل هاتف أندرويد آخر في الحاسوب. أو يمكنك العمل على أداة VM في Android Studio.

  لا بأس في إضفاء المزيد من التحسينات. على سبيل المثال سنقوم بتغيير أيقونة المستخدمين من خلال مكتبة Apache المرخصة. بالتالي ستبدو أيقونة المستخدمين على النحو التالي: على سبيل المثال , تستطيع الوصول لأيقونة المستخدمين في UserAdapter.

كذلك الأمر بالنسبة للشريط الرئيسي في الأعلى , حيث تستطيع الوصول له من خلال MainActivity. ويتم الوصول لامتداد الأيقونات من خلال R.drawable.  

بناء واجهات الرسائل

عند النقر على المستخدم , لا تتوفر أحداث لتوجيه الصفحات. ولذلك الأمر سنركز الآن على بناء صفحة إعداد الرسائل والتي منها يستطيع المستخدمين تبادل الرسائل النصية فيما بينهم. بالتالي فإن تطبيق برنامج دردشه يتطلب إضافة Empty Activity بعنوان Message Activity.

لا ننسى أنه عند إضافة أي Activity فإنه يتم توليد صفحة xml خاصة بها كما حصل أثناء بناء صفحة MessageActivity وبقية الصفحات القديمة. 

 

توجيه إلى صفحات المستخدمين

تتشابه جافا مع لغات الويب في إدارة الصفحات , فهي تحمل طابع Post و Request. مصحوبا معها بعض القيم التي تستعين بها لكل عملية استعلام. على سبيل المثال , وعند الدخول إلى صفحة salma 1990.

على سبيل المثال يتم إرفاق عناوين id فريدة لعرض الرسائل المتعلقة بهذا الحساب. سنستأنف هذا القسم في السطور القادمة من تطبيق برنامج دردشه. والآن نود الإشارة إلى الصفحات عن طريق الكائن Intent.

وهي حزمة تصحب معها بعض القيم التي يتم استغلالها في صفحات المستخدمين. الآن نريد النقر على إحدى المستخدمين وذلك من خلال الدالة setOnClickListener. والتي تعني عند النقر قم بعمل أي حدث.

والحدث الخاص بنا الآن هو زيارة صفحة المستخدم. بالتالي سيتم تطبيق الكود التالي في صفحة User Adapter.

 

//On click on user open MessageActivity
       holder.itemView.setOnClickListener(new View.OnClickListener() {
           @Override
           public void onClick(View view) {
               Intent intent = new Intent(mContext , MessageActivity.class);
               intent.putExtra("userid" , user.getId());
               mContext.startActivity(intent);
           }
       });

لقد قمنا بوضع حزمة Extra وذلك لتمرير القيم عند الدخول إلى صفحة Message Activity. والتي أهمها عنوان id. حيث سيتم جلب كل ما نريده من قواعد البيانات. وهي طريقة عزل جيدة للبيانات في تطبيقات برنامج دردشه وحتى في برمجة الويب. UserAdapter.java

package com.example.free chat app.Adapter;
import android.content.Context;
import android.content.Intent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.Glide;
import com.example.freechatapp.MessageActivity;
import com.example.freechatapp.Model.User;
import com.example.free chat app.R;
import java.util.List;
public class UserAdapter extends RecyclerView.Adapter<UserAdapter.ViewHolder> {
    private Context mContext;
    private List<User> mUser;
    public UserAdapter(Context mContext , List<User> mUsers){
        this.mContext = mContext;
        this.mUser = mUsers;
    }
    @NonNull
    @Override
    public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(mContext).inflate(R.layout.user_item , parent , false);
        return new UserAdapter.ViewHolder(view);
    }
    @Override
    public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
        //When call database by using this function we will get all data of user for views
        User user = mUser.get(position);
        holder.username.setText(user.getUsername());
        if(user.getImageUrl().equals("default")){
            holder.profileImage.setImageResource(R.drawable.ic_user); //put your icon resource here
        } else{
            Glide.with(mContext).load(user.getImageUrl()).into(holder.profileImage);
        }
        //On click on user open MessageActivity
        holder.itemView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Intent intent = new Intent(mContext , MessageActivity.class);
                intent.putExtra("userid" , user.getId());
                mContext.startActivity(intent);
            }
        });
    }
    @Override
    public int get ItemCount() {
        return mUser.size();
    }
    public class ViewHolder extends RecyclerView.ViewHolder{
        public TextView username;
        public ImageView profileImage;
        public ViewHolder(@NonNull View itemView) {
            super(itemView);
            username = itemView.findViewById(R.id.username);
            profileImage = itemView.findViewById(R.id.profileImage);
        }
    }
}

وبعد أن قمنا بتجهيز صفحة التوجيه إلى الرسائل. سنعمل الآن على تخصيص بعض التصاميم فيها. وتستطيع اختيار الألوان التي تريدها عند نسخ الشيفرة. MessageActivity.java

package com.example.free chatapp;
import androidx.annotation.NonNull;
import android x .appcompat.app.AppCompatActivity;
import android x .appcompat.widget.Toolbar;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.TextView;
import android.widget.Toast;
import com.bumptech.glide.Glide;
import com.example.freechatapp.Model.User;
import com.google.firebase.auth.FirebaseAuth;
import com.google.firebase.auth.FirebaseUser;
import com.google.firebase.database.DataSnapshot;
import com.google.firebase.database.DatabaseError;
import com.google.firebase.database.DatabaseReference;
import com.google.firebase.database.FirebaseDatabase;
import com.google.firebase.database.ValueEventListener;
import java.util.HashMap;
import de.hdodenhof.circleimageview.CircleImageView;
public class MessageActivity extends AppCompatActivity {
    CircleImageView profileImage;
    TextView username;
    FirebaseUser fUser;
    DatabaseReference reference;
    ImageButton btn_send;
    EditText text_send;
    Intent intent;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_message);
        Toolbar toolbar = findViewById(R.id.toolBar);
        //setSupportActionBar(toolbar);
        getSupportActionBar().setTitle("");
        getSupportActionBar().setDisplayHomeAsUpEnabled(true);
        toolbar.setNavigationOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                finish();
            }
        });
        profileImage = findViewById(R.id.profileImage);
        username = findViewById(R.id.username);
        btn_send = findViewById(R.id.btn_send);
        text_send = findViewById(R.id.text_send);
        //Receiving id from previous page
        intent = getIntent();
        String userid = intent.getStringExtra("userid");
        fUser = FirebaseAuth.getInstance().getCurrentUser();
        btn_send.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                String msg = text_send.getText().toString();
                if(!msg.equals("")){
                    //Send messages to firebase database from sender to receiver
                    //fUser is declared above and it is allowed to be used everywhere
                    send_message(fUser.getUid() , userid , msg);
                }else{
                    Toast.makeText(MessageActivity.this, "You can't send an empty message!", Toast.LENGTH_SHORT).show();
                }
                text_send.setText("");
            }
        });
        //Access Users table in database where userid = this userid
        reference  = FirebaseDatabase.getInstance().getReference("Users").child(userid);
        reference.addValueEventListener(new ValueEventListener() {
            @Override
            public void onDataChange(@NonNull DataSnapshot snapshot) {
                User user = snapshot.getValue(User.class);
                username.setText(user.getUsername());
                if(user.getImageUrl().equals("default")){
                    profileImage.setImageResource(R.drawable.ic_user);
                }else{
                    Glide.with(MessageActivity.this).load(user.getImageUrl()).into(profileImage);
                }
            }
            @Override
            public void onCancelled(@NonNull DatabaseError error) {
            }
        });
    }
    private void send_message (String sender , String receiver , String message){
        //Make a connection with database
        DatabaseReference reference = FirebaseDatabase.getInstance().getReference();
        //Filling data in an array inside JAVA tools
        HashMap<String , Object> hashMap = new HashMap<>();
        hashMap.put("sender" , sender);
        hashMap.put("receiver" , receiver);
        hashMap.put("message" , message);
        //Push data inside chats table in database
        reference.child("Chats").push().setValue(hashMap);
    }
}

ستلاحظ أننا قمنا بعملية الإستعلام في قواعد البيانات عن طريق عنوان id. بالتالي لقد تم تمريره من خلال استلام عناصر Extra. وبعد التواصل مع قواعد البيانات نتج لدينا اسم المستخدم وصورة حسابه. activity_message.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/complexYellow_light"
    tools:context=".MessageActivity">
    <com.google.android.material.appbar.AppBarLayout
        android:id="@+id/bar_layout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
        <android x.appcompat.widget.Toolbar
            android:id="@+id/toolBar"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="@color/purple_500"
            android:theme="@style/Base.ThemeOverlay.AppCompat.Dark.ActionBar"
            app:popupTheme="@style/MenuStyle">
            <de.hdodenhof.circleimageview.CircleImageView
                android:id="@+id/profileImage"
                android:layout_width="30dp"
                android:layout_height="30dp"/>
            <TextView
                android:id="@+id/username"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginStart="25sp"
                android:text="@string/username"
                android:textColor="#FFF"
                android:textStyle="bold"/>
        </android x.appcompat.widget.Toolbar>
        <com.google.android.material.tabs.TabLayout
            android:id="@+id/tab_layout"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="@color/complexYellow"
            app:tabIndicatorColor="@color/white"
            app:tabSelectedTextColor="@color/complexRed"
            app:tabTextColor="@color/purple_500">
        </com.google.android.material.tabs.TabLayout>
    </com.google.android.material.appbar.AppBarLayout>
    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recycler_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_above="@+id/bottom"
        android:layout_below="@id/bar_layout" />
    <RelativeLayout
        android:id="@+id/bottom"
        android:layout_width="match_parent"
        android:padding="5dp"
        android:background="#fff"
        android:layout_alignParentBottom="true"
        android:layout_height="wrap_content">
        <EditText
            android:id="@+id/text_send"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="@android:color/transparent"
            android:hint="@string/type_message"
            android:layout_toLeftOf="@+id/btn_send"
            android:layout_centerVertical="true"
            />
        <ImageButton
            android:id="@+id/btn_send"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentEnd="true"
            android:src="@drawable/ic_send" />
    </RelativeLayout>
</RelativeLayout>

لكي تتحقق من أن كل مستخدم له صفة فريدة في صفحة Message Activity. تستطيع القيام بإنشاء حسابات أخرى لتجد نتيجة حقيقية ومطابقة للمتغيرات. ستلاحظ عند ارسال رسالة سيتم تخزينها في قواعد البيانات.

وفي المرحلة القادمة من برنامج دردشه سيتم تفعيل عرض الرسائل لكل من المرسل والمستقبل. بالتالي تتطلب تلك المرحلة عملية تركيز وتتبع خلال تطبيق الشيفرة.

تحديث صفحة الألوان

من الجيد تحديث صفحة الألوان. خاصة بعد أن قمنا بإضافة بعضها. لقد قمنا باختيار ألوان تفي بالغرض لمشروع برنامج دردشة. وفي حال كنت تريد شيفرة الألوان قم باستبدالها بالتي هي لديك.

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="purple_200">#FFBB86FC</color>
    <color name="purple_500">#726C7C</color>
    <color name="purple_700">#D1CAE3</color>
    <color name="teal_200">#FF03DAC5</color>
    <color name="teal_700">#FF018786</color>
    <color name="black">#FF000000</color>
    <color name="white">#FFFFFFFF</color>
    <color name="colorPrimaryDark">#226e6e</color>
    <color name="colorPrimary"></color>
    <color name="complexRed"></color>
    <color name="complexYellow">#E8E2AD</color>
    <color name="complexYellow_light">#FFFFF0</color>
    <color name="right_background">#FFFFFF</color>
    <color name="left_background">#D1CAE3</color>
</resources>

إضافة Drawable

لا بد من اضافة بعض الخصائص الجمالية في صفحات الشات. خاصة وأن تطبيق برنامج دردشة يقد يبدو متواضعا للغاية. وسنقوم بإضافة ملفين xml لتشكيل نصوص المحادثة. بالتالي قد تساعدك الصورة في طريقة الإضافة.

ندعوك للحذر عند الإضافة حيث أن الملف الرئيسي الخاص بأشكال الأيقونات هو drawable. بالتالي سيتم إضافة أيقونتين الأولى باسم background left والثانية باسم background right. background_right.xml

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">
    <corners
        android:radius="10dp"
        android:topRightRadius="0dp"/>
    <solid android:color="@color/right_background"/>
</shape>

background left.xml

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">
    <corners
        android:radius="10dp"
        android:topLeftRadius="0dp"/>
    <solid android:color="@color/left_background"/>
</shape>

   

عرض الرسائل في الحسابات

قبل كل شيء , ولكي نقم بالاستعلام عن الرسائل لا بد من بناء كلاس مع getters. وسيتم وضع متغيراته بنفس أسماء الحقول التي قمنا بارسالها لقواعد البيانات.

بالتالي تطبيق برنامج دردشه سيقبل التطوير بشكل كبير من خلال الكائنات الموجهة. في حزمة Model. نقوم الآن ببناء كلاس , وليكن باسم Chat.

 

Chat.java

package com.example.free chat app.Model;
public class Chat {
    private String sender;
    private String receiver;
    private String message;
    public Chat(String sender, String receiver, String message) {
        this.sender = sender;
        this.receiver = receiver;
        this.message = message;
    }
    public Chat(){
    }
    public String getSender() {
        return sender;
    }
    public void setSender(String sender) {
        this.sender = sender;
    }
    public String getReceiver() {
        return receiver;
    }
    public void setReceiver(String receiver) {
        this.receiver = receiver;
    }
    public String getMessage() {
        return message;
    }
    public void setMessage(String message) {
        this.message = message;
    }
}

    نحن في صدد عرض الرسائل في كل من حسابات المرسل والمستقبل. وهذه العملية تتطلب عمل Adapter خاص لكل منهما. على سبيل المثال , ليس من المعقول عرض جميع الرسائل في خانة واحدة مثل المجموعات.

ولذلك سنقوم ببناء صفحة Java وسيتم وضعها في حزمة Adapter من المشروع. بالتالي نقم بتسمية المحول باسم MessageAdapter. وكما ذكرنا سابقا , فإنه هو المخول بعرض تصميمين من xml. الجهة اليمنى سيتم تخصيصها للمستخدم الحالي.   MessageAdapter.java

package com.example.free chat app.Adapter;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.Glide;
import com.example.freechatapp.Model.Chat;
import com.example.free chat app.R;
import com.google.firebase.auth.FirebaseAuth;
import com.google.firebase.auth.FirebaseUser;
import java.util.List;
public class MessageAdapter extends RecyclerView.Adapter<MessageAdapter.ViewHolder> {
    public static final int MSG_TYPE_LEFT = 0;
    public static final int MSG_TYPE_RIGHT = 1;
    private Context mContext;
    private List<Chat> mChat;
    private String imageUrl;
    FirebaseUser fUser;
    public MessageAdapter(Context mContext , List<Chat> mChat , String imageUrl){
        this.mContext = mContext;
        this.mChat = mChat;
        this.imageUrl = imageUrl;
    }
    @NonNull
    @Override
    public MessageAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        //Set direct of messages LEFT/RIGHT when chat starts
        if (viewType == MSG_TYPE_RIGHT) {
            View view = LayoutInflater.from(mContext).inflate(R.layout.chat_item_right, parent, false);
            return new MessageAdapter.ViewHolder(view);
        }else{
            View view = LayoutInflater.from(mContext).inflate(R.layout.chat_item_left, parent, false);
            return new MessageAdapter.ViewHolder(view);
        }
    }
    @Override
    public void onBindViewHolder(@NonNull MessageAdapter.ViewHolder holder, int position) {
        Chat chat = mChat.get(position);
        holder.show_message.setText(chat.getMessage());
        if(imageUrl.equals("default")){
            holder.profileImage.setImageResource(R.drawable.ic_user);
        }else {
            Glide.with(mContext).load(imageUrl).into(holder.profileImage);
        }
    }
    @Override
    public int get ItemCount() {
        return mChat.size();
    }
    public class ViewHolder extends RecyclerView.ViewHolder{
        public TextView show_message;
        public ImageView profileImage;
        public ViewHolder(@NonNull View itemView) {
            super(itemView);
            show_message = itemView.findViewById(R.id.show_message);
            profileImage = itemView.findViewById(R.id.profileImage);
        }
    }
    @Override
    public int getItemViewType(int position) {
        fUser = FirebaseAuth.getInstance().getCurrentUser();
        if(mChat.get(position).getSender().equals(fUser.getUid())){
            return MSG_TYPE_RIGHT;
        }else{
            return MSG_TYPE_LEFT;
        }
    }
}

يعتبر هذا المكون أساسيا لكل برنامج دردشة في العالم. فهو يقوم بتخصيص ملفين من مصادر xml. والتي تنظم عملية عرض الرسائل. ولذلك سنعمل على إضافتها الآن. إذن نقوم الآن ببناء ملفين xml. الأول اسم chat_item_right والآخر باسم chat_item_left. chat_item_right.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="8dp">
    <RelativeLayout
        android:layout_width="300dp"
        android:layout_height="wrap_content"
        android:layout_alignParentEnd="true">
        <de.hdodenhof.circleimageview.CircleImageView
            android:id="@+id/profileImage"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:visibility="gone" />
        <TextView
            android:id="@+id/show_message"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentEnd="true"
            android:background="@drawable/background_right"
            android:padding="8dp"
            android:text="message_will_appear_here"
            android:textSize="18sp" />
    </RelativeLayout>
</RelativeLayout>

chat_item_left.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="300dp"
    android:padding="8dp"
    android:layout_height="wrap_content">
    <de.hdodenhof.circleimageview.CircleImageView
        android:id="@+id/profileImage"
        android:layout_width="40dp"
        android:layout_height="40dp"
        android:src="@mipmap/ic_launcher"/>
    <TextView
        android:id="@+id/show_message"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_toEndOf="@id/profileImage"
        android:layout_marginStart="5dp"
        android:textSize="18sp"
        android:text="message_will_appear_here"
        android:padding="8dp"
        android:background="@color/left_background"
        />
</RelativeLayout>

 

تعديلات على Message Activity

يصح لنا الآن إضافة الدالة readMessages في صفحة MessageActivity. وبدور هذه الدالة سيتم طلب المحول MessageAdapter وعرض الرسائل إن وجدت.

 

private void readMessages(String myid , String userid , String img Url){
      mChat = new ArrayList<>();
      reference = FirebaseDatabase.getInstance().getReference("Chats");
      reference.addValueEventListener(new ValueEventListener() {
          @Override
          public void onDataChange(@NonNull DataSnapshot dataSnapshot) {
              mChat.clear();
              for(DataSnapshot snapshot : dataSnapshot.getChildren()){
                  Chat chat = snapshot.getValue(Chat.class);
                  assert chat != null;
                  if(chat.getReceiver().equals(myid) && chat.getSender().equals(userid) || chat.getReceiver().equals(userid) && chat.getSender().equals(myid)){
                      mChat.add(chat);
                  }
                  //Log.i(TAG, "userid :" + userid);
                  messageAdapter = new MessageAdapter(MessageActivity.this , mChat , img Url);
                  recyclerView.setAdapter(messageAdapter);
              }
          }
          @Override
          public void onCancelled(@NonNull DatabaseError error) {
          }
      });
  }

وبالتالي يصبح الشكل الحقيقي لصفحة Message Activity كما في الكود التالي: MessageActivity.java

package com.example.free chat app;
import android x .annotation.NonNull;
import android x.appcompat.app.AppCompatActivity;
import android x .appcompat.widget.Toolbar;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.TextView;
import android.widget.Toast;
import com.bumptech.glide.Glide;
import com.example.freechatapp.Adapter.MessageAdapter;
import com.example.freechatapp.Model.Chat;
import com.example.freechatapp.Model.User;
import com.google.firebase.auth.FirebaseAuth;
import com.google.firebase.auth.FirebaseUser;
import com.google.firebase.database.DataSnapshot;
import com.google.firebase.database.DatabaseError;
import com.google.firebase.database.DatabaseReference;
import com.google.firebase.database.FirebaseDatabase;
import com.google.firebase.database.ValueEventListener;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import de.hdodenhof.circleimageview.CircleImageView;
public class MessageActivity extends AppCompatActivity {
    CircleImageView profileImage;
    TextView username;
    FirebaseUser fUser;
    DatabaseReference reference;
    ImageButton btn_send;
    EditText text_send;
    MessageAdapter messageAdapter;
    List<Chat> mChat;
    RecyclerView recyclerView;
    Intent intent;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_message);
        Toolbar toolbar = findViewById(R.id.toolBar);
      //  setSupportActionBar(toolbar);
        getSupportActionBar().setTitle("");
        getSupportActionBar().setDisplayHomeAsUpEnabled(true);
        toolbar.setNavigationOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                finish();
            }
        });
        recyclerView = findViewById(R.id.recycler_view);
        recyclerView.setHasFixedSize(true);
        LinearLayoutManager linearLayoutManager = new LinearLayoutManager(getApplicationContext());
        linearLayoutManager.setStackFromEnd(true);
        recyclerView.setLayoutManager(linearLayoutManager);
        profileImage = findViewById(R.id.profileImage);
        username = findViewById(R.id.username);
        btn_send = findViewById(R.id.btn_send);
        text_send = findViewById(R.id.text_send);
        //Receiving id from previous page
        intent = getIntent();
        String userid = intent.getStringExtra("userid");
        fUser = FirebaseAuth.getInstance().getCurrentUser();
        btn_send.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                String msg = text_send.getText().toString();
                if(!msg.equals("")){
                    //Send messages to firebase database from sender to receiver
                    //fUser is declared above and it is allowed to be used everywhere in this class
                    send_message(fUser.getUid() , userid , msg);
                }else{
                    Toast.makeText(MessageActivity.this, "You can't send an empty message!", Toast.LENGTH_SHORT).show();
                }
                text_send.setText("");
            }
        });
        //Access Users table in database where userid = this userid
        reference  = FirebaseDatabase.getInstance().getReference("Users").child(userid);
        reference.addValueEventListener(new ValueEventListener() {
            @Override
            public void onDataChange(@NonNull DataSnapshot snapshot) {
                User user = snapshot.getValue(User.class);
                assert user != null;
                username.setText(user.getUsername());
                if(user.getImageUrl().equals("default")){
                    profileImage.setImageResource(R.drawable.ic_user);
                }else{
                    Glide.with(MessageActivity.this).load(user.getImageUrl()).into(profileImage);
                }
                readMessages(fUser.getUid() , userid , user.getImageUrl());
            }
            @Override
            public void onCancelled(@NonNull DatabaseError error) {
            }
        });
    }
    private void send_message (String sender , String receiver , String message){
        //Make a connection with database
        DatabaseReference reference = FirebaseDatabase.getInstance().getReference();
        //Filling data in an array inside JAVA tools
        HashMap<String , Object> hashMap = new HashMap<>();
        hashMap.put("sender" , sender);
        hashMap.put("receiver" , receiver);
        hashMap.put("message" , message);
        //Push data inside chats table in database
        reference.child("Chats").push().setValue(hashMap);
    }
    private void readMessages(String myid , String userid , String img Url){
        mChat = new ArrayList<>();
        reference = FirebaseDatabase.getInstance().getReference("Chats");
        reference.addValueEventListener(new ValueEventListener() {
            @Override
            public void onDataChange(@NonNull DataSnapshot dataSnapshot) {
                mChat.clear();
                for(DataSnapshot snapshot : dataSnapshot.getChildren()){
                    Chat chat = snapshot.getValue(Chat.class);
                    assert chat != null;
                    if(chat.getReceiver().equals(myid) && chat.getSender().equals(userid) || chat.getReceiver().equals(userid) && chat.getSender().equals(myid)){
                        mChat.add(chat);
                    }
                    //Log.i(TAG, "userid :" + userid);
                    messageAdapter = new MessageAdapter(MessageActivity.this , mChat , img Url);
                    recyclerView.setAdapter(messageAdapter);
                }
            }
            @Override
            public void onCancelled(@NonNull DatabaseError error) {
            }
        });
    }
}

وبعد إتمام عملية البناء. ستظهر لنا تلك النتائج الرائعة!. في حال وجدت بعض المسائل التقنية , فإننا ندعوك إلى الرجوع للشيفرة السابقة وإعادة تطبيقها بشكل جيد.  

عرض المحادثات التي تمت

كما نعلم أنه ليس من المعقول البحث عن المستخدم في صفحة كبيرة ومشاهدة الرسائل التي تمت معه. على سبيل المثال , هذه فرصة جيدة لتطوير قسم المحادثات الخاص والابتعاد عن عرض الرسائل في قسم المستخدمين العامة.

الآن سنقوم بالذهاب إلى صفحة Chats Fragment و fragment chats من جهة التصميم. وذلك لفرض عمليات ظهور آخر المحادثات. ربما نستخدم بعض مصفوفات وقوائم جافا. وتصبح الشيفرة بالشكل التالي:

 

ChatsFragment.java

package com.example.free chat app.Fragments;
import android.os.Bundle;
import androidx.annotation.NonNull;
import android x.fragment.app.Fragment;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import com.example.freechatapp.Adapter.UserAdapter;
import com.example.freechatapp.Model.Chat;
import com.example.freechatapp.Model.User;
import com.example.free chat app.R;
import com.google.firebase.auth.FirebaseAuth;
import com.google.firebase.auth.FirebaseUser;
import com.google.firebase.database.DataSnapshot;
import com.google.firebase.database.DatabaseError;
import com.google.firebase.database.DatabaseReference;
import com.google.firebase.database.FirebaseDatabase;
import com.google.firebase.database.ValueEventListener;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class ChatsFragment extends Fragment {
    private RecyclerView recyclerView;
    private UserAdapter userAdapter;
    private List<User> mUsers;
    FirebaseUser fuser;
    DatabaseReference reference;
    private List<String> usersList;
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_chats, container, false);
        recyclerView = view.findViewById(R.id.recycler_view);
        recyclerView.setHasFixedSize(true);
        recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
        fuser = FirebaseAuth.getInstance().getCurrentUser();
        usersList = new ArrayList<>();
        reference = FirebaseDatabase.getInstance().getReference("Chats");
        reference.addValueEventListener(new ValueEventListener() {
            @Override
            public void onDataChange(@NonNull DataSnapshot dataSnapshot) {
                usersList.clear();
                for(DataSnapshot snapshot : dataSnapshot.getChildren() ){
                    Chat chat = snapshot.getValue(Chat.class);
                    if(chat.getSender().equals(fuser.getUid())){
                        usersList.add(chat.getReceiver());
                    }
                    if(chat.getReceiver().equals(fuser.getUid())){
                        usersList.add(chat.getSender());
                    }
                }
                readChats();
            }
            @Override
            public void onCancelled(@NonNull DatabaseError error) {
            }
        });
        return view;
    }
    private void readChats(){
        mUsers = new ArrayList<>();
        //Remove duplicate content from userList String
        Set<String> set = new HashSet<>(usersList);
        usersList.clear();
        usersList.addAll(set);
        reference = FirebaseDatabase.getInstance().getReference("Users");
        reference.addValueEventListener(new ValueEventListener() {
            @Override
            public void onDataChange(@NonNull DataSnapshot dataSnapshot) {
                mUsers.clear();
                for(DataSnapshot snapshot : dataSnapshot.getChildren()){
                    User user = snapshot.getValue(User.class);
                    for(String id : usersList){
                        assert user != null;
                        if(user.getId().equals(id)){
                            if(mUsers.contains(user.getId()));
                            mUsers.add(user);
                        }
                        // Log.i(TAG, "usersList :" + usersList);
                    }
                    // Log.i(TAG, "userid :" + user.getId());
                }
                userAdapter = new UserAdapter(getContext() , mUsers);
                recyclerView.setAdapter(userAdapter);
            }
            @Override
            public void onCancelled(@NonNull DatabaseError error) {
            }
        });
    }
}

fragment_chats.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/white"
    tools:context=".Fragments.ChatsFragment">
    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recycler_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>
    <TextView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"
        android:textSize="18sp"
        android:textStyle="bold"
        android:visibility="gone"
        android:text="there_is_nothing_to_show"/>
</RelativeLayout>

تستطيع تجربة إضافة حساب جديد إلى القائمة لديك وملاحظة الفرق بين قسم Chats و Users. حيث أن قسم الشات لا يظهر أكثر من الحسابات التي تمت المحادثة معها. وبآخر محادثة تمت بين حساب سلمى وحساب محمد فإنها ستظهر في قسم المحادثات.

الآن وبعد أن انتهينا من إنجاز أهم الأساسيات في تطبيق برنامج دردشه. سنتطرق إلى ملئ قسم الحساب Profile ببعض المعلومات الأساسية. ولتكن صورة المستخدم والاسم الخاص به.  

تحضير صفحة الحساب

صفحة الحساب لا تستغرق لدينا الكثير من الوقت. فكل ما نريده هو ملئ Profile Fragment. ببعض المهام الرئيسية التي تمكنه من عرض المعلومات وفقًا إلى أعمدتها في قواعد البيانات. ولتحقيق ذلك يتطلب منا نسخ هذه الشيفرة.

 

ProfileFragment.java

package com.example.free chat app.Fragments;
import android.os.Bundle;
import androidx.annotation.NonNull;
import android x.fragment.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import com.bumptech.glide.Glide;
import com.example.freechatapp.Model.User;
import com.example.free chat app.R;
import com.google.firebase.auth.FirebaseAuth;
import com.google.firebase.auth.FirebaseUser;
import com.google.firebase.database.DataSnapshot;
import com.google.firebase.database.DatabaseError;
import com.google.firebase.database.DatabaseReference;
import com.google.firebase.database.FirebaseDatabase;
import com.google.firebase.database.ValueEventListener;
import de.hdodenhof.circleimageview.CircleImageView;
public class ProfileFragment extends Fragment {
    CircleImageView profileImage;
    TextView userName;
    DatabaseReference reference;
    FirebaseUser fuser;
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_profile, container, false);
        profileImage = view.findViewById(R.id.profileImage);
        userName = view.findViewById(R.id.username);
        fuser = FirebaseAuth.getInstance().getCurrentUser();
        reference = FirebaseDatabase.getInstance().getReference("Users").child(fuser.getUid());
        reference.addValueEventListener(new ValueEventListener() {
            @Override
            public void onDataChange(@NonNull DataSnapshot snapshot) {
                User user = snapshot.getValue(User.class);
                userName.setText(user.getUsername());     //Assigning user name from database
                if(user.getImageUrl().equals("default")){
                    profileImage.setImageResource(R.drawable.ic_user);
                }else{
                    Glide.with(getContext()).load(user.getImageUrl()).into(profileImage);
                }
            }
            @Override
            public void onCancelled(@NonNull DatabaseError error) {
            }
        });
        return view;
    }
}

fragment_profile.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/white"
    android:padding="8dp"
    tools:context=".Fragments.ProfileFragment">
    <androidx.cardview.widget.CardView
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
        <RelativeLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:padding="8dp">
            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="Profile"
                android:textColor="@color/design_default_color_primary_dark"
                android:textStyle="bold"
                />
            <de.hdodenhof.circleimageview.CircleImageView
                android:id="@+id/profileImage"
                android:layout_width="100dp"
                android:layout_height="100dp"
                android:layout_centerHorizontal="true"
                android:layout_marginTop="50dp"
                android:src="@mipmap/ic_launcher"/>
            <TextView
                android:id="@+id/username"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_below="@id/profileImage"
                android:layout_centerHorizontal="true"
                android:layout_marginTop="15dp"
                android:text="@string/username"
                android:textColor="@color/design_default_color_primary"
                android:textSize="18sp"
                android:textStyle="bold" />
        </RelativeLayout>
    </androidx.cardview.widget.CardView>
</RelativeLayout>

في الوقت الحالي , لا نود إضافة الكثير في صفحة الحساب. بالتالي نحن نركز على أساسيات التطوير وتكوين الصفحات بطريقة تسهل من عملية التطوير لاحقا. وبعد بناء الشيفرة الخاصة بالحساب سيظهر لنا بالشكل التالي:

تمكين صلاحيات التخزين

قبل البدء في رسم وتكوين الشيفرة الخاصة بتغيير صورة الحساب. يجب أولا أن نحدد صلاحية استخدام التخزين في Firebase Console Panel. وتلك العملية لا تستغرق الكثير من الوقت. فكل ما لدينا الآن هو الذهاب إلى Build->Storage.

نقوم الآن بالضغط على Get Started. ومن ثم تفعيل وضعية Test Mode. والتي تتيح لنا القراءة والكتابة في وسائط التخزين.

تغيير صورة الحساب

لقد وصلنا إلى المنطقة التي من خلالها سنبدأ برفع الصور وعرضها للآخرين في قواعد البيانات. بالتالي يتعين علينا إضافة دالة في قسم Profile Fragment. والتي من خلالها تسهل عملية رفع الصور.

 

private void uploadImage(){
        final ProgressDialog pd = new ProgressDialog(getContext());
        pd.setMessage("Uploading..");
        pd.show();
        if(imageUri != null){
            //Rename picture in database including uploading time
            final StorageReference fileReference = storageReference.child(System.currentTimeMillis() +"."+getFileExtension(imageUri));
            uploadTask = fileReference.putFile(imageUri);
            uploadTask.continueWithTask(new Continuation<UploadTask.TaskSnapshot , Task<Uri>>() {
                @Override
                public Task<Uri> then(@NonNull Task<UploadTask.TaskSnapshot> task) throws Exception {
                    if(!task.isSuccessful()){
                        throw task.getException();
                    }
                    return fileReference.getDownloadUrl();
                }
            }).addOnCompleteListener(new OnCompleteListener<Uri>() {
                @Override
                public void onComplete(@NonNull Task<Uri> task) {
                    if(task.isSuccessful()){
                        Uri downloadUri = task.getResult();
                        String mUri = downloadUri.toString();
                        //Link uploaded image with current user
                        reference = FirebaseDatabase.getInstance().getReference("Users").child(fuser.getUid());
                        HashMap<String , Object> map = new HashMap<>();
                        map.put("imageUrl" , mUri);
                        reference.updateChildren(map);
                        pd.dismiss();
                    } else{
                        Toast.makeText(getContext(), "Failed", Toast.LENGTH_SHORT).show();
                        pd.dismiss();
                    }
                }
            }).addOnFailureListener(new OnFailureListener() {
                @Override
                public void onFailure(@NonNull Exception e) {
                    Toast.makeText(getContext(), e.getMessage(), Toast.LENGTH_SHORT).show();
                    pd.dismiss();
                }
            });
        }else{
            Toast.makeText(getContext(), "No image selected!", Toast.LENGTH_SHORT).show();
        }
    }
ملاحظة : يجب عليك إضافة مصادر رفع الصور إلى Firebase. وذلك من خلال تمكين بعض المكتبات.
dependencies {
 ......
      implementation 'com.google.firebase:firebase-storage:20.0.2'
  ......
}

لقد قمنا بإضافة الشيفرة السابقة إلى مصادر Groovy. المتواجدة في Build.gradle(module). حيث بدونها لن يتم إجراء رفع الصور أو وسائط التخزين إلى Firebase storage. والآن نقوم بالكشف عن الشيفرة المحدثة من ProfileFragment. ProfileFragment.xml

package com.example.free chat app.Fragments;
import android.app.ProgressDialog;
import android.content.ContentResolver;
import android.net.Uri;
import android.os.Bundle;
import androidx.activity.result.ActivityResultCallback;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull;
import android x.fragment.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.webkit.MimeTypeMap;
import android.widget.TextView;
import android.widget.Toast;
import com.bumptech.glide.Glide;
import com.example.freechatapp.Model.User;
import com.example.free chat app.R;
import com.google.android.gms.tasks.Continuation;
import com.google.android.gms.tasks.OnCompleteListener;
import com.google.android.gms.tasks.OnFailureListener;
import com.google.android.gms.tasks.Task;
import com.google.firebase.auth.FirebaseAuth;
import com.google.firebase.auth.FirebaseUser;
import com.google.firebase.database.DataSnapshot;
import com.google.firebase.database.DatabaseError;
import com.google.firebase.database.DatabaseReference;
import com.google.firebase.database.FirebaseDatabase;
import com.google.firebase.database.ValueEventListener;
import com.google.firebase.storage.FirebaseStorage;
import com.google.firebase.storage.StorageReference;
import com.google.firebase.storage.StorageTask;
import com.google.firebase.storage.UploadTask;
import java.util.HashMap;
import de.hdodenhof.circleimageview.CircleImageView;
public class ProfileFragment extends Fragment {
    CircleImageView profileImage;
    TextView userName;
    DatabaseReference reference;
    FirebaseUser fuser;
    //******************************************Preparing upload image*********************************************
    StorageReference storageReference;
    private static final int IMAGE_REQUEST = 1;
    private Uri imageUri;
    private StorageTask uploadTask;
    //*************************************************************************************************************
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_profile, container, false);
        profileImage = view.findViewById(R.id.profileImage);
        userName = view.findViewById(R.id.username);
        //******************************************Preparing upload image*********************************************
        //Preparing to Create uploads table for a first time or add images inside this table in db
        storageReference = FirebaseStorage.getInstance().getReference("uploads");
        //*************************************************************************************************************
        fuser = FirebaseAuth.getInstance().getCurrentUser();
        reference = FirebaseDatabase.getInstance().getReference("Users").child(fuser.getUid());
        reference.addValueEventListener(new ValueEventListener() {
            @Override
            public void onDataChange(@NonNull DataSnapshot snapshot) {
                User user = snapshot.getValue(User.class);
                userName.setText(user.getUsername());     //Assigning user name from database
                if(user.getImageUrl().equals("default")){
                    profileImage.setImageResource(R.drawable.ic_user);
                }else{
                     Glide.with(getContext()).load(user.getImageUrl()).into(profileImage);
                }
            }
            @Override
            public void onCancelled(@NonNull DatabaseError error) {
            }
        });
        //******************************************Preparing upload image*********************************************
        profileImage.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                mGetContent.launch("image/*");
            }
        });
        //*************************************************************************************************************
        return view;
    }
    //******************************************Preparing upload image*********************************************
    ActivityResultLauncher<String> mGetContent = registerForActivityResult(new ActivityResultContracts.GetContent(),
            new ActivityResultCallback<Uri>() {
                @Override
                public void onActivityResult(Uri uri ) {
                    // Handle the returned Uri
                    image Uri = uri.normalize Scheme();
                    // Toast.makeText(getContext(), "Get path of URI :" + imageUri, Toast.LENGTH_SHORT).show();
                    if(uploadTask != null && uploadTask.isInProgress()){
                        Toast.makeText(getContext(), "Upload in progress..", Toast.LENGTH_SHORT).show();
                    }else{
                        upload Image();
                    }
                }
            });
    private void uploadImage(){
        final ProgressDialog pd = new ProgressDialog(getContext());
        pd.setMessage("Uploading..");
        pd.show();
        if(imageUri != null){
            //Rename picture in database including uploading time
            final StorageReference fileReference = storageReference.child(System.currentTimeMillis() +"."+getFileExtension(imageUri));
            uploadTask = fileReference.putFile(imageUri);
            uploadTask.continueWithTask(new Continuation<UploadTask.TaskSnapshot , Task<Uri>>() {
                @Override
                public Task<Uri> then(@NonNull Task<UploadTask.TaskSnapshot> task) throws Exception {
                    if(!task.isSuccessful()){
                        throw task.getException();
                    }
                    return fileReference.getDownloadUrl();
                }
            }).addOnCompleteListener(new OnCompleteListener<Uri>() {
                @Override
                public void onComplete(@NonNull Task<Uri> task) {
                    if(task.isSuccessful()){
                        Uri downloadUri = task.getResult();
                        String mUri = downloadUri.toString();
                        //Link uploaded image with current user
                        reference = FirebaseDatabase.getInstance().getReference("Users").child(fuser.getUid());
                        HashMap<String , Object> map = new HashMap<>();
                        map.put("imageUrl" , mUri);
                        reference.updateChildren(map);
                        pd.dismiss();
                    } else{
                        Toast.makeText(getContext(), "Failed", Toast.LENGTH_SHORT).show();
                        pd.dismiss();
                    }
                }
            }).addOnFailureListener(new OnFailureListener() {
                @Override
                public void onFailure(@NonNull Exception e) {
                    Toast.makeText(getContext(), e.getMessage(), Toast.LENGTH_SHORT).show();
                    pd.dismiss();
                }
            });
        }else{
            Toast.makeText(getContext(), "No image selected!", Toast.LENGTH_SHORT).show();
        }
    }
    private String getFileExtension(Uri uri){
        ContentResolver contentResolver = getContext().getContentResolver();
        MimeType Map mimeTypeMap = MimeTypeMap.getSingleton();
        return mimeTypeMap.getExtensionFromMimeType(contentResolver.getType(uri));
    }
}

لقد قمت بارفاق صورتي الشخصية لـ أتفحص عملية التدوير التلقائي للصور. وقد كانت مكتبة Glide على قدر رائع من تلك المهمة!. نلاحظ أن الصورة تظهر أيضا عند البدء في المحادثات. حيث أن الشيفرة كانت تشير إلى عرض الصورة المحددة أو التطرق إلى الصورة الإفتراضية التي قمنا بتعيينها.

تفعيل الظهور كمتصل

إن الظهور كمتصل هي إحدى مهام أي برنامج دردشة او موقع تواصل اجتماعي. ووجودها يعزز من قوة البرامج المنتشرة. البعض من المبرمجين يتطرق إلى إضافات جديدة مثل إخفاء حالات الظهور أو عرض المشاهدة وعدم المشاهدة.

بالتالي هي جميعها مزايا إضافية تعزز من جودة التطبيقات. وفي هذه الدورة سنحاول تغطية مصادر عمليات الاتصال من خلال الجلسات التي تجمع بين خصائص الحسابات.

في البداية سنقوم بربط عمليات الاتصال في الصفحة الرئيسية MainActivity. وذلك لأنها هي الرابط الوحيد لتواجد المستخدم في التطبيق. فهي تعمل بمثابة طبقة سفلية يتم من خلالها زيارة الأقسام وعمليات طلب الخادم.

يمنحنا AppCompatActivity الاتصال بوظائف متعددة بالوراثة. فهو يوفر لنا مساحة لإضافة الأحداث عند دخول وخروج المستخدم من التطبيق.

على سبيل المثال في حالتنا سنحتاج الدالة onResume و onPause , وذلك للتعبير عن عمليات الاتصال وعدم الاتصال. بالتالي سنعمل على اضافة الكود التالي في Main Activity.

 

//********************************************************Set User Status Method**************************************************//
  private void status(String status){
      reference = FirebaseDatabase.getInstance().getReference("Users").child(firebaseUser.getUid());
      HashMap<String , Object> hashMap = new HashMap<>();
      hashMap.put("status" , status);
      reference.updateChildren(hashMap);
  }
  @Override
  protected void onResume() {
      super.onResume();
      status("Online");
  }
  @Override
  protected void onPause() {
      super.onPause();
      status("Offline");
  }

وبما أن قواعد البيانات تعمل بخاصية Realtime Database. فإن من المؤكد تحديث عمليات توافد وخروج الزائرين من التطبيق , وهذا ما قامت به الدالة status مؤخرًا.   MainActivity.java

package com.example.free chatapp;
import androidx.annotation.NonNull;
import android x.appcompat.app.AppCompatActivity;
import android x.appcompat.widget.Toolbar;
import android x.viewpager2.widget.ViewPager2;
import android.content.Intent;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.TextView;
import com.bumptech.glide.Glide;
import com.example.freechatapp.Fragments.FragmentAdapter;
import com.example.freechatapp.Model.User;
import com.google.android.material.tabs.TabLayout;
import com.google.android.material.tabs.tabLe layout Mediator;
import com.google.firebase.auth.FirebaseAuth;
import com.google.firebase.auth.FirebaseUser;
import com.google.firebase.database.DataSnapshot;
import com.google.firebase.database.DatabaseError;
import com.google.firebase.database.DatabaseReference;
import com.google.firebase.database.FirebaseDatabase;
import com.google.firebase.database.ValueEventListener;
import java.util.HashMap;
import de.hdodenhof.circleimageview.CircleImageView;
public class MainActivity extends AppCompatActivity {
    CircleImageView profile_image;
    TextView userName;
    FirebaseUser firebaseUser;
    DatabaseReference reference;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Toolbar toolbar = findViewById(R.id.toolBar);
        //setSupportActionBar(toolbar);
        getSupportActionBar().setTitle("");
        profile_image = findViewById(R.id.profileImage);
        userName = findViewById(R.id.username);
        //Prepare database connection
        firebaseUser = FirebaseAuth.getInstance().getCurrentUser();
        reference = FirebaseDatabase.getInstance().getReference("Users").child(firebaseUser.getUid());
        reference.addValueEventListener(new ValueEventListener() {
            @Override
            public void onDataChange(@NonNull DataSnapshot snapshot) {
                User user = snapshot.getValue(User.class);  //Send requirement to database
                userName.setText(user.getUsername());
                if(user.getImageUrl().equals("default")){
                    profile_image.setImageResource(R.drawable.ic_user);
                } else{
                    //Loading image  / Check Glide library if crashed
                    Glide.with(MainActivity.this).load(user.getImageUrl()).into(profile_image);
                }
            }
            @Override
            public void onCancelled(@NonNull DatabaseError error) {
            }
        });
        TabLayout tabLayout = findViewById(R.id.tab_layout);
        ViewPager2 viewPager = findViewById(R.id.view_pager);
        /***********************************Preparing Pager Fragments************************************************************/
        viewPager.setAdapter(new FragmentAdapter(this));
        tabLe layout Mediator tabLe layout Mediator = new tabLe layout Mediator (tabLayout, viewPager, new tabLe layout Mediator .TabConfigurationStrategy() {
            @Override
            public void onConfigureTab(@NonNull TabLayout.Tab tab, int position) {
                switch (position){
                    case 0:{
                        tab.setText("Users");
                        tab.setIcon(getResources().getDrawable(R.drawable.ic_users));
                        break;
                    }
                    case 1:{
                        tab.setText("Chat");
                        tab.setIcon(getResources().getDrawable(R.drawable.ic_chat));
                        break;
                    }
                    case 2:{
                        tab.setText("Profile");
                        tab.setIcon(getResources().getDrawable(R.drawable.ic_profile));
                        break;
                    }
                }
            }
        });
        tabLe layout Mediator .attach();
    }
    //****************************************************Menu Items Here************************************************************
    //This source for menu activation
    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.menu , menu);
        return true;
    }
    @Override
    public boolean onOptionsItemSelected(@NonNull MenuItem item) {
        switch (item.getItemId()){
            case R.id.logout:
                FirebaseAuth.getInstance().signOut();  //Call database to create sighout task
                startActivity(new Intent(MainActivity.this , StartActivity.class));
                finish();
                return true;
        }
        return false;
    }
    //***********************************************************************************************************************************
    //********************************************************Set User Status Method**************************************************//
    private void status(String status){
        reference = FirebaseDatabase.getInstance().getReference("Users").child(firebaseUser.getUid());
        HashMap<String , Object> hashMap = new HashMap<>();
        hashMap.put("status" , status);
        reference.updateChildren(hashMap);
    }
    @Override
    protected void onResume() {
        super.onResume();
        status("Online");
    }
    @Override
    protected void onPause() {
        super.onPause();
        status("Offline");
    }
}

بالتالي وعند عملية البناء لو قمنا بالتحقق من قواعد البيانات Firebase فسنجد بأن هناك عمود status. يظهر لنا Offline و Online. أثناء قدوم أو خروج المستخدم من التطبيق. الآن وبعد أن قمنا ببقاء الحالة الخاصة بالمستخدمين قيد التحديث.

سيتطلب ذلك التأثير ببعض المتغيرات والعناصر المتواجدة في صورة المستخدمين وحساباتهم. على سبيل المثال , مثل إضافة نقطة خضراء أو حمراء.

التعديل في user item

سنقوم بإضافة صور حالات الإتصال الصغيرة في صفحة user item.xml. بالتالي ستصبح على النحو التالي:

 

user item.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:padding="10dp">
    <de.hdodenhof.circleimageview.CircleImageView
        android:id="@+id/profileImage"
        android:layout_width="50dp"
        android:layout_height="50dp"
        android:src="@mipmap/ic_launcher"/>
    <TextView
        android:id="@+id/username"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/username"
        android:layout_toEndOf="@+id/profileImage"
        android:layout_marginStart="10dp"
        android:layout_centerVertical="true"
        android:textSize="18sp"/>
    <de.hdodenhof.circleimageview.CircleImageView
        android:id="@+id/onlineStatus"
        android:layout_width="15dp"
        android:layout_height="15dp"
        app:civ_border_width="10dp"
        app:civ_border_color="#05de29"
        android:visibility="gone"
        android:src="@mipmap/ic_launcher"
        android:layout_below="@id/username"
        android:layout_marginTop="10dp"
        android:layout_marginStart="-15dp"
        android:layout_toEndOf="@id/profileImage"/>
    <de.hdodenhof.circleimageview.CircleImageView
        android:id="@+id/offlineStatus"
        android:layout_width="15dp"
        android:layout_height="15dp"
        app:civ_border_width="10dp"
        app:civ_border_color="#"
        android:visibility="gone"
        android:src="@mipmap/ic_launcher"
        android:layout_below="@id/username"
        android:layout_marginTop="10dp"
        android:layout_marginStart="-15dp"
        android:layout_toEndOf="@id/profileImage"/>
</RelativeLayout>

إضافة عنصر حالة الإتصال

لقد قمنا بإضافة صورتين ووضعناها في حالة إخفاء. بالتالي فإن لدينا فرصة قوية في التعديل على الشيفرة في صفحة User Adapter.

لكن قبل ذلك يجب علينا اضافة عنصر جديد في كلاس User. وذلك لنتمكن من الاستعلام عنه قبل اتخاذ الحدث. بالتالي تصبح شيفرة User تماما كما هي الشيفرة التالية:

package com.example.free chatapp.Model;
public class User {
    private String id;
    private String username;
    private String imageUrl;
    private String status;
    public User(String id, String username, String imageUrl , String status) {
        this.id = id;
        this.username = username;
        this.imageUrl = imageUrl;
        this.status = status;
    }
    public User(){
    }
    public String getId() {
        return id;
    }
    public String getUsername() {
        return username;
    }
    public String getImageUrl() {
        return imageUrl;
    }
    public String getStatus() { return status; }
    public void setUsername(String username) {
        this.username = username;
    }
    public void setImageUrl(String imageUrl) {
        this.imageUrl = imageUrl;
    }
    public void setStatus(String status) {
        this.status = status;
    }
}

إجراء تعديل على شيفرة User Adapter

سيتم الاشارة إلى التعديلات بأكثر من مكان في الكلاس UserAdapter. وذلك فقط من أجل أن تعمل الشيفرة التالية:

 

//*************************************************Check Chat Status******************************************
       if(is Chat){
           if(user.getStatus().equals("Online")){
               holder.onlineStatus.setVisibility(View.VISIBLE);
               holder.offlineStatus.setVisibility(View.GONE);
           }else{
               holder.onlineStatus.setVisibility(View.GONE);
               holder.offlineStatus.setVisibility(View.VISIBLE);
           }
       }else{
           holder.onlineStatus.setVisibility(View.GONE);
           holder.offlineStatus.setVisibility(View.GONE);
       }

وبالتالي سيطرأ تغيير كبير على إضافة بعض العناصر في مناطق holder غيرها. وذلك لأننا نعمل في Adapter وليس في AppCompatActivity.

كما أننا قمنا بإجراء تغيير في Constructor. و لتجنب تلك المشكلة في مناطق أخرى من المشروع سنعمل على عمل overloading.

UserAdapter.java

package com.example.free chatapp.Adapter;
import android.content.Context;
import android.content.Intent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.Glide;
import com.example.freechatapp.MessageActivity;
import com.example.freechatapp.Model.User;
import com.example.free chat app.R;
import java.util.List;
public class UserAdapter extends RecyclerView.Adapter<UserAdapter.ViewHolder> {
    private Context mContext;
    private List<User> mUser;
    private boolean isChat;
    public UserAdapter(Context mContext , List<User> mUsers){
        this.mContext = mContext;
        this.mUser = mUsers;
    }
    public UserAdapter(Context mContext , List<User> mUsers , boolean isChat){
        this.mContext = mContext;
        this.mUser = mUsers;
        this.isChat = isChat;
    }
    @NonNull
    @Override
    public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(mContext).inflate(R.layout.user_item , parent , false);
        return new UserAdapter.ViewHolder(view);
    }
    @Override
    public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
        //When call database by using this function we will get all data of user for views
        User user = mUser.get(position);
        holder.username.setText(user.getUsername());
        if(user.getImageUrl().equals("default")){
            holder.profileImage.setImageResource(R.drawable.ic_user); //put your icon resource here
        } else{
            Glide.with(mContext).load(user.getImageUrl()).into(holder.profileImage);
        }
        //*************************************************Check Chat Status******************************************
        if(user.getStatus().equals("Online")){
            holder.onlineStatus.setVisibility(View.VISIBLE);
            holder.offlineStatus.setVisibility(View.GONE);
        }else{
            holder.onlineStatus.setVisibility(View.GONE);
            holder.offlineStatus.setVisibility(View.VISIBLE);
        }
        //On click on user open MessageActivity
        holder.itemView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Intent intent = new Intent(mContext , MessageActivity.class);
                intent.putExtra("userid" , user.getId());
                mContext.startActivity(intent);
            }
        });
    }
    @Override
    public int get ItemCount() {
        return mUser.size();
    }
    public class ViewHolder extends RecyclerView.ViewHolder{
        public TextView username;
        public ImageView profileImage;
        public ImageView onlineStatus;
        public ImageView offlineStatus;
        public ViewHolder(@NonNull View itemView) {
            super(itemView);
            username = itemView.findViewById(R.id.username);
            profileImage = itemView.findViewById(R.id.profileImage);
            onlineStatus = itemView.findViewById(R.id.onlineStatus);
            offlineStatus = itemView.findViewById(R.id.offlineStatus);
        }
    }
}

    بالرغم من أن الشيفرة تعمل الآن لكن في حال كان لديك أكثر من حساب فإن انكسار التطبيق أثناء التطبيق سيحدث. وذلك بسبب عدم إضافة عمود الحالة الخاصة أثناء تفعيل أي حساب من قبل.

لذلك الأمر سوف نتجاوز المشكلة بالذهاب إلى صفحة Register وإضافة العنصر التالي في المصفوفة.  

hashMap.put("status" , "default");

لكن ولسوء الحظ فإن التطبيق لن يحافظ على حالة الاتصال عند فتح صفحة المحادثات. وذلك بسبب مغادرة صفحة MainActivity. ونستطيع تجاوز المشكلة بشكل مؤقت من خلال تكرار الكود التالي في صفحة المحادثات.

//********************************************************Set User Status Method**************************************************//
   private void status(String status){
       reference = FirebaseDatabase.getInstance().getReference("Users").child(firebaseUser.getUid());
       HashMap<String , Object> hashMap = new HashMap<>();
       hashMap.put("status" , status);
       reference.updateChildren(hashMap);
   }
   @Override
   protected void onResume() {
       super.onResume();
       status("Online");
   }
   @Override
   protected void onPause() {
       super.onPause();
       status("Offline");
   }

 

تفعيل خاصية مشاهدة الرسائل

تعتبر خصائص مشاهدة الرسائل ميزة إضافية لكل من يريد إرفاقها بتطبيقه. على سبيل المثال , يمتلك أي برنامج دردشه مزايا جيدة في حال تم تفعيل مشاهدات الرسائل.

ولأن الشيفرة التي لدينا أساسية للغاية فإننا سنركز على إضافة حالات المشاهدة في صفحة MessageAdapter.

بداية سنحلل الإجراء قبل المباشرة بعرض الشيفرة. على سبيل المثال فإن صفحة MessageAdapter. تحتوي على مصدرين XML , الأول يقوم بعرض رسائل المرسل والثاني يقوم بعرض رسائل الردود.

وفي حال تفعيل حالات المشاهدة من قبل الآخرين فإن اهتمامنا يقتصر على  chat_item_right وهو الخاص بالمرسل. سنذهب الآن إلى chat_item_right ومن ثم نعمل على تخصيص منطقة حالات المشاهدة. بالتالي يصبح الكود:

 

chat_item_right.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="8dp">
    <RelativeLayout
        android:layout_width="300dp"
        android:layout_height="wrap_content"
        android:layout_alignParentEnd="true">
        <de.hdodenhof.circleimageview.CircleImageView
            android:id="@+id/profileImage"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:visibility="gone" />
        <TextView
            android:id="@+id/show_message"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentEnd="true"
            android:background="@drawable/background_right"
            android:padding="8dp"
            android:text="message_will_appear_here"
            android:textSize="18sp" />
        <TextView
            android:id="@+id/text_seen"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_below="@id/show_message"
            android:layout_alignParentEnd="true"/>
    </RelativeLayout>
</RelativeLayout>

لقد تم إضافة العنصر الجديد في آخر الكود , وهو الذي يساعد على تفعيل حالات المشاهدة من عدمها.     يتم استخدام خدعة بسيطة في برنامج دردشه , حيث لا يتم الإعتماد على ظهور الرسالة خلال التمرير بـ scrollbars. بينما يتم الاعتماد على الوصول إلى آخر رسالة تم جلبها من كائن chat.

وفي حالة دخول المستقبل إلى المحادثة وعرض آخر رسالة من المحول سيتم تحديث حالة المشاهدة لدى المرسل. يستدعي ذلك إضافة متغير جديد للكلاس Chat. وهو is Seen. من نوع Boolean. لتصبح شيفرة Chat على النحو التالي:   Chat.java

package com.example.free chatapp.Model;
public class Chat {
    private String sender;
    private String receiver;
    private String message;
    private boolean isseen;
    public Chat(String sender, String receiver, String message , boolean isseen) {
        this.sender = sender;
        this.receiver = receiver;
        this.message = message;
        this.is seen = isseen;
    }
    public Chat(){
    }
    public String getSender() {
        return sender;
    }
    public void setSender(String sender) {
        this.sender = sender;
    }
    public String getReceiver() {
        return receiver;
    }
    public void setReceiver(String receiver) {
        this.receiver = receiver;
    }
    public String getMessage() {
        return message;
    }
    public void setMessage(String message) {
        this.message = message;
    }
    public boolean isIsseen() {
        return isseen;
    }
    public void setIsseen(boolean isseen) {
        this.is seen = isseen;
    }
}

 

ملاحظة : قد تظهر مشاكل مكتبة Glide عند عملية المحادثات. وتستطيع تجاوزها باستخدام Picasso.
dependencies {
....
implementation 'com.square up.picasso:picasso:2.5.2'
....
}

كما تستطيع استبدال Glide بـمشغل Picasso ليصبح بالشكل التالي:

Picasso.with(getApplicationContext())
                           .load(user.getImageUrl()) // web image url
                           .fit().centerInside()
                           .rotate(90)                    //if you want to rotate by 90 degrees
                           .error(R.drawable.ic_user)
                           .placeholder(R.drawable.ic_user)
                           .into(profileImage);

      في صفحة MessageAdapter , يتم الاستعانة بعمليات التحقق عن طريق الشرط التالي:

//*********************************************Check if chat is seen or not*******************************************
        if(position == mChat.size()-1){   // last message in chat list
            if(chat.isIsseen()){
                holder.text_seen.setText("seen");
            }else{
                holder.text_seen.setText("delivered");
            }
        }else{
            holder.text_seen.setVisibility(View.GONE);
        }
        //*********************************************************************************************************************

وبالتالي فإن الكود يصبح بهذا الشكل : MessageAdapter.java

package com.example.free chatapp.Adapter;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.Glide;
import com.example.freechatapp.Model.Chat;
import com.example.free chat app.R;
import com.google.firebase.auth.FirebaseAuth;
import com.google.firebase.auth.FirebaseUser;
import java.util.List;
public class MessageAdapter extends RecyclerView.Adapter<MessageAdapter.ViewHolder> {
    public static final int MSG_TYPE_LEFT = 0;
    public static final int MSG_TYPE_RIGHT = 1;
    private Context mContext;
    private List<Chat> mChat;
    private String imageUrl;
    FirebaseUser fUser;
    public MessageAdapter(Context mContext , List<Chat> mChat , String imageUrl){
        this.mContext = mContext;
        this.mChat = mChat;
        this.imageUrl = imageUrl;
    }
    @NonNull
    @Override
    public MessageAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        //Set direct of messages LEFT/RIGHT when chat starts
        if (viewType == MSG_TYPE_RIGHT) {
            View view = LayoutInflater.from(mContext).inflate(R.layout.chat_item_right, parent, false);
            return new MessageAdapter.ViewHolder(view);
        }else{
            View view = LayoutInflater.from(mContext).inflate(R.layout.chat_item_left, parent, false);
            return new MessageAdapter.ViewHolder(view);
        }
    }
    @Override
    public void onBindViewHolder(@NonNull MessageAdapter.ViewHolder holder, int position) {
        Chat chat = mChat.get(position);
        holder.show_message.setText(chat.getMessage());
        if(imageUrl.equals("default")){
            holder.profileImage.setImageResource(R.drawable.ic_user);
        }else {
               Glide.with(mContext).load(imageUrl).into(holder.profileImage);
        }
        if(chat.getSender().equals(fUser.getUid()))
        {
            //*********************************************Check if chat is seen or not*******************************************
            if(position == mChat.size()-1){   // last message in chat list
                if(chat.isIsseen()){
                     holder.text_seen.setText("seen");
                }else{
                     holder.text_seen.setText("delivered");
                }
            }else{
                holder.text_seen.setVisibility(View.GONE);
            }
            //*********************************************************************************************************************
        }
    }
    @Override
    public int get ItemCount() {
        return mChat.size();
    }
    public class ViewHolder extends RecyclerView.ViewHolder{
        public TextView show_message;
        public ImageView profileImage;
        //Chat seen textView
        public TextView text_seen;
        public ViewHolder(@NonNull View itemView) {
            super(itemView);
            show_message = itemView.findViewById(R.id.show_message);
            profileImage = itemView.findViewById(R.id.profileImage);
            text_seen = itemView.findViewById(R.id.text_seen);
        }
    }
    @Override
    public int getItemViewType(int position) {
        fUser = FirebaseAuth.getInstance().getCurrentUser();
        if(mChat.get(position).getSender().equals(fUser.getUid())){
            return MSG_TYPE_RIGHT;
        }else{
            return MSG_TYPE_LEFT;
        }
    }
}

  في حال قمت بتشغيل التطبيق , سيتم العثور على مشاكل بسبب وجود رسائل قديمة بدون قيم عمود is Seen. ولتجاوز تلك المشكلة سنعمل على حذف جدول المحادثات Chats من Firebase. وتستطيع القيام بذلك بنفسك الآن.

لا ننسى أننا قمنا بإضافة متغير جديد في كلاس Chat. وهو العنصر الذي يمكنك من تفعيل عمود is Seen في قواعد البيانات. بالتالي يتعين علينا إضافة قيمة جديدة عند عمليات إرسال الرسائل في Message Activity. على سبيل المثال الدالة send_message. ستتأثر بتلك القيمة وهي :  

hashMap.put("isseen" , false); //create primary table for seen or not seen chat

لتصبح شيفرة Message Activity على النحو التالي:   MessageActivity.java

package com.example.free chatapp;
import androidx.annotation.NonNull;
import android x.appcompat.app.AppCompatActivity;
import android x.appcompat.widget.Toolbar;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.TextView;
import android.widget.Toast;
import com.bumptech.glide.Glide;
import com.example.freechatapp.Adapter.MessageAdapter;
import com.example.freechatapp.Model.Chat;
import com.example.freechatapp.Model.User;
import com.google.firebase.auth.FirebaseAuth;
import com.google.firebase.auth.FirebaseUser;
import com.google.firebase.database.DataSnapshot;
import com.google.firebase.database.DatabaseError;
import com.google.firebase.database.DatabaseReference;
import com.google.firebase.database.FirebaseDatabase;
import com.google.firebase.database.ValueEventListener;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import de.hdodenhof.circleimageview.CircleImageView;
public class MessageActivity extends AppCompatActivity {
    CircleImageView profileImage;
    TextView username;
    FirebaseUser fUser;
    DatabaseReference reference;
    ImageButton btn_send;
    EditText text_send;
    MessageAdapter messageAdapter;
    List<Chat> mChat;
    RecyclerView recyclerView;
    Intent intent;
    //This is for seen or not seen messages
    Value EventListener seen Listener;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_message);
        Toolbar toolbar = findViewById(R.id.toolBar);
        //setSupportActionBar(toolbar);
        getSupportActionBar().setTitle("");
        getSupportActionBar().setDisplayHomeAsUpEnabled(true);
        toolbar.setNavigationOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                //finish();
                startActivity(new Intent(MessageActivity.this , MainActivity.class).setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP));
            }
        });
        recyclerView = findViewById(R.id.recycler_view);
        recyclerView.setHasFixedSize(true);
        LinearLayoutManager linearLayoutManager = new LinearLayoutManager(getApplicationContext());
        linearLayoutManager.setStackFromEnd(true);
        recyclerView.setLayoutManager(linearLayoutManager);
        profileImage = findViewById(R.id.profileImage);
        username = findViewById(R.id.username);
        btn_send = findViewById(R.id.btn_send);
        text_send = findViewById(R.id.text_send);
        //Receiving id from previous page
        intent = getIntent();
        String userid = intent.getStringExtra("userid");
        fUser = FirebaseAuth.getInstance().getCurrentUser();
        btn_send.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                String msg = text_send.getText().toString();
                if(!msg.equals("")){
                    //Send messages to firebase database from sender to receiver
                    //fUser is declared above and it is allowed to be used everywhere in this class
                    send_message(fUser.getUid() , userid , msg);
                }else{
                    Toast.makeText(MessageActivity.this, "You can't send an empty message!", Toast.LENGTH_SHORT).show();
                }
                text_send.setText("");
            }
        });
        //Access Users table in database where userid = this userid
        reference  = FirebaseDatabase.getInstance().getReference("Users").child(userid);
        reference.addValueEventListener(new ValueEventListener() {
            @Override
            public void onDataChange(@NonNull DataSnapshot snapshot) {
                User user = snapshot.getValue(User.class);
                assert user != null;
                username.setText(user.getUsername());
                if(user.getImageUrl().equals("default")){
                    profileImage.setImageResource(R.drawable.ic_user);
                }else{
                      Glide.with(MessageActivity.this).load(user.getImageUrl()).into(profileImage);
                   /* Picasso.with(getApplicationContext())
                            .load(user.getImageUrl()) // web image url
                            .fit().centerInside()
                            .rotate(90)                    //if you want to rotate by 90 degrees
                            .error(R.drawable.ic_users)
                            .placeholder(R.drawable.ic_users)
                            .into(profileImage);*/
                }
                readMessages(fUser.getUid() , userid , user.getImageUrl());
            }
            @Override
            public void onCancelled(@NonNull DatabaseError error) {
            }
        });
        seenMessage(userid);//This is to call seen method
    }
    //************************************************This code for seen or not seen messages*****************************************
    private void seenMessage(String userId){
        reference = FirebaseDatabase.getInstance().getReference("Chats");
        seenListener = reference.addValueEventListener(new ValueEventListener() {
            @Override
            public void onDataChange(@NonNull DataSnapshot dataSnapshot) {
                for(DataSnapshot snapshot : dataSnapshot.getChildren()){
                    Chat chat = snapshot.getValue(Chat.class);
                    if(chat.getReceiver().equals(fUser.getUid()) && chat.getSender().equals(userId)){
                        HashMap<String , Object> hashMap = new HashMap<>();
                        hashMap.put("isseen" , true);
                        snapshot.get Ref().updateChildren(hashMap);
                    }
                }
            }
            @Override
            public void onCancelled(@NonNull DatabaseError error) {
            }
        });
    }
    //*********************************************************************************************************************************
    private void send_message (String sender , String receiver , String message){
        //Make a connection with database
        DatabaseReference reference = FirebaseDatabase.getInstance().getReference();
        //Filling data in an array inside JAVA tools
        HashMap<String , Object> hashMap = new HashMap<>();
        hashMap.put("sender" , sender);
        hashMap.put("receiver" , receiver);
        hashMap.put("message" , message);
        hashMap.put("isseen" , false); //create primary table for seen or not seen chat
        //Push data inside chats table in database
        reference.child("Chats").push().setValue(hashMap);
    }
    private void readMessages(String myid , String userid , String img Url){
        mChat = new ArrayList<>();
        reference = FirebaseDatabase.getInstance().getReference("Chats");
        reference.addValueEventListener(new ValueEventListener() {
            @Override
            public void onDataChange(@NonNull DataSnapshot dataSnapshot) {
                mChat.clear();
                for(DataSnapshot snapshot : dataSnapshot.getChildren()){
                    Chat chat = snapshot.getValue(Chat.class);
                    assert chat != null;
                    if(chat.getReceiver().equals(myid) && chat.getSender().equals(userid) || chat.getReceiver().equals(userid) && chat.getSender().equals(myid)){
                        mChat.add(chat);
                    }
                    //Log.i(TAG, "userid :" + userid);
                    messageAdapter = new MessageAdapter(MessageActivity.this , mChat , img Url);
                    recyclerView.setAdapter(messageAdapter);
                }
            }
            @Override
            public void onCancelled(@NonNull DatabaseError error) {
            }
        });
    }
    //********************************************************Set User Status Method**************************************************//
    private void status(String status){
        reference = FirebaseDatabase.getInstance().getReference("Users").child(fUser.getUid());
        HashMap<String , Object> hashMap = new HashMap<>();
        hashMap.put("status" , status);
        reference.updateChildren(hashMap);
    }
    @Override
    protected void onResume() {
        super.onResume();
        status("Online");
    }
    @Override
    protected void onPause() {
        super.onPause();
        reference.removeEventListener(seenListener);  //On leave app we will remove seen method
        status("Offline");
    }
}

نلاحظ أنه عند إرسال الرسائل سيتم إضافة عمود جديد في المحادثات , بالتالي يتم الاستعانة به في MessageAdapter. وتأتي نتائج التطبيق وبناء الشيفرة بالشكل التالي:  

نسيت كلمة المرور

نأتي الآن إلى خاصية نسيان كلمة المرور , والتي يتم من خلالها الاستعانة بالخادم عبر البريد الإلكتروني. لذا من الحرص أن يقوم المستخدم بإدخال حساب حقيقي في كل برنامج دردشه. بالتالي تتطلب منا عملية حماية الحساب إضافة خاصيتين وهما:

  1. إضافة عنصر وحزمة توجيه لصفحة الحماية.
  2. الاتصال بقواعد البيانات وطلب تعيين كلمة المرور من جديد.

 

التوجيه لصفحة الحماية

نقوم الآن بالدخول إلى صفحة activity login.xml وننسخ العنصر التالي:

 

<TextView
            android:id="@+id/forgot_password"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:gravity="center"
            android:text="Forgot Password"
            android:layout_marginTop="10dp"
            android:textColor="@color/purple 500"/>

activity login.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".LoginActivity">
    <include
        android:id="@+id/toolBar"
        layout="@layout/bar_layout" />
    <LinearLayout
        android:layout_width="match_parent"
        android:orientation="vertical"
        android:gravity="center_horizontal"
        android:layout_below="@+id/toolBar"
        android:padding="16dp"
        android:layout_height="wrap_content">
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Login"
            android:textSize="20sp"
            android:textStyle="bold"/>
        <com. reng wuxian.material edittext.Material EditText
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:id="@+id/email"
            android:inputType="textEmailAddress"
            android:layout_marginTop="10dp"
            app:met_floatingLabel="normal"
            android:hint="@string/email"/>
        <com. reng wuxian.material edittext.Material EditText
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:id="@+id/password"
            android:inputType="textPassword"
            android:layout_marginTop="10dp"
            app:met_floatingLabel="normal"
            android:hint="@string/password"/>
        <Button
            android:id="@+id/btn_login"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="Login"
            android:layout_marginTop="10dp"
            android:background="@color/white"
            android:textColor="#FFF"/>
        <TextView
            android:id="@+id/forgot_password"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:gravity="center"
            android:text="Forgot Password"
            android:layout_marginTop="10dp"
            android:textColor="@color/purple 500"/>
    </LinearLayout>
</RelativeLayout>

وفي صفحة Login Activity يتم إضافة صفحة متغير من نوع TextView. مع تحديد القيام بوظيفة عند النقر فوقه:

forgot_password.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                startActivity(new Intent(LoginActivity.this , ResetPasswordActivity.class));
            }
        });

  ليصبح كود LoginActivity على النحو التالي: LoginActivity.java

package com.example.free chatapp;
import androidx.annotation.NonNull;
import android x.appcompat.app.AppCompatActivity;
import android x.appcompat.widget.Toolbar;
import android.content.Intent;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
import com.google.android.gms.tasks.OnCompleteListener;
import com.google.android.gms.tasks.Task;
import com.google.firebase.auth.AuthResult;
import com.google.firebase.auth.FirebaseAuth;
import com. reng wuxian.material edittext.Material EditText;
public class LoginActivity extends AppCompatActivity {
    MaterialEditText email , password;
    Button btn_login;
    FirebaseAuth auth;
    TextView forgot_password;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_login);
        Toolbar toolbar = findViewById(R.id.toolBar);
       // setSupportActionBar(toolbar);
        getSupportActionBar().setTitle("Login");
        getSupportActionBar().setDisplayHomeAsUpEnabled(true);
        auth = FirebaseAuth.getInstance();
        email = findViewById(R.id.email);
        password = findViewById(R.id.password);
        btn_login = findViewById(R.id.btn_login);
        forgot_password = findViewById(R.id.forgot_password);
        forgot_password.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                startActivity(new Intent(LoginActivity.this , ResetPasswordActivity.class));
            }
        });
        btn_login.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                String txt_email = email.getText().toString();
                String txt_password = password.getText().toString();
                if(TextUtils.isEmpty(txt_email) || TextUtils.isEmpty(txt_password)){
                    Toast.makeText(LoginActivity.this, "All Fields Are Required!", Toast.LENGTH_SHORT).show();
                }else{
                    auth.signInWithEmailAndPassword(txt_email , txt_password)
                            .addOnCompleteListener(new OnCompleteListener<AuthResult>() {
                                @Override
                                public void onComplete(@NonNull Task<AuthResult> task) {
                                    if(task.isSuccessful()){
                                        Intent intent = new Intent(LoginActivity.this , MainActivity.class);
                                        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
                                        startActivity(intent);
                                        finish();
                                    }else{
                                        Toast.makeText(LoginActivity.this, "task: " + task.getException(), Toast.LENGTH_SHORT).show();
                                    }
                                }
                            });
                }
            }
        });
    }
}

  بالتالي سنقوم الآن ببناء Empty Activity ولتكن بعنوان ResetPassword Activity.

تحديث صفحة الحماية مع قواعد البيانات

بعد أن قمنا بإضافة Activity بعنوان ResetPasswordActivity , يتطلب منا ملئها بعض الشيفرة الخاصة بحماية الحساب. على سبيل المثال سنحتاج إلى صندوق في صفحة activity reset password وذلك لوضع البريد الإلكتروني.

كما لا بد من وجود وظائف الاتصال بقواعد البيانات. حيث يعمل كائن FirebaseAuth على ارسال كلمة المرور إلى بريدك الإلكتروني.   ResetPasswordActivity.java

package com.example.free chatapp;
import androidx.annotation.NonNull;
import android x.appcompat.app.AppCompatActivity;
import android x.appcompat.widget.Toolbar;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;
import com.google.android.gms.tasks.OnCompleteListener;
import com.google.android.gms.tasks.Task;
import com.google.firebase.auth.FirebaseAuth;
public class ResetPasswordActivity extends AppCompatActivity {
    EditText send mail;
    Button btn_reset;
    FirebaseAuth firebaseAuth;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_reset_password);
        Toolbar toolbar = findViewById(R.id.toolBar);
       // setSupportActionBar(toolbar);
        getSupportActionBar().setTitle("Reset Password");
        getSupportActionBar().setDisplayHomeAsUpEnabled(true);
        send_mail = findViewById(R.id.send_email);
        btn_reset = findViewById(R.id.btn_reset);
        firebaseAuth = FirebaseAuth.getInstance();
        //****************************************************************How to reset mail password**************************************************
        btn_reset.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                String mail= send_mail.getText().toString();
                if(mail.equals("")){
                    Toast.makeText(ResetPasswordActivity.this, "All field are required!", Toast.LENGTH_SHORT).show();
                }else{
                    firebaseAuth.sendPasswordResetEmail(mail).addOnCompleteListener(new OnCompleteListener<Void>() {
                        @Override
                        public void onComplete(@NonNull Task<Void> task) {
                            if(task.isSuccessful()){
                                Toast.makeText(ResetPasswordActivity.this, "Please check your mail...", Toast.LENGTH_SHORT).show();
                                startActivity(new Intent(ResetPasswordActivity.this , LoginActivity.class));
                            }else{
                                String error = task.getException().getMessage();
                                Toast.makeText(ResetPasswordActivity.this, error, Toast.LENGTH_SHORT).show();
                            }
                        }
                    });
                }
            }
        });
    }
}

activity_reset_password.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".ResetPasswordActivity">
    <include
        android:id="@+id/toolBar"
        layout="@layout/bar_layout"/>
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:padding="16dp">
        <com. reng wuxian.material edittext.Material EditText
            android:id="@+id/send_email"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:inputType="textEmailAddress"
            app:met_floatingLabel="normal"
            android:hint="Your Mail"
            android:layout_marginTop="20dp"/>
        <Button
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="Reset"
            android:id="@+id/btn_reset"
            android:textColor="#fff"
            android:background="@color/design_default_color_primary"
            android:layout_marginTop="10dp"/>
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:gravity="center"
            android:layout_marginTop="15dp"
            android:textColor="#000"
            android:text="After click reset you will receive an email, so please check your mail inbox"/>
    </LinearLayout>
</LinearLayout>

 

إدارة مركز الإشعارات

سنحاول في برنامج دردشه توفير أهم الطرق الحديثة في تفعيل مركز الإشعارات , بالتالي وعند إرسال رسالة يتم إخطار المستقبل فيها. ولأن مركز الإشعارات في جوجل تخضع للكثير من التحديثات وإتلاف الكائنات القديمة.

بالتالي يتطلب منك ذلك التحلي ببعض الصبر والتركيز لكي نتجاوز سويا حلول عرض الإشعارات.

 

ما هو FCM؟

هي اختصار لمصطلح Firebase Cloud Messaging. حيث تعبر عن استخدام الرسائل من خلال عنوان فريد يصعب الوصول إليه أو اختراقه. وهي آلية تفعيل الإشعارات من خلال الأجهزة التي تعمل بنظام API 31 فما فوق.

بالتالي وفي حال قمت بتجربة ارسال الرسائل عبر الوصول لعنوان id فإن الشيفرة في الغالب لن تعمل لديك. لقد فرضت Android على جميع التطبيقات الجديدة التقيد في الإشعارات عبر FCM. لذا يتطلب ذلك منا العديد من الكائنات في برنامج دردشه مثل:

  • اختبار محاولة الإتصال.
  • الحصول على كود الإتصال.
  • إرفاق FCM عند تفعيل الحساب.
  • تفعيل مكتبة Retrofit.
  • إضافة المكونات.
  • إضافة صفحة Constant.
  • الوراثة من FirebaseMessagingService.
  • طلب الدوال والكائنات في صفحة Message Activity.

اختبار محاولة الإتصال

يتم الحصول على شيفرة الإتصال , من خلال Engage->Messaging فهي بدورها التوثيق الأول لبناء الإشعارات لهاتف ما وذلك من خلال شاشة الكونسول والضغط على Create your first campaign. ثم بعدها اختيار Firebase Notification messages .

  تظهر لك قائمة تطلب منك تحديد FCM الخاص بهاتف ما تود إرسال الإشعارات إليه. وتستطيع الحصول على FCM الخاص به من خلال الكود التالي:

FirebaseMessaging.getInstance().getToken()
               .addOnCompleteListener(new OnCompleteListener<String>() {
                   @Override
                   public void onComplete(@NonNull Task<String> task) {
                       if (!task.isSuccessful()) {
                           Log.w(TAG, "Fetching FCM registration token failed", task.getException());
                           return;
                       }
                       // Get new FCM registration token
                       String token = task.getResult();
                       myToken = token;
                       // Log and toast
                       // String msg = getString(R.string.msg_token_fmt, token);
                       //  Log.d(TAG, msg);
                       // Toast.makeText(getApplicationContext(), token, Toast.LENGTH_SHORT).show();
                       //  text_send.setText(token.toString());
                   }
               });

   

تمكين شيفرة الإتصال

لكي نتمكن من توليد شيفرة إتصال يتوجب علينا الذهاب إلى شاشة الكونسول واختيار Project overview->project setting->cloud messaging.

تمامًا كما في الصورة التالية: ثم بعد ذلك نقوم بفتح القائمة كما في الصورة واختيار Manage API In Google Cloud Console.

بالتالي سيتم فتح صفحة في Tab جديدة , ونقوم بعمل تمكين عن طريق Enable.   والآن نقوم بعمل تحديث لصفحة الكونسول السابقة , وعند ظهور المفتاح في خانة server key. نقوم بنسخه ووضعه في مكان آمن على الحاسوب.

وبالعودة إلى مشروعنا الآن , فإننا نفضل عمل Package جديدة لوضع كافة مستندات الإشعارات في صفحة منفصلة. وذلك لتسهيل إدارة الكود فيما بعد. بالتالي سنقوم بعمل حزمة باسم Notifications.    

إرفاق FCM عند تفعيل الحساب

كما نعلم فإن كل هاتف ذكي يمتلك شيفرة FCM خاصة به وعند استخدام تطبيق برنامج دردشه. فإنه يجب أن يحتوي على عنوان فريد , ولتحقيق ذلك الأمر سنقوم بالذهاب إلى صفحة Register ونعمل على إضافة السطر التالي في دالة register.

hashMap.put("FCM" , get CurrentUser Token);

ليصبح الكود على النحو الآتي: RegisterActivity.java

package com.example.free chat app;
import android x .annotation.NonNull;
import android x .appcompat.app.AppCompatActivity;
import  android x .appcompat.widget.Toolbar;
import android.content.Intent;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;
import com.google.android.gms.tasks.OnCompleteListener;
import com.google.android.gms.tasks.Task;
import com.google.firebase.auth.AuthResult;
import com.google.firebase.auth.FirebaseAuth;
import com.google.firebase.auth.FirebaseUser;
import com.google.firebase.database.DatabaseReference;
import com.google.firebase.database.FirebaseDatabase;
import com.google.firebase.messaging.FirebaseMessaging;
import com.rengwuxian.material edit text.Material Edit Text;
import java.util.HashMap;
public class RegisterActivity extends AppCompatActivity {
    MaterialEditText username , email , password;
    Button btn_register;
    FirebaseAuth auth;
    DatabaseReference reference;
    String get CurrentUser Token;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_register);
        Toolbar toolbar = findViewById(R.id.toolBar);
        //setSupportActionBar(toolbar);
        getSupportActionBar().setTitle("Register");
        getSupportActionBar().setDisplayHomeAsUpEnabled(true);
        username = findViewById(R.id.username);
        email = findViewById(R.id.email);
        password = findViewById(R.id.password);
        btn_register = findViewById(R.id.btn_register);
        FirebaseMessaging.getInstance().getToken()
                .addOnCompleteListener(new OnCompleteListener<String>() {
                    @Override
                    public void onComplete(@NonNull Task<String> task) {
                        if (!task.isSuccessful()) {
                            // Log.w(TAG, "Fetching FCM registration token failed", task.getException());
                            return;
                        }
                        // Get new FCM registration token
                        String token = task.getResult();
                        get CurrentUser Token= token;
                    }
                });
        auth = FirebaseAuth.getInstance();
        btn_register.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                String txt_username = username.getText().toString();
                String txt_email = email.getText().toString();
                String txt_password = password.getText().toString();
                //Check if values is empty
                if(TextUtils.isEmpty(txt_username) || TextUtils.isEmpty(txt_email) || TextUtils.isEmpty(txt_password)){
                    Toast.makeText(RegisterActivity.this, "All field are required!", Toast.LENGTH_SHORT).show();
                }else if(txt_password.length() < 6){
                    Toast.makeText(RegisterActivity.this, "Password must be longer!", Toast.LENGTH_SHORT).show();
                }
                else{
                    //Here we will register new account
                    register(txt_username , txt_email , txt_password);
                }
            }
        });
    }
    //Connect to firebase database and register new account
    private void register(String username , String email , String password){
        auth.createUserWithEmailAndPassword(email , password)
                .addOnCompleteListener(new OnCompleteListener<AuthResult>() {
                    @Override
                    public void onComplete(@NonNull Task<AuthResult> task) {
                        if(task.isSuccessful()){
                            FirebaseUser firebaseUser = auth.getCurrentUser();
                            assert firebaseUser != null;
                            String userId = firebaseUser.getUid();
                            reference = FirebaseDatabase.getInstance().getReference("Users").child(userId);
                            HashMap<String , String> hashMap = new HashMap<>();
                            hashMap.put("id" , userId);
                            hashMap.put("username" , username);
                            hashMap.put("imageUrl" , "default");
                            hashMap.put("status" , "default");
                            hashMap.put("FCM" , get CurrentUser Token);
                            reference.setValue(hashMap).addOnCompleteListener(new OnCompleteListener<Void>() {
                                @Override
                                public void onComplete(@NonNull Task<Void> task) {
                                    if(task.isSuccessful()){
                                        Intent intent = new Intent(RegisterActivity.this , MainActivity.class);
                                        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
                                        startActivity(intent);
                                        finish();
                                    }
                                }
                            });
                        } else{
                            Toast.makeText(RegisterActivity.this, "You can't register with this mail", Toast.LENGTH_SHORT).show();
                        }
                    }
                });
    }
}

  وفي حال تعذر تعريف الكائن FirebaseMessaging. فذلك يعود إلى نقص بالمصادر. بالتالي قم بإضافة المصدر التالي في صفحة Build. implementation 'com.google.firebase:firebase-messaging:23.0.8' نقوم الآن بعمل فحص سريع , من فضلك قم بإغلاق الحسابات بعمل signout.

والخروج منها بشكل نهائي , وتستطيع حذفها من خلال Build->Authentication والتي قمنا بأعدادها سابقا في صفحة Firebase Console. كذلك الأمر بالنسبة لقواعد البيانات , حيث يمكنك تفريغها من كافة القيم الموجودة فيها.

سنقوم الآن بإعادة تفعيل الحسابات من جديد , وبالنظر للصورة التالية سنجد أن عملية توثيق FCM تمت بنجاح.    

تفعيل مكتبة Retrofit

مكتبة Retrofit يتم الاستعانة بها عند عمليات POST و REQUEST. على سبيل المثال , قد يتشابه الأمر كثيرا مع تطوير الويب. وخاصة عندما يقوم المستخدمين بإنشاء الطلبات للخادم. بالتالي سنقوم بإضافة مصادر مكتبة Retrofit.

implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.4.0'
implementation 'com.squareup.retrofit2:converter-scalars:2.9.0'

 

إضافة المكونات

سنقوم الآن بملئ مجموعة من أدوات الإشعارات داخل الحزمة Notification. بالتالي نبدأ بأول الصفحات في ترتيب متتالي: Client.java

package com.example.free chatapp.Notifications;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;
public class Client {
    private static Retrofit retrofit = null;
    public static Retrofit getClient(String url){
        if(retrofit == null){
            retrofit = new Retrofit.Builder()
                    .baseUrl(url)
                    .addConverterFactory(GsonConverterFactory.create())
                    .build();
        }
        return retrofit;
    }
}

Data.java

package com.example.free chatapp.Notifications;
public class Data {
    private String user;
    private int icon;
    private String body;
    private String title;
    private String scented;
    public Data(String user, int icon, String body, String title, String scented) {
        this.user = user;
        this.icon = icon;
        this.body = body;
        this.title = title;
        this.scented= scented;
    }
    public Data() {
    }
    public String getUser() {
        return user;
    }
    public void setUser(String user) {
        this.user = user;
    }
    public int getIcon() {
        return icon;
    }
    public void setIcon(int icon) {
        this.icon = icon;
    }
    public String getBody() {
        return body;
    }
    public void setBody(String body) {
        this.body = body;
    }
    public String getTitle() {
        return title;
    }
    public void setTitle(String title) {
        this.title = title;
    }
    public String getscented() {
        return scented;
    }
    public void setSented(String scented) {
        this.scented= scented;
    }
}

Fcm.java

package com.example.free chatapp.Notifications;
public class Fcm {
    private String FCM;
    public Fcm() {
    }
    public Fcm(String FCM) {
        this.FCM = FCM;
    }
    public String getFCM() {
        return FCM;
    }
    public void setFCM(String FCM) {
        this.FCM = FCM;
    }
}

MyResponse.java

package com.example.free chatapp.Notifications;
public class MyResponse {
    public int success;
}

Sender.java

package com.example.free chatapp.Notifications;
public class Sender {
    public Data data;
    public String to;
    public Sender(Data data, String to) {
        this.data = data;
        this.to = to;
    }
}

Token.java

package com.example.free chatapp.Notifications;
public class Token {
    private String token;
    public Token(String token) {
        this.token = token;
    }
    public Token() {
    }
    public String getToken() {
        return token;
    }
    public void setToken(String token) {
        this.token = token;
    }
}

RemoteUser.java

package com.example.free chatapp.Notifications;
import java.io.Serializable;
public class RemoteUser implements Serializable {
    public String name , image , email , token , id;
}

ApiClient.java

package com.example.free chatapp.Notifications;
import retrofit2.Retrofit;
import retrofit2.converter.scalars.ScalarsConverterFactory;
public class ApiClient {
    private static Retrofit retrofit = null;
    public static Retrofit getClient(){
        if(retrofit == null)
        {
            retrofit = new Retrofit.Builder()
                    .baseUrl("https://fcm.googleapis.com/fcm/")
                    .addConverterFactory(ScalarsConverterFactory.create())
                    .build();
        }
        return retrofit;
    }
}

 

ملاحظة : ApiService.java من نوع Interface.

ApiService.java

package com.example.free chatapp.Notifications;
import java.util.Map;
import retrofit2.Call;
import retrofit2.Response;
import retrofit2.http.Body;
import retrofit2.http.HeaderMap;
import retrofit2.http.Headers;
import retrofit2.http.POST;
public interface ApiService {
    @POST("send")
    Call<String> sendMessage(
            @HeaderMap Map<String, String> headers,
            @Body String messageBody
    );
}

     

إضافة صفحة Constant

صفحة Constant تمكننا من الاستعانة ببعض الثوابت خلال عمليات تمرير الإشعارات. بالإضافة إلى أنها تمنح عملية الإتصال من خلال الشيفرة التي قمنا بحفظها. Constant.java

package com.example.free chatapp.Notifications;
import java.util.HashMap;
import java.util.Map;
public class Constant {
    public static final String KEY_COLLECTION_USERS = "users";
    public static final String KEY_NAME = "name";
    public static final String KEY_EMAIL= "email";
    public static final String KEY_PASSWORD= "password";
    public static final String KEY_PREFERENCE_NAME = "chatAppPreference";
    public static final String KEY_IS_SIGNED_IN = "isSignedIn";
    public static final String KEY_USER_ID = "userId";
    public static final String KEY_IMAGE = "image";
    public static final String KEY_FCM_TOKEN = "fcmToken";
    public static final String KEY_USER = "user";
    public static final String KEY_COLLECTION_CHAT = "chat";
    public static final String KEY_SENDER_ID = "senderId";
    public static final String KEY_RECEIVER_ID = "receiverId";
    public static final String KEY_MESSAGE = "message";
    public static final String KEY_TIMESTAMP = "timestamp";
    public static final String KEY_COLLECTION_CONVERSATIONS = "conversations";
    public static final String KEY_SENDER_NAME = "senderName";
    public static final String KEY_RECEIVER_NAME = "receiverName";
    public static final String KEY_SENDER_IMAGE = "senderImage";
    public static final String KEY_RECEIVER_IMAGE = "receiverImage";
    public static final String KEY_LAST_MESSAGE = "lastMessage";
    public static final String KEY_AVAILABILITY = "availability";
    public static final String REMOTE_MSG_AUTHORIZATION = "Authorization";
    public static final String REMOTE_MSG_CONTENT_TYPE = "Content-Type";
    public static final String REMOTE_MSG_DATA = "data";
    public static final String REMOTE_MSG_REGISTRATION_IDS = "registration_ids";
    public static Map<String , String> remoteMsgHeaders = null;
    public static Map<String, String> getRemoteMsgHeaders() {
        if(remoteMsgHeaders == null){
            remoteMsgHeaders = new HashMap<>();
            remoteMsgHeaders.put(
                    REMOTE_MSG_AUTHORIZATION ,
                    "key="
            );
            remoteMsgHeaders.put(
                    REMOTE_MSG_CONTENT_TYPE ,
                    "application/json"
            );
        }
        return remoteMsgHeaders;
    }
}

والآن وبعد أن قمت بتوليد المفتاح الخاص بك. قم بلصقه بعد إشارة = في REMOTE MSG AUTHORIZATION بجانب كلمة key=.

بالتالي ندعوك بالحذر في نقل المفتاح خوفا من مسح محتويات الإتصال بطريقة خاطئة.  

الوراثة من FirebaseMessagingService

تحقق لك الوراثة من FirebaseMessagingService. عملية تواصل ناجحة عبر token. فتخيل معنا أن لغة PHP لديها صفحة عميل وصفحة خادم. 

وكذلك الأمر بالنسبة للجافا حيث ستتركز مهامنا على صفحة الرسائل لدى العميل ومن ثم صفحة الخادم عن طريق FirebaseMessagingService.

قد يشكل الأمر صعوبة عند عملية الشرح لكن الآن ستظهر الشيفرة بوضوح. لذا سنقوم ببناء كلاس في حزمة Notification وليكن باسم MyFirebaseMessaging. ليصبح بالشكل التالي: MyFirebaseMessaging.java

package com.example.free chatapp.Notifications;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Intent;
import android.os.Build;
import androidx.annotation.NonNull;
import android x.core.app.NotificationCompat;
import android x.core.app.NotificationManagerCompat;
import com.example.freechatapp.MessageActivity;
import com.example.free chat app.R;
import com.google.firebase.messaging.FirebaseMessagingService;
import com.google.firebase.messaging.RemoteMessage;
import java.util.Random;
public class MyFirebaseMessaging extends FirebaseMessagingService {
    @Override
    public void on New Token(@NonNull String token) {
        super.on New Token(token);
    }
    @Override
    public void onMessageReceived(RemoteMessage remoteMessage) {
        super.onMessageReceived(remoteMessage);
        // System.out.println("You received a message from:");
        //  Log.d("FCM" , "Message: " + remoteMessage.getNotification().getBody());
        RemoteUser user = new RemoteUser();
        user.id = remoteMessage.getData().get(Constant.KEY_USER_ID);
        user.name = remoteMessage.getData().get(Constant.KEY_NAME);
        user.token = remoteMessage.getData().get(Constant.KEY_FCM_TOKEN);
        int notificationId = new Random().nextInt();
        String channelId = "chat_message";
        Intent intent = new Intent(this , MessageActivity.class);
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
        intent.putExtra(Constant.KEY_USER , user);
        PendingIntent pendingIntent= PendingIntent.getActivity(this , 0 , intent , 0);
        NotificationCompat.Builder builder = new NotificationCompat.Builder(this , channelId);
        builder.setSmallIcon(R.drawable.ic_notification);
        builder.setContentTitle(user.name);
        builder.setContentText(remoteMessage.getData().get(Constant.KEY_MESSAGE));
        builder.setStyle(new NotificationCompat.BigTextStyle().bigText(
                remoteMessage.getData().get(Constant.KEY_MESSAGE)
        ));
        builder.setPriority(NotificationCompat.PRIORITY_DEFAULT);
        builder.setContentIntent(pendingIntent);
        builder.setAutoCancel(true);
        if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O){
            CharSequence channelName = "Chat Message";
            String channelDescription = "This notification channel is used for chat message notifications";
            int importance = NotificationManager.IMPORTANCE_DEFAULT;
            NotificationChannel channel = new NotificationChannel(channelId , channelName , importance);
            channel.setDescription(channelDescription);
            NotificationManager notificationManager = getSystemService(NotificationManager.class);
            notificationManager.createNotificationChannel(channel);
        }
        NotificationManagerCompat notificationManagerCompat = NotificationManagerCompat.from(this);
        notificationManagerCompat.notify(notificationId , builder.build());
    }
}

 

طلب الدوال والكائنات في صفحة Message Activity

نحن الآن على مشارف الإنتهاء من بناء تطبيق برنامج دردشه الجزء الأول, يتبقى لدينا آخر ما يجب أن يعمل في صفحة Message Activity. ولكي نتمكن من إرسال الإشعارات. يتعين علينا إضافة دالة send Notifications. وهي القيام بإتصال POST.

 

private void send Notifications(String messageBody){
    ApiClient.getClient().create(ApiService.class).sendMessage(
            Constant.getRemoteMsgHeaders(),
            messageBody
    ).enqueue(new Callback<String>() {
        @Override
        public void onResponse(@NonNull Call<String> call,@NonNull Response<String> response) {
            if(response.isSuccessful()){
                try {
                    if(response.body() != null){
                        JSONObject responseJson = new JSONObject(response.body());
                        JSONArray results = responseJson.getJSONArray("results");
                        if(responseJson.getInt("failure") ==1){
                            JSONObject error = (JSONObject) results.get(0);
                            //showToast(error.getString("error"));
                            Toast.makeText(MessageActivity.this, error.getString("error"), Toast.LENGTH_SHORT).show();
                            return;
                        }
                    }
                }catch (JSONException e){
                    e.printStackTrace();
                    Toast.makeText(MessageActivity.this, e.getMessage(), Toast.LENGTH_SHORT).show();
                }
                Toast.makeText(MessageActivity.this, "Notification sent successfully", Toast.LENGTH_SHORT).show();
            }else{
                //  showToast("Error :" +response.code());
                Toast.makeText(MessageActivity.this, "Error :" +response.code(), Toast.LENGTH_SHORT).show();
            }
        }
        @Override
        public void onFailure(@NonNull Call<String> call,@NonNull Throwable t) {
            //showToast(t.getMessage());
        }
    });
}

وتجنبًا لنقص المتغيرات فإن الكود النهائي لصفحة Message Activity. يصبح على النحو التالي: MessageActivity.java

package com.example.free chatapp;
import static android.content.ContentValues.TAG;
import androidx.annotation.NonNull;
import android x.appcompat.app.AppCompatActivity;
import android x.appcompat.widget.Toolbar;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.TextView;
import android.widget.Toast;
import com.example.freechatapp.Adapter.MessageAdapter;
import com.example.freechatapp.Notifications.ApiClient;
import com.example.freechatapp.Notifications.ApiService;
import com.example.freechatapp.Model.Chat;
import com.example.freechatapp.Model.User;
import com.example.freechatapp.Notifications.Client;
import com.example.freechatapp.Notifications.Constant;
import com.example.freechatapp.Notifications.Fcm;
import com.google.android.gms.tasks.OnCompleteListener;
import com.google.android.gms.tasks.Task;
import com.google.firebase.auth.FirebaseAuth;
import com.google.firebase.auth.FirebaseUser;
import com.google.firebase.database.DataSnapshot;
import com.google.firebase.database.DatabaseError;
import com.google.firebase.database.DatabaseReference;
import com.google.firebase.database.FirebaseDatabase;
import com.google.firebase.database.Query;
import com.google.firebase.database.ValueEventListener;
import com.google.firebase.messaging.FirebaseMessaging;
import com.square up.picasso.Picasso;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import de.hdodenhof.circleimageview.CircleImageView;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
public class MessageActivity extends AppCompatActivity {
    CircleImageView profileImage;
    TextView username;
    FirebaseUser fUser;
    DatabaseReference reference;
    ImageButton btn_send;
    EditText text_send;
    MessageAdapter messageAdapter;
    List<Chat> mChat;
    RecyclerView recyclerView;
    Intent intent;
    String userid;
    //Notification code
    Api Service apiService;
    boolean notify = false;
    //This is for seen or not seen messages
    Value EventListener seen Listener;
    String myToken;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_message);
        Toolbar toolbar = findViewById(R.id.toolBar);
        //setSupportActionBar(toolbar);
        getSupportActionBar().setTitle("");
        getSupportActionBar().setDisplayHomeAsUpEnabled(true);
        toolbar.setNavigationOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                //finish();
                startActivity(new Intent(MessageActivity.this , MainActivity.class).setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP));
            }
        });
        //Notification code
        apiService = Client.getClient("https://fcm.googleapis.com/fcm/").create(ApiService.class);
        FirebaseMessaging.getInstance().getToken()
                .addOnCompleteListener(new OnCompleteListener<String>() {
                    @Override
                    public void onComplete(@NonNull Task<String> task) {
                        if (!task.isSuccessful()) {
                            Log.w(TAG, "Fetching FCM registration token failed", task.getException());
                            return;
                        }
                        // Get new FCM registration token
                        String token = task.getResult();
                        myToken = token;
                        // Log and toast
                        // String msg = getString(R.string.msg_token_fmt, token);
                        //  Log.d(TAG, msg);
                        // Toast.makeText(getApplicationContext(), token, Toast.LENGTH_SHORT).show();
                        //  text_send.setText(token.toString());
                    }
                });
        recyclerView = findViewById(R.id.recycler_view);
        recyclerView.setHasFixedSize(true);
        LinearLayoutManager linearLayoutManager = new LinearLayoutManager(getApplicationContext());
        linearLayoutManager.setStackFromEnd(true);
        recyclerView.setLayoutManager(linearLayoutManager);
        profileImage = findViewById(R.id.profileImage);
        username = findViewById(R.id.username);
        btn_send = findViewById(R.id.btn_send);
        text_send = findViewById(R.id.text_send);
        //Receiving id from previous page
        intent = getIntent();
        userid = intent.getStringExtra("userid");
        fUser = FirebaseAuth.getInstance().getCurrentUser();
        btn_send.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                String msg = text_send.getText().toString();
                notify = true;
                if(!msg.equals("")){
                    //Send messages to firebase database from sender to receiver
                    //fUser is declared above and it is allowed to be used everywhere in this class
                    send_message(fUser.getUid() , userid , msg);
                }else{
                    Toast.makeText(MessageActivity.this, "You can't send an empty message!", Toast.LENGTH_SHORT).show();
                }
                text_send.setText("");
            }
        });
        //Access Users table in database where userid = this userid
        reference  = FirebaseDatabase.getInstance().getReference("Users").child(userid);
        reference.addValueEventListener(new ValueEventListener() {
            @Override
            public void onDataChange(@NonNull DataSnapshot snapshot) {
                User user = snapshot.getValue(User.class);
                assert user != null;
                username.setText(user.getUsername());
                if(user.getImageUrl().equals("default")){
                    profileImage.setImageResource(R.drawable.ic_user);
                }else{
                    //   Glide.with(MessageActivity.this).load(user.getImageUrl()).into(profileImage);
                    Picasso.with(getApplicationContext())
                            .load(user.getImageUrl()) // web image url
                            .fit().centerInside()
                            .rotate(90)                    //if you want to rotate by 90 degrees
                            .error(R.drawable.ic_users)
                            .placeholder(R.drawable.ic_users)
                            .into(profileImage);
                }
                readMessages(fUser.getUid() , userid , user.getImageUrl());
            }
            @Override
            public void onCancelled(@NonNull DatabaseError error) {
            }
        });
        seenMessage(userid);//This is to call seen method
    }
    //************************************************This code for seen or not seen messages*****************************************
    private void seenMessage(String userId){
        reference = FirebaseDatabase.getInstance().getReference("Chats");
        seenListener = reference.addValueEventListener(new ValueEventListener() {
            @Override
            public void onDataChange(@NonNull DataSnapshot dataSnapshot) {
                for(DataSnapshot snapshot : dataSnapshot.getChildren()){
                    Chat chat = snapshot.getValue(Chat.class);
                    if(chat.getReceiver().equals(fUser.getUid()) && chat.getSender().equals(userId)){
                        HashMap<String , Object> hashMap = new HashMap<>();
                        hashMap.put("isseen" , true);
                        snapshot.get Ref().updateChildren(hashMap);
                    }
                }
            }
            @Override
            public void onCancelled(@NonNull DatabaseError error) {
            }
        });
    }
    //*********************************************************************************************************************************
    private void send_message (String sender , String receiver , String message){
        //Make a connection with database
        DatabaseReference reference = FirebaseDatabase.getInstance().getReference();
        //Filling data in an array inside JAVA tools
        HashMap<String , Object> hashMap = new HashMap<>();
        hashMap.put("sender" , sender);
        hashMap.put("receiver" , receiver);
        hashMap.put("message" , message);
        hashMap.put("isseen" , false); //create primary table for seen or not seen chat
        //Push data inside chats table in database
        reference.child("Chats").push().setValue(hashMap);
        //This is code from me
        //  reference.child("Tokens").push().setValue( sender);
        //Add user to chat fragment
        //************* Send Chat to chatlist table for each sender and receiver tables****************************
        DatabaseReference createchat = FirebaseDatabase.getInstance().getReference("chatlist").child(fUser.getUid()).child(userid);
        HashMap<String , String> chat = new HashMap<>();
        chat.put("id" , userid);
        createchat.setValue(chat).addOnCompleteListener(new OnCompleteListener<Void>() {
            @Override
            public void onComplete(@NonNull Task<Void> task) {
                if(task.isSuccessful()){
                }
            }
        });
        DatabaseReference createchatTo = FirebaseDatabase.getInstance().getReference("chatlist").child(userid).child(fUser.getUid());
        HashMap<String , String> chatTo = new HashMap<>();
        chatTo.put("id" , fUser.getUid());
        createchatTo.setValue(chatTo).addOnCompleteListener(new OnCompleteListener<Void>() {
            @Override
            public void onComplete(@NonNull Task<Void> task) {
                if(task.isSuccessful()){
                }
            }
        });
        //***********************************************Notification code**************************************************************
        final String msg = message;
        reference = FirebaseDatabase.getInstance().getReference("Users").child(fUser.getUid());
        reference.addValueEventListener(new ValueEventListener() {
            @Override
            public void onDataChange(@NonNull DataSnapshot dataSnapshot) {
                User user = dataSnapshot.getValue(User.class);
                if (notify) {
                    DatabaseReference FCM = FirebaseDatabase.getInstance().getReference("Users");
                    Query query = FCM.orderByKey().equalTo(receiver);
                    query.addValueEventListener(new ValueEventListener() {
                        @Override
                        public void onDataChange(@NonNull DataSnapshot dataSnapshot) {
                            for(DataSnapshot snapshot : dataSnapshot.getChildren()){
                                Fcm fcm = snapshot.getValue(Fcm.class);
                                try {
                                    JSONArray tokens = new JSONArray();
                                    tokens.put(fcm.getFCM());
                                   // Toast.makeText(MessageActivity.this, myToken, Toast.LENGTH_SHORT).show();
                                    JSONObject data a = new JSONObject();
                                    data.put(Constant.KEY USER_ID , fUser.getUid());
                                    data.put(Constant.KEY_NAME , fUser.getDisplayName());
                                    data.put(Constant.KEY_FCM_TOKEN , myToken);
                                    data.put(Constant.KEY_MESSAGE , message);
                                    JSONObject body = new JSONObject();
                                    body. put(Constant.REMOTE MSG DATA , data);
                                    body.put(Constant.REMOTE_MSG_REGISTRATION_IDS , tokens);
                                     //Toast.makeText(MessageActivity.this, tokens.toString(), Toast.LENGTH_SHORT).show();
                                    send Notifications(body.toString());
                                }catch (Exception exception){
                                    Toast.makeText(MessageActivity.this, exception.getMessage(), Toast.LENGTH_SHORT).show();
                                }
                            }
                        }
                        @Override
                        public void onCancelled(@NonNull DatabaseError error) {
                        }
                    });
                }
                notify = false;
            }
            @Override
            public void onCancelled(@NonNull DatabaseError error) {
            }
        });
        //*********************************************************************************************************************************
    }
    private void send Notifications(String messageBody){
        ApiClient.getClient().create(ApiService.class).sendMessage(
                Constant.getRemoteMsgHeaders(),
                messageBody
        ).enqueue(new Callback<String>() {
            @Override
            public void onResponse(@NonNull Call<String> call,@NonNull Response<String> response) {
                if(response.isSuccessful()){
                    try {
                        if(response.body() != null){
                            JSONObject responseJson = new JSONObject(response.body());
                            JSONArray results = responseJson.getJSONArray("results");
                            if(responseJson.getInt("failure") ==1){
                                JSONObject error = (JSONObject) results.get(0);
                                //showToast(error.getString("error"));
                                Toast.makeText(MessageActivity.this, error.getString("error"), Toast.LENGTH_SHORT).show();
                                return;
                            }
                        }
                    }catch (JSONException e){
                        e.printStackTrace();
                        Toast.makeText(MessageActivity.this, e.getMessage(), Toast.LENGTH_SHORT).show();
                    }
                    Toast.makeText(MessageActivity.this, "Notification sent successfully", Toast.LENGTH_SHORT).show();
                }else{
                    //  showToast("Error :" +response.code());
                    Toast.makeText(MessageActivity.this, "Error :" +response.code(), Toast.LENGTH_SHORT).show();
                }
            }
            @Override
            public void onFailure(@NonNull Call<String> call,@NonNull Throwable t) {
                //showToast(t.getMessage());
            }
        });
    }
    private void readMessages(String myid , String userid , String img Url){
        mChat = new ArrayList<>();
        reference = FirebaseDatabase.getInstance().getReference("Chats");
        reference.addValueEventListener(new ValueEventListener() {
            @Override
            public void onDataChange(@NonNull DataSnapshot dataSnapshot) {
                mChat.clear();
                for(DataSnapshot snapshot : dataSnapshot.getChildren()){
                    Chat chat = snapshot.getValue(Chat.class);
                    assert chat != null;
                    if(chat.getReceiver().equals(myid) && chat.getSender().equals(userid) || chat.getReceiver().equals(userid) && chat.getSender().equals(myid)){
                        mChat.add(chat);
                    }
                    //Log.i(TAG, "userid :" + userid);
                    messageAdapter = new MessageAdapter(MessageActivity.this , mChat , img Url);
                    recyclerView.setAdapter(messageAdapter);
                }
            }
            @Override
            public void onCancelled(@NonNull DatabaseError error) {
            }
        });
    }
    //********************************************************Set User Status Method**************************************************//
    private void status(String status){
        reference = FirebaseDatabase.getInstance().getReference("Users").child(fUser.getUid());
        HashMap<String , Object> hashMap = new HashMap<>();
        hashMap.put("status" , status);
        reference.updateChildren(hashMap);
    }
    @Override
    protected void onResume() {
        super.onResume();
        status("Online");
    }
    @Override
    protected void onPause() {
        super.onPause();
        reference.removeEventListener(seenListener);  //On leave app we will remove seen method
        status("Offline");
    }
}

لو أننا قمنا ببناء وتشغيل البرنامج لن يعمل جيدًا . وذلك بسبب عدم منح صلاحيات الإنترنت في AndroidManifest.xml.

التعديل في Manifest

يتعين علينا إضافة صلاحيات الإنترنت لكي يتم تحقيق الإتصال POST. ولذلك نقم بنسخ مجموعة من الصلاحيات مثل:

<uses-permission android:name="android.permission.INTERNET" />
 <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
 <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
 <uses-permission android:name="android.permission.WAKE_LOCK" />

وأخيرًا إضافة خدمة MESSAGING EVENT من جوجل:

<service android:name=".Notifications.MyFirebaseMessaging"
          android:exported="false">
          <intent-filter>
              <action android:name="com.google.firebase.MESSAGING_EVENT"/>
          </intent-filter>
      </service>

وبالنظر إلى الشيفرة النهائية في AndroidManifest.xml , فإنها ستبدو على النحو التالي:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    package="com.example.free chat app">
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.WAKE_LOCK" />
    <application
        android:usesCleartextTraffic="true"
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.AppCompat.DayNight"
        tools:targetApi="31">
        <activity
            android:name=".ResetPasswordActivity"
            android:exported="false"
            android:parentActivityName=".LoginActivity"/>
        <activity
            android:name=".MessageActivity"
            android:exported="false" />
        <activity
            android:name=".LoginActivity"
            android:exported="false"
            android:parentActivityName=".StartActivity" />
        <activity
            android:name=".MainActivity"
            android:exported="false" />
        <activity
            android:name=".RegisterActivity"
            android:exported="false"
            android:parentActivityName=".StartActivity" />
        <activity
            android:name=".StartActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <service android:name=".Notifications.MyFirebaseMessaging"
            android:exported="false">
            <intent-filter>
                <action android:name="com.google.firebase.MESSAGING_EVENT"/>
            </intent-filter>
        </service>
    </application>
</manifest>

 

كيف بنيت هذا التطبيق؟

تتميز لغة جافا عن غيرها في البيئة الافتراضية حيث تعتمد على الكائنات الموجهة بشكل مبالغ به في بعض الأحيان وفي حال وجدت صعوبة في إدراك كافة الأكواد التي تم شرحها سابقا فلا بأس بذلك.

على سبيل المثال انت مطور ولست مبرمج وأنا كذلك لقد حصلت على هذه الشيفرة من احدى قنوات يوتيوب الموجودة أسفل المراجع.

و بالتالي  وجدت نفسي قادرًا على تطويرها في منتصف طريق وبالفعل فتحت أبواب كثيرة ومساحات واسعة من عمليات تطوير هذا التطبيق. البعض يقوم باستخدام الاتصال عبر POST عن طريق بي اتش بي قواعد البيانات SQL  وأنا لا أرجح ذلك الأمر عندما  تقرر البدء في تطوير أجهزة الهاتف.

وذلك بسبب  الثغرات الأمنية التي من الممكن أن يتعرض لها تطبيقك في المستقبل. نصيحتي لك أن تبقى مع جوجل حيث أنها مجموعة من الأدوات المجانية التي توفر لك مصادره متعددة و قابله للتحديث في المستقبل.

بالتالي ربما تستطيع إجراء العديد من التعديلات على هذا التطبيق وخاصة عند إدراك واستيعاب الفكرة الأساسية التي تشكل من خلالها.

على سبيل المثال لاحظ انني اقوم بمضاعفة الشيفرة والصقها في أماكن مختلفة من المشروع مع إجراء تعديلات طفيفة على شاشات تصميم و عناوين الاتصال بقواعد البيانات دون الخوض في بنائها من البدايه.

هذه هي البرامج التي سوف تتمكن من خلالها إنجاز أكثر المهام تعقيدًا دون الخوض في تفاصيل تكوين الشيفرة. ادعوك دائما في البحث عن البرامج القابلة للتطوير والابتعاد عن البرامج التي ربما لن يفهمها من قام ببرمجتها عند العودة إليها بعد سنه او سنتين.    

المراجع
  1. Firebase Realtime Database.
  2. مقالة من ويكيبيديا.
  3. A failure occurred while executing com.android.build.gradle.internal.tasks.
  4. Do not request Window.FEATURE_SUPPORT_ACTION_BAR and set windowActionBar to false.
  5. Firebase Cloud Messaging.
  6. Chat App with Firebase Part 18 - Sending Notifications.

 

وقت النشر : 2023-02-04 19:24:01 ·

1    التصنيفات






2    مقالات من التقنية

يعتمد هذا الموقع على عرض الإعلانات في تحقيق الدخل ، نشكر تفهمكم الدائم ونتمنى لكم قضاء وقت رائع ... وشكراً :D .