针对计步器的基本需求功能如下:
1.通过传感器得到步数
2.通过数据库储存步数
3.从数据库调出相应数据显示步数
分析ios系统自带健康软件中的计步器,数据显示为:
1.步行+跑步距离
2.步数
3.已爬楼层
其中每项显示均包含日、周、月、年的数据柱形图显示和对应时长的平均值显示,并能显示所有数据的具体时间和数值。
当传感器的值发生变化时,会调用onSensorChanged方法。当传感器的精度变化时会调用onAccuracyChanged方法。onSensorChanged方法里根据stepSensor(计步传感器类型 0-counter 1-detector)的类型设置StepDcretor.CURRENT_SETP的值,然后调用updateNotification方法更新通知栏。在onCreate方法里看到,首先获取了当前的日期,然后初始化一个广播接收者,接着开启子线程执行startStepDetector方法。
先看addCountStepListener方法,API版本大于19执行这个方法,首先先拿到两种传感器,TYPE_STEP_COUNTER 计步传感器,用于记录激活后的步伐数。TYPE_STEP_DETECTOR 步行检测传感器,用户每走一步就触发一次事件。根据拿到的情况来注册监听,sensorManager.registerListener(StepService.this, countSensor, SensorManager.SENSOR_DELAY_UI);查资料后得知这里第一个参数是Listener,第二个参数是所得传感器类型,第三个参数值获取传感器信息的频率 。如果两种传感器都没拿到,就还是执行addBasePedoListener方法。在addBasePedoListener方法里获得加速度传感器,注册监听,然后调stepDetector里的回调接口更新通知栏。
总结一下就是开启一个服务,在服务里注册一个广播接收者监听屏幕和电源情况保存数据,并且新建子线程,在子线程里开启计步检测,根据不同的API版本获取传感器管理器和传感器实例并注册监听,然后更具传感器返回的数据,然后更新界面。
代码主要包括:
1.数据库的实现
2.时间戳工具
3.获取步数信息
5.计步器服务器
计步器原理
实现计步器几步一般有2种方法:
1.利用加速传感器,通过计算二次波峰来确定每一步,这种方法误差比较大,但普适性好;
2.利用Android4.4之后的Sensor.TYPE_STEP_DETECTOR来计步,缺点是这个传感器只有4.4以后的版本才能使用而且手机要有三轴陀螺仪,限制比较大,鉴于微信运动也是使用的第二种方法,这里也放弃第一种方法的使用。(备注:可以找个方法一的算法对接到方法二上,因为Android自带的计步器太简陋了,一切都需要
自己实现,这个下文有说明)
关于TYPE_STEP_DETECTOR
通过TYPE_STEP_DETECTOR传感器我们只能获得2个数据:
1.时间戳(这个在开关机面前就废了,不做考虑);
2.从上次开机到现在的总步数(总步数会在开关机时清零),对,只是总步数……这样就不能确定某一天的步数了。
说明
关于跨天会想到Date_Changed这个系统广播,然而该广播不可信任,原因:该广播只会发出一次,
如:用户往后调整过时间,并在00:00(记此时为t)发送过该天的广播,那么当用户再次将时间拨回到正常时间时,再次经过t时刻,
系统不会发送Date_Changed广播;另外,开机广播也不可信任,原因:部分手机厂商会屏蔽掉该广播。
实现步骤
1.计步器步数数据库的实现
关于数据库这里没有使用第三方框架,已接入第三方框架的,请自行修改。数据库结构如下:
date:时间戳,表明是哪一天的数据
steps:修正步数,非真实步数,下面会解释(已DES加密)
sensor:修正传感器步数(该值非负,只有off为1或插入新数据时才为0),和上一天的sensor及off的状态有关(已des加密),之所以用修正结果,是为了监听异常关机
off:手机是否正常关机,1表示正常关机,0表示未关机,请无视这么命名,这得改啊
数据库操作代码:
/**
* 这里为了方便查看数据创建了2个表格一个DES加密,一个未加密,根据需要 自行修改
*/
public class Database extends SQLiteOpenHelper {
private final static String DB_NAME = "steps"; private final static String DB_NAME_ENCRYPT = "steps_encrypt"; private final static int DB_VERSION = 1; private static Database instance; private static final AtomicInteger openCounter = new AtomicInteger(); private Database(final Context context) { super(context, DB_NAME, null, DB_VERSION); } public static synchronized Database getInstance(final Context c) { if (instance == null) { instance = new Database(c.getApplicationContext()); } openCounter.incrementAndGet(); return instance; } @Override public void close() { if (openCounter.decrementAndGet() == 0) { super.close(); } } /** * date : 计步的日期 * steps : 开关机后存储的步数 * sensor : 虚拟传感器的步数(非真实值,和上一天是否关机有关),计步器传感器每次变化都会触发 * off : 是否关机过,默认false(0);关机时置为true(1),待计步服务开启后将当天的off置为false(0) ; * * @param db */ @Override public void onCreate(final SQLiteDatabase db) { db.execSQL("CREATE TABLE " + DB_NAME + " (date INTEGER, steps INTEGER , sensor INTEGER , off INTEGER)"); db.execSQL("CREATE TABLE " + DB_NAME_ENCRYPT + " (date INTEGER, steps TEXT , sensor TEXT , off INTEGER)"); } @Override public void onUpgrade(final SQLiteDatabase db, int oldVersion, int newVersion) { // 关于升级数据库,暂时不处理 } /** * Query the 'steps' table. Remember to close the cursor! * * @param columns the colums * @param selection the selection * @param selectionArgs the selction arguments * @param groupBy the group by statement * @param having the having statement * @param orderBy the order by statement * @return the cursor */ public Cursor query(final String[] columns, final String selection, final String[] selectionArgs, final String groupBy, final String having, final String orderBy, final String limit) { return getReadableDatabase() .query(DB_NAME, columns, selection, selectionArgs, groupBy, having, orderBy, limit); } /** * 插入新的数据 * * @param date * @param steps * @param correctSensorSteps 需要根据上一天的开关机判断 * @param off */ public void insertNewDay(long date, int steps, int correctSensorSteps, int off) { getWritableDatabase().beginTransaction(); try { ContentValues values = new ContentValues(); values.put("date", date); values.put("steps", steps); values.put("sensor", correctSensorSteps); values.put("off", off); getWritableDatabase().insert(DB_NAME, null, values); getWritableDatabase().setTransactionSuccessful(); } finally { getWritableDatabase().endTransaction(); } insertNewDayEncrypt(date , steps , correctSensorSteps , off); } public void insertNewDayEncrypt(long date, int steps, int correctSensorSteps, int off) { getWritableDatabase().beginTransaction(); try { ContentValues values = new ContentValues(); values.put("date", date); values.put("steps", DesTool.encrypt(steps + "" )); values.put("sensor", DesTool.encrypt(correctSensorSteps + "" )); values.put("off", off); getWritableDatabase().insert(DB_NAME_ENCRYPT , null, values); getWritableDatabase().setTransactionSuccessful(); } finally { getWritableDatabase().endTransaction(); } } /** * 更新steps(关机、插入新数据、异常关机、数据异常时才会调用) * * @param date * @param correctSensorSteps 应该传的传感器数值,不一定为读取到的传感器数值 */ public void updateSteps(long date, int correctSensorSteps) { Cursor c = getReadableDatabase().query(DB_NAME, new String[]{"date"}, "date = ?", new String[]{String.valueOf(date)}, null, null, null); c.moveToFirst(); if (c.getCount() == 0) { // 没有记录,说明之前传感器没有动,所以一概为0 (sensor在计步传感器发生改变时会改变, // 此时为0,是为了区别计步器是否监听到了运动,为0显然传感器没有改变);off传1是因为: // 当天的会在启动监听计步的服务时置为0,如果前一天没数据置为1视为关机状态,便于逻辑的实现 insertNewDay(date, 0, 0, 1); } else { getWritableDatabase().execSQL( "UPDATE " + DB_NAME + " SET steps = steps + " + correctSensorSteps + " WHERE date = " + date); } c.close(); updateStepsEncrypt(date , correctSensorSteps); } public void updateStepsEncrypt(long date, int correctSensorSteps) { Cursor c = getReadableDatabase().query(DB_NAME_ENCRYPT , new String[]{"date"}, "date = ?", new String[]{String.valueOf(date)}, null, null, null); c.moveToFirst(); if (c.getCount() == 0) { // 同上 insertNewDayEncrypt(date, 0, 0, 1); } else { int steps = 0 ; try{ steps = Integer.parseInt(DesTool.decrypt(getStepsEncrypt(date))) ; }catch (Exception e){ } getWritableDatabase().execSQL( "UPDATE " + DB_NAME_ENCRYPT + " SET steps = \"" + DesTool.encrypt((steps + correctSensorSteps) + "") + "\" WHERE date = " + date); } c.close(); } /** * @param date the date in millis since 1970 * @return steps的值 */ public int getSteps(long date) { Cursor c = getReadableDatabase().query(DB_NAME, new String[]{"steps"}, "date = ?", new String[]{String.valueOf(date)}, null, null, null); c.moveToFirst(); int re = 0; if (c.getCount() == 0) { } else { re = c.getInt(0); } return re; } public String getStepsEncrypt(long date) { Cursor c = getReadableDatabase().query(DB_NAME_ENCRYPT , new String[]{"steps"}, "date = ?", new String[]{String.valueOf(date)}, null, null, null); c.moveToFirst(); String re = null ; if (c.getCount() == 0) { re = "" ; } else { re = c.getString(0); if(null == re){ re = "" ; } } c.close(); return re; } /** * 更新某一天(date)的传感器步数(sensor) * * @param date 所属的时间,in millis since 1970 * @param correctSensorSteps */ public void updateSensorSteps(long date, int correctSensorSteps) { Cursor c = getReadableDatabase().query(DB_NAME, new String[]{"date"}, "date = ?", new String[]{String.valueOf(date)}, null, null, null); c.moveToFirst(); if (c.getCount() == 0) { // 没有记录,这一行执行不到,因为更新sensor的操作总在其他getXXX之后执行 insertNewDay(date, 0, correctSensorSteps, 0); } else { getWritableDatabase().execSQL( "UPDATE " + DB_NAME + " SET sensor = " + correctSensorSteps + " WHERE date = " + date); } c.close(); updateSensorStepsEncrypt(date , correctSensorSteps); } public void updateSensorStepsEncrypt(long date, int correctSensorSteps) { Cursor c = getReadableDatabase().query(DB_NAME_ENCRYPT, new String[]{"date"}, "date = ?", new String[]{String.valueOf(date)}, null, null, null); c.moveToFirst(); if (c.getCount() == 0) { // 没有记录 insertNewDayEncrypt(date, 0, correctSensorSteps, 0); } else { getWritableDatabase().execSQL( "UPDATE " + DB_NAME_ENCRYPT + " SET sensor = \"" + DesTool.encrypt(correctSensorSteps + "") + "\" WHERE date = " + date); } c.close(); } /** * @param date the date in millis since 1970 * @return 存储的传感器的步数 */ public int getSensorSteps(long date) { Cursor c = getReadableDatabase().query(DB_NAME, new String[]{"sensor"}, "date = ?", new String[]{String.valueOf(date)}, null, null, null); c.moveToFirst(); int re; if (c.getCount() == 0) { re = 0; } else { re = c.getInt(0); } c.close(); return re; } public String getSensorStepsEncrypt(long date) { Cursor c = getReadableDatabase().query(DB_NAME_ENCRYPT, new String[]{"sensor"}, "date = ?", new String[]{String.valueOf(date)}, null, null, null); c.moveToFirst(); String re = null ; if (c.getCount() == 0) { re = "" ; } else { re = c.getString(0); if(null == re){ re = "" ; } } c.close(); return re; } /** * 更新off的状态 * * @param date * @param offStatus */ public void updateOffStatus(long date, int offStatus) { Cursor c = getReadableDatabase().query(DB_NAME, new String[]{"date"}, "date = ?", new String[]{String.valueOf(date)}, null, null, null); c.moveToFirst(); if (c.getCount() == 0) { // 同上 insertNewDay(date, 0 , 0 , offStatus); }else { getWritableDatabase().execSQL( "UPDATE " + DB_NAME + " SET off = " + offStatus + " WHERE date = " + date); } c.close(); updateOffStatusEncrypt(date , offStatus) ; } public void updateOffStatusEncrypt(long date, int offStatus) { Cursor c = getReadableDatabase().query(DB_NAME_ENCRYPT , new String[]{"date"}, "date = ?", new String[]{String.valueOf(date)}, null, null, null); c.moveToFirst(); if (c.getCount() == 0) { // 没有记录,同上 insertNewDay(date, 0 , 0 , offStatus); }else { getWritableDatabase().execSQL( "UPDATE " + DB_NAME_ENCRYPT + " SET off = " + offStatus + " WHERE date = " + date); } c.close(); } /** * 得到off的值 0 = false ; 1 = true ; * * @param date * @return */ public int getOffStatus(long date) { Cursor c = getReadableDatabase().query(DB_NAME, new String[]{"off"}, "date = ?", new String[]{String.valueOf(date)}, null, null, null); c.moveToFirst(); int hasOffed = 0; if (c.getCount() == 0) { } else { hasOffed = c.getInt(0); } c.close(); return hasOffed; } public int getOffStatusEncypt(long date) { Cursor c = getReadableDatabase().query(DB_NAME_ENCRYPT, new String[]{"off"}, "date = ?", new String[]{String.valueOf(date)}, null, null, null); c.moveToFirst(); int hasOffed = 0; if (c.getCount() == 0) { } else { hasOffed = c.getInt(0); } c.close(); return hasOffed; } /** * 用于关机时修改整个数据库(所以数据库中内容不宜过多,要定期清理,清理方法暂时略过) */ public void updateAll() { getWritableDatabase().execSQL( "UPDATE " + DB_NAME + " SET off = 1,steps = steps + sensor,sensor = 0"); updateAllEncrypt() ; } public void updateAllEncrypt() { Cursor c = getReadableDatabase().query(DB_NAME_ENCRYPT , new String[]{"date"}, "date > ?", new String[]{String.valueOf(1)}, null, null, null); if (c.getCount() == 0) { // 没有记录,说明之前传感器没有动,不进行任何操作 } else { while (c.moveToNext()){ long date = c.getLong(0) ; int steps = getSteps(date) ; int sensor = getSensorSteps(date) ; getWritableDatabase().execSQL( "UPDATE " + DB_NAME_ENCRYPT + " SET off = 1,steps = \"" + DesTool.encrypt((steps + sensor) + "") + "\",sensor = \"" + DesTool.encrypt("0") + "\" WHERE date = " + date); } } c.close(); }
}
2.时间戳工具
public abstract class Util4Pedometer {
/** * @return milliseconds since 1.1.1970 for today 0:00:00 */ public static long getToday() { Calendar c = Calendar.getInstance(); c.setTimeInMillis(System.currentTimeMillis()); c.set(Calendar.HOUR_OF_DAY, 0); c.set(Calendar.MINUTE, 0); c.set(Calendar.SECOND, 0); c.set(Calendar.MILLISECOND, 0); return c.getTimeInMillis(); }
}
3.获取当天的步数
以往的步数建议服务器获取。
获取当天步数代码:
/**
* 使用该方法前必须对context进行初始化
*/
public class StepsCountTool {
public static Context context; /** * 获得当天的步数,若步数异常重置步数 * 这里返回String是因为方便往服务器提交数据时加密(如:RSA) */ public static String getTodaySteps() { if (null == context) { return "0"; } int result = 0; Database db = Database.getInstance(context); long today = Util4Pedometer.getToday(); // 当天步数为当天的steps数据与sensor数据之和 result = db.getSteps(today) + db.getSensorSteps(today); if (0 > result) { // 说明数据异常(只有在跨天且计步器逻辑没有被触发才有可能异常,此处触发几率不大,保险起见, // 看计步器服务调用该方法的位置可理解),应该重置当前的步数 db.updateSteps(today, -db.getSteps(today)); db.updateSensorSteps(today, 0); result = 0; } return result + ""; }
}
4.计步器服务的实现
计步器要一直在后台运行就使用到了Service,如果不能一直运行则某些情况会使计数偏小,关于如何保活,这是个问题,这里只尽可能的使服务唤醒
(请自行解决,最后会给出源码,仅供参考)。
计步服务代码:
@TargetApi(Build.VERSION_CODES.KITKAT)
public class SensorListener extends Service implements SensorEventListener {
public static boolean isShutdowning = false;// 只有接收到关机广播才会置为true private Database db; public static int steps = 0; private final static int MICROSECONDS_IN_ONE_MINUTE = 60000000; @Override public void onAccuracyChanged(Sensor sensor, int accuracy) { } @Override public void onSensorChanged(final SensorEvent event) { // 关机时不往下执行 if (isShutdowning) { return; } // 若步数异常(用户随意调整时间后,开关机等等),修复步数 StepsCountTool.getTodaySteps(); Sensor sensor = event.sensor; if (sensor.getType() != Sensor.TYPE_STEP_COUNTER) { return; } steps = (int) event.values[0]; db = Database.getInstance(this); long today = Util4Pedometer.getToday(); long yesterDay = today - 24 * 60 * 60 * 1000; int correctSensorSteps; int off = db.getOffStatus(yesterDay); if (off == 0) { // 说明昨天没有关机,得到修正值:当前计步器数据 - 上一天sensor修正值,手机异常关机,会使steps的值从0开始 // 正常情况correctSensorSteps 为正 correctSensorSteps = steps - db.getSensorSteps(yesterDay); if (correctSensorSteps < 0) { // 说明手机异常关机:扣电池或使用脚本关机导致关机广播没有发出,正常关机时会将所有off置为1, db.updateOffStatus(yesterDay, 1); // 异常关机后重启先保存关机前的数据 db.updateSteps(today, db.getSensorSteps(today)); // 再重新计算步数,这样能最大可能的接近真实值,此时steps从1开始 db.updateSensorSteps(today, steps); } else { // 如果没有关机,跨天时一定会执行if中的代码,因为insertNewDay的sensor和steps的参数都是0, // 在第一次监听到计步器改变时,senor的值还未改变 if (db.getSensorSteps(today) <= 0) { // 这里每天只更新一次sensor,更新后和sensor的和为0,跨天问题解决 // steps此时为0,将sensor偏移量取反放入steps中, db.updateSteps(today, -correctSensorSteps); } db.updateSensorSteps(today, correctSensorSteps); } } else if (off == 1) { // 说明昨天关机,correctSensorSteps 就是传感器的真实值 correctSensorSteps = steps; db.updateSensorSteps(today, correctSensorSteps); } else { // 计步器异常 } } @Override public IBinder onBind(final Intent intent) { return null; } @Override public int onStartCommand(final Intent intent, int flags, int startId) { if (isShutdowning) { return START_STICKY; } db = Database.getInstance(this); // 每次启动服务都将今天的状态置为0 db.updateOffStatus(Util4Pedometer.getToday(), 0); // restart service every hour to get the current step count ((AlarmManager) getApplicationContext().getSystemService(Context.ALARM_SERVICE)) .set(AlarmManager.RTC, System.currentTimeMillis() + AlarmManager.INTERVAL_HOUR, PendingIntent.getService(getApplicationContext(), 2, new Intent(this, SensorListener.class), PendingIntent.FLAG_UPDATE_CURRENT)); return START_STICKY; } @Override public void onCreate() { super.onCreate(); reRegisterSensor(); } @Override public void onTaskRemoved(final Intent rootIntent) { super.onTaskRemoved(rootIntent); // Restart service in 500 ms ((AlarmManager) getSystemService(Context.ALARM_SERVICE)) .set(AlarmManager.RTC, System.currentTimeMillis() + 500, PendingIntent .getService(this, 3, new Intent(this, SensorListener.class), 0)); } @Override public void onDestroy() { super.onDestroy(); if (null != db) { db.close(); } try { SensorManager sm = (SensorManager) getSystemService(SENSOR_SERVICE); sm.unregisterListener(this); } catch (Exception e) { e.printStackTrace(); } reRegisterSensor(); } @TargetApi(19) private void reRegisterSensor() { if (Build.VERSION.SDK_INT < 19) { return; } SensorManager sm = (SensorManager) getSystemService(SENSOR_SERVICE); if (null == sm) { // 无法获得传感器管理组件 } else { // 获得传感器管理组件 } try { sm.unregisterListener(this); } catch (Exception e) { e.printStackTrace(); } if (null == sm.getDefaultSensor(Sensor.TYPE_STEP_COUNTER)) { // 没有计步器 } else { // 有计步器 } sm.registerListener(this, sm.getDefaultSensor(Sensor.TYPE_STEP_COUNTER), SensorManager.SENSOR_DELAY_NORMAL, 5 * MICROSECONDS_IN_ONE_MINUTE); }
}
参考:https://blog.csdn.net/niuzhucedenglu/article/details/53378265
作者:陈绮婧
原文链接:https://blog.csdn.net/forget323/article/details/80703598