致力于稳定误差小的 android 平台计步器

2,907 阅读7分钟

背景

市面上出现了越来越多的计步设备,也有越来越多的app将计步器这块添加作为功能模块.IOS平台,苹果公司已经开放了提供计步数据的api,而android平台作为一个开发的平台,只提供了对应的传感器,具体怎么实现计步,实现方式很多,有传感器、定位等等,然而市面上android app计步并不是很精确.

目的

启动计步开源项目、致力于打造稳定误差小的android平台计步器。

主题

接下来笔者从传感器角度入手做一个android平台的计步器.首先我们需要做的优先任务就是完成计步核心代码编写.参考网上通用的传感器计步算法,通过梯度化阈值、阈值的计算、 检测波峰等算法推算出人是否在行走,然后对两次行走步伐之间的时间差值正常范围应该为200ms-2000ms的判断,进行矫正计步精度.

计算梯度化阈值核心代码如下:

    /**
     * 梯度化阈值
     * 计算数组的均值
     * 通过均值将阈值梯度化在一个范围里
     * @author leibing
     * @createTime 2016/08/31
     * @lastModify 2016/08/31
     * @param value
     * @param n
     * @return
     */
    public float averageValue(float value[], int n) {
        float ave = 0;
        for (int i = 0; i < n; i++) {
            ave += value[i];
        }
        ave = ave / valueNum;
        if (ave >= 8) {
            Log.v(TAG, "超过8");
            ave = (float) 4.3;
        } else if (ave >= 7 && ave < 8) {
            Log.v(TAG, "7-8");
            ave = (float) 3.3;
        } else if (ave >= 4 && ave < 7) {
            Log.v(TAG, "4-7");
            ave = (float) 2.3;
        } else if (ave >= 3 && ave < 4) {
            Log.v(TAG, "3-4");
            ave = (float) 2.0;
        } else {
            Log.v(TAG, "else");
            ave = (float) 1.7;
        }
        return ave;
    }

阈值的计算核心代码如下:

    /**
     * 阈值的计算
     * 通过波峰波谷的差值计算阈值
     * 记录4个值,存入tempValue[]数组中
     * 在将数组传入函数averageValue中计算阈值
     * @author leibing
     * @createTime 2016/08/31
     * @lastModify 2016/08/31
     * @param value
     * @return
     */
    public float Peak_Valley_Thread(float value) {
        float tempThread = ThreadValue;
        if (tempCount < valueNum) {
            tempValue[tempCount] = value;
            tempCount++;
        } else {
            tempThread = averageValue(tempValue, valueNum);
            for (int i = 1; i < valueNum; i++) {
                tempValue[i - 1] = tempValue[i];
            }
            tempValue[valueNum - 1] = value;
        }
        return tempThread;
    }

检测波峰核心代码如下:

    /**
     * 检测波峰
     * 以下四个条件判断为波峰:
     * 目前点为下降的趋势:isDirectionUp为false
     * 之前的点为上升的趋势:lastStatus为true
     * 到波峰为止,持续上升大于等于2次
     * 波峰值大于1.2g,小于2g
     * 记录波谷值
     * 观察波形图,可以发现在出现步子的地方,波谷的下一个就是波峰,有比较明显的特征以及差值
     * 所以要记录每次的波谷值,为了和下次的波峰做对比
     * @author leibing
     * @createTime 2016/08/31
     * @lastModify 2016/08/31
     * @param newValue
     * @param oldValue
     * @return
     */
    public boolean DetectorPeak(float newValue, float oldValue) {
        lastStatus = isDirectionUp;
        if (newValue >= oldValue) {
            isDirectionUp = true;
            continueUpCount++;
        } else {
            continueUpFormerCount = continueUpCount;
            continueUpCount = 0;
            isDirectionUp = false;
        }

        Log.v(TAG, "oldValue:" + oldValue);
        if (!isDirectionUp && lastStatus
                && (continueUpFormerCount >= 2 && (oldValue >= minValue && oldValue < maxValue))) {
            peakOfWave = oldValue;
            return true;
        } else if (!lastStatus && isDirectionUp) {
            valleyOfWave = oldValue;
            return false;
        } else {
            return false;
        }
    }

根据以上算法得到计步,在此我对计步做了相关优化,连续运动一段时间才开始计步,屏蔽细微移动或者驾车时震动所带来的干扰.停止运动一段时间后,需要连续运动一段时间才会计步.至此就完成计步核心服务类代码,因此就能从这个核心服务类中拿到我们所需要的计步数据.

接下来,我们需要对计步数据做相应处理,开启一个计步服务用于拿取计步数据、更新ui以及缓存数据.为了避免影响计步app性能,我们将开启一个新的进程用于计步服务,这样我们就需要进行进程间的通信,这里笔者是采用Messenger进行进程通信.

计步服务代码如下:

/**
 * @className: StepService
 * @classDescription: 计步服务
 * @author: leibing
 * @createTime: 2016/08/31
 */
@TargetApi(Build.VERSION_CODES.CUPCAKE)
public class StepService extends Service implements SensorEventListener {
    // TAG
    private final String TAG = "StepService";
    // 默认int错误码
    public static final int INT_ERROR = -12;
    // 停止广播动作
    public static final String ACTION_STOP_SERVICE = "action_stop_service";
    // step key
    public final static String STEP_KEY = "step_key";
    // 传感器管理
    private SensorManager sensorManager;
    // 计步核心类
    private StepDcretor stepDetector;
    // 自定义Handler
    private MsgHandler msgHandler = new MsgHandler();
    // Messenger 用于跨进程通信
    private Messenger messenger = new Messenger(msgHandler);
    // 计步需要缓存的数据
    private StepModel mStepModel;
    // 计步服务广播
    private BroadcastReceiver stepServiceReceiver;
    // 是否手动停止服务
    private boolean isNeedStopService = false;

    /**
     * @className: MsgHandler
     * @classDescription: 用于更新客户端UI
     * @author: leibing
     * @createTime: 2016/08/31
     */
    class MsgHandler extends Handler {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case Constant.MSG_FROM_CLIENT:
                    try {
                        // 缓存数据
                        cacheStepData(StepService.this,StepDcretor.CURRENT_STEP + "");
                        // 更新通知栏
                        updateNotification(msg.getData());
                        // 回复消息给Client
                        Messenger messenger = msg.replyTo;
                        Message replyMsg = Message.obtain(null, Constant.MSG_FROM_SERVER);
                        Bundle bundle = new Bundle();
                        bundle.putInt(STEP_KEY, StepDcretor.CURRENT_STEP);
                        replyMsg.setData(bundle);
                        messenger.send(replyMsg);
                    } catch (RemoteException e) {
                        e.printStackTrace();
                    }
                    break;
                default:
                    super.handleMessage(msg);
            }
        }
    }

    /**
     * 更新通知栏
     * @author leibing
     * @createTime 2016/09/02
     * @lastModify 2016/09/02
     * @param bundle 数据
     * @return
     */
    private void updateNotification(Bundle bundle) {
        if (bundle == null) {
            NotificationUtils.getInstance(StepService.this).
                    updateNotification("今日行走" + StepDcretor.CURRENT_STEP + "步");
        }else {
            // 内容
            String content = (String) bundle.getSerializable(Constant.CONTENT_KEY);
            // ticker
            String ticker = (String) bundle.getSerializable(Constant.TICKER_KEY);
            // 标题
            String contentTile = (String) bundle.getSerializable(Constant.CONTENTTITLE_KEY);
            // 需要跳转的Activity
            Class pendingClass = (Class) bundle.getSerializable(Constant.PENDINGCLASS_KEY);
            // 是否不可取消
            boolean isOngoing = true;
            if (bundle.getSerializable(Constant.ISONGOING_KEY) != null){
                isOngoing = (boolean) bundle.getSerializable(Constant.ISONGOING_KEY);
            }
            // 头像
            int icon = INT_ERROR;
            if (bundle.getSerializable(Constant.ICON_KEY) != null){
                icon = (int) bundle.getSerializable(Constant.ICON_KEY);
            }
            // id
            int notifyId = INT_ERROR;
            if (bundle.getSerializable(Constant.NOTIFYID_KEY) != null){
                notifyId = (int) bundle.getSerializable(Constant.NOTIFYID_KEY);
            }
            if (StringUtil.isEmpty(content)
                    || StringUtil.isEmpty(ticker)
                    || StringUtil.isEmpty(contentTile)){
                NotificationUtils.getInstance(StepService.this).
                        updateNotification("今日行走" + StepDcretor.CURRENT_STEP + "步");
            }else {
                NotificationUtils.getInstance(StepService.this).
                        updateNotification(content + StepDcretor.CURRENT_STEP + "步",
                                ticker,
                                contentTile,
                                StepService.this,
                                pendingClass,
                                isOngoing,
                                notifyId,
                                icon);
            }
        }
    }

    /**
     * 启动服务为前台服务( 让该service前台运行,避免手机休眠时系统自动杀掉该服务)
     * @author leibing
     * @createTime 2016/09/07
     * @lastModify 2016/09/07
     * @param
     * @return
     */
    public void startForeground(){
        NotificationCompat.Builder builder = new NotificationCompat.Builder(this);
        // 设置头像
        builder.setSmallIcon(R.mipmap.ic_launcher);
        // 设置标题
        builder.setContentTitle("foreground service");
        // 设置内容
        builder.setContentText("try to avoid this service be killed!");
        // 创建notification
        Notification notification = builder.build();
        //如果 id 为 0 ,那么状态栏的 notification 将不会显示。
        startForeground(0, notification);
    }

  @Override
    public void onCreate() {
        super.onCreate();
        // 初始化计步服务广播
        initStepServiceReceiver();
        // 启动计步
        startStep();
        // 启动服务为前台服务( 让该service前台运行,避免手机休眠时系统自动杀掉该服务)
        startForeground();
        Log.v(TAG,"onCreate");
    }

    /**
     * 初始化计步服务广播
     * @author leibing
     * @createTime 2016/09/01
     * @lastModify 2016/09/01
     * @param
     * @return
     */
    private void initStepServiceReceiver() {
        final IntentFilter filter = new IntentFilter();
        // 添加停止当前服务广播动作
        filter.addAction(ACTION_STOP_SERVICE);
        // 实例化广播接收器
        stepServiceReceiver = new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                String action = intent.getAction();
                if (ACTION_STOP_SERVICE.equals(action)){
                    Log.v(TAG,"停止服务");
                    // 停止服务
                    isNeedStopService = true;
                    StepService.this.stopSelf();
                }
            }
        };
        // 注册计步服务广播
        registerReceiver(stepServiceReceiver, filter);
    }

    /**
     * 启动计步
     * @author leibing
     * @createTime 2016/08/31
     * @lastModify 2016/08/31
     * @param
     * @return
     */
    private void startStep() {
        // 启动计步器
        startStepDetector();
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        Log.v(TAG, "onStartCommand");
        return START_STICKY;
    }

    /**
     * 缓存计步数据
     * @author leibing
     * @createTime 2016/08/31
     * @lastModify 2016/08/31
     * @param context 上下文
     * @param stepCount 计步数
     * @return
     */
    private void cacheStepData(Context context, String stepCount){
        mStepModel = new StepModel();
        mStepModel.setDate(DateUtils.simpleDateFormat(new Date()));
        mStepModel.setStep(stepCount);
        DataCache.getInstance().addStepCache(context, mStepModel);
    }

    @Override
    public IBinder onBind(Intent intent) {
        Log.v(TAG, "onBind");
        return messenger.getBinder();
    }

    /**
     * 启动计步器
     * @author leibing
     * @createTime 2016/08/31
     * @lastModify 2016/08/31
     * @param
     * @return
     */
    private void startStepDetector() {
        if (sensorManager != null && stepDetector != null) {
            sensorManager.unregisterListener(stepDetector);
            sensorManager = null;
            stepDetector = null;
        }
        // 初始化计步(拿缓存更新计步数)
        DataCache.getInstance().getTodayCache(this, new DataCache.DataCacheListener() {
            @Override
            public void readListCache(StepModel stepModel) {
                if (stepModel != null){
                   StepDcretor.CURRENT_STEP = Integer.parseInt(stepModel.getStep());
                }
            }
        });

        sensorManager = (SensorManager) this
                .getSystemService(SENSOR_SERVICE);
        // 添加自定义
        addBasePedoListener();
        // 添加传感器监听
        addCountStepListener();
    }

    /**
     * 停止计步器
     * @author leibing
     * @createTime 2016/09/01
     * @lastModify 2016/09/01
     * @param
     * @return
     */
    public void stopStepDetector(){
        if (sensorManager != null && stepDetector != null) {
            sensorManager.unregisterListener(stepDetector);
            sensorManager = null;
            stepDetector = null;
        }
    }

    /**
     * 添加传感器监听(步行检测传感器、计步传感器)
     * @author leibing
     * @createTime 2016/08/31
     * @lastModify 2016/08/31
     * @param
     * @return
     */
    private void addCountStepListener() {
        // 步行检测传感器,用户每走一步就触发一次事件
        Sensor detectorSensor = sensorManager.getDefaultSensor(Sensor.TYPE_STEP_DETECTOR);
        // 计步传感器
        Sensor countSensor = sensorManager.getDefaultSensor(Sensor.TYPE_STEP_COUNTER);
        if (detectorSensor != null) {
            sensorManager.registerListener(StepService.this, detectorSensor, SensorManager.SENSOR_DELAY_UI);
        } else if (countSensor != null) {
            sensorManager.registerListener(StepService.this, countSensor, SensorManager.SENSOR_DELAY_UI);
        } else {
            Log.v(TAG, "Count sensor not available!");
        }
    }

    /**
     *添加传感器监听(加速度传感器)
     * @author leibing
     * @createTime 2016/08/31
     * @lastModify 2016/08/31
     * @param
     * @return
     */
    private void addBasePedoListener() {
        stepDetector = new StepDcretor();
        // 获得传感器的类型,这里获得的类型是加速度传感器
        // 此方法用来注册,只有注册过才会生效,参数:SensorEventListener的实例,Sensor的实例,更新速率
        Sensor sensor = sensorManager
                .getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
        // sensorManager.unregisterListener(stepDetector);
        sensorManager.registerListener(stepDetector, sensor,
                SensorManager.SENSOR_DELAY_UI);
        stepDetector
                .setOnSensorChangeListener(new StepDcretor.OnSensorChangeListener() {

                    @Override
                    public void onChange() {
                    }
                });
    }

    @Override
    public void onSensorChanged(SensorEvent event) {
    }

    @Override
    public void onAccuracyChanged(Sensor sensor, int accuracy) {
    }

    @Override
    public void onDestroy() {
        // 取消前台进程
        stopForeground(true);
        // 解注册计步服务广播
        unregisterReceiver(stepServiceReceiver);
        // 停止计步器
        stopStepDetector();
        // 非手动停止服务,则自动重启服务
        if (!isNeedStopService){
            Intent intent = new Intent(this, StepService.class);
            startService(intent);
        }else {
            isNeedStopService = false;
        }
        Log.v(TAG,"onDestroy");
        super.onDestroy();
    }

    @Override
    public boolean onUnbind(Intent intent) {
        Log.v(TAG,"onUnbind");
        return super.onUnbind(intent);
    }
}

创建计步服务的时候,笔者将计步服务置为前台服务,减少计步服务进程被杀几率。有童鞋会问,为何不开启静态广播去定期唤醒计步服务?其实这样做,并不能做到适配,android 6.0以后已经去掉了相关的系统静态广播.真正能做到百分百进程保活,只能靠手机厂家白名单了,其他的都是浮云,顶多只是降低被杀的几率而已.对于进程保活这块就不纠结了.

本项目已开源,项目地址:JkStepSensor.

如有疑问,请联系!