background

More and more pedometer devices appear on the market, and more and more apps add pedometer as a function module. On the IOS platform, Apple Has opened the API to provide step counting data, while the Android platform, as a development platform, only provides corresponding sensors. There are many ways to realize step counting, including sensors, positioning, etc. However, the Android app on the market is not very accurate in step counting.

purpose

I started the step counting open source project and committed to creating a stable and small error pedometer on android platform.

The theme

Next, the author makes a pedometer on Android platform from the perspective of sensors. The first priority we need to do is to complete the step counting core code. With reference to the general sensor step counting algorithm on the Internet, whether people are walking can be calculated by gradient threshold, threshold calculation, wave peak detection and other algorithms. Then, the accuracy of step counting can be corrected by judging that the normal range of time difference between two steps should be 200ms-2000ms.

The core code for calculating the gradient threshold is as follows:

* @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; }Copy the code

The core code of threshold calculation is as follows:

/** * Calculate the threshold * calculate the threshold by the difference between the peaks and valleys of waves * record 4 values, Store in the tempValue[] array * Calculate the threshold by passing the array into 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; }Copy the code

The core code of wave peak detection is as follows:

/** * The current point is in a downward trend: isDirectionUp is false * the previous point is in an upward trend: LastStatus = true * Until the crest of the wave, it continues to rise more than 2 times * The crest of the wave is greater than 1.2g, less than 2g * Record the troughs * Observe the waveform, you can see that where the steps occur, the troughs are next to the crest, with distinct features and differences * So record the troughs each time, To compare with the next wave peak * @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; }}Copy the code

Step counting is obtained according to the above algorithm, and I have made relevant optimization on step counting. Step counting can be started after a period of continuous movement, so as to shield the interference caused by subtle movement or vibration while driving. After you stop moving for a period of time, you need to move continuously for a period of time before you can count your steps. This completes the step-counting core service class code, so we can get the step-counting data we need from this core service class.

Next, we need to do something about the step-counting data, start a step-counting service for fetching step-counting data, updating UI, and caching data. In order to avoid affecting the performance of step counting app, we will start a new process for step counting service, so that we need to carry out inter-process communication. Here, the author uses Messenger for process communication.

Step counting service code is as follows:

/**
 * @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);
    }
}Copy the code

When the step counting service is created, the author sets the step counting service as the front desk service to reduce the probability of the step counting service process being killed. Why not turn on static radio to wake up the pedometer regularly? In fact, this can not be adapted, Android 6.0 has removed the relevant system static broadcast. Can really achieve 100 percent process alive, can only rely on the mobile phone manufacturer whitelist, the other are floating clouds, at most just reduce the probability of being killed. There is no problem with the process survival.

This project has been open source, project address :JkStepSensor.

If you have any questions, please contact!