简介:调节系统音量的小工具,能够快捷的调节系统铃声,媒体音乐、闹钟和通话声音。你可能会想,手机自带的音量键还不够快捷吗?还得写个程序?首先,用音量键调音只能调节一种声音,像闹钟这种声音不能直接调。其次,我这个小工具支持自动调音。只要事先设定,就能在不同时段把手机音量改成你的计划音量。在安静场合避免因为手机铃声导致的尴尬,休闲的时候不会错过重要的电话。
最终效果图:
第一阶段:获取并修改系统音量
布局:文字(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&¬ificationManager.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