نحوه کار با Geofences در اندروید – بخش دوم

نحوه کار با Geofences در اندروید – بخش دوم


در مطلب قبلی با کلیاتی از geofence آشنایی پیدا کردید، در ادامه قصد داریم با ساخت یک اپلیکیشن ساده اندروید نحوه کار با geofence را به شما آموزش دهیم، با ما همراه باشید.

ساخت یک اپلیکیشن Geofencing

در این مطلب آموزشی به ساخت یک اپلیکیشن ساده پرداخته می شود که به نظارت بر موقعیت قرارگیری کاربر پرداخته و زمانی که کاربر به یک منطقه Geofence وارد یا از آن خارج شود یک نوتیفیکیشن ارسال می کند. این اپلیکیشن تنها شامل یک Activity و یک IntentSerivce است. یک نگاه سریع و اجمالی نیز به GoogleMap ،GoogleApiClient و FusedLocationProviderApi می اندازیم و چند نکته از Geofence API را نیز مورد بررسی قرار می دهیم.

blog_19517_1

گام 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 در قسمت راست و پایین کلیک کنید.

blog_19517_2

در تب Location در قسمت چپ مختصات موقعیت موردنظر خود را وارد نمایید.

blog_19517_3

استفاده از کامندهای telnet برای کنترل گوشی مجازی راه حل خوبی است، برای این کار باید از طریق کامند لاین و دستور زیر به گوشی موردنظر خود متصل شوید:

telnet localhost [DEVICE_PORT]

پورت گوشی در پنجره گوشی مجازی نمایش داده خواهد شد که معمولا برابر با عدد 5554 است.

احتمال دارد که نیاز به تایید این اتصال با استفاده از auth_token داشته باشید، اما کامند لاین مکان آن را به شما نمایش خواهد داد. به آن مکان رفته و توکن را کپی کرده و [auth [YOUR_AUTH_TOKEN را تایپ کنید.

در این مرحله قادر به تعیین موقعیت گوشی با اجرای کامند زیر هستید:

geo fix [LATITUDE] [LONGITUDE]

جمع بندی

استفاده از Geofencing به شما در راستای افزایش تعامل با کاربران یاری می رساند. افزودن Geofencing به یک پروژه ساده است، اما باید به میزان مصرف باتری و توان گوشی نیز توجه کنید. برای این کار باید اندازه geofence و نرخ آپدیت را به دقت انتخاب کرد، چرا که هر دوی این موارد میزان مصرف اپلیکیشن شما را تحت تاثیر قرار می دهند.

تست در این مورد بسیار اهمیت دارد، چرا که تنها از طریق تست اپلیکیشن میزان مصرف آن مشخص می شود. لازم است تا امکان غیرفعال سازی قابلیت geofencing به طور کامل نیز در اختیار کاربر قرار گیرد، چرا که ممکن است کاربران تمایل و نیازی به استفاده از این قابلیت نداشته باشند.

 

http://code.tutsplus.com برگرفته از

اینها را هم بخوانید