android小工具-系统音量管理器

简介:一个调节系统音量的小工具,在自动模式下,不同时段能够按照设定的值自动调节系统音量。为什么做这个呢?在一些需要安静的场合,突然的手机铃声总是很尴尬。很多环境又需要铃声,毕竟不能错过几个亿的大单子啊。

最终效果图:

开发过程分阶段完成。

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

功能在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

猜你喜欢

转载自www.cnblogs.com/suen061/p/10602617.html