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

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

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

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

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

 

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

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

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

 

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

 

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

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

 

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

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

برنامج دردشة
صورة يظهر فيها اختيار Empty Project من قائمة إضافة المشروع.

بعد ذلك نقوم بتسمية المشروع بالاسم الذي نريده وليكن Free Chat App.

تسمية المشروع
صورة يظهر فيها تسمية المشروع باسم Free Chat App.

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

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

 

صفحات المشروع الأولى
صفحات المشروع الأولى عند الانتهاء من ملفات التكوين في Android Studio.

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

 

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

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

 

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

بالتالي نقم بالضغط على Get started ومن ثم Add project.

صفحة google firebase
محاولة الدخول لصفحة Google Firebases وانشاء مشروعنا فيها.

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

 

تسمبة مشروع Firebase
صورة يظهر فيها تسمية المشروع في Google Firebase.

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

بالتالي تظهر لنا صفحة استكمال المشروع بهذا الشكل.

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

 

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

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

البحث عن Realtime database
صورة يتم من خلالها تحديد مكان Realtime Database.

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

اضافة قاعدة بيانات.
صورة يظهر من خلالها محاولة إضافة قاعدة بيانات في Firebase Console.

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

 

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

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

تحديد مراكز البيانات في Google Firebase.
تحديد مراكز البيانات في Google Firebase.

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

 

تصدير ملفات JSON

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

ربط ملفات المشروع
نقم باختيار أيقونة أندرويد لتوثيق بيانات المشروع.

 

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

توثيق Package name
صورة يظهر من خلالها عملية توثيق المشروع باستخدام Package Name.

نضغط على Rigester App . وبعدها يتم تحميل ملفات المشروع كما في الصورة التالية.

تحميل ملف Json
صورة يظهر من خلالها تحميل ملف JSON. الخاص بمشروع Free App Chat.

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

نسخ ملف Json في المشروع
صورة يظهر فيها مكان نسخ ملف JSON الذي قمنا بتحميله من Google Firebase Console.

كما تلاحظ. فإننا نشير إلى مكان تواجد الملف في اللون الأصفر. وتستطيع فتح دليل الملفات من خلال اختيار 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.

نجاح عملية الربط
صورة يظهر فيها نجاح عملية ربط Firebase مع اندرويد استويو.

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

عمل مزامنة مع قواعد البيانات
مزامنة عملية الإتصال بقواعد البيانات.

 

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

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

 

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

صورة يظهر من خلالها نجاح المزامنة مع Google Firebase Console.
صورة يظهر من خلالها نجاح المزامنة مع Google Firebase Console.

 

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

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

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

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

 

أساسيات تكوين الصفحات
صورة يظهر فيها طرق بناء الصفحات في Android Studio.

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

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

 

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

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

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

 

LoginActivity.java

package com.example.freechatapp;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.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.rengwuxian.materialedittext.MaterialEditText;

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.rengwuxian.materialedittext.MaterialEditText
            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.rengwuxian.materialedittext.MaterialEditText
            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. تستطيع تجاوز الأخطاء من خلال الصورة التالية:

تعريف النصوص
صورة يظهر من خلالها إضافة النصوص في مستودع String.

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

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

implementation 'androidx.appcompat:appcompat:1.4.2'
implementation 'com.google.android.material:material:1.6.1'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'

//firebase
implementation 'androidx.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 'androidx.cardview:cardview:1.0.0'
implementation 'com.rengwuxian.materialedittext:library:2.1.4'
implementation 'androidx.annotation:annotation:1.4.0'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.3.1'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1'

testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'

 

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

<?xml version="1.0" encoding="utf-8"?>
<androidx.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">



</androidx.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">#3eaeae</color>
    <color name="complexRed">#F44336</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. ولكي نتجاوز تلك المشكلة سنجري بعض التعديلات فيها.

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

إعدادت ملف Manifest
صورة تظهر الإعدادت الرئيسية لملف AndroidManifest.

نلاحظ هنا وجود صفحتين في برنامج دردشة. وتحتوي كل واحدة منها على إعدادات في هذا الملف. بالتالي تستطيع جعل احداها أساسية بمجرد عكس القيم من 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.freechatapp">

    <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. فهي تعتبر أساسية لغالبية التطبيقات. لكننا سنخصصها لأمور أخرى في هذه الدورة. وسنستبدلها بصفحة رئيسية يتم من خلالها جمع الكثير من العناصر في تطبيقنا برنامج دردشه.

نقم الآن بإضافة Empty Activity. وبعنوان StartActivity. ثم بعد ذلك نقوم بوضع الشيفرة التالية فيها.

StartActivity.java

package com.example.freechatapp;

import androidx.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.freechatapp">

    <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>

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

الصفحة الرئيسية للتطبيق
صورة يظهر فيها صفحة StartActivity في برنامج دردشه.

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

 

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

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

RegisterActivity.java

package com.example.freechatapp;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.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.rengwuxian.materialedittext.MaterialEditText;

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.rengwuxian.materialedittext.MaterialEditText
            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.rengwuxian.materialedittext.MaterialEditText
            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.rengwuxian.materialedittext.MaterialEditText
            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.freechatapp;

import androidx.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 mail. وهي مشكلة لها سببين:

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

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

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

تفعيل خاصية تسجيل الدخول
صورة يظهر فيها تفعيل صلاحيات تسجيل الدخول في Firebase.

 

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

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

 

 

صفحة Main Activity
صورة تظهر عملية التوجيه لصفحة MainActivity.

 

توثيق المعلومات في Firebase
نجاح عملية التوثيق في Firebase Authentication.

 

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

 

عمل Sign-in و Sign-out

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

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

عمل قائمة

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

إضافة قائمة
عملية إضافة Menu من ملف Android Resource File.

 

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

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

   //***********************************************************************************************************************************

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

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

MainActivity.java

package com.example.freechatapp;

import androidx.annotation.NonNull;
import androidx.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.freechatapp;

import androidx.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;
    }
}

ولمن لا يعرف الصفوف , فإننا ندعوك لدراستها في الجزء الخاص بلغة c++. ريثما يتم توفير دورات خاصة للغة جافا. بالتالي سيتم الوصول إلى أعمدة قواعد البيانات عبر الــ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">
        
        <androidx.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"

                />

        </androidx.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.chatapplicationbasics;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.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.chatapplicationbasics.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;
    }

    //***********************************************************************************************************************************
}

 

وعند بناء الشيفرة ستظهر كما في الصورة التالية.

الشريط العلوي للحساب
ظهور تفاصيل الحساب عبر الإتصال بقواعد البيانات Firebase.

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

 

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

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

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

آلية عمل الــFragments

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

إضافة Fragment
صورة يظهر فيها عملية إضافة Fragment من قبل Android Studio.

تعتمد الــFragment على العمل عبر عنصرين وهما:

  1. الوراثة من FragmentStateAdapter.
  2. وأيضا ورائة من Fragment.

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

عمل صفحات Fragment

يتطلب عمل الصفحات الطريقة التي عرضناها في الصورة السابقة. لذلك سنقوم ببناء ثلاث صفحات. الأولى ChatsFragment. والثانية UsersFragment. بينما الثالثة ستكون ProfileFragments. ونظرا لعدم وجود 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 أيقونات مرخصة وتستطيع استخدامها عند عملية النشر في المتاجر.

بالتالي يتعين علينا تحضير بعض الأيقونات.

على سبيل المثال , فإنا نقوم بتخصيص أيقونة قسم الحساب الشخصي من خلال الصورة التالية:

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

 

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

 

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

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

FragmentAdapter.java

package com.example.freechatapp.Fragments;

import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.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 getItemCount() {
        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>


<androidx.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">

<androidx.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"

/>

</androidx.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>


<androidx.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));

       TabLayoutMediator tabLayoutMediator = new TabLayoutMediator(tabLayout, viewPager, new TabLayoutMediator.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;
                   }

               }
           }
       });
       tabLayoutMediator.attach();

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

MainActivity.java

package com.example.freechatapp;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import androidx.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.TabLayoutMediator;
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));

        TabLayoutMediator tabLayoutMediator = new TabLayoutMediator(tabLayout, viewPager, new TabLayoutMediator.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;
                    }

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

    //***********************************************************************************************************************************
}

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

أقسام التطبيق
صورة يظهر من خلالها أقسام برنامج دردشة.

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

 

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

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

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

UserAdapter.java

package com.example.freechatapp.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.User;
import com.example.freechatapp.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 getItemCount() {
        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.freechatapp.Fragments;


import android.os.Bundle;

import androidx.annotation.NonNull;
import androidx.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.User;
import com.example.freechatapp.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<>();

        readUsers();


        return view;
    }

    private void readUsers() {

        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.

 

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

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

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

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

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

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

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

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

بالتالي سيتم التطبيق الشيفرة التالية في صفحة UserAdapter.

//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 وذلك لتمرير القيم عند الدخول إلى صفحة MessageActivity. والتي أهمها عنوان id. حيث سيتم جلب كل ما نريده من قواعد البيانات. وهي طريقة عزل جيدة للبيانات في تطبيقات برنامج دردشه وحتى في برمجة الويب.

UserAdapter.java

package com.example.freechatapp.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.freechatapp.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 getItemCount() {
        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.freechatapp;


import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.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">

        <androidx.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"/>

        </androidx.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>

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

صفحة المحادثة
صورة تظهر صفحة المحادثة في برنامج دردشه.

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

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

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

<?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">#3eaeae</color>
    <color name="complexRed">#F44336</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 لتشكيل نصوص المحادثة. بالتالي قد تساعدك الصورة في طريقة الإضافة.

اضافة أشكال
صورة يظهر من خلالها اضافة shape في برنامج دردشه.

ندعوك للحذر عند الإضافة حيث أن الملف الرئيسي لأشكال الأيقونات هو 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.freechatapp.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.freechatapp.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.freechatapp.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 getItemCount() {
        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>

 

تعديلات على MessageActivity

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

private void readMessages(String myid , String userid , String imgUrl){

      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 , imgUrl);
                  recyclerView.setAdapter(messageAdapter);
              }
          }

          @Override
          public void onCancelled(@NonNull DatabaseError error) {

          }
      });
  }

وبالتالي يصبح الشكل الحقيقي لصفحة MessageActivity كما في الشيفرة التالية:

MessageActivity.java

package com.example.freechatapp;


import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.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 imgUrl){

        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 , imgUrl);
                    recyclerView.setAdapter(messageAdapter);
                }
            }

            @Override
            public void onCancelled(@NonNull DatabaseError error) {

            }
        });
    }
}

وبعد إتمام عملية البناء. ستظهر لنا تلك النتائج الرائعة!.

المرسل
صورة يظهر من خلالها عرض الرسائل من جهة المرسل في برنامج دردشه.
المستقبل
عرض الرسائل من جهة المستقبل في برنامج دردشه.

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

 

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

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

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

ChatsFragment.java

package com.example.freechatapp.Fragments;

import android.os.Bundle;

import androidx.annotation.NonNull;
import androidx.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.freechatapp.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 ببعض المعلومات الأساسية. ولتكن صورة المستخدم والإسم الخاص به.

 

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

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

ProfileFragment.java

package com.example.freechatapp.Fragments;


import android.os.Bundle;

import androidx.annotation.NonNull;
import androidx.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.freechatapp.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());     //Assiging 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. والتي تتيح لنا القراءة والكتابة في وسائط التخزين.

تمكين وسائط التخزين
صورة يظهر من خلالها تمكين وسائط التخزين في Firebase Storage.

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

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

private void uploadImage(){
        final ProgressDialog pd = new ProgressDialog(getContext());
        pd.setMessage("Uploading..");
        pd.show();

        if(imageUri != null){
            //Rename picture in database includeing 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.freechatapp.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 androidx.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.freechatapp.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());     //Assiging 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

                    imageUri = uri.normalizeScheme();
                    // 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{
                        uploadImage();
                    }
                }
            });




    private void uploadImage(){
        final ProgressDialog pd = new ProgressDialog(getContext());
        pd.setMessage("Uploading..");
        pd.show();

        if(imageUri != null){
            //Rename picture in database includeing 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();
        MimeTypeMap 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.freechatapp;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import androidx.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.TabLayoutMediator;
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));

        TabLayoutMediator tabLayoutMediator = new TabLayoutMediator(tabLayout, viewPager, new TabLayoutMediator.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;
                    }

                }
            }
        });
        tabLayoutMediator.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. أثناء قدوم أو خروج المستخدم من التطبيق.

تحديث الحالة
صورة يظهر فيها عمليات تحديث الحالة في برنامج دردشه من خلال Firebase.

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

التعديل في 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="#bfbfbf"
        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>

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

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

بالتالي تصبح شيفرة User تماما كما هي الشيفرة التالية:

package com.example.freechatapp.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;
    }
}

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

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

//*************************************************Check Chat Status******************************************
       if(isChat){
           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 غيرها. وذلك لأننا نعمل في Adpter وليس في AppCompatActivity. كما أننا قمنا بإجراء تغيير في Constructor. ولتجنب تلك المشكلة في مناطق أخرى من المشروع سنعمل على عمل overloading.

UserAdapter.java

package com.example.freechatapp.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.freechatapp.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 getItemCount() {
        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");
اضافة عمود الحالة أثناء تسجيل الدخول
صورة يظهر فيها إضافة عمود الحالة في صفحة Register لدى برنامج دردشه.

لكن ولسوء الحظ فإن التطبيق لن يحافظ على حالة الاتصال عند فتح صفحة المحادثات. وذلك بسبب مغادرة صفحة 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. وهو isSeen. من نوع Boolean. لتصبح شيفرة Chat على النحو التالي:

Chat.java

package com.example.freechatapp.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.isseen = 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.isseen = isseen;
    }
}

 

ملاحظة : قد تظهر مشاكل مكتبة Glide عند عملية المحادثات. وتستطيع تجاوزها باستخدام Picasso.

dependencies {

....

implementation 'com.squareup.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.freechatapp.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.freechatapp.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 getItemCount() {
        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;
        }
    }
}

 

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

لا ننسى أننا قمنا بإضافة متغير جديد في كلاس Chat. وهو العنصر الذي يمكننا من تفعيل عمود isSeen في قواعد البيانات. بالتالي يتعين علينا إضافة قيمة جديدة عند عمليات ارسال الرسائل في MessageActivity.

على سبيل المثال الدالة send_message. ستتأثر بتلك القيمة وهي :

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

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

MessageActivity.java

package com.example.freechatapp;


import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.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
    ValueEventListener seenListener;

    @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.getRef().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 imgUrl){

        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 , imgUrl);
                    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.rengwuxian.materialedittext.MaterialEditText
            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.rengwuxian.materialedittext.MaterialEditText
            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>

وفي صفحة LoginActivity يتم إضافة صفحة متغير من نوع 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.freechatapp;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.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.rengwuxian.materialedittext.MaterialEditText;

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

    }
}

 

بالتالي سنقوم الآن ببناء EmptyActivity ولتكن بعنوان ResetPasswordActivity.

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

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

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

ResetPasswordActivity.java

package com.example.freechatapp;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.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.rengwuxian.materialedittext.MaterialEditText
            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.
  • طلب الدوال والكائنات في صفحة MessageActivity.
  • التعديل في Manifest.

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

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

 

صفحة تفعيل fcm من الكونسول
صورة يظهر فيها عملية تفعيل اشعارات الرسائل في برنامج دردشه.

تظهر لك قائمة تطلب منك تحديد 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.

Manage API
صورة يظهر فيها الدخول إلى شاشة Cloud Messaging في مشروع برنامج دردشه.

بالتالي سيتم فتح صفحة في Tab جديدة , ونقوم بعمل تمكين عن طريق Enable.

تمكين Cloud Messaging.
صورة يظهر فيها تمكين Cloud Messaging في تطبيق برنامج دردشه.

 

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

 

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

 

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

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

hashMap.put("FCM" , getCurrentUserToken);

لتصبح الشيفرة كاملة على النحو الآتي:

RegisterActivity.java

package com.example.freechatapp;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.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.materialedittext.MaterialEditText;

import java.util.HashMap;

public class RegisterActivity extends AppCompatActivity {

    MaterialEditText username , email , password;
    Button btn_register;

    FirebaseAuth auth;
    DatabaseReference reference;

    String getCurrentUserToken;

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

                        getCurrentUserToken = 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" , getCurrentUserToken);

                            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 تمت بنجاح.

الحصول على fcm
صورة يظهر فيها توثيق 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.freechatapp.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.freechatapp.Notifications;


public class Data {

    private String user;
    private int icon;
    private String body;
    private String title;
    private String sented;

    public Data(String user, int icon, String body, String title, String sented) {
        this.user = user;
        this.icon = icon;
        this.body = body;
        this.title = title;
        this.sented = sented;
    }

    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 getSented() {
        return sented;
    }

    public void setSented(String sented) {
        this.sented = sented;
    }
}

Fcm.java

package com.example.freechatapp.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.freechatapp.Notifications;

public class MyResponse {

    public int success;

}

Sender.java

package com.example.freechatapp.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.freechatapp.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.freechatapp.Notifications;

import java.io.Serializable;

public class RemoteUser implements Serializable {
    public String name , image , email , token , id;
}

ApiClient.java

package com.example.freechatapp.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.freechatapp.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.freechatapp.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.freechatapp.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 androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;


import com.example.freechatapp.MessageActivity;
import com.example.freechatapp.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 onNewToken(@NonNull String token) {
        super.onNewToken(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());

    }
}

 

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

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

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

وتجنبا لنقص المتغيرات فإن الشيفرة النهائية لصفحة MessageActivity. تصبح على النحو التالي:

MessageActivity.java

package com.example.freechatapp;

import static android.content.ContentValues.TAG;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.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.squareup.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
    ApiService apiService;
    boolean notify = false;


    //This is for seen or not seen messages
    ValueEventListener seenListener;

    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.getRef().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 dataa = new JSONObject();
                                    dataa.put(Constant.KEY_USER_ID , fUser.getUid());
                                    dataa.put(Constant.KEY_NAME , fUser.getDisplayName());
                                    dataa.put(Constant.KEY_FCM_TOKEN , myToken);


                                    dataa.put(Constant.KEY_MESSAGE , message);

                                    JSONObject body = new JSONObject();
                                    body.put(Constant.REMOTE_MSG_DATA , dataa);
                                    body.put(Constant.REMOTE_MSG_REGISTRATION_IDS , tokens);
                                     //Toast.makeText(MessageActivity.this, tokens.toString(), Toast.LENGTH_SHORT).show();
                                    sendNotificationss(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 sendNotificationss(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 imgUrl){

        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 , imgUrl);
                    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.freechatapp">

    <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>

 

 

المراجع
  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.

 

محمد مطاوع
محمد مطاوع
مبرمج وخبير في خوادم الويب
هل أعجبك المقال؟

اترك تعليقاً

لن يتم نشر عنوان بريدك الإلكتروني.

 - 
Arabic
 - 
ar
Bengali
 - 
bn
German
 - 
de
English
 - 
en
French
 - 
fr
Hindi
 - 
hi
Indonesian
 - 
id
Portuguese
 - 
pt
Russian
 - 
ru
Spanish
 - 
es