Geofence في Android
بسم الله الرحمن الرحيم
سنتحدث اليوم بإذن الله عن الـ 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
لايوجد لديك حساب في عالم البرمجة؟
تحب تنضم لعالم البرمجة؟ وتنشئ عالمك الخاص، تنشر المقالات، الدورات، تشارك المبرمجين وتساعد الآخرين، اشترك الآن بخطوات يسيرة !