首页 > 代码库 > 【Android Developers Training】 107. 认知用户当前的行为

【Android Developers Training】 107. 认知用户当前的行为

注:本文翻译自Google官方的Android Developers Training文档,译者技术一般,由于喜爱安卓而产生了翻译的念头,纯属个人兴趣爱好。

原文链接:http://developer.android.com/training/location/activity-recognition.html


样例代码:

ActivityRecognition.zip


行为认知会尝试检测当前用户的物理行为,比如:行走,驾驶或者静止站立。从一个行为认知客户端发出更新信息的请求,同之前的定位或者地理围栏所使用的定位客户端相比有所不同,但大致思路是一致的。基于你所选择的的更新间隔,定位服务会发出一个或更多个用户当前可能的行为信息,同时每一个信息都会有一个可能性级别。这节课将向你展示如何从定位服务实现用户的行为识别。


一). 请求行为认知更新

从定位服务请求一个行为认知更新与请求定期的地点更新是比较类似的。你通过客户端发出请求,然后定位服务通过一个PendingIntent将更新信息发回给你的应用。然而,在你请求行为识别更新之前,你需要申请一些特别的权限,然后你使用不同类型的客户端发出申请。下面各个章节将会讲解如何请求权限,连接客户端并请求更新。

申请就收更新的权限

一个应用如果想要获取行为认知更新,必须有“com.google.android.gms.permission.ACTIVITY_RECOGNITION”的权限。要为你的权限请求这一权限,将下列的XML标签作为<manifest>标签的子标签添加到你的清单列表当中:

<uses-permission
    android:name="com.google.android.gms.permission.ACTIVITY_RECOGNITION"/>

另外,行为认知不需要ACCESS_COARSE_LOCATION权限和ACCESS_FINE_LOCATION权限。

检查Google Play服务

位置服务是Google Play服务APK的其中一部分。由于用户设备的状态时难以预料的,你应该一直在你尝试连接定位服务之前,检查APK是否已经安装。要检查APK是否安装,可以调用GooglePlayServicesUtil.isGooglePlayServicesAvailable(),它会返回一个整形的结果码,其含义可以参阅:ConnectionResult。如果你遇到了一个错误,可以调用GooglePlayServicesUtil.getErrorDialog(),来获取一个本地的对话框,引导用户执行正确地行为,之后将这一对话框显示在一个DialogFragment上。这一对话框可能允许用户解决当前的问题,此时Google Play服务会发回一个结果到你的activity中。要处理这一结果,需要覆写onActivityResult()方法。

Note:

要使你的应用可以兼容1.6及以后版本的系统,显示DialogFragment的activity必须是FragmentActivity的子类,而非Activity。使用FragmentActivity还可以允许你调用getSupportFragmentManager()方法来显示DialogFragment。

由于你一直需要在你的代码多个地方检查Google Play服务,所以应该定义一个方法将检查行为进行封装,之后在每次连接尝试之前进行检查。下面的代码片段包含了检查Google Play服务所需要的代码:

public class MainActivity extends FragmentActivity {
    ...
    // Global constants
    /*
     * Define a request code to send to Google Play services
     * This code is returned in Activity.onActivityResult
     */
    private final static int
            CONNECTION_FAILURE_RESOLUTION_REQUEST = 9000;
    ...
    // Define a DialogFragment that displays the error dialog
    public static class ErrorDialogFragment extends DialogFragment {
        // Global field to contain the error dialog
        private Dialog mDialog;
        // Default constructor. Sets the dialog field to null
        public ErrorDialogFragment() {
            super();
            mDialog = null;
        }
        // Set the dialog to display
        public void setDialog(Dialog dialog) {
            mDialog = dialog;
        }
        // Return a Dialog to the DialogFragment.
        @Override
        public Dialog onCreateDialog(Bundle savedInstanceState) {
            return mDialog;
        }
    }
    ...
    /*
     * Handle results returned to the FragmentActivity
     * by Google Play services
     */
    @Override
    protected void onActivityResult(
            int requestCode, int resultCode, Intent data) {
        // Decide what to do based on the original request code
        switch (requestCode) {
            ...
            case CONNECTION_FAILURE_RESOLUTION_REQUEST :
            /*
             * If the result code is Activity.RESULT_OK, try
             * to connect again
             */
                switch (resultCode) {
                    case Activity.RESULT_OK :
                    /*
                     * Try the request again
                     */
                    ...
                    break;
                }
            ...
        }
        ...
    }
    ...
    private boolean servicesConnected() {
        // Check that Google Play services is available
        int resultCode =
                GooglePlayServicesUtil.
                        isGooglePlayServicesAvailable(this);
        // If Google Play services is available
        if (ConnectionResult.SUCCESS == resultCode) {
            // In debug mode, log the status
            Log.d("Activity Recognition",
                    "Google Play services is available.");
            // Continue
            return true;
        // Google Play services was not available for some reason
        } else {
            // Get the error dialog from Google Play services
            Dialog errorDialog = GooglePlayServicesUtil.getErrorDialog(
                    resultCode,
                    this,
                    CONNECTION_FAILURE_RESOLUTION_REQUEST);

            // If Google Play services can provide an error dialog
            if (errorDialog != null) {
                // Create a new DialogFragment for the error dialog
                ErrorDialogFragment errorFragment =
                        new ErrorDialogFragment();
                // Set the dialog in the DialogFragment
                errorFragment.setDialog(errorDialog);
                // Show the error dialog in the DialogFragment
                errorFragment.show(
                        getSupportFragmentManager(),
                        "Activity Recognition");
            }
            return false;
        }
    }
    ...
}

在后续章节的代码片段中,都会调用这一方法来验证是否可获取Google Play服务。

发送行文更新请求

从一个实现了定位服务所需要额回调函数的Activity或者Fragment发送更新请求。当你请求连接到一个行为认知客户端时,最好将请求做成异步的进程。当客户端已经连接了,定位服务会调用你的onConnected()实现。在这个方法中,你可以将更新请求发送给定位服务;该请求是同步的。一旦你发出了请求,你可以关闭客户端的连接。

整个过程会在下面的各个代码片段中展开。

定义Activity或者Fragment

定义一个FragmentActivity或者Fragment实现下列的接口:

ConnectionCallbacks

当客户端连接或者断开连接时定位服务调用的方法。

OnConnectionFailedListener

当请求连接到客户端时发生错误,定位服务调用的方法。

例如:

public class MainActivity extends FragmentActivity implements
        ConnectionCallbacks, OnConnectionFailedListener {
    ...
}

之后定义全局变量和常量。给更新间隔定义常量,为行为识别客户端添加变量,同时还需要为定位服务发送更新时使用的PendingIntent添加另一个变量:

public class MainActivity extends FragmentActivity implements
        ConnectionCallbacks, OnConnectionFailedListener {
    ...
    // Constants that define the activity detection interval
    public static final int MILLISECONDS_PER_SECOND = 1000;
    public static final int DETECTION_INTERVAL_SECONDS = 20;
    public static final int DETECTION_INTERVAL_MILLISECONDS =
            MILLISECONDS_PER_SECOND * DETECTION_INTERVAL_SECONDS;
    ...
    /*
     * Store the PendingIntent used to send activity recognition events
     * back to the app
     */
    private PendingIntent mActivityRecognitionPendingIntent;
    // Store the current activity recognition client
    private ActivityRecognitionClient mActivityRecognitionClient;
    ...
}

在onCreate()中,实例化行为认知客户端以及PendingIntent:

public class MainActivity extends FragmentActivity implements
        ConnectionCallbacks, OnConnectionFailedListener {
    ...
    @Override
    onCreate(Bundle savedInstanceState) {
        ...
        /*
         * Instantiate a new activity recognition client. Since the
         * parent Activity implements the connection listener and
         * connection failure listener, the constructor uses "this"
         * to specify the values of those parameters.
         */
        mActivityRecognitionClient =
                new ActivityRecognitionClient(mContext, this, this);
        /*
         * Create the PendingIntent that Location Services uses
         * to send activity recognition updates back to this app.
         */
        Intent intent = new Intent(
                mContext, ActivityRecognitionIntentService.class);
        /*
         * Return a PendingIntent that starts the IntentService.
         */
        mActivityRecognitionPendingIntent =
                PendingIntent.getService(mContext, 0, intent,
                PendingIntent.FLAG_UPDATE_CURRENT);
        ...
    }
    ...
}

开始请求进程

定义一个方法来请求行为识别更新。在该方法中,请求一个到定位服务的连接。你可以在你的activity中任何地方调用该方法;其目标是启动请求更新的一系列操作。

要保证竞争情况的发生(当你的应用尝试在第一个请求没有执行完毕时就启动另一个请求),定义一个布尔标记变量,它跟踪当前请求的状态。当你开始请求时将它设置为True,当请求完毕后,将它设置为false

下面的代码片段展示了如何启动一个更新请求:

public class MainActivity extends FragmentActivity implements
        ConnectionCallbacks, OnConnectionFailedListener {
    ...
    // Global constants
    ...
    // Flag that indicates if a request is underway.
    private boolean mInProgress;
    ...
    @Override
    onCreate(Bundle savedInstanceState) {
        ...
        // Start with the request flag set to false
        mInProgress = false;
        ...
    }
    ...
    /**
     * Request activity recognition updates based on the current
     * detection interval.
     *
     */
     public void startUpdates() {
        // Check for Google Play services

        if (!servicesConnected()) {
            return;
        }
        // If a request is not already underway
        if (!mInProgress) {
            // Indicate that a request is in progress
            mInProgress = true;
            // Request a connection to Location Services
            mActivityRecognitionClient.connect();
        //
        } else {
            /*
             * A request is already underway. You can handle
             * this situation by disconnecting the client,
             * re-setting the flag, and then re-trying the
             * request.
             */
        }
    }
    ...
}

实现onConnected()。在该方法中,从定位服务请求行为识别更新。当定位服务完成了连接,并调用了onConnected(),更新请求会被立即调用:

public class MainActivity extends FragmentActivity implements
        ConnectionCallbacks, OnConnectionFailedListener {
    ...
    /*
     * Called by Location Services once the location client is connected.
     *
     * Continue by requesting activity updates.
     */
    @Override
    public void onConnected(Bundle dataBundle) {
        /*
         * Request activity recognition updates using the preset
         * detection interval and PendingIntent. This call is
         * synchronous.
         */
        mActivityRecognitionClient.requestActivityUpdates(
                DETECTION_INTERVAL_MILLISECONDS,
                mActivityRecognitionPendingIntent);
        /*
         * Since the preceding call is synchronous, turn off the
         * in progress flag and disconnect the client
         */
        mInProgress = false;
        mActivityRecognitionClient.disconnect();
    }
    ...
}

处理连接断开的情况

在一些情况下,定位服务可能会在你调用disconnect()之前就从行为识别客户端就端开了连接。要处理这一情况,实现onDisconnected()方法。在该方法中,对请求标记变量进行设置,来指出目前请求并不在流程中,然后删除客户端:

public class MainActivity extends FragmentActivity implements
        ConnectionCallbacks, OnConnectionFailedListener {
    ...
    /*
     * Called by Location Services once the activity recognition
     * client is disconnected.
     */
    @Override
    public void onDisconnected() {
        // Turn off the request flag
        mInProgress = false;
        // Delete the client
        mActivityRecognitionClient = null;
    }
    ...
}

处理连接错误

除了处理定位服务的常规回调函数外,你还需要提供一个回调函数,该函数会在连接错误发生的时候被定为服务调用。该回调函数可以重用DialogFragment类(你在检查Google Play服务时所定义的类)。同时它也可以重用当用户与错误对话框交互时,接收任何由Google Play服务返回的结果的onActivityResult()函数。下面的代码片段展示了该回调函数的一个例子:

public class MainActivity extends FragmentActivity implements
        ConnectionCallbacks, OnConnectionFailedListener {
    ...
    // Implementation of OnConnectionFailedListener.onConnectionFailed
    @Override
    public void onConnectionFailed(ConnectionResult connectionResult) {
        // Turn off the request flag
        mInProgress = false;
        /*
         * If the error has a resolution, start a Google Play services
         * activity to resolve it.
         */
        if (connectionResult.hasResolution()) {
            try {
                connectionResult.startResolutionForResult(
                        this,
                        CONNECTION_FAILURE_RESOLUTION_REQUEST);
            } catch (SendIntentException e) {
                // Log the error
                e.printStackTrace();
            }
        // If no resolution is available, display an error dialog
        } else {
            // Get the error code
            int errorCode = connectionResult.getErrorCode();
            // Get the error dialog from Google Play services
            Dialog errorDialog = GooglePlayServicesUtil.getErrorDialog(
                    errorCode,
                    this,
                    CONNECTION_FAILURE_RESOLUTION_REQUEST);
            // If Google Play services can provide an error dialog
            if (errorDialog != null) {
                // Create a new DialogFragment for the error dialog
                ErrorDialogFragment errorFragment =
                        new ErrorDialogFragment();
                // Set the dialog in the DialogFragment
                errorFragment.setDialog(errorDialog);
                // Show the error dialog in the DialogFragment
                errorFragment.show(
                        getSupportFragmentManager(),
                        "Activity Recognition");
            }
        }
        ...
    }
    ...
}

二). 处理行为更新

要处理每个更新间隔中定位服务发送的Intent,定义一个IntentService以及它所需要的方法onHandleIntent()。定位服务会将行为认知更新以Intent对象的形式发出,当你调用了requestActivityUpdates()后使用你提供的PendingIntent。一旦你为PendingIntent提供了一个显式地intent,只有你定义的IntentService会接收你的intent。

下面的代码片段阐述了如何在一个行为认知更新中处理数据。

定义一个IntentService

首先定义类及其需要的方法onHandleIntent():

/**
 * Service that receives ActivityRecognition updates. It receives
 * updates in the background, even if the main Activity is not visible.
 */
public class ActivityRecognitionIntentService extends IntentService {
    ...
    /**
     * Called when a new activity detection update is available.
     */
    @Override
    protected void onHandleIntent(Intent intent) {
        ...
    }
    ...
}

之后,处理intent中的数据。从更新中,你可以获取一个可能行为的列表以及每个行为的可能性级别。下面的代码片段展示了如何获取最可能的行为信息,行为的可能性级别和它的类型:

public class ActivityRecognitionIntentService extends IntentService {
    ...
    @Override
    protected void onHandleIntent(Intent intent) {
        ...
        // If the incoming intent contains an update
        if (ActivityRecognitionResult.hasResult(intent)) {
            // Get the update
            ActivityRecognitionResult result =
                    ActivityRecognitionResult.extractResult(intent);
            // Get the most probable activity
            DetectedActivity mostProbableActivity =
                    result.getMostProbableActivity();
            /*
             * Get the probability that this activity is the
             * the user‘s actual activity
             */
            int confidence = mostProbableActivity.getConfidence();
            /*
             * Get an integer describing the type of activity
             */
            int activityType = mostProbableActivity.getType();
            String activityName = getNameFromType(activityType);
            /*
             * At this point, you have retrieved all the information
             * for the current update. You can display this
             * information to the user in a notification, or
             * send it to an Activity or Service in a broadcast
             * Intent.
             */
            ...
        } else {
            /*
             * This implementation ignores intents that don‘t contain
             * an activity update. If you wish, you can report them as
             * errors.
             */
        }
        ...
    }
    ...
}

方法getNameFromType()会将activity类型转换为带有描述性的字符串。在一个需要发布的应用中,你应该从资源文件中获取字符串而非使用固定的变量值:

public class ActivityRecognitionIntentService extends IntentService {
    ...
    /**
     * Map detected activity types to strings
     *@param activityType The detected activity type
     *@return A user-readable name for the type
     */
    private String getNameFromType(int activityType) {
        switch(activityType) {
            case DetectedActivity.IN_VEHICLE:
                return "in_vehicle";
            case DetectedActivity.ON_BICYCLE:
                return "on_bicycle";
            case DetectedActivity.ON_FOOT:
                return "on_foot";
            case DetectedActivity.STILL:
                return "still";
            case DetectedActivity.UNKNOWN:
                return "unknown";
            case DetectedActivity.TILTING:
                return "tilting";
        }
        return "unknown";
    }
    ...
}

在清单文件中指明IntentService

要在系统中指明IntentService,需要再应用的清单文件中添加一个<service>标签,例如:

<service
    android:name="com.example.android.location.ActivityRecognitionIntentService"
    android:label="@string/app_name"
    android:exported="false">
</service>

注意,你不需要为该服务指定intent过滤器,因为它仅会接收显式的intent。如何创建接收的行为更新intent在之前的章节中已经说过了。


三). 停止行为认知更新

要停止行为认知更新,其思路和请求更新是一致的,但是调用的函数是removeActivityUpdates()而不是requestActivityUpdates()。

由于移除更新会使用一些你在添加更新时所用到的方法,我们首先为两个操作定义请求类型:

public class MainActivity extends FragmentActivity implements
        ConnectionCallbacks, OnConnectionFailedListener {
    ...
    public enum REQUEST_TYPE {START, STOP}
    private REQUEST_TYPE mRequestType;
    ...
}

修改启动行为认知的代码,这样它就能使用“START”请求类型:

public class MainActivity extends FragmentActivity implements
        ConnectionCallbacks, OnConnectionFailedListener {
    ...
    public void startUpdates() {
        // Set the request type to START
        mRequestType = REQUEST_TYPE.START;
        /*
         * Test for Google Play services after setting the request type.
         * If Google Play services isn‘t present, the proper request type
         * can be restarted.
         */
        if (!servicesConnected()) {
            return;
        }
        ...
    }
    ...
    public void onConnected(Bundle dataBundle) {
        switch (mRequestType) {
            case START :
                /*
                 * Request activity recognition updates using the
                 * preset detection interval and PendingIntent.
                 * This call is synchronous.
                 */
                mActivityRecognitionClient.requestActivityUpdates(
                        DETECTION_INTERVAL_MILLISECONDS,
                        mActivityRecognitionPendingIntent);
                break;
                ...
                /*
                 * An enum was added to the definition of REQUEST_TYPE,
                 * but it doesn‘t match a known case. Throw an exception.
                 */
                default :
                throw new Exception("Unknown request type in onConnected().");
                break;
        }
        ...
    }
    ...
}

开始过程

定义一个方法,用来请求停止行为认知更新。在该方法中,设置请求类型,并请求一个到定位服务的连接。你可以在你的activity的任何地方调用该方法;其目的是要开始一系列方法的调用来停止更新:

public class MainActivity extends FragmentActivity implements
        ConnectionCallbacks, OnConnectionFailedListener {
    ...
    /**
     * Turn off activity recognition updates
     *
     */
    public void stopUpdates() {
        // Set the request type to STOP
        mRequestType = REQUEST_TYPE.STOP;
        /*
         * Test for Google Play services after setting the request type.
         * If Google Play services isn‘t present, the request can be
         * restarted.
         */
        if (!servicesConnected()) {
            return;
        }
        // If a request is not already underway
        if (!mInProgress) {
            // Indicate that a request is in progress
            mInProgress = true;
            // Request a connection to Location Services
            mActivityRecognitionClient.connect();
        //
        } else {
            /*
             * A request is already underway. You can handle
             * this situation by disconnecting the client,
             * re-setting the flag, and then re-trying the
             * request.
             */
        }
        ...
    }
    ...
}

在onConnected()方法中,如果请求类型是“STOP”,那么调用removeActivityUpdates()。将你用来启动更新的PendingIntent作为参数传递给removeActivityUpdates():

public class MainActivity extends FragmentActivity implements
        ConnectionCallbacks, OnConnectionFailedListener {
    ...
    public void onConnected(Bundle dataBundle) {
        switch (mRequestType) {
            ...
            case STOP :
            mActivityRecognitionClient.removeActivityUpdates(
                    mActivityRecognitionPendingIntent);
            break;
            ...
        }
        ...
    }
    ...
}

你不需要修改onDisconnected()或onConnectionFailed()的实现,因为这些方法并不依赖于该请求类型。

现在你已经有了一个行为认知应用的基本框架了。你可以将行为认知的功能和其它定位相关的功能结合在一起,比如定期的地点更新,地理围栏等,这些内容都在这系列课程中的其它课中讲授过了。