Android 来电悬浮框的探索

基于项目中需要,监听系统来电弹出自定义的悬浮框,综合其他博主共享的资源,成功在项目中实现了这一功能。本着达则兼济天下的崇高理想,分享出来,以供参阅。

根据需求我把整个功能分割了以下几个部分:

  • 监听系统来电的广播
  • 接收广播处理通话状态
  • 悬浮框界面
  • 接听功能实现
  • 挂断功能实现

监听系统来电的广播

AndroidManifest.xml中注册监听系统来电的广播之前需要添加权限:

<uses-permission
android:name="android.permission.READ_PHONE_STATE">
</uses-permission>

注册广播:

<receiver android:name="com.softi.cs.ui.receiver.CSCallReceiver" >
<intent-filter>
 <action android:name="android.intent.action.PHONE_STATE" />
 <action android:name="android.intent.action.NEW_OUTGOING_CALL" />
 <action android:name="call.cs.call.ending.action" />
</intent-filter>
</receiver>

接收广播处理通话状态

接收广播的类:CSCallReceiver .java

public class CSCallReceiver extends BroadcastReceiver {

    private Context mContext;
    private boolean incomingFlag=false;
    private String incoming_number="";
    private String TAG="CSCallReceiver";
    private String phoneNumber;

    @Override
    public void onReceive(Context context, Intent intent) {
        mContext = context;
        TelephonyManager sTelephoneyManager = (TelephonyManager) context.getSystemService(Service.TELEPHONY_SERVICE);

        //如果是拨打电话  
        if(intent.getAction().equals(Intent.ACTION_NEW_OUTGOING_CALL)){ //外呼不做处理                       
                incomingFlag = false;  
                phoneNumber = intent.getStringExtra(Intent.EXTRA_PHONE_NUMBER);          
                Log.i(TAG, "call OUT:"+phoneNumber);                          
        }else if(intent.getAction().equals("call.cs.call.ending.action")){//处理挂断,在PhoneActivity接收广播 并延时4秒关闭界面,为了遮盖系统电话关闭页面

            sendCloseBroadCast();

        }else{                          
                //如果是来电  ,开启悬浮页面。
                TelephonyManager tm =   
                    (TelephonyManager)context.getSystemService(Service.TELEPHONY_SERVICE);                          

                switch (tm.getCallState()) {  
                case TelephonyManager.CALL_STATE_RINGING:  
                        incomingFlag = true;//标识当前是来电  
                        incoming_number = intent.getStringExtra("incoming_number");  
                        Intent myIntent =new Intent(context,PhoneActivity.class);
                        myIntent.putExtra("incoming_number", incoming_number);
                        myIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                        context.startActivity(myIntent);
                        Log.i(TAG, "RINGING :"+ incoming_number);  
                        break;  
                case TelephonyManager.CALL_STATE_OFFHOOK:                                  
                        if(incomingFlag){  
                                Log.i(TAG, "incoming ACCEPT :"+ incoming_number);  
                        }  
                        break;  

                case TelephonyManager.CALL_STATE_IDLE:                                  
                        if(incomingFlag){  
                                Log.i(TAG, "incoming IDLE");                                  
                        }  
                        break;  
                }   
        }  

    }

private void sendCloseBroadCast() {
                                                                Intent intent = new  Intent("com.chinasofti.rcs.finishcall");
mContext.sendBroadcast(intent);
    }


}

悬浮框界面

收到来电广播,弹出悬浮框界面:PhoneActivity.java

public class PhoneActivity extends Activity implements OnClickListener {
    private MyBroadcastReceiver mBroadcastReceiver;

    public static int OVERLAY_PERMISSION_REQ_CODE = 1234;

    private static final String LOG_TAG = "PhoneActivity";
    private static View mView = null;
    private static WindowManager mWindowManager = null;
    private static Context mContext = null;
    public static Boolean isShown = false;
    TelephonyManager telMgr;

    private Button declinButton;

    private Button answerButton;
    private static final String MANUFACTURER_HTC = "HTC";

    private AudioManager audioManager;

    private ImageView incomingThumb;

    private TextView callName;

    private String phoneNumber;

    Handler handler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            int what = msg.what;
            if (what == 0) {
                hidePopupWindow();
            } 
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.layout_cs_main);
        Intent intent = getIntent();
        phoneNumber = intent.getStringExtra("incoming_number");
        mBroadcastReceiver = new MyBroadcastReceiver();
        IntentFilter intentFilter = new IntentFilter();
        intentFilter.addAction("com.chinasofti.rcs.finishcall");
        registerReceiver(mBroadcastReceiver, intentFilter);
        telMgr = (TelephonyManager) this.getSystemService(Service.TELEPHONY_SERVICE);
        audioManager = (AudioManager) this.getSystemService(Context.AUDIO_SERVICE);
        askForPermission();
    }

    @Override
    protected void onResume() {
        // TODO Auto-generated method stub
        super.onResume();
    }

    @Override
    protected void onPause() {
        // TODO Auto-generated method stub
        super.onPause();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        unregisterReceiver(mBroadcastReceiver);
    }

    public class MyBroadcastReceiver extends BroadcastReceiver {

        @Override
        public void onReceive(Context context, Intent intent) {
            Log.i(LOG_TAG, "MyBroadcastReceiver onReceive ");
            handler.sendEmptyMessageDelayed(0, 4000);
        }
    }


    /**
     * 请求用户给予悬浮窗的权限
     */
    public void askForPermission() {
        if (!Settings.canDrawOverlays(this)) {
            Toast.makeText(PhoneActivity.this, "当前无权限,请授权!", Toast.LENGTH_SHORT).show();
            Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + getPackageName()));
            startActivityForResult(intent, OVERLAY_PERMISSION_REQ_CODE);
        } else {
            showPopupWindow();
        }
    }



    /**
     * 显示弹出框
     *
     * @param context
     * @param view
     */
    public void showPopupWindow() {
        if (isShown) {
            Log.i(LOG_TAG, "return cause already shown");
            return;
        }
        isShown = true;
        Log.i(LOG_TAG, "showPopupWindow");
        // 获取应用的Context
        mContext = this.getApplicationContext();
        // 获取WindowManager
        mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
        mView = setUpView(this);

        WindowManager.LayoutParams params = new WindowManager.LayoutParams();
        // 类型
        params.type = WindowManager.LayoutParams.TYPE_SYSTEM_ERROR;
        // WindowManager.LayoutParams.TYPE_SYSTEM_ALERT
        // 设置flag
        int flags = WindowManager.LayoutParams.FLAG_FULLSCREEN;
        // int flags = WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM;
        // | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
        // 如果设置了WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,弹出的View收不到Back键的事件
        params.flags = flags;
        // 不设置这个弹出框的透明遮罩显示为黑色
        params.format = PixelFormat.TRANSLUCENT;
        // FLAG_NOT_TOUCH_MODAL不阻塞事件传递到后面的窗口
        // 设置 FLAG_NOT_FOCUSABLE 悬浮窗口较小时,后面的应用图标由不可长按变为可长按
        // 不设置这个flag的话,home页的划屏会有问题
        params.width = LayoutParams.MATCH_PARENT;
        params.height = LayoutParams.MATCH_PARENT;
        params.gravity = Gravity.CENTER;
        mWindowManager.addView(mView, params);
        Log.i(LOG_TAG, "add view");
    }

    /**
     * 隐藏弹出框
     */
    public void hidePopupWindow() {
        Log.i(LOG_TAG, "hide " + isShown + ", " + mView);
        if (isShown && null != mView) {
            Log.i(LOG_TAG, "hidePopupWindow");
            mWindowManager.removeView(mView);
            isShown = false;
            this.finish();
        }
    }

    private View setUpView(Context context) {
        Log.i(LOG_TAG, "setUp view");
        View view = LayoutInflater.from(context).inflate(R.layout.layout_cs_incomming, null);
        declinButton = (Button) view.findViewById(R.id.call_cs_decline);
        answerButton = (Button) view.findViewById(R.id.call_cs_answer);
        incomingThumb = (ImageView) view.findViewById(R.id.cs_incoming_thumb);
        callName = (TextView)view.findViewById(R.id.cs_call_name);
        declinButton.setOnClickListener(this);
        answerButton.setOnClickListener(this);
        //loadPhotoAndName();加载用户头像,和名称
        return view;
    }


    @Override
    public void onClick(View v) {

        switch (v.getId()) {
            case R.id.call_cs_decline:
                telEndCall();
                handler.sendEmptyMessageDelayed(0, 4000);
                break;
            case R.id.call_cs_answer:
                 acceptCall();
                break;

            default:
                break;
        }
    }

    private void telEndCall() {
        Class<TelephonyManager> c = TelephonyManager.class;
        try {
            Method getITelephonyMethod = c.getDeclaredMethod("getITelephony", (Class[]) null);
            getITelephonyMethod.setAccessible(true);
            ITelephony iTelephony = null;
            iTelephony = (ITelephony) getITelephonyMethod.invoke(telMgr, (Object[]) null);
            iTelephony.endCall();
        } catch (Exception e) {
            Log.e(LOG_TAG, "Fail to answer ring call.", e);
        }
    }

    public void acceptCall() {
        try {
            Method method = Class.forName("android.os.ServiceManager").getMethod("getService", String.class);
            IBinder binder = (IBinder) method.invoke(null, new Object[] { Context.TELEPHONY_SERVICE });
            ITelephony telephony = ITelephony.Stub.asInterface(binder);
            telephony.answerRingingCall();
        } catch (Exception e) {
            Log.e(LOG_TAG, "for version 4.1 or larger");
            acceptCall_4_1();
        }
    }

    public void acceptCall_4_1() {
        // 模拟无线耳机的按键来接听电话
        // for HTC devices we need to broadcast a connected headset
        boolean broadcastConnected = MANUFACTURER_HTC.equalsIgnoreCase(Build.MANUFACTURER) && !audioManager.isWiredHeadsetOn();
        if (broadcastConnected) {
            broadcastHeadsetConnected(false);
        }
        try {
            try {
                Runtime.getRuntime().exec("input keyevent " + Integer.toString(KeyEvent.KEYCODE_HEADSETHOOK));
            } catch (IOException e) {
                // Runtime.exec(String) had an I/O problem, try to fall back
                String enforcedPerm = "android.permission.CALL_PRIVILEGED";
                Intent btnDown = new Intent(Intent.ACTION_MEDIA_BUTTON).putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_HEADSETHOOK));
                Intent btnUp = new Intent(Intent.ACTION_MEDIA_BUTTON).putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_HEADSETHOOK));
                this.sendOrderedBroadcast(btnDown, enforcedPerm);
                this.sendOrderedBroadcast(btnUp, enforcedPerm);
            }
        } finally {
            if (broadcastConnected) {
                broadcastHeadsetConnected(false);
            }
        }
    }

    private void broadcastHeadsetConnected(boolean connected) {
        Intent i = new Intent(Intent.ACTION_HEADSET_PLUG);
        i.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
        i.putExtra("state", connected ? 1 : 0);
        i.putExtra("name", "mysms");
        try {
            this.sendOrderedBroadcast(i, null);
        } catch (Exception e) {
        }
    }
    /**
     * 用户返回
     * 
     * @param requestCode
     * @param resultCode
     * @param data
     */
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (requestCode == OVERLAY_PERMISSION_REQ_CODE) {
            if (!Settings.canDrawOverlays(this)) {
                Toast.makeText(PhoneActivity.this, "权限授予失败,无法开启悬浮窗", Toast.LENGTH_SHORT).show();
            } else {
                Toast.makeText(PhoneActivity.this, "权限授予成功!", Toast.LENGTH_SHORT).show();
                showPopupWindow();
            }

        }
    }   
}

接听功能实现

接听和挂断功能来源于其他博主

public void acceptCall() {
        try {
            Method method = Class.forName("android.os.ServiceManager").getMethod("getService", String.class);
            IBinder binder = (IBinder) method.invoke(null, new Object[] { Context.TELEPHONY_SERVICE });
            ITelephony telephony = ITelephony.Stub.asInterface(binder);
            telephony.answerRingingCall();
        } catch (Exception e) {
            Log.e(LOG_TAG, "for version 4.1 or larger");
            acceptCall_4_1();
        }
    }

    public void acceptCall_4_1() {
        // 模拟无线耳机的按键来接听电话
        // for HTC devices we need to broadcast a connected headset
        boolean broadcastConnected = MANUFACTURER_HTC.equalsIgnoreCase(Build.MANUFACTURER) && !audioManager.isWiredHeadsetOn();
        if (broadcastConnected) {
            broadcastHeadsetConnected(false);
        }
        try {
            try {
                Runtime.getRuntime().exec("input keyevent " + Integer.toString(KeyEvent.KEYCODE_HEADSETHOOK));
            } catch (IOException e) {
                // Runtime.exec(String) had an I/O problem, try to fall back
                String enforcedPerm = "android.permission.CALL_PRIVILEGED";
                Intent btnDown = new Intent(Intent.ACTION_MEDIA_BUTTON).putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_HEADSETHOOK));
                Intent btnUp = new Intent(Intent.ACTION_MEDIA_BUTTON).putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_HEADSETHOOK));
                this.sendOrderedBroadcast(btnDown, enforcedPerm);
                this.sendOrderedBroadcast(btnUp, enforcedPerm);
            }
        } finally {
            if (broadcastConnected) {
                broadcastHeadsetConnected(false);
            }
        }
    }

挂断功能实现

private void telEndCall() {
        Class<TelephonyManager> c = TelephonyManager.class;
        try {
            Method getITelephonyMethod = c.getDeclaredMethod("getITelephony", (Class[]) null);
            getITelephonyMethod.setAccessible(true);
            ITelephony iTelephony = null;
            iTelephony = (ITelephony) getITelephonyMethod.invoke(telMgr, (Object[]) null);
            iTelephony.endCall();
        } catch (Exception e) {
            Log.e(LOG_TAG, "Fail to answer ring call.", e);
        }
    }