نحوه کار با Geofences در اندروید – بخش دوم
در مطلب قبلی با کلیاتی از geofence آشنایی پیدا کردید، در ادامه قصد داریم با ساخت یک اپلیکیشن ساده اندروید نحوه کار با geofence را به شما آموزش دهیم، با ما همراه باشید.
ساخت یک اپلیکیشن Geofencing
در این مطلب آموزشی به ساخت یک اپلیکیشن ساده پرداخته می شود که به نظارت بر موقعیت قرارگیری کاربر پرداخته و زمانی که کاربر به یک منطقه Geofence وارد یا از آن خارج شود یک نوتیفیکیشن ارسال می کند. این اپلیکیشن تنها شامل یک Activity و یک IntentSerivce است. یک نگاه سریع و اجمالی نیز به GoogleMap ،GoogleApiClient و FusedLocationProviderApi می اندازیم و چند نکته از Geofence API را نیز مورد بررسی قرار می دهیم.
گام 1: راه اندازی پروژه
GeofenceApi بخشی از Google Play Services می باشد، به منظور دسترسی به آن لازم است تا محیط توسعه خود را به درستی تنظیم کرده و یک نمونه اولیه از GoogleApiClient بسازید. یک پروژه با Activity خالی ساخته و فایل build.gradle آن را با استفاده از کدهای زیر تغییر دهید و پروژه را همگام سازی نمایید.
گام 2: مجوزها
به منظور ساخت و استفاده از Geofence باید مجوزهای درستی را تعیین کنیم، به این منظور لازم است تا مجوزهای زیر را به فایل منیفست پروژه اضافه نمایید.
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
از نسخه 6.0 اندروید اپلیکیشن در طول اجرا از کاربر درخواست مجوز می کند، در حالی که این فرآیند در گذشته در مرحله نصب اپلیکیشن صورت می پذیرفت و در ادامه به این موضوع نیز پرداخته خواهد شد.
گام 3: ساخت یک لی اوت
پروژه موردنظر شامل یک لی اوتMainActivity می باشد، این لی اوت latitude و longitude کنونی گوشی و فرگمنت GoogleMap که به نمایش Geofence و موقعیت قرارگیری کاربر می پردازد را در بر می گیرد.
از آنجا که activity_main.xml بسیار ساده است، تنها بر روی عنصر MapFragment متمرکز می شویم. لی اوت تکمیل شده در فایل های منبع این مطلب آموزشی قرار داده شده و به منظور کسب جزئیات بیشتر می توانید به آن مراجعه نمایید.
<!--GoogleMap fragment-->
<fragment xmlns:android="http://schemas.android.com/apk/res/android"
android:name="com.google.android.gms.maps.MapFragment"
android:id="@+id/map"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
گام 4: Google Maps API Key
به منظور استفاده از MapFragment، لازم است تا یک نمونه اولیه GoogleMap نیز ساخته شود. اول از همه به یک API key نیاز دارید، پس از دریافت این کلید آن را به فایل منیفست پروژه خود بیفزایید.
<meta-data
android:name="com.google.android.geo.API_KEY"
android:value="YOUR_API_KEY"/>
کار را با ساخت نمونه اولیه از GoogleMap آغاز می کنیم.
GoogleMap.OnMapReadyCallBack ،GoogleMap.OnMapClickListener و GoogleMap.OnMarkerClickListener را در کلاس Activity جاسازی کرده و مپ را اینشیالایز کنید.
public class MainActivity extends AppCompatActivity
implements
OnMapReadyCallback,
GoogleMap.OnMapClickListener,
GoogleMap.OnMarkerClickListener
{
private static final String TAG = MainActivity.class.getSimpleName();
private TextView textLat, textLong;
private MapFragment mapFragment;
private GoogleMap map;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
textLat = (TextView) findViewById(R.id.lat);
textLong = (TextView) findViewById(R.id.lon);
// initialize GoogleMaps
initGMaps();
}
// Initialize GoogleMaps
private void initGMaps(){
mapFragment = (MapFragment) getFragmentManager().findFragmentById(R.id.map);
mapFragment.getMapAsync(this);
}
// Callback called when Map is ready
@Override
public void onMapReady(GoogleMap googleMap) {
Log.d(TAG, "onMapReady()");
map = googleMap;
map.setOnMapClickListener(this);
map.setOnMarkerClickListener(this);
}
// Callback called when Map is touched
@Override
public void onMapClick(LatLng latLng) {
Log.d(TAG, "onMapClick("+latLng +")");
}
// Callback called when Marker is touched
@Override
public boolean onMarkerClick(Marker marker) {
Log.d(TAG, "onMarkerClickListener: " + marker.getPosition() );
return false;
}
}
گام 5: GoogleApiClient
به منظور استفاده از رابط GeofenceApi باید از GoogleApiClient استفاده کنیم. برای این کار باید GoogleApiClient.ConnectionCallbacks و GoogleApiClient.OnConnectFailedListener را مانند زیر در Activity قرار دهیم.
public class MainActivity extends AppCompatActivity
implements
GoogleApiClient.ConnectionCallbacks,
GoogleApiClient.OnConnectionFailedListener,
OnMapReadyCallback,
GoogleMap.OnMapClickListener,
GoogleMap.OnMarkerClickListener {
// ...
private GoogleApiClient googleApiClient;
@Override
protected void onCreate(Bundle savedInstanceState) {
// ...
// create GoogleApiClient
createGoogleApi();
}
// Create GoogleApiClient instance
private void createGoogleApi() {
Log.d(TAG, "createGoogleApi()");
if ( googleApiClient == null ) {
googleApiClient = new GoogleApiClient.Builder( this )
.addConnectionCallbacks( this )
.addOnConnectionFailedListener( this )
.addApi( LocationServices.API )
.build();
}
}
@Override
protected void onStart() {
super.onStart();
// Call GoogleApiClient connection when starting the Activity
googleApiClient.connect();
}
@Override
protected void onStop() {
super.onStop();
// Disconnect GoogleApiClient when stopping Activity
googleApiClient.disconnect();
}
// GoogleApiClient.ConnectionCallbacks connected
@Override
public void onConnected(@Nullable Bundle bundle) {
Log.i(TAG, "onConnected()");
}
// GoogleApiClient.ConnectionCallbacks suspended
@Override
public void onConnectionSuspended(int i) {
Log.w(TAG, "onConnectionSuspended()");
}
// GoogleApiClient.OnConnectionFailedListener fail
@Override
public void onConnectionFailed(@NonNull ConnectionResult connectionResult) {
Log.w(TAG, "onConnectionFailed()");
}
}
گام 6: FusedLocationPoviderApi
برای ادامه کار به موقعیت کنونی کاربر نیاز داریم و رابط FusefdLocationProviderApi این اطلاعات را در اختیار ما قرار می دهد و ارائه دهنده سطح بالایی از کنترل برای درخواست موقعیت است. توجه به این نکته ضروری است، چرا که این درخواست ها تاثیر مستقیمی بر روی مصرف باتری گوشی دارند و باید در استفاده از آنها محتاطانه عمل کرد.
در این مرحله باید LocationListener را جاسازی کنیم. با ساخت یک درخواست Location از این بابت که کاربر مجوز موردنظر را در اختیار قرار داده مطمئن شوید و سپس موقعیت کنونی آنها را بر روی صفحه به نمایش بگذارید.
public class MainActivity extends AppCompatActivity
implements
// ....
LocationListener
{
private Location lastLocation;
//...
// GoogleApiClient.ConnectionCallbacks connected
@Override
public void onConnected(@Nullable Bundle bundle) {
Log.i(TAG, "onConnected()");
getLastKnownLocation();
}
// Get last known location
private void getLastKnownLocation() {
Log.d(TAG, "getLastKnownLocation()");
if ( checkPermission() ) {
lastLocation = LocationServices.FusedLocationApi.getLastLocation(googleApiClient);
if ( lastLocation != null ) {
Log.i(TAG, "LasKnown location. " +
"Long: " + lastLocation.getLongitude() +
" | Lat: " + lastLocation.getLatitude());
writeLastLocation();
startLocationUpdates();
} else {
Log.w(TAG, "No location retrieved yet");
startLocationUpdates();
}
}
else askPermission();
}
private LocationRequest locationRequest;
// Defined in mili seconds.
// This number in extremely low, and should be used only for debug
private final int UPDATE_INTERVAL = 1000;
private final int FASTEST_INTERVAL = 900;
// Start location Updates
private void startLocationUpdates(){
Log.i(TAG, "startLocationUpdates()");
locationRequest = LocationRequest.create()
.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY)
.setInterval(UPDATE_INTERVAL)
.setFastestInterval(FASTEST_INTERVAL);
if ( checkPermission() )
LocationServices.FusedLocationApi.requestLocationUpdates(googleApiClient, locationRequest, this);
}
@Override
public void onLocationChanged(Location location) {
Log.d(TAG, "onLocationChanged ["+location+"]");
lastLocation = location;
writeActualLocation(location);
}
// Write location coordinates on UI
private void writeActualLocation(Location location) {
textLat.setText( "Lat: " + location.getLatitude() );
textLong.setText( "Long: " + location.getLongitude() );
}
private void writeLastLocation() {
writeActualLocation(lastLocation);
}
// Check for permission to access Location
private boolean checkPermission() {
Log.d(TAG, "checkPermission()");
// Ask for permission if it wasn't granted yet
return (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION)
== PackageManager.PERMISSION_GRANTED );
}
// Asks for permission
private void askPermission() {
Log.d(TAG, "askPermission()");
ActivityCompat.requestPermissions(
this,
new String[] { Manifest.permission.ACCESS_FINE_LOCATION },
REQ_PERMISSION
);
}
// Verify user's response of the permission requested
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
Log.d(TAG, "onRequestPermissionsResult()");
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
switch ( requestCode ) {
case REQ_PERMISSION: {
if ( grantResults.length > 0
&& grantResults[0] == PackageManager.PERMISSION_GRANTED ){
// Permission granted
getLastKnownLocation();
} else {
// Permission denied
permissionsDenied();
}
break;
}
}
}
// App cannot work without the permissions
private void permissionsDenied() {
Log.w(TAG, "permissionsDenied()");
}
}
شایان ذکر است که LocationRequest ساخته شده در بالا برای استفاده در محیط تولید بهینه نیست، چرا که وقفه UPDATE_INTERVAL بسیار کوتاه است و همین امر مصرف باتری را افزایش می دهد و راه حل بهتر به صورت زیر می باشد:
private final int UPDATE_INTERVAL = 3 * 60 * 1000; // 3 minutes
private final int FASTEST_INTERVAL = 30 * 1000; // 30 secs
private void startLocationUpdates(){
Log.i(TAG, "startLocationUpdates()");
locationRequest = LocationRequest.create()
.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY)
.setInterval(UPDATE_INTERVAL)
.setFastestInterval(FASTEST_INTERVAL);
if ( checkPermission() )
LocationServices.FusedLocationApi.requestLocationUpdates(googleApiClient, locationRequest, this);
}
گام 7: مارکرهای GoogleMap
Activity به دو marker نیاز دارد، LocationMarker از latitude و longitude داده شده توسط FusedLocationProviderApi برای اطلاع رسانی درباره موقعیت کنونی گوشی استفاده می کند. GeofenceMarker نیز هدف ساخت geofence است و از آخرین لمس بر روی مپ برای بازیابی موقعیت استفاده می کند.
@Override
public void onMapClick(LatLng latLng) {
Log.d(TAG, "onMapClick("+latLng +")");
markerForGeofence(latLng);
}
private void writeActualLocation(Location location) {
// ...
markerLocation(new LatLng(location.getLatitude(), location.getLongitude()));
}
private Marker locationMarker;
// Create a Location Marker
private void markerLocation(LatLng latLng) {
Log.i(TAG, "markerLocation("+latLng+")");
String title = latLng.latitude + ", " + latLng.longitude;
MarkerOptions markerOptions = new MarkerOptions()
.position(latLng)
.title(title);
if ( map!=null ) {
// Remove the anterior marker
if ( locationMarker != null )
locationMarker.remove();
locationMarker = map.addMarker(markerOptions);
float zoom = 14f;
CameraUpdate cameraUpdate = CameraUpdateFactory.newLatLngZoom(latLng, zoom);
map.animateCamera(cameraUpdate);
}
}
private Marker geoFenceMarker;
// Create a marker for the geofence creation
private void markerForGeofence(LatLng latLng) {
Log.i(TAG, "markerForGeofence("+latLng+")");
String title = latLng.latitude + ", " + latLng.longitude;
// Define marker options
MarkerOptions markerOptions = new MarkerOptions()
.position(latLng)
.icon(BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_ORANGE))
.title(title);
if ( map!=null ) {
// Remove last geoFenceMarker
if (geoFenceMarker != null)
geoFenceMarker.remove();
geoFenceMarker = map.addMarker(markerOptions);
}
}
گام 8: ساخت یک Geofence
در این مرحله نوبت ساخت یک geofence رسیده، ما از geoFenceMarker به عنوان نقطه مرکزی geofence استفاده می کنیم.
private static final long GEO_DURATION = 60 * 60 * 1000;
private static final String GEOFENCE_REQ_ID = "My Geofence";
private static final float GEOFENCE_RADIUS = 500.0f; // in meters
// Create a Geofence
private Geofence createGeofence( LatLng latLng, float radius ) {
Log.d(TAG, "createGeofence");
return new Geofence.Builder()
.setRequestId(GEOFENCE_REQ_ID)
.setCircularRegion( latLng.latitude, latLng.longitude, radius)
.setExpirationDuration( GEO_DURATION )
.setTransitionTypes( Geofence.GEOFENCE_TRANSITION_ENTER
| Geofence.GEOFENCE_TRANSITION_EXIT )
.build();
}
بعد از آن یک آبجکت GeofencingRequest می سازیم.
// Create a Geofence Request
private GeofencingRequest createGeofenceRequest( Geofence geofence ) {
Log.d(TAG, "createGeofenceRequest");
return new GeofencingRequest.Builder()
.setInitialTrigger( GeofencingRequest.INITIAL_TRIGGER_ENTER )
.addGeofence( geofence )
.build();
}
از آبجکت PendingIntent برای فراخوانی IntentService که GeofenceEvent را مدیریت می کند استفاده می کنیم و بعدا کلاس GeofenceTransitionService.class را می سازیم.
private PendingIntent geoFencePendingIntent;
private final int GEOFENCE_REQ_CODE = 0;
private PendingIntent createGeofencePendingIntent() {
Log.d(TAG, "createGeofencePendingIntent");
if ( geoFencePendingIntent != null )
return geoFencePendingIntent;
Intent intent = new Intent( this, GeofenceTrasitionService.class);
return PendingIntent.getService(
this, GEOFENCE_REQ_CODE, intent, PendingIntent.FLAG_UPDATE_CURRENT );
}
// Add the created GeofenceRequest to the device's monitoring list
private void addGeofence(GeofencingRequest request) {
Log.d(TAG, "addGeofence");
if (checkPermission())
LocationServices.GeofencingApi.addGeofences(
googleApiClient,
request,
createGeofencePendingIntent()
).setResultCallback(this);
}
یک geofence را نیز به عنوان مرجع بر روی نقشه ترسیم می کنیم.
@Override
public void onResult(@NonNull Status status) {
Log.i(TAG, "onResult: " + status);
if ( status.isSuccess() ) {
drawGeofence();
} else {
// inform about fail
}
}
// Draw Geofence circle on GoogleMap
private Circle geoFenceLimits;
private void drawGeofence() {
Log.d(TAG, "drawGeofence()");
if ( geoFenceLimits != null )
geoFenceLimits.remove();
CircleOptions circleOptions = new CircleOptions()
.center( geoFenceMarker.getPosition())
.strokeColor(Color.argb(50, 70,70,70))
.fillColor( Color.argb(100, 150,150,150) )
.radius( GEOFENCE_RADIUS );
geoFenceLimits = map.addCircle( circleOptions );
}
متد ()startGeofence مسئول شروع فرآیند geofencing در کلاس MainActivity می باشد.
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch ( item.getItemId() ) {
case R.id.geofence: {
startGeofence();
return true;
}
}
return super.onOptionsItemSelected(item);
}
// Start Geofence creation process
private void startGeofence() {
Log.i(TAG, "startGeofence()");
if( geoFenceMarker != null ) {
Geofence geofence = createGeofence( geoFenceMarker.getPosition(), GEOFENCE_RADIUS );
GeofencingRequest geofenceRequest = createGeofenceRequest( geofence );
addGeofence( geofenceRequest );
} else {
Log.e(TAG, "Geofence marker is null");
}
}
گام 9: سرویس انتقال Geofence
در این مرحله قادر به ساخت کلاس GeofenceTransitionService.class می باشیم، این کلاس از IntentService اکستند شده و مسئول مدیریت GeofencingEvent می باشد. اول از همه ایونت را از ایتنت دریافت شده می گیریم.
GeofencingEvent geofencingEvent = GeofencingEvent.fromIntent(intent);
بعد نوع انتقال geofencing موردنظر را چک می کنیم. اگر انتقال صورت گرفته همان باشد که در نظر داشته ایم، به بازیابی لیستی از geofenceهای تریگر شده پرداخته و با استفاده از اکشن های مناسب یک نوتیفیکیشن را به نمایش می گذاریم.
// Retrieve GeofenceTrasition
int geoFenceTransition = geofencingEvent.getGeofenceTransition();
// Check if the transition type
if ( geoFenceTransition == Geofence.GEOFENCE_TRANSITION_ENTER ||
geoFenceTransition == Geofence.GEOFENCE_TRANSITION_EXIT ) {
// Get the geofence that were triggered
List<Geofence> triggeringGeofences = geofencingEvent.getTriggeringGeofences();
// Create a detail message with Geofences received
String geofenceTransitionDetails = getGeofenceTrasitionDetails(geoFenceTransition, triggeringGeofences );
// Send notification details as a String
sendNotification( geofenceTransitionDetails );
}
متدهای کمکی نیز به منظور درک بهتر پیاده سازی کلاس درج شده اند.
public class GeofenceTrasitionService extends IntentService {
private static final String TAG = GeofenceTrasitionService.class.getSimpleName();
public static final int GEOFENCE_NOTIFICATION_ID = 0;
public GeofenceTrasitionService() {
super(TAG);
}
@Override
protected void onHandleIntent(Intent intent) {
// Retrieve the Geofencing intent
GeofencingEvent geofencingEvent = GeofencingEvent.fromIntent(intent);
// Handling errors
if ( geofencingEvent.hasError() ) {
String errorMsg = getErrorString(geofencingEvent.getErrorCode() );
Log.e( TAG, errorMsg );
return;
}
// Retrieve GeofenceTrasition
int geoFenceTransition = geofencingEvent.getGeofenceTransition();
// Check if the transition type
if ( geoFenceTransition == Geofence.GEOFENCE_TRANSITION_ENTER ||
geoFenceTransition == Geofence.GEOFENCE_TRANSITION_EXIT ) {
// Get the geofence that were triggered
List<Geofence> triggeringGeofences = geofencingEvent.getTriggeringGeofences();
// Create a detail message with Geofences received
String geofenceTransitionDetails = getGeofenceTrasitionDetails(geoFenceTransition, triggeringGeofences );
// Send notification details as a String
sendNotification( geofenceTransitionDetails );
}
}
// Create a detail message with Geofences received
private String getGeofenceTrasitionDetails(int geoFenceTransition, List<Geofence> triggeringGeofences) {
// get the ID of each geofence triggered
ArrayList<String> triggeringGeofencesList = new ArrayList<>();
for ( Geofence geofence : triggeringGeofences ) {
triggeringGeofencesList.add( geofence.getRequestId() );
}
String status = null;
if ( geoFenceTransition == Geofence.GEOFENCE_TRANSITION_ENTER )
status = "Entering ";
else if ( geoFenceTransition == Geofence.GEOFENCE_TRANSITION_EXIT )
status = "Exiting ";
return status + TextUtils.join( ", ", triggeringGeofencesList);
}
// Send a notification
private void sendNotification( String msg ) {
Log.i(TAG, "sendNotification: " + msg );
// Intent to start the main Activity
Intent notificationIntent = MainActivity.makeNotificationIntent(
getApplicationContext(), msg
);
TaskStackBuilder stackBuilder = TaskStackBuilder.create(this);
stackBuilder.addParentStack(MainActivity.class);
stackBuilder.addNextIntent(notificationIntent);
PendingIntent notificationPendingIntent = stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);
// Creating and sending Notification
NotificationManager notificatioMng =
(NotificationManager) getSystemService( Context.NOTIFICATION_SERVICE );
notificatioMng.notify(
GEOFENCE_NOTIFICATION_ID,
createNotification(msg, notificationPendingIntent));
}
// Create a notification
private Notification createNotification(String msg, PendingIntent notificationPendingIntent) {
NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(this);
notificationBuilder
.setSmallIcon(R.drawable.ic_action_location)
.setColor(Color.RED)
.setContentTitle(msg)
.setContentText("Geofence Notification!")
.setContentIntent(notificationPendingIntent)
.setDefaults(Notification.DEFAULT_LIGHTS | Notification.DEFAULT_VIBRATE | Notification.DEFAULT_SOUND)
.setAutoCancel(true);
return notificationBuilder.build();
}
// Handle errors
private static String getErrorString(int errorCode) {
switch (errorCode) {
case GeofenceStatusCodes.GEOFENCE_NOT_AVAILABLE:
return "GeoFence 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 error.";
}
}
}
تست کردن
تست بر روی یک گوشی مجازی
تست geofencing بر روی یک گوشی مجازی بسیار ساده تر بوده و راه های متعددی برای این کار وجود دارند. در اندروید استودیو یک گوشی مجازی را باز کنید و بر روی دکمه more options در قسمت راست و پایین کلیک کنید.
در تب Location در قسمت چپ مختصات موقعیت موردنظر خود را وارد نمایید.
استفاده از کامندهای telnet برای کنترل گوشی مجازی راه حل خوبی است، برای این کار باید از طریق کامند لاین و دستور زیر به گوشی موردنظر خود متصل شوید:
telnet localhost [DEVICE_PORT]
پورت گوشی در پنجره گوشی مجازی نمایش داده خواهد شد که معمولا برابر با عدد 5554 است.
احتمال دارد که نیاز به تایید این اتصال با استفاده از auth_token داشته باشید، اما کامند لاین مکان آن را به شما نمایش خواهد داد. به آن مکان رفته و توکن را کپی کرده و [auth [YOUR_AUTH_TOKEN را تایپ کنید.
در این مرحله قادر به تعیین موقعیت گوشی با اجرای کامند زیر هستید:
geo fix [LATITUDE] [LONGITUDE]
جمع بندی
استفاده از Geofencing به شما در راستای افزایش تعامل با کاربران یاری می رساند. افزودن Geofencing به یک پروژه ساده است، اما باید به میزان مصرف باتری و توان گوشی نیز توجه کنید. برای این کار باید اندازه geofence و نرخ آپدیت را به دقت انتخاب کرد، چرا که هر دوی این موارد میزان مصرف اپلیکیشن شما را تحت تاثیر قرار می دهند.
تست در این مورد بسیار اهمیت دارد، چرا که تنها از طریق تست اپلیکیشن میزان مصرف آن مشخص می شود. لازم است تا امکان غیرفعال سازی قابلیت geofencing به طور کامل نیز در اختیار کاربر قرار گیرد، چرا که ممکن است کاربران تمایل و نیازی به استفاده از این قابلیت نداشته باشند.