Geofence في Android

3zcsمنذ 7 سنوات

بسم الله الرحمن الرحيم

سنتحدث اليوم بإذن الله عن الـ Geofence, وهو من المواضيع الشيقة في التطوير لأندرويد, جميل جدا, لنبدأ.

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

بداية ما هو الـ Geofence, هو عبارة عن API يتيح لك معرفة ما إذا كان المستخدم قريب من أماكن تود أنت كمبرمج أن تقدم له تنبيه أو Action معين.

قبل البدأ سنقوم بتجهير ملفي gradle و manifest.

بالنسبة لملف gradle سنحتاج لأن نضيف google play services لتطبيق, عن طريق إضافة  هذا السطر في dependencies - هذه المكتبة بأحدث إصدار وقت كتابة هذه المقالة -


compile 'com.google.android.gms:play-services:10.0.1'

أما في ملف manifest سنضيف رقم النسخة كـ meta-data, ورقم النسخة يضاف مباشرة في string.xml بعد عمل sync إذا تم تحديث gradle
 


<meta-data android:name="com.google.android.gms.version"

android:value="@integer/google_play_services_version" />

ولا ننسى أن نطلب صلاحية الوصول لـ Location


    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>

الاّن التطبيق جاهز من ناحية المكتبات لنبدأ في كتابة الكود

سيكون لدينا intent service تقوم بإضافة المواقع التي نرغب أن ننبه المستخدم إذا كان بمقربة منها, و service أخرى لإظهار notification

سنبدأ بـ intent service ونسميها AddLocationService, أولا نقوم بتعريفها في ملف manifest


<service android:name=".AddLocationService" />

نبدأ بعمل implements لثلاث Interfaces وهي

ConnectionCallbacks  وهي تطلب منك عمل override لـ onConnected

OnConnectionFailedListener وتطلب من عمل override لـ onConnectionSuspended و onConnectionFailed

ResultCallback<T> وتطلب من عمل override لـ onResult والتي تخبرك عن حالة الطلب الخاص بك من حيث النجاح أو الفشل

ثم نحتاج لـ object من نوع GoogleApiClient حيث سيكون الاتصال عن طريقه ويقوم بشكل ذاتي بإدارة الإتصال, ونسند له API الذي نريد استخدامه ألا وهو LocationServices

لنلقي نظرة على شكل الكود حتى هذه اللحظة


public class AddLocationService extends IntentService implements
        GoogleApiClient.ConnectionCallbacks,
        GoogleApiClient.OnConnectionFailedListener,
        ResultCallback<Status>{

    GoogleApiClient mGoogleApiClient;



    /**
     * Creates an IntentService.  Invoked by your subclass's constructor.
     *
     * String AddLocationService important only for debugging.
     */
    public AddLocationService() {
        super("AddLocationService");
    }

    @Override
    protected void onHandleIntent(@Nullable Intent intent) {
        mGoogleApiClient = new GoogleApiClient.Builder(this)
                .addOnConnectionFailedListener(this)
                .addConnectionCallbacks(this)
                .addApi(LocationServices.API)
                .build();

        mGoogleApiClient.connect();
    }

    @Override
    public void onConnected(@Nullable Bundle bundle) {}

    @Override
    public void onConnectionSuspended(int i) {}

    @Override
    public void onConnectionFailed(@NonNull ConnectionResult connectionResult) {}

    @Override
    public void onResult(@NonNull Status status) {}

جميل جدا, في حالة كان هناك مشكلة في الاتصال أو تعطل الاتصال لفترة بسيطة سنضع log في onConnectionSuspended, onConnectionFailed كنوع من debug لمعرفة نوع الخطأ .

أما في حالة onConnected سنبدأ في إنشاء geofence الخاص بنا كالتالي


    @Override
    public void onConnected(@Nullable Bundle bundle) {
        try {
            LocationServices.GeofencingApi.addGeofences(
                    mGoogleApiClient,
                    // The GeofenceRequest object.
                    getGeofencingRequest(),
                    getGeofencePendingIntent()
            ).setResultCallback(this); // Result processed in onResult().
        } catch (SecurityException securityException) {
            // Catch exception generated if the app does not use ACCESS_FINE_LOCATION permission.
            Log.i(getClass().getSimpleName(),securityException.getMessage());
        }
    }

كما تلاحظ استدعينا دالة addGeofences التي نبني فيها Geofence الخاص بنا, وتستقبل ثلاث متغيرات وهي google api client وهو الذي قمنا ببناءه سلفاً, وقلنا أنه مسؤول عن إدارة الاتصال, و geofencingRequest سنتحدث عنها بعد قليل, وPendingIntent سنتحدث عنها لاحقا في هذه المقالة هي الأخرى أيضا.

فيكون شكل الدالة كالتالي

PendingResult<Status> addGeofences(GoogleApiClient var1, GeofencingRequest var2, PendingIntent var3)

وسيرجع لنا status التي ستبين لنا حالة الـ request من حيث النجاح أو الفشل.

نلاحظ أن لدينا two function وهي getGeofencingRequest و getGeofencePendingIntent

لنبدأ بطريقة عمل GeofencingRequest وهي المسؤلة عن إضافة geofence list التي نريد متابعتها وإظهار التنبيه بشكل مناسب, وهي كالتالي


    private GeofencingRequest getGeofencingRequest() {
        GeofencingRequest.Builder builder = new GeofencingRequest.Builder();
        builder.setInitialTrigger(GeofencingRequest.INITIAL_TRIGGER_ENTER | GeofencingRequest.INITIAL_TRIGGER_DWELL);
        builder.addGeofences(getGeofecne());
        return builder.build();
    }

طبعا هو يستخدم builder pattern ونلاحظ أنه يطلب setInitialTrigger و addGeofences

لدينا ثلاث أنوع من Triggers

INITIAL_TRIGGER_DWELL : نستخدمه عندما يكون المستخدم داخل المنطقة التي حددناها لبعض الوقت, ويكون لدينا GEOFENCE_TRANSITION_DWELL.

INITIAL_TRIGGER_ENTER : نستخدمه عندما يكون المستخدم داخل المنطقة التي حددناها, ويكون لدينا GEOFENCE_TRANSITION_ENTER

INITIAL_TRIGGER_EXIT : نستخدمه عندما يكون المستخدم خارج المنطقة التي حددناها, ويكون لدينا GEOFENCE_TRANSITION_EXIT

جميل جداً تعرفنا على أنواع Triggers التي قد نضعها في setInitialTrigger, وفي حالتنا سنستخدم INITIAL_TRIGGER_DWELL و INITIAL_TRIGGER_ENTER

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

بالنسبة لـ addGeofences فهي list of geofence وهو من أهم الجزئيات في geofencing

getGeofecne تبنى كالتالي


   private List<Geofence> getGeofecne(){
        List<Geofence> mGeofenceList = new ArrayList<>();

        //add one object
         mGeofenceList.add(new Geofence.Builder()
                    // Set the request ID of the geofence. This is a string to identify this
                    // geofence.
                    .setRequestId("key")

                    // Set the circular region of this geofence.
                    .setCircularRegion(
                            25.768466, //lat
                            47.567625, //long
                            50) // radios

                    // Set the expiration duration of the geofence. This geofence gets automatically
                    // removed after this period of time.
                    //1000 millis  * 60 sec * 5 min
                    .setExpirationDuration(1000 * 60 * 5)

                    // Set the transition types of interest. Alerts are only generated for these
                    // transition. We track entry and exit transitions in this sample.
                    .setTransitionTypes(
                    Geofence.GEOFENCE_TRANSITION_DWELL)
                    //Time before fire notification 
                    .setLoiteringDelay(3000)
                    // Create the geofence.
                    .build());

        return mGeofenceList;

    }

قمنا بإنشاء list of geofence وأضفنا Object واحد ولإنشاء object ستحتاج إلى عدة أمور

Id نعرف به geofence ويكون من نوع string

تحديد المحيط الخاص بالمنطقة عن طريق ثلاث أمور setCircularRegion( LATITUDE, LONGITUDE, RADIUS)

 تحديد الوقت الذي بعده يتم حذف geofence من list والوقت يحدد بالجزء من الثانية  في setExpirationDuration(1000 * 60 * 5) حالة وضعنا NEVER_EXPIRE سيتم إشعار اليوزر في كل مرة حسب نوع الحالة التي نضعها, عند كل دخول, أو عند بقائه فترة محدد, أو عند خروجه.

تحديد متى يتم تنبيه المستخدم, حال الدخول, أو الخروج أو عندما يكون داخل المنطقة لمدة من الزمن عن طريق setTransitionTypes() وتستخدم أحد هذه الثوابت أو جميعها

GEOFENCE_TRANSITION_ENTER تحدثنا عنها في getGeofencingRequest أنها تستخدم مع INITIAL_TRIGGER_ENTER

GEOFENCE_TRANSITION_DWELL فتستخدم مع INITIAL_TRIGGER_ENTER و INITIAL_TRIGGER_DWELL

GEOFENCE_TRANSITION_EXIT تستخدم مع INITIAL_TRIGGER_EXIT

لذلك يجب التنبه لوضع trigger في request مع transition الصحيح في geofecne.

أضفت هنا object واحد لتبسيط المثال لكن بإمكانك إضافة الكثير وبطريقة dynmic أيضا

الاّن أكلمنا GeofenceRequest

لننتقل لـ PendingIntent


    private PendingIntent getGeofencePendingIntent() {
        // Reuse the PendingIntent if we already have it.
        if (mGeofencePendingIntent != null) {
            return mGeofencePendingIntent;
        }
        Intent intent = new Intent(this, GeofenceTransitionsIntentService.class);
        return PendingIntent.getService(this, 0, intent, PendingIntent.
                FLAG_UPDATE_CURRENT);
    }

قمنا ببناء pending intent بسيطة جدا الغرض منها إخراج notification للمستخدم, عن طريق استدعاء GeofenceTransitionsIntentService class وهو class الثاني الذي تحدثنا عنه في بداية المقالة

جميل جدا


public class GeofenceTransitionsIntentService extends IntentService {
    protected static final String TAG = "GeofenceTransitionsIS";

    /**
     * This constructor is required, and calls the super IntentService(String)
     * constructor with the name for a worker thread.
     */
    public GeofenceTransitionsIntentService() {
        // Use the TAG to name the worker thread.
        super(TAG);
    }

    /**
     * Handles incoming intents.
     * @param intent sent by Location Services. This Intent is provided to Location
     *               Services (inside a PendingIntent) when addGeofences() is called.
     */
    @Override
    protected void onHandleIntent(Intent intent) {
    }

الشكل الأولي لـ service, سنضيف logic الخاص بنا في onHandleIntnet, وهو قراءة البيانات القادمة في intent عن طريق دالة GeofencingEvent.fromIntent(intent) وسنحفظها في geofencingEvent

ثم نتأكد من أنها سليمة عن طريق code التالي


        if (geofencingEvent.hasError()) {
            Log.e(TAG, getErrorString(geofencingEvent.getErrorCode()));
            return;
        }


    public String getErrorString(int errorCode) {
        switch (errorCode) {
            case GeofenceStatusCodes.GEOFENCE_NOT_AVAILABLE:
                return "not Available";
            case GeofenceStatusCodes.GEOFENCE_TOO_MANY_GEOFENCES:
                return "Too many Geofences";
            case GeofenceStatusCodes.GEOFENCE_TOO_MANY_PENDING_INTENTS:
                return "Too many Pending Intents";
            default:
                return "unknown geofence error";
        }
    }

لكل خطأ في geofence رقم معين وفي حال حدوثه سيظهر هنا في هذا log, فأحد الأخطاء مثلا, هو أن تضيف أكثر من 100 geofecne في geofence list, فسيظهر لك خطأ GEOFENCE_TOO_MANY_GEOFENCES, وبقية الأخطاء على نفس النسق

بعد ذلك نتعرف على نوع Transition القادم والذي تم اسناده سابقا عن طريق دالة setTransitionTypes

وعلى ضوءه نحدد notification التي نخرجها فيكون شكل class النهائي, كالتالي


public class GeofenceTransitionsIntentService extends IntentService {
    protected static final String TAG = "GeofenceTransitionsIS";

    /**
     * This constructor is required, and calls the super IntentService(String)
     * constructor with the name for a worker thread.
     */
    public GeofenceTransitionsIntentService() {
        // Use the TAG to name the worker thread.
        super(TAG);
    }

    /**
     * Handles incoming intents.
     * @param intent sent by Location Services. This Intent is provided to Location
     *               Services (inside a PendingIntent) when addGeofences() is called.
     */
    @Override
    protected void onHandleIntent(Intent intent) {
        GeofencingEvent geofencingEvent = GeofencingEvent.fromIntent(intent);
        if (geofencingEvent.hasError()) {
            Log.e(TAG, getErrorString(geofencingEvent.getErrorCode()));
            return;
        }

        // Get the transition type.
        int geofenceTransition = geofencingEvent.getGeofenceTransition();

        // Test that the reported transition was of interest.
        if (geofenceTransition == Geofence.GEOFENCE_TRANSITION_ENTER ||
                geofenceTransition == Geofence.GEOFENCE_TRANSITION_DWELL) {

            // Get the transition details as a String.
            String geofenceTransitionDetails = "Discount 10% for you";

            // Send notification and log the transition details.
            sendNotification(geofenceTransitionDetails);
            Log.i(TAG, geofenceTransitionDetails);
        } else {
            // Log the error.
            Log.e(TAG, getString(R.string.geofence_transition_invalid_type + geofenceTransition));
        }
    }

    public String getErrorString(int errorCode) {
        switch (errorCode) {
            case GeofenceStatusCodes.GEOFENCE_NOT_AVAILABLE:
                return "not Available";
            case GeofenceStatusCodes.GEOFENCE_TOO_MANY_GEOFENCES:
                return "Too many Geofences";
            case GeofenceStatusCodes.GEOFENCE_TOO_MANY_PENDING_INTENTS:
                return "Too many Pending Intents";
            default:
                return "unknown geofence error";
        }
    }


    /**
     * Posts a notification in the notification bar when a transition is detected.
     * If the user clicks the notification, control goes to the MainActivity.
     */
    private void sendNotification(String notificationDetails) {
        // Create an explicit content Intent that starts the main Activity.
        Intent notificationIntent = new Intent(getApplicationContext(), MainActivity.class);

        // Construct a task stack.
        TaskStackBuilder stackBuilder = TaskStackBuilder.create(this);

        // Add the main Activity to the task stack as the parent.
        stackBuilder.addParentStack(MainActivity.class);

        // Push the content Intent onto the stack.
        stackBuilder.addNextIntent(notificationIntent);

        // Get a PendingIntent containing the entire back stack.
        PendingIntent notificationPendingIntent =
                stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);

        // Get a notification builder that's compatible with platform versions >= 4
        NotificationCompat.Builder builder = new NotificationCompat.Builder(this);

        // Define the notification settings.
        builder.setSmallIcon(R.drawable.common_google_signin_btn_icon_dark_normal)
                // In a real app, you may want to use a library like Volley
                // to decode the Bitmap.
                .setLargeIcon(BitmapFactory.decodeResource(getResources(),
                        R.drawable.cast_abc_scrubber_primary_mtrl_alpha))
                .setColor(Color.RED)
                .setContentTitle(notificationDetails)
                .setContentText(getString(R.string.geofence_transition_notification_text))
                .setContentIntent(notificationPendingIntent);

        // Dismiss notification once the user touches it.
        builder.setAutoCancel(true);

        // Get an instance of the Notification manager
        NotificationManager mNotificationManager =
                (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);

        // Issue the notification
        mNotificationManager.notify(0, builder.build());
    }

}

وقد أضفت فيه جزئية إظهار notifiction ولن أتطرق لها بالشرح في هذه المقالة لكيلا تطول فيمل القارئ, في الـ Main Activity أضفت سطر واحد يشغل service


    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        startService(new Intent(this,AddLocationService.class));
    }

طيب بكذا يكون تطبيقنا أكتمل وقابل لتجربة لكن بعطيك معلومة سريعة بما إننا استخدمنا IntentService, فهي راح تستقبل الأوامر عن طريق onHandleIntent حتى تنهيها وبعد كذا راح تسوي stop وبعدها destroy, طبعا قريب حلول كثيرة أشهرها استخدام broadcast receiver, اختبر نفسك وحاول تطبق geofence معاه, ورفعت هنا على github الكود الخاص بالمثال هذا مع  broadcast receiver.

 

تأكد من أن تكون قد قمت بإعطاء التطبيق الصلاحيات المطلوبة قبل عمل اختبار لتطبيق, عن طريق الإعدادات -> التطبيقات -> الصلاحيات

 

هذا وصلى الله وسلم وبارك

المراجع:

https://developer.android.com/training/location/geofencing.html

https://code.tutsplus.com/tutorials/how-to-work-with-geofences-on-android--cms-26639

  Android programming big nerd ranch guide 

Android Application Development Cookbook

واستفدت كثيرا من الأسئلة المطروحة في stack overflow

كلمات دليلية:
0
إعجاب
5237
مشاهدات
0
مشاركة
0
متابع
متميز
محتوى رهيب

التعليقات (1)

Alhazmy13:

مقال جميل, استمر!!

لايوجد لديك حساب في عالم البرمجة؟

تحب تنضم لعالم البرمجة؟ وتنشئ عالمك الخاص، تنشر المقالات، الدورات، تشارك المبرمجين وتساعد الآخرين، اشترك الآن بخطوات يسيرة !