简介:一个调节系统音量的小工具,在自动模式下,不同时段能够按照设定的值自动调节系统音量。为什么做这个呢?在一些需要安静的场合,突然的手机铃声总是很尴尬。很多环境又需要铃声,毕竟不能错过几个亿的大单子啊。
最终效果图:
开发过程分阶段完成。
第一阶段:获取并修改系统音量
功能在MainActivity里面实现。
1、 android系统音量的大小分级。
Android系统的声音分为铃声,媒体音乐,闹钟,通话声音等。每种声音的音量大小分多个等级,默认铃声有15级,通话有7级等。具体可以参考最后的链接。
2、 获取各音频的最大值
用拖动进度条的方式来改变音频的值,使用进度条需要设置一个最大值。而且每种音频的最值也不同。获取各音频最值要借助系统服务AudioManager。
代码
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类表示一个计划。
字段:开始时间和结束时间都是一个String和一个int。String用来展示,int是用来排序的。有一个静态int,counter代表Event的总数。
排序:按照开始时间降序排列。
存储:为了便于存储,重写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、 展示界面
显示界面使用了一个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); ..... } } }
4、 编辑界面
这个界面跟MainActivity很相似,要给进度条,编辑框等都加上观察者。在内容改变的时候,修改对应的计划的值。修改完后把计划交给计划控制器。
滚动时间设置使用对话框和TimePicker控件实现,参考的链接在最后。
5、 细节
从展示界面点击list的子项或者点击新建进入编辑界面,编辑完成之后需要更新显示界面。为此在进入编辑界面时采用带返回值的方式启动。这样显示界面就能知道什么时候编辑完成了。
展示界面list的子项在使用进度条展示设定值时,进度条不能被拖动。因此要把进度条设置成禁用状态。
第三阶段:按照设定自动调节系统音量。
1、 负责调节的服务
用一个后台服务一直运行,每隔一分钟检查一下是否需要改变音量。难点在与如何判断当前的有效计划。
代码:
package com.example.administrator.soundmanager; import android.app.AlarmManager; import android.app.PendingIntent; import android.app.Service; import android.content.Context; import android.content.Intent; import android.media.AudioManager; import android.os.Binder; import android.os.Handler; import android.os.IBinder; import android.os.Message; import android.os.SystemClock; import android.util.Log; import com.example.administrator.soundmanager.controler.EventControler; import com.example.administrator.soundmanager.model.Event; import java.util.Calendar; import java.util.Collections; import java.util.Iterator; import java.util.List; public class SoundSetService extends Service { private List<Event> eventList; private boolean isRun=true; private AudioManager am; private Handler soundHandler=new Handler(){ @Override public void handleMessage(Message msg) { setSysSound(); } }; public SoundSetService() { } @Override public void onCreate() { eventList=new EventControler(this).getEvents(); } @Override public int onStartCommand(Intent intent, int flags, int startId) { //设置系统音量 Message msg=soundHandler.obtainMessage(); soundHandler.sendMessage(msg); //注册定时事件,每过1分钟自动唤醒服务,使得服务得以长期运行 final AlarmManager alarmManager=(AlarmManager)getSystemService(Context.ALARM_SERVICE); final PendingIntent weakupIntent=PendingIntent.getService(this,0,new Intent(this, SoundSetService.class),0); alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime()+60000, weakupIntent); return START_NOT_STICKY; } @Override public IBinder onBind(Intent intent) { // TODO: Return the communication channel to the service. return new MyBinder(); } //设置系统音量 private void setSysSound(){ if(isRun){ if(am==null){ am= (AudioManager) getSystemService(this.AUDIO_SERVICE); } //按开始时间降序排列 Collections.sort(eventList); 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; //得到当前最近有效事件。 Iterator<Event> eventIterator=eventList.iterator(); while (eventIterator.hasNext()){ Event e=eventIterator.next(); if(e.getsTime()<=currentTime&&e.geteTime()>=currentTime) currentEvent=e; } if(currentEvent!=null) setSysSound(currentEvent); } } private void setSysSound(Event e){ 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); } public class MyBinder extends Binder{ public boolean isRuning(){ return isRun; } public void start(){ isRun=true; } public void end(){ isRun=false; } } }
2、 MainActivity设置是否使用自动调节。
在MainActivity中通过绑定的方式,可以控制Service的状态。
项目源码: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