简介:调节系统音量的小工具,能够快捷的调节系统铃声,媒体音乐、闹钟和通话声音。你可能会想,手机自带的音量键还不够快捷吗?还得写个程序?首先,用音量键调音只能调节一种声音,像闹钟这种声音不能直接调。其次,我这个小工具支持自动调音。只要事先设定,就能在不同时段把手机音量改成你的计划音量。在安静场合避免因为手机铃声导致的尴尬,休闲的时候不会错过重要的电话。

最终效果图:

第一阶段:获取并修改系统音量

布局:文字(TextView)+进度条(SeekBar)

 

需要实现的功能是拖动进度条,改变对应的系统音量。使用进度条之前要先设置一个最大值,100就是从0—100,15就是从0—15.我们这里的最值就是系统各音量的最大值。各音量的最大值是不同的。

1、  android系统音量的大小分级。

Android系统的声音分为铃声,媒体音乐,闹钟,通话声音等。每种声音的音量大小分多个等级,默认铃声有15级,通话有7级等。具体可以参考最后的链接。

2、  获取各音频的最大值

获取各音频最值要借助系统AudioManager。从名字上我们就能看出这是系统提供管理声音的Manager.然后调用getStreamMaxVolume(type);传入音频类型。音频的类型可以参考最后的链接。

代码

am = (AudioManager) getSystemService(this.AUDIO_SERVICE);
//各音频流的最大值,参数代表类型
ringMax=am.getStreamMaxVolume(AudioManager.STREAM_RING);
musicMax=am.getStreamMaxVolume(AudioManager.STREAM_MUSIC);
alarmMax=am.getStreamMaxVolume(AudioManager.STREAM_ALARM);
callMax=am.getStreamMaxVolume(AudioManager.STREAM_VOICE_CALL);

3、  修改各音频的值

修改同样使用AudioManager

代码

//i是一个整形,要设置的音量大小。
am.setStreamVolume(AudioManager.STREAM_RING,i,0);
am.setStreamVolume(AudioManager.STREAM_MUSIC,i,0);
am.setStreamVolume(AudioManager.STREAM_VOICE_CALL,i,0);
am.setStreamVolume(AudioManager.STREAM_ALARM,i,0);

上面的i从哪里来的?在你拖动进度条的时候,系统会传进来。前提是得告诉系统你需要这个信息。所以我们要为进度条加上观察者。

代码:

ringBar=(SeekBar)findViewById(R.id.main_event_ring);
ringBar.setOnSeekBarChangeListener(barChangeListener); 

观察者代码:

//创建进度条触摸事件观察者
SeekBar.OnSeekBarChangeListener barChangeListener = new SeekBar.OnSeekBarChangeListener() {
	@Override
	public void onProgressChanged(SeekBar seekBar, int i, boolean b) {
		//seekBar.setPressed(b);
		//seekBar.setProgress(i);  bug
		if(seekBar==ringBar){
			am.setStreamVolume(AudioManager.STREAM_RING,i,0);
		}else if (seekBar==musicBar){
			am.setStreamVolume(AudioManager.STREAM_MUSIC,i,0);
		}else if (seekBar==callBar){
			am.setStreamVolume(AudioManager.STREAM_VOICE_CALL,i,0);
		}else if (seekBar==alarmBar){
			am.setStreamVolume(AudioManager.STREAM_ALARM,i,0);
		}
	}
	@Override
	public void onStartTrackingTouch(SeekBar seekBar) {

	}

	@Override
	public void onStopTrackingTouch(SeekBar seekBar) {

	}
}; 

4、  监听系统声音的改变。

上面我们已经可以通过拖动进度条的方式来设置系统音量。但是当系统音量改变时,进度条不会主动改变,没点反应不行啊。所以在系统音量改变的时候,我们要把进度的值设成当前音量值。

代码:

//获取当前系统音量
void changeBar(){
	alarmBar.setProgress(am.getStreamVolume(AudioManager.STREAM_ALARM));
	musicBar.setProgress(am.getStreamVolume(AudioManager.STREAM_MUSIC));
	ringBar.setProgress(am.getStreamVolume(AudioManager.STREAM_RING));
	callBar.setProgress(am.getStreamVolume(AudioManager.STREAM_VOICE_CALL));
}

怎么知道系统音量改变了?让系统告诉我们音量改变了。嗯!这是个好方法,所以我们写一个系统音量改变广播的接收器。

代码:

 /**
     * 处理音量变化时的界面显示
     */
    private class MyVolumeReceiver extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            //如果音量发生变化则更改seekbar的位置
            if(intent.getAction().equals("android.media.VOLUME_CHANGED_ACTION"))    {
               changeBar();
            }
        }
    }

考虑到只有界面出现的时候才需要进度条跟着系统音量一起变化,所以在onStart里面注册,在onStop里面销毁。

注册代码:

//注册音量发生变化时接收的广播
private void myRegisterReceiver(){
	mVolumeReceiver = new MyVolumeReceiver() ;
	IntentFilter filter = new IntentFilter() ;
	filter.addAction("android.media.VOLUME_CHANGED_ACTION") ;
	registerReceiver(mVolumeReceiver, filter) ;
}

销毁代码:

	
//销毁监听音量的广播
private void myUnRegisterRecevier(){
	unregisterReceiver(mVolumeReceiver);
}

第二阶段:添加计划和计划展示功能。

最初的需求是按照计划设定来设置系统的音量。所以在这阶段要实现计划的管理,增删改查。

布局:

 

1、  一个计划(Event)

上面的布局展示了一个计划有什么。标题,开始时间,结束时间,各音频的设定值。为了处理这些内容,新建一个Event表示一个计划。

字段:静态counter代表Event的总数。每次添加一个Event,counter就加1。Id是每个Event的标识。时间有两个值,String用来显示,int是用来排序的。

 

 

Id的设置为counter,之后counter加1

 

排序:按照开始时间降序排列。计划展示按照时间顺序更方便。

 

 

存储:为了便于存储,重写toString()方法。同时提供一个静态方法返回字符串代表的Event.

 

 

2、  计划控制器

主要是增删查,改的功能通过删除原来的计划,然后新增一个改动后的计划实现。其他功能是把计划存储到文件里面,从文件里面读出来。

字段:一个静态的Event表,确保所有的操作都在同一份数据上。Context用来获取文件的存取路径。

 

 

代码:

package com.example.administrator.soundmanager.controler;

import android.content.Context;
import android.os.Environment;
import android.util.Log;

import com.example.administrator.soundmanager.model.Event;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public class EventControler {
    private Context mContext;
    private static List<Event> events=new ArrayList<>();
    public EventControler(Context mContext) {
        this.mContext=mContext;
        if(events.size()<1){
            getEventFromFile();
        }
    }

    //在表中添加一个事件
    public boolean addEvent(Event e){
        events.add(e);
        saveEvents();
        return true;
    }
    //从表中删除id为eventId的事件。
    public boolean deleteEvent(int eventId){
        Iterator<Event> iterator =events.iterator();
        while (iterator.hasNext()){
            if(iterator.next().getEventId()==eventId){
                iterator.remove();
            }
        }
        if (events.size()>0){
            saveEvents();
        }else{
            deleteFile("events.evt");
        }

        return true;
    }
    //获取id为eventId的事件。
    public Event getEvent(int eventId){
        Iterator<Event> iterator =events.iterator();
        while (iterator.hasNext()){
            Event e=iterator.next();
            if(e.getEventId()==eventId){
               return e;
            }
        }
        return null;
    }
    //获取事件记录表
    public  List<Event> getEvents(){
        return events;
    }

    //将事件记录表中的数据保存到文件中
    private boolean saveEvents(){
        if(events.size()>0){
            StringBuilder stringBuilder=new StringBuilder();
            for(Event e:events)
                stringBuilder.append(e+"\n");
            saveFile(stringBuilder.toString(),"events.evt");
            return true;
        }else{
            return false;
        }
    }
    //从数据文件中读取事件记录。
    private boolean getEventFromFile(){
        events.clear();
        String content=getFile("events.evt");
        if(content!=null){
            for(String s: content.split("\n"))
                events.add(Event.getEvent(s));
            return true;
        }
        return false;
    }

    //文件操作。
   private void saveFile(String str, String fileName) {
        String cachePath = getCachePath();
        try {
            //创建临时文件
            File tmpFile=new File(cachePath,"temp.evt");
            // 如果文件存在
            if (tmpFile.exists()) {
                // 创建新的空文件
                tmpFile.delete();
            }
            tmpFile.createNewFile();
            // 获取文件的输出流对象
            FileOutputStream outStream = new FileOutputStream(tmpFile);
            // 获取字符串对象的byte数组并写入文件流
            outStream.write(str.getBytes());
            // 最后关闭文件输出流
            outStream.close();
            // 创建指定路径的文件
            File file = new File(cachePath, fileName);
            if(file.exists()){
                file.delete();
            }
            //文件重命名
            tmpFile.renameTo(file);
        } catch (Exception e) {
            e.printStackTrace();
            Log.e("EventControler","IOException saveFile failed");
        }
    }
    private String getFile(String fileName) {
        try {
            // 创建文件
            File file = new File(getCachePath(),fileName);
            if(file.exists()){
                // 创建FileInputStream对象
                FileInputStream fis = new FileInputStream(file);
                // 创建字节数组 每次缓冲1M
                byte[] b = new byte[1024];
                int len = 0;// 一次读取1024字节大小,没有数据后返回-1.
                // 创建ByteArrayOutputStream对象
                ByteArrayOutputStream baos = new ByteArrayOutputStream();
                // 一次读取1024个字节,然后往字符输出流中写读取的字节数
                while ((len = fis.read(b)) != -1) {
                    baos.write(b, 0, len);
                }
                // 将读取的字节总数生成字节数组
                byte[] data = baos.toByteArray();
                // 关闭字节输出流
                baos.close();
                // 关闭文件输入流
                fis.close();
                // 返回字符串对象
                return new String(data);
            }else {
                return null;
            }
        } catch (Exception e) {
            e.printStackTrace();
            Log.e("EventControler","IOException getFile failed");
            return null;
        }
    }
    private void deleteFile(String fileName){
        File file=new File(getCachePath(),fileName);
        if(file.exists()){
            file.delete();
        }
    }
    private String getCachePath(){
        String cachePath ;
        //外部存储可用
        if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())
                || !Environment.isExternalStorageRemovable()) {
            cachePath = mContext.getExternalCacheDir().getPath() ;
        }else{
            cachePath=mContext.getCacheDir().getPath();
        }
        return cachePath;
    }
}

 

3、  编辑界面

布局在开始的时候展示了。在这个界面需要实现什么呢?新建一个计划和修改一个已有的计划这两个功能。其实修改是把原来的删除之后新建。

因为eventId可以标识一个计划,所以通过eventId的值来判别是修改还是新建。

 

下面的工作就简单了,只要界面的内容有改动,就把event对应的值进行修改就好。所以,

编辑框添加内容改变观察者

 

进度条的内容改变观察者跟第一阶段的相似

开始时间和结束时间的获取,使用对话框和TimePicker控件实现。具体参考的链接在最后。

 

一切都修改完成后,点击完成按钮.如果是修改就先删除原来的值。

 

 

4、  展示界面

显示界面使用了一个RecycleView控件。这个控件的使用主要是,子项的布局文件,数据源,适配器,布局管理器。

布局

子项布局

 

数据源:从计划控制器得到

布局管理:竖直方向的线性布局

适配器:直接上代码

 

 class ListAdapter extends RecyclerView.Adapter<ListAdapter.ViewHolder>{
        private List<Event> events;
        public ListAdapter(List<Event> eventList) {
        events=eventList;
        }

        @Override
        public void onBindViewHolder(ViewHolder holder, final int position) {
            Event e=events.get(position);
            holder.title.setText(e.getEventName());

            holder.startTime.setText(e.getStartTime());
            holder.endTime.setText(e.getEndTime());

            holder.ring.setProgress(e.getRing());
            holder.ring.setMax(ringMax);
            holder.ring.setEnabled(false);//禁止拖动
            ......

        }

        @Override
        public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            Context mContext=parent.getContext();
            final ViewHolder holder=new ViewHolder(LayoutInflater.from(mContext).inflate(R.layout.item_event,parent,false));
            //点击进入编辑界面
			View.OnClickListener clickListener=new View.OnClickListener() {
                @Override
                public void onClick(View view) {
                    Intent intent =new Intent(ShowEventsActivity.this,EditEventActivity.class);
                    intent.putExtra("eventId",events.get(holder.getPosition()).getEventId());
                    notifyDataSetChanged();
                    startActivityForResult(intent,1000);
                }
            };
			//长按删除
            View.OnLongClickListener longClickListener=new View.OnLongClickListener() {
                @Override
                public boolean onLongClick(View view) {
                    AlertDialog.Builder builder = new AlertDialog.Builder(ShowEventsActivity.this);
                    builder.setTitle("删除该计划");
                    builder.setPositiveButton("确定", new DialogInterface.OnClickListener() {
                        @Override
                        public void onClick(DialogInterface dialogInterface, int i) {
                            eventControler.deleteEvent(events.get(holder.getPosition()).getEventId());
                            notifyDataSetChanged();
                            dialogInterface.cancel();
                        }
                    });
                    builder.setNegativeButton("取消", new DialogInterface.OnClickListener() {
                        @Override
                        public void onClick(DialogInterface dialogInterface, int i) {
                            dialogInterface.cancel();
                        }
                    });
                    builder.create().show();
                return true;
                }
            };
            holder.view.setOnClickListener(clickListener);
            holder.view.setOnLongClickListener(longClickListener);
            return holder;
        }

        @Override
        public int getItemCount() {
            return events.size();
        }
         class ViewHolder extends RecyclerView.ViewHolder{
             TextView title;
             TextView startTime,endTime;
             SeekBar ring,music,call,alarm;
             View view;
            public ViewHolder(View view) {
                super(view);
                this.view=view;
                title=(TextView)view.findViewById(R.id.item_event_title);
				.....
            }
        }
    }

5、  细节

从展示界面点击list的子项或者点击新建进入编辑界面,编辑完成之后需要更新显示界面。为此在进入编辑界面时采用带返回值的方式启动。这样显示界面就能知道什么时候编辑完成了。

展示界面list的子项在使用进度条展示设定值时,进度条不能被拖动。因此要把进度条设置成禁用状态。

第三阶段:按照设定自动调节系统音量。

第二阶段过后我们就有了计划表,所以自动执行的功能就是这阶段的事了。这里用一个服务完成该功能。服务在运行的时候用当前时间与计划表中的时间作对比,以此判断出当前有效的计划,然后按照计划设定系统音量。

1、  负责调节的服务

后台服务一直运行,每隔一分钟检查一下是否需要改变音量。

代码:

package com.example.administrator.soundmanager;

import android.app.AlarmManager;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.media.AudioManager;
import android.os.Binder;
import android.os.Build;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.os.SystemClock;
import android.preference.PreferenceManager;

import com.example.administrator.soundmanager.controler.EventControler;
import com.example.administrator.soundmanager.model.Event;
import com.example.administrator.soundmanager.util.LOG;

import java.util.Calendar;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;

public class SoundSetService extends Service {
    private final String TAG="SoundSetService";
    private List<Event> eventList;
    private boolean isRun=false;
    private int stopCounter=0;
    private AudioManager am;
    private NotificationManager notificationManager;
    //配置文件
    SharedPreferences preferences;
    private MyBinder mBinder;
    //判断是否为服务自己启动
    private boolean fromMySelf=false;
    private Handler soundHandler=new Handler(){
        @Override
        public void handleMessage(Message msg) {
            setSysSound();
        }
    };
    public SoundSetService() {
    }
    @Override
    public void onCreate() {
        super.onCreate();
        LOG.d(TAG,"..............onCreate");
        eventList=new EventControler(this).getEvents();
        //获得配置文件
       preferences=PreferenceManager.getDefaultSharedPreferences(this);
        //读取数据,如果无法找到,则使用默认值
        isRun=preferences.getBoolean("isRun",false);
        //免打扰权限检测
        notificationManager=(NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE);
        mBinder=new MyBinder();
        //使用前台服务保活
        startForeground(1001,new Notification());
    }
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        LOG.d(TAG,"..............onStartCommand");
        //设置系统音量
        Message msg=soundHandler.obtainMessage();
        soundHandler.sendMessage(msg);
        fromMySelf=intent.getBooleanExtra("fromMySelf",false);
        //注册定时事件,每过1分钟通过广播自动唤醒服务,使得服务得以长期运行。如果过服务被销毁,则失效
        final AlarmManager alarmManager=(AlarmManager)getSystemService(Context.ALARM_SERVICE);
        Intent i=new Intent(this, SoundSetService.class);
        i.putExtra("fromMySelf",true);
        final PendingIntent weakUpIntent=PendingIntent.getService(this,0,i,0);
        alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,
                SystemClock.elapsedRealtime()+60000, weakUpIntent);
        /**
         * 应用需要加入内存清理白名单
         */
        return START_STICKY;
    }

    @Override
    public IBinder onBind(Intent intent) {
        LOG.d(TAG,"..............onBind");
        // TODO: Return the communication channel to the service.
       return mBinder;
    }

    //设置系统音量
    private void setSysSound(){
        if(isRun){
            Event currentEvent=null;
            //获取当前时间
            Calendar calendar= Calendar.getInstance();
            calendar.setTimeInMillis(System.currentTimeMillis());
            int mHour= calendar.get(Calendar.HOUR_OF_DAY);
            int mMinute=calendar.get(Calendar.MINUTE);
            int currentTime=mHour*60+mMinute;

            //按开始时间降序排列
           Collections.sort(eventList);

            //计算当前有效事件。
            Iterator<Event> eventIterator=eventList.iterator();
            while (eventIterator.hasNext()){
                Event e=eventIterator.next();
                LOG.d(TAG,e.toString()+"sTime:"+e.getsTime()+"eTime:"+e.geteTime());
                if(e.getsTime()<=currentTime && e.geteTime()>=currentTime){
                    currentEvent=e;
                    break;
                }
            }
            if(currentEvent!=null){
                setSysSound(currentEvent);
                LOG.d(TAG,"currentEvent : "+currentEvent.toString());
            }
            //服务关闭计数清空
            if(stopCounter==0){
            }else{
                stopCounter=0;
            }
        }else if(fromMySelf){//3次试探之后,服务关闭。
            if(stopCounter>3){
                stopCounter=0;
                LOG.d(TAG,"stopSelf()");
                stopSelf();
            }else{
                stopCounter++;
            }
        }
    }
    private void setSysSound(Event e){
        if(am==null){
            am= (AudioManager) getSystemService(this.AUDIO_SERVICE);
        }
        if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.N){
            if(notificationManager!=null&&notificationManager.isNotificationPolicyAccessGranted()) {
                am.setStreamVolume(AudioManager.STREAM_RING,e.getRing(),0);
                am.setStreamVolume(AudioManager.STREAM_SYSTEM,e.getRing(),0);
                am.setStreamVolume(AudioManager.STREAM_NOTIFICATION,e.getRing(),0);
            }
        }else{
            am.setStreamVolume(AudioManager.STREAM_RING,e.getRing(),0);
            am.setStreamVolume(AudioManager.STREAM_SYSTEM,e.getRing(),0);
            am.setStreamVolume(AudioManager.STREAM_NOTIFICATION,e.getRing(),0);
        }
        am.setStreamVolume(AudioManager.STREAM_RING,e.getRing(),0);
        //如果当前有音乐播放,则不改变音量。
        if(!am.isMusicActive()){
            am.setStreamVolume(AudioManager.STREAM_MUSIC,e.getMusic(),0);
        }
        am.setStreamVolume(AudioManager.STREAM_VOICE_CALL,e.getCall(),0);
        am.setStreamVolume(AudioManager.STREAM_ALARM,e.getAlarm(),0);
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        LOG.d(TAG,"..............onDestroy");
        mBinder=null;
    }

    public class MyBinder extends Binder{
        public boolean isRuning(){
            return isRun;
        }
        public void start(){
            isRun=true;
            setSysSound();
            //更新配置文件
            preferences.edit().putBoolean("isRun",isRun).commit();
        }
        public void end(){
            isRun=false;
            //更新配置文件
            preferences.edit().putBoolean("isRun",isRun).commit();
        }
    }


}
  

2、  MainActivity设置是否使用自动调节。

在MainActivity中通过绑定的方式,可以控制Service的状态。

 

在最终效果图可以看到,有个CheckBox,这个用来设置是否使用自动音量调节服务。状态值保留在配置文件里面

CheckBox的观察者。

//设置音量管理服务的运行。
    void doServiceSet(){
        if(!serviceBox.isChecked()){
            if(soundSer==null){
            }else{
                if(!soundSer.isRuning()){
                }else{
                    soundSer.end();
                }
            }
        }else{
            if(soundSer!=null){
                if(soundSer.isRuning()){
                }else{
                    soundSer.start();
                }
            }else{
                bindService(new Intent(MainActivity.this,SoundSetService.class),serviceConnection,0);
                startService(new Intent(MainActivity.this,SoundSetService.class));
                serviceBox.setChecked(false);
            }
        }
    }

  

3、  开机自启

这个为了防止开机之后忘记打开软件,造成不好的体验。增加了开机自启。通过静态注册广播接收系统的开机实现。

接收器执行代码

 

 第四阶段、测试

1、sdk>=23的版本上程序闪退

调试报错java.lang.SecurityException: Not allowed to change Do Not Disturb state。查询得知需要动态获取免打扰修改的权限,才能设置系统的音量,媒体、闹钟不需要该权限。

解决:

//获取Do not disturb权限,才可进行音量操作
private void getDoNotDisturb(){
	NotificationManager nm =
			(NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
	if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N& nm!=null &!nm.isNotificationPolicyAccessGranted()) {
		Intent intent = new Intent(
		android.provider.Settings.ACTION_NOTIFICATION_POLICY_ACCESS_SETTINGS);
		
		startActivity(intent);
	}
}

代码在start后面调用,由用户选择是否授权。

  

源码:https://github.com/Sutg/SoundManager 

参考:

文件读写:https://blog.csdn.net/yoryky/article/details/78675373

内置存储和外部存储:https://zm12.sm-tc.cn/?src=l4uLj4zF0NCIiIjRnJGdk5CYjNGckJLQlZaRmJKQz8zOxtCejYuWnJOajNDKysfJysrG0ZeLkpM%3D&uid=7151bc062e1aec6ad263c0f024a4b76e&hid=91d8f653d3b2c3b90d2b247107e56400&pos=1&cid=9&time=1553565744734&from=click&restype=1&pagetype=0020004002000402&bu=ss_doc&query=Android%E7%9A%84%E5%86%85%E7%BD%AE%E5%AD%98%E5%82%A8&mode=&v=1&force=true&wap=false&uc_param_str=dnntnwvepffrgibijbprsvdsdichei

解决RecyclerView item 宽度没有填充屏幕:https://blog.csdn.net/json_corleone/article/details/84230546

广播实现音量同步:https://m.jb51.net/article/101825.htm

音量的获取与设置:https://blog.csdn.net/coderder/article/details/78436892

滚动时间选择器:https://www.cnblogs.com/android-zcq/p/5435681.html

Android系统的音量默认和最大值:https://blog.csdn.net/l0605020112/article/details/35570543

服务保活:https://www.jianshu.com/p/20801232bc7e