论技术手段解决睡眠打呼问题--别打呼App诞生记

     今天终于把程序调通了,聊一聊我做这款应用的思路。首先说一说做这个程序的初衷吧,某天在办公室午睡打呼后,整整被嘲笑了一个下午啊,天呐。。。然后接下来一个礼拜都不敢午睡了。。。。 于是当天晚上就准备开发一款,打呼检测app,只要检测到打呼,手机就会发出震动,把你吵醒,让你换个姿势继续睡。。

好的,如何实现呢。。首先从手机的麦克风获取实时的分贝值,然后设定一个临界值,超过临界值就报警震动,那么为了防止极端数据的影响,不能仅仅检测瞬间的音量值,不然人家打个喷嚏手机就要震起来那还不睡什么觉啊,所以要检测的是一段时间内音量的平均值,并且这个时间段是可以调整的。

所以我们需要获取一个分贝值和设置两个数值(临界值和平均检测时间值),然后去分析获取的这三个数据,当满足在单位时间内环境的声音平均值超过某个临界值的时候手机发出震动通知即可。

说说我们需要用到哪些Android的控件吧:

首先设置两个数值,可以直接在活动中使用SeekBar控件来定义数值;

那么分析数据呢,这么吃力不讨好的活当然是交给服务去后台默默的执行咯;

那么如何反复的在后台执行这个检测任务呢,服务就要和广播接收器打个配合咯,当然不能少了Alarm;

对了,手机震动要怎么发出呢,这个简单直接发出一个通知,发出通知设置一个震动属性就行了;

好了,基本就是这些组件了,我们要准备把他们拼接起来了。。。

(那么还有什么要注意呢,我们发现当我们睡觉时手机可能已经自动锁屏了,那么虽然我们用的是服务模块,但为了安全起见,服务不会被回收我们当然是使用前台服务咯。)

说说具体的实现原流程,首先在activity中放两个控件手动设置值,获取到值后传入到服务中进行检测,看是否满足条件,满足条件则返回活动,在活动中发送震动通知,否则就继续检测,直到满足条件为止。


首先先放一张完成图吧



页面布局就不详说了,还是比较简单的,主要就是两个seekBar这个控件再加上两个按钮,和一些文本显示框。详细可以看github的源码

好了,原理也讲好了,让我们一步一步开始吧,先来做个最简单的功能,从手机的麦克风中获取分贝值并实时显示在界面

说实话,这个功能坑了我几天时间,没想到最后实现起来竟然这么简单,直接贴出结果吧,

首先你要有一段获取分贝值的代码,什么,你说你不会写,没关系反正我也不会,直接去谷歌吧

演示代码如下:

	
package com.example.myapp;
 
import android.media.AudioFormat;
import android.media.AudioRecord;
import android.media.MediaRecorder;
import android.util.Log;
 
/**
 * Created by greatpresident on 2014/8/5.
 */
public class AudioRecordDemo {
 
  private static final String TAG = "AudioRecord";
  static final int SAMPLE_RATE_IN_HZ = 8000;
  static final int BUFFER_SIZE = AudioRecord.getMinBufferSize(SAMPLE_RATE_IN_HZ,
          AudioFormat.CHANNEL_IN_DEFAULT, AudioFormat.ENCODING_PCM_16BIT);
  AudioRecord mAudioRecord;
  boolean isGetVoiceRun;
  Object mLock;
 
  public AudioRecordDemo() {
    mLock = new Object();
  }
 
  public void getNoiseLevel() {
    if (isGetVoiceRun) {
      Log.e(TAG, "还在录着呢");
      return;
    }
    mAudioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC,
        SAMPLE_RATE_IN_HZ, AudioFormat.CHANNEL_IN_DEFAULT,
        AudioFormat.ENCODING_PCM_16BIT, BUFFER_SIZE);
    if (mAudioRecord == null) {
      Log.e("sound", "mAudioRecord初始化失败");
    }
    isGetVoiceRun = true;
 
    new Thread(new Runnable() {
      @Override
      public void run() {
        mAudioRecord.startRecording();
        short[] buffer = new short[BUFFER_SIZE];
        while (isGetVoiceRun) {
          //r是实际读取的数据长度,一般而言r会小于buffersize
          int r = mAudioRecord.read(buffer, 0, BUFFER_SIZE);
          long v = 0;
          // 将 buffer 内容取出,进行平方和运算
          for (int i = 0; i < buffer.length; i++) {
            v += buffer[i] * buffer[i];
          }
          // 平方和除以数据总长度,得到音量大小。
          double mean = v / (double) r;
          double volume = 10 * Math.log10(mean);
          Log.d(TAG, "分贝值:" + volume);
          // 大概一秒十次
          synchronized (mLock) {
            try {
              mLock.wait(100);
            } catch (InterruptedException e) {
              e.printStackTrace();
            }
          }
        }
        mAudioRecord.stop();
        mAudioRecord.release();
        mAudioRecord = null;
      }
    }).start();
  }

 }

好了,通过这段代码就可以获取手机的分贝值了,我们去新建一个类把这段代码放进去吧,那么我们要如何让这个数值显示在activity中呢。

我们还需要对这段代码进行下修改。

我们是不是要让分贝值在界面实时显示呢,但是目前代码只能在日志里输出哦。怎么办呢。。。。

看着,这里我们就用android的回调机制来解决这个问题吧。首先去创建一个接口类,就叫CallBack好了,然后在里面定义完成一个方法,

然后呢,然后给这段代码添加一个参数呗,看到里面的getNoiseLevel()方法没,给他加一个参数上去

getNoiseLevel(final Callback listener)
然后在下面那段拿到分贝值那里插入


if (volume != 0) {
    listener.onFinish(volume);
}


把数据传入到我们的回调方法里,这样就拿到这个分贝数据了。不过这里注意这个代码段是运行在一个子线程里面的哦。

接下来怎么办呢,当然是在activity中去调用这段代码咯。


private void showDb() {
    audioRecordDemo.getNoiseLevel(new Callback() {
        @Override
        public void onFinish(final double vol) {
            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    if (status==TURNON) {
                        textView.setText((int) vol + "");
                        voll = (int) vol;
                    }
                }
            });
        }
    });
}

audioRecord类的声明已经在开头声明过了,所以这里直接创建它的对象了,并且通过runOnUiThread()方法回到主线程去更新UI,然后把这个操作封装成一个方法,这样分贝值就实时可以在活动中实时显示出来了。那这个status是怎么回事呢,这个是用来标记程序运行状态的,当按下开始按钮时这个值为TURNON,按下停止按钮时TURNOFF,这样就可以控制程序在活动中开始和停止更新ui了。


好了,第二步

开启前台服务,利用和广播的配合进行循环启动,这个比较比较简单,大家直接去看源码吧,就是应用了一个定时器。

不过这里有个坑,下面这段代码的最后一个参数要注意必须是这个,不然就不能通过intent把值传传递过去了,之前设置的都是0,结果。。都是泪。。。

PendingIntent.getBroadcast(this, 0, bIntent, PendingIntent.FLAG_UPDATE_CURRENT);

下面这个代码就是每隔0.1秒发送一个广播了,然后在广播中再启动服务就这样不断循环咯

AlarmManager manager = (AlarmManager) getSystemService(ALARM_SERVICE);
int anHour = 100;
long triggerAtTime = SystemClock.elapsedRealtime() + anHour;
PendingIntent pi = PendingIntent.getBroadcast(this, 0, bIntent, PendingIntent.FLAG_UPDATE_CURRENT);
manager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, triggerAtTime, pi );

并在循环中进行判断是否满足设定的临界值。

这里我详细讲下如何判断是否满足设定临界值,其实也非常简单了,首先就是获取三个值咯,通过广播把分贝值和自定义的两个值检测阈值和检测平均时间通过intent传递进来

然后创建一个list每次都把这个分贝值添加进去,那么问题来了,添加进去之后干嘛呢?
我们知道这个循环是0.1秒执行一次的,所以只要加一个判断,比如说现在的检测时间是1秒钟,那么就是当list里面的对象数量是1*10的时候就说明一次检测可以开始了,因为这样刚好就是1秒钟,然后把这1秒钟内获取的10次分贝值总和除以10就可以得到这一秒钟内的平均分贝值了,最后再去对比下设置的检测阈值就可以知道是否满足条件了。

用代码表示就是这样:

public int onStartCommand(Intent intent, int flags, int startId) {
    Log.d("service", "服务开始");
    Intent bIntent=new Intent("newValue");//发送广播的intent
    vol = intent.getIntExtra("voll", 0);
    checkTime1 = intent.getExtras().getDouble("checkTime");
    if (checkTime1 == 0.0) {
        checkTime2=1;
    }else{
    checkTime2=(int)(checkTime1*10);//checktime1是从活动中传递过来的检测时间间隔值,是double类型的,checktime2 是转换后成int后的值,因为这个值不能为0,所以强制规定设置为0时程序内部作为1处理。
 volMax = intent.getIntExtra("volMax", 90);
    list.add(vol);

 if (list.size() == 31){  //这段代码是以防万一用的,当list里的数量大于31时,强制进行清空。
        list.clear();
    }

 if (list.size() == checkTime2) {
        Log.d("service", "在list中");
        for (int array : list) {

            sum = sum + array;
        }
        int lastValue = sum / list.size();
       isMax=lastValue >= volMax;
        Log.d("service", "isMax"+isMax);
        if (isMax) {

            bIntent.putExtra("checked",true);//如果满足这个设置的阈值就把这个好事告诉intent,他会传递给广播接收器,广播接收器就会做判断发送震动咯。
            Log.d("service", "放入checked为真");
        }

        list.clear();
        sum=0;//求和结束后这个值要记得清零哦,否则你就等着变傻逼吧,这个值会被重复利用,然后你就懵逼了
    }

这段代码是写在onStartCommand()方法里的,根据服务的生命周期他就会被不断的执行了。好的,服务模块时间结束,让我们去看看广播接收器的部分吧。

广播接收器

这边的逻辑也非常简单,它的主要任务就是去启动服务。

但是请注意,没啥事的时候它就启动下服务就完了,但是万一服务检测到了满足阈值了呢,所以这里首先就是做个判断看看服务是否检测到了满足的阈值。如果没有检测到天下太平,一切照旧。当然我们还是讨论下检测到了的情况吧,首先从intent里取值,如果为真,就去发送一个带有震动属性的通知,这里我已经把它提取为一个sendNotify()方法了。

这个时候要注意,问题来了,如果就这样结束,继续去启动服务,那么在震动的这段时间内,服务照样是在检测分贝值的,但这时手机正在震动呢,麦克风也在震动啊,这时候检测的分贝值有个卵用啊,手机就会不断的震啊震,直接变傻逼啊。

所以这里用Timer和TimerTask,众所周知他们是一对好基友,在定时器任务里设定好要执行的语句内容,然后由定时器去指定何时执行,这里我们就延迟一秒执行就好了,因为我们手机就震动一秒钟嘛。看看实现代码吧

class MyTimerTask extends TimerTask{
        @Override
        public void run() {
            Intent ser = new Intent(MainActivity.this, DbListenerService.class);
            ser.putExtra("voll", voll);
            ser.putExtra("volMax", seekBar1.getProgress());
            ser.putExtra("checkTime", timeSeek.getProgress() / 10d);
            startService(ser);
            Log.d("activity", "延迟一秒启动服务成功");
            timer.cancel();
            task.cancel();
        }
    }
//  接收到广播就启动服务
    class MyReceiver extends BroadcastReceiver {


        @Override
        public void onReceive(Context context, Intent intent) {

            Log.d("activity",  "活动开始检查checked值:"+intent.getBooleanExtra("checked", false) );
            if (intent.getBooleanExtra("checked", false)) {
                Log.d("activity", "checked值为真,发送震动通知");
                sendNotify();
                task=new MyTimerTask();
                timer = new Timer();
                timer.schedule(task, 1000);
                Log.d("activity", "checked值为真,准备延迟1秒启动服务3");

            } else {
                Log.d("activity4", "checked为假时立即启动服务");
                Intent ser = new Intent(context, DbListenerService.class);
                ser.putExtra("voll", voll);
                ser.putExtra("volMax", seekBar1.getProgress());
                ser.putExtra("checkTime", timeSeek.getProgress() / 10d);
                startService(ser);
            }
        }
    }

这里有个坑注意下,TimerTask是一次性的,只能用一次,下一次继续用的话就会报错了,所以这里每次启用都去创建一个新的对象就可以了。用完后记得调cancel()方法,把他们关闭。。。


总结一下做这个应用碰到的坑:

1、double转int的数据转换,int a=(int)0.8*10d和int a=(int)(0.8*10d)的值是不一样的前者是0后者是8,切记;

2、程序运行的状态需要进行标记,比如这个应用,当停止服务方法重复执行两次时就会报错,因为第一次已经把服务给停止了,第二次程序就会懵逼了,所以要在停止按钮按下时先做一个判断,判断目前程序的状态。

3、数值的传递,从活动传值到服务时,每一次传递必须完整的把所有值都传递进去,不能漏掉一个。

4、要有始有终,动态注册的广播结束时就不要忘了取消注册,开启的定时器和定时器任务用完要关掉(不关掉系统会来打扫垃圾),list和sum用完记得清空。不要只想着往前走,就忘记擦屁股。

5、开启的子线程目前没有找到更好的办法能快速关闭,就用if(true/false)进行状态判断再执行呗。

6、程序尽量简单,一个类一个模块,能分就分分开,数值可以重用的就重用,少去创建新的变量。

7、TimerTask是一次性的,重复使用要去继承他并复写它的run方法,并新建对象

8、数据传递的情况可以用打印log的方法得到,便于后期分析使用,注意数据命名要清晰,流程运行要写的具体明白。

9、要想应用在手机旋转时保持竖屏状态,在配置文件相应的activity标签加这句
横屏是属性是--landscape

android:screenOrientation="portrait"

完整代码参考我的github

github:https://github.com/superman4933/sleepHelp

 

猜你喜欢

转载自blog.csdn.net/superman4933/article/details/50859289