在 Android* 商业应用程序中实现地图和地理围栏功能





0/5 (0投票)
本案例研究讨论了如何在 Android* 商务应用中构建地图和地理定位功能,包括在 Google Maps* 上叠加商店位置,以及使用地理围栏在设备进入商店附近时通知用户。
摘要
本案例研究讨论了如何在 Android* 商务应用中构建地图和地理定位功能,包括在 Google Maps* 上叠加商店位置,以及使用地理围栏在设备进入商店附近时通知用户。
目录
概述
在本案例研究中,我们将把地图和地理定位功能集成到一款面向 Android 平板电脑的餐厅商务应用中(图 1)。用户可以从主菜单项“Locations and Geofences”(图 2)访问地理定位功能。


在 Google Maps 上显示商店位置
对于商务应用而言,在地图上显示商店位置非常直观且对用户很有帮助(图 3)。Google Maps Android API 提供了一种将 Google Maps 集成到您的 Android 应用中的便捷方法。
Google Maps Android API v2
Google Maps Android API v2 是 Google Play Services APK 的一部分。要创建使用 Google Maps Android API v2 的 Android 应用,需要通过下载和配置 Google Play Services SDK、获取 API 密钥以及在应用的 AndroidManifest.xml 文件中添加必要的设置来设置开发环境。
首先,您需要按照 https://developer.android.com.cn/google/play-services/setup.html 设置 Google Play Services SDK。
然后,您需要从 Google Developers Console https://console.developers.google.com/project 注册您的项目并获取 API 密钥。您需要在 AndroidManifest.xml 文件中添加 API 密钥。

在应用程序清单中指定应用设置
要使用 Google Maps Android API v2,需要在 <manifest> 元素下指定一些权限和功能(代码示例 1)。这些包括网络连接、外部存储以及位置访问的必要权限。Google Maps Android API 还需要 OpenGL ES 版本 2 功能。
	<uses-permission android:name="android.permission.INTERNET"/>
	<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
	<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
	<uses-permission android:name="com.google.android.providers.gsf.permission.READ_GSERVICES"/>
	<!-- The following two permissions are not required to use
	     Google Maps Android API v2, but are recommended. -->
	<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
	<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
	<uses-permission android:name="android.permission.ACCESS_MOCK_LOCATION" />
      <uses-feature
        android:glEsVersion="0x00020000"
        android:required="true"/>
同样,在 <application> 元素下,我们在 <meta-data> 元素中指定 Google Play Services 版本和获取的 API 密钥(代码示例 2)。
      <meta-data
              android:name="com.google.android.gms.version"
              android:value="@integer/google_play_services_version" />
        
      <meta-data
	        android:name="com.google.android.maps.v2.API_KEY"
		  android:value="copy your API Key here"/>
添加地图 Fragment
首先,在您的 activity 布局 xml 文件中,添加一个 MapFragment 元素(代码示例 3)。
    <fragment
        android:id="@+id/storelocationmap"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:name="com.google.android.gms.maps.MapFragment"
    />
在您的 activity 类中,您可以检索 Google Maps MapFragment 对象并在每个商店位置绘制商店图标。
…
private static final LatLng CHANDLER = new LatLng(33.455,-112.0668);
…
private static final StoreLocation[] ALLRESTURANTLOCATIONS = new StoreLocation[] {
        new StoreLocation(new LatLng(33.455,-112.0668), new String("Phoenix, AZ")),
        new StoreLocation(new LatLng(33.5123,-111.9336), new String("SCOTTSDALE, AZ")),
        new StoreLocation(new LatLng(33.3333,-111.8335), new String("Chandler, AZ")),
        new StoreLocation(new LatLng(33.4296,-111.9436), new String("Tempe, AZ")),
        new StoreLocation(new LatLng(33.4152,-111.8315), new String("Mesa, AZ")),
        new StoreLocation(new LatLng(33.3525,-111.7896), new String("Gilbert, AZ"))
};
…    
      @Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.geolocation_view);
		
		mMap = ((MapFragment) getFragmentManager().findFragmentById(R.id.storelocationmap)).getMap();
		mMap.moveCamera(CameraUpdateFactory.newLatLngZoom(CHANDLER, ZOOM_LEVEL));
		Drawable iconDrawable = getResources().getDrawable(R.drawable.ic_launcher);
		Bitmap iconBmp = ((BitmapDrawable) iconDrawable).getBitmap();
		for(int ix = 0; ix < ALLRESTURANTLOCATIONS.length; ix++) {
			mMap.addMarker(new MarkerOptions()
			    .position(ALLRESTURANTLOCATIONS[ix].mLatLng)
		        .icon(BitmapDescriptorFactory.fromBitmap(iconBmp)));
		}
…
发送地理围栏通知
地理围栏是由点的纬度和经度坐标和半径定义的圆形区域。Android 应用可以将地理围栏注册到 Android 位置服务。Android 应用还可以为地理围栏指定到期持续时间。每当发生地理围栏转换时,例如,当 Android 设备进入或离开注册的地理围栏时,Android 位置服务就会通知 Android 应用。
在我们的餐厅应用中,我们可以为每个商店位置定义一个地理围栏。当设备进入商店附近时,应用会发送通知,例如,“您已进入您最喜欢的餐厅附近!”(图 4)。

注册和注销地理围栏
在 Android SDK 中,位置服务也是 Google Play Services APK 中“Extras”目录下的一个组件。
要请求地理围栏监控,首先我们需要在应用的清单中指定“ACCESS_FINE_LOCATION”权限,这在前面的部分已经完成。
我们还需要检查 Google Play Services 的可用性(代码示例 5 中的 checkGooglePlayServices() 方法)。在调用 locationClient().connect() 并成功建立连接后,位置服务将调用 onConnected(Bundle bundle) 函数,在该函数中,位置客户端可以请求添加或删除地理围栏。
public class GeolocationActivity extends Activity implements
        GooglePlayServicesClient.ConnectionCallbacks
…
{
…	
    private LocationClient mLocationClient;
    
…
    static class StoreLocation {
        public LatLng mLatLng;
        public String mId;
        StoreLocation(LatLng latlng, String id) {
            mLatLng = latlng;
            mId = id;
        }
    }
    @Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.geolocation_view);
        mLocationClient = new LocationClient(this, this, this);
        // Create a new broadcast receiver to receive updates from the listeners and service
        mGeofenceBroadcastReceiver = new ResturantGeofenceReceiver();
        // Create an intent filter for the broadcast receiver
        mIntentFilter = new IntentFilter();
        // Action for broadcast Intents that report successful addition of geofences
        mIntentFilter.addAction(ACTION_GEOFENCES_ADDED);
        // Action for broadcast Intents that report successful removal of geofences
        mIntentFilter.addAction(ACTION_GEOFENCES_REMOVED);
        // Action for broadcast Intents containing various types of geofencing errors
        mIntentFilter.addAction(ACTION_GEOFENCE_ERROR);
        // All Location Services sample apps use this category
        mIntentFilter.addCategory(CATEGORY_LOCATION_SERVICES);
		createGeofences();
		mRegisterGeofenceButton = (Button)findViewById(R.id.geofence_switch);
		mGeofenceState = CAN_START_GEOFENCE;
    
    }
    
    @Override
    protected void onResume() {
        super.onResume();
        // Register the broadcast receiver to receive status updates
        LocalBroadcastManager.getInstance(this).registerReceiver(
            mGeofenceBroadcastReceiver, mIntentFilter);
    }
        
    /**
     * Create a Geofence list
     */
    public void createGeofences() {
        for(int ix=0; ix > ALLRESTURANTLOCATIONS.length; ix++) {
            Geofence fence = new Geofence.Builder()
                .setRequestId(ALLRESTURANTLOCATIONS[ix].mId)
                .setTransitionTypes(Geofence.GEOFENCE_TRANSITION_ENTER)
                .setCircularRegion(
                    ALLRESTURANTLOCATIONS[ix].mLatLng.latitude, ALLRESTURANTLOCATIONS[ix].mLatLng.longitude, GEOFENCERADIUS)
                .setExpirationDuration(Geofence.NEVER_EXPIRE)
                .build();
            mGeofenceList.add(fence);
        }
    }
    // callback function when the mRegisterGeofenceButton is clicked
    public void onRegisterGeofenceButtonClick(View view) {
        if (mGeofenceState == CAN_REGISTER_GEOFENCE) {
            registerGeofences();
            mGeofenceState = GEOFENCE_REGISTERED;
            mGeofenceButton.setText(R.string.unregister_geofence);
            mGeofenceButton.setClickable(true);            
        else {
            unregisterGeofences();
            mGeofenceButton.setText(R.string.register_geofence);
            mGeofenceButton.setClickable(true);
            mGeofenceState = CAN_REGISTER_GEOFENCE;
        }
    }
    private boolean checkGooglePlayServices() {
        int result = GooglePlayServicesUtil.isGooglePlayServicesAvailable(this);
        if (result == ConnectionResult.SUCCESS) {
            return true;
        } 
        else {
            Dialog errDialog = GooglePlayServicesUtil.getErrorDialog(
                    result,
                    this,
                    CONNECTION_FAILURE_RESOLUTION_REQUEST);
            if (errorDialog != null) {
                errorDialog.show();
            }
        }
        return false;
   }
    public void registerGeofences() {
	
        if (!checkGooglePlayServices()) {
            return;
        }
        mRequestType = REQUEST_TYPE.ADD;
        try {
            // Try to add geofences
            requestConnectToLocationClient();
        } catch (UnsupportedOperationException e) {
            // handle the exception
        }
        
    }
    public void unregisterGeofences() {
        if (!checkGooglePlayServices()) {
            return;
        }
        // Record the type of removal
    	  mRequestType = REQUEST_TYPE.REMOVE;
        // Try to make a removal request
        try {
            mCurrentIntent = getRequestPendingIntent());
            requestConnectToLocationClient();
        } catch (UnsupportedOperationException e) {
            // handle the exception
        }
    }
    public void requestConnectToLocationServices () throws UnsupportedOperationException {
        // If a request is not already in progress
        if (!mRequestInProgress) {
            mRequestInProgress = true;
            locationClient().connect();
        } 
        else {
            // Throw an exception and stop the request
            throw new UnsupportedOperationException();
        }
    }
    /**
     * Get a location client and disconnect from Location Services
     */
    private void requestDisconnectToLocationServices() {
        // A request is no longer in progress
        mRequestInProgress = false;
        locationClient().disconnect();
        
        if (mRequestType == REQUEST_TYPE.REMOVE) {
            mCurrentIntent.cancel();
        }
    }
    /**
     * returns A LocationClient object
     */
    private GooglePlayServicesClient locationClient() {
        if (mLocationClient == null) {
            mLocationClient = new LocationClient(this, this, this);
        }
        return mLocationClient;
}
    /*
     Called back from the Location Services when the request to connect the client finishes successfully. At this point, you can
request the current location or start periodic updates
     */
    @Override
    public void onConnected(Bundle bundle) {
        if (mRequestType == REQUEST_TYPE.ADD) {
        // Create a PendingIntent for Location Services to send when a geofence transition occurs
        mGeofencePendingIntent = createRequestPendingIntent();
        // Send a request to add the current geofences
        mLocationClient.addGeofences(mGeofenceList, mGeofencePendingIntent, this);
        } 
        else if (mRequestType == REQUEST_TYPE.REMOVE){
            mLocationClient.removeGeofences(mCurrentIntent, this);        
        } 
}
…
}
实现位置服务回调
位置服务请求通常是非阻塞的或异步调用。实际上,在前一部分的代码示例 5 中,我们已经实现了其中一个函数:在调用 locationClient().connect() 并建立连接后,位置服务将调用 onConnected(Bundle bundle) 函数。代码示例 6 列出了我们需要实现的其他位置回调函数。
public class GeolocationActivity extends Activity implements
        OnAddGeofencesResultListener,
        OnRemoveGeofencesResultListener,
        GooglePlayServicesClient.ConnectionCallbacks,
        GooglePlayServicesClient.OnConnectionFailedListener {
…	
    @Override
    public void onDisconnected() {
        mRequestInProgress = false;
        mLocationClient = null;
}
    
    /*
     * Handle the result of adding the geofences
     */
    @Override
    public void onAddGeofencesResult(int statusCode, String[] geofenceRequestIds) {
        // Create a broadcast Intent that notifies other components of success or failure
        Intent broadcastIntent = new Intent();
        // Temp storage for messages
        String msg;
        // If adding the geocodes was successful
        if (LocationStatusCodes.SUCCESS == statusCode) {
            // Create a message containing all the geofence IDs added.
            msg = getString(R.string.add_geofences_result_success,
                    Arrays.toString(geofenceRequestIds));
            // Create an Intent to broadcast to the app
            broadcastIntent.setAction(ACTION_GEOFENCES_ADDED)
                           .addCategory(CATEGORY_LOCATION_SERVICES)
                           .putExtra(EXTRA_GEOFENCE_STATUS, msg);
        // If adding the geofences failed
        } else {
            msg = getString(
                    R.string.add_geofences_result_failure,
                    statusCode,
                    Arrays.toString(geofenceRequestIds)
            );
            broadcastIntent.setAction(ACTION_GEOFENCE_ERROR)
                           .addCategory(CATEGORY_LOCATION_SERVICES)
                           .putExtra(EXTRA_GEOFENCE_STATUS, msg);
        }
        LocalBroadcastManager.getInstance(this)
            .sendBroadcast(broadcastIntent);
        // request to disconnect the location client
        requestDisconnectToLocationServices();
    }
    /*
     * Implementation of OnConnectionFailedListener.onConnectionFailed
     * If a connection or disconnection request fails, report the error
     * connectionResult is passed in from Location Services
     */
    @Override
    public void onConnectionFailed(ConnectionResult connectionResult) {
        mInProgress = false;
        if (connectionResult.hasResolution()) {
            try {
                connectionResult.startResolutionForResult(this,
                    CONNECTION_FAILURE_RESOLUTION_REQUEST);
            } 
            catch (SendIntentException e) {
                // log the error
            }
        } 
        else {
            Intent errorBroadcastIntent = new Intent(ACTION_CONNECTION_ERROR);
            errorBroadcastIntent.addCategory(CATEGORY_LOCATION_SERVICES)
                     .putExtra(EXTRA_CONNECTION_ERROR_CODE,
                                 connectionResult.getErrorCode());
             LocalBroadcastManager.getInstance(this)
                 .sendBroadcast(errorBroadcastIntent);
        }
    }
    
    @Override
    public void onRemoveGeofencesByPendingIntentResult(int statusCode,
            PendingIntent requestIntent) {
        // Create a broadcast Intent that notifies other components of success or failure
        Intent broadcastIntent = new Intent();
        // If removing the geofences was successful
        if (statusCode == LocationStatusCodes.SUCCESS) {
            // Set the action and add the result message
            broadcastIntent.setAction(ACTION_GEOFENCES_REMOVED);
            broadcastIntent.putExtra(EXTRA_GEOFENCE_STATUS,
                    getString(R.string.remove_geofences_intent_success));
        } 
        else {
            // removing the geocodes failed
            // Set the action and add the result message
            broadcastIntent.setAction(ACTION_GEOFENCE_ERROR);
            broadcastIntent.putExtra(EXTRA_GEOFENCE_STATUS,
                    getString(R.string.remove_geofences_intent_failure, 
                        statusCode));
        }
        LocalBroadcastManager.getInstance(this)
                .sendBroadcast(broadcastIntent);
        // request to disconnect the location client
        requestDisconnectToLocationServices();
    }
        
    public class ResturantGeofenceReceiver extends BroadcastReceiver {
  
	  @Override
        public void onReceive(Context context, Intent intent) {
            String action = intent.getAction();
            // Intent contains information about errors in adding or removing geofences
            if (TextUtils.equals(action, ACTION_GEOFENCE_ERROR)) {
                // handleGeofenceError(context, intent);
            } 
            else if (TextUtils.equals(action, ACTION_GEOFENCES_ADDED)
                    ||
                    TextUtils.equals(action, ACTION_GEOFENCES_REMOVED)) {
                // handleGeofenceStatus(context, intent);
            } 
            else if (TextUtils.equals(action, ACTION_GEOFENCE_TRANSITION)) {
                // handleGeofenceTransition(context, intent);
            } 
            else {
                // handle error
            }
			
        }
    }
    public PendingIntent getRequestPendingIntent() {
        return createRequestPendingIntent();
    }
    private PendingIntent createRequestPendingIntent() {
        if (mGeofencePendingIntent != null) {
            // Return the existing intent
            return mGeofencePendingIntent;
        // If no PendingIntent exists
        } else {
            // Create an Intent pointing to the IntentService
            Intent intent = new Intent(this,
                ReceiveGeofenceTransitionIntentService.class);
            return PendingIntent.getService(
                    this,
                    0,
                    intent,
                    PendingIntent.FLAG_UPDATE_CURRENT);
        }
    }
    @Override
public void onRemoveGeofencesByRequestIdsResult(int statusCode, 
    String[] geofenceRequestIds) {
        // it should not come here because we only remove geofences by PendingIntent
        // Disconnect the location client
        requestDisconnection();
    }
实现 IntentService
最后,我们需要实现 IntentService 类来处理地理围栏转换(代码示例 7)。
public class ReceiveGeofenceTransitionIntentService extends IntentService {
    /**
     * Sets an identifier for the service
     */
    public ReceiveGeofenceTransitionIntentService() {
        super("ReceiveGeofenceTransitionsIntentService");
    }
    @Override
    protected void onHandleIntent(Intent intent) {
    	
        // Create a local broadcast Intent
        Intent broadcastIntent = new Intent();
        // Give it the category for all intents sent by the Intent Service
        broadcastIntent.addCategory(CATEGORY_LOCATION_SERVICES);
   	
        // First check for errors
        if (LocationClient.hasError(intent)) {
            // Get the error code with a static method
            int errorCode = LocationClient.getErrorCode(intent);
        } 
        else {
            // Get the type of transition (entry or exit)
            int transition =
                    LocationClient.getGeofenceTransition(intent);
            
            if ((transition == Geofence.GEOFENCE_TRANSITION_ENTER)  ||
                    (transition == Geofence.GEOFENCE_TRANSITION_EXIT)) {
                // Post a notification
            } 
            else {
                // handle the error
            }
        }
    }
}
总结
在本文中,我们讨论了如何将地图和地理围栏功能集成到 Android 商务应用中。这些功能支持应用中丰富的地理定位内容和强大的基于位置的服务及用例。
参考资料
- Google Maps Android API V2 https://developers.google.com/maps/documentation/android/start#getting_the_google_maps_android_api_v2
- 创建和监控地理围栏 https://developer.android.com.cn/training/location/geofencing.html
关于作者
苗炜是英特尔软件与服务事业部的一名软件工程师。他目前从事英特尔® Atom™ 处理器平台优化项目。
*其他名称和品牌可能被声明为他人的财产。
**此示例源代码根据英特尔示例源代码许可协议发布
优化通知
英特尔编译器可能不会对非英特尔微处理器进行与英特尔微处理器相同的优化程度的优化,这些优化包括 SSE2、SSE3 和 SSE3 指令集以及其他优化。英特尔不保证在非英特尔制造的微处理器上任何优化的可用性、功能或有效性。
本产品中的微处理器相关优化旨在用于英特尔微处理器。某些非英特尔微体系结构特有的优化保留给英特尔微处理器。请参阅适用的产品用户和参考指南,了解有关本声明涵盖的具体指令集的更多信息。
其他相关文章和资源
- Gayathri Murali 的《Intel 平台上的 Android》 - Bay Area Android Fest
- Intel® Atom™ x86 镜像 for Android* 4.4 KitKat 安装说明 - 手动
- 编码 Android* 游戏应用以支持 Intel x86?这里有一些示例!
- Intel for Android* 开发者学习系列 #7:为 Intel® 架构创建和移植基于 NDK 的 Android* 应用程序
- 使用案例研究进行 Android* 上的 NFC 应用开发
- 在 Android* 商业应用程序中实现地图和地理围栏功能
- 自动 Android* 应用程序测试
- 如何在 Intel 架构上优化您的 Android* 应用 (NDK) - 两分钟搞定
- 如何设置 NDK 项目以编译适用于多个目标平台的应用

