package cri.sanity; import cri.sanity.util.*; import android.telephony.PhoneStateListener; import android.telephony.TelephonyManager; import android.hardware.Sensor; import android.hardware.SensorManager; import android.hardware.SensorEvent; import android.hardware.SensorEventListener; import android.media.AudioManager; import android.os.SystemClock; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; public final class PhoneListener extends PhoneStateListener implements SensorEventListener { public static final int LISTEN = LISTEN_CALL_STATE|LISTEN_CALL_FORWARDING_INDICATOR; private static final int FORCE_AUTOSPEAKER_DELAY = Conf.FORCE_AUTOSPEAKER_DELAY; private static final int CALL_STATE_NONE = -1; private static final int SPEAKER_CALL_INCOMING = 1; private static final int SPEAKER_CALL_OUTGOING = 2; private static final int TASK_DEVS = Task.idNew(); private static final int TASK_SPEAKER = Task.idNew(); private static PhoneListener activeInst; public int btCount; public SpeakerListener speakerListener; private String phoneNumber; private int lastCallState, speakerCall, speakerOnCount, speakerOffCount; private boolean outgoing, offhook, shutdown, rec, notifyEnable, notifyDisable; private boolean headsetRegistered, proximRegistered, proximReverse, proximDisable, proximEnable; private boolean autoSpeaker, speakerOn, speakerOff; private boolean gpsAuto, btAuto, btReverse, skipBtConn, screenOff, screenOn; private boolean lastFar, volSolo, headsetOn, wiredHeadsetOn, devsLastEnable; private long devsLastTime; private int volRestore, volPhone, volWired, volBt, volSpeaker, volFlags, ringMode, vibrMode; private int disableDelay, enableDelay, speakerDelay, speakerCallDelay; private float proximFar; private TTS tts; private WifiTracker wifiTrack; private MobDataTracker mobdTrack; private final AudioManager audioMan = A.audioMan(); private Sensor proximSensor; private Task taskSpeakerOn; private Task taskSpeakerOff; private Task taskDevsOn; private Task taskDevsOff; private Task taskSpeakerOnFar; private BroadcastReceiver screenOnReceiver, screenOffReceiver; private final BroadcastReceiver headsetWiredReceiver = new BroadcastReceiver() { @Override public void onReceive(Context c, Intent i) { final boolean on = i.getIntExtra("state",0) != 0; if(on == wiredHeadsetOn) return; updateHeadset(wiredHeadsetOn = on, volWired); if(on && btReverse) { btReverse = false; Dev.enableBt(false); } } }; //---- static public api public static final PhoneListener getActiveInstance() { return activeInst; } public static final boolean isRunning() { return activeInst != null; } //---- public api public final void startup() { activeInst = this; shutdown = false; outgoing = true; offhook = false; phoneNumber = null; lastFar = true; rec = false; btReverse = false; btCount = Math.max(A.geti(K.BT_COUNT), 0); ringMode = -1; vibrMode = -1; proximRegistered = headsetRegistered = false; devsLastTime = 0; devsLastEnable = true; speakerListener = null; lastCallState = CALL_STATE_NONE; wiredHeadsetOn = audioMan.isWiredHeadsetOn(); headsetOn = wiredHeadsetOn || (btCount>0 && A.is(K.FORCE_BT_AUDIO)) || audioMan.isBluetoothA2dpOn() || audioMan.isBluetoothScoOn(); } public final boolean isOutgoing () { return outgoing; } public final boolean isHeadsetOn () { return headsetOn; } public final boolean hasAutoDev () { return mobdTrack!=null || wifiTrack!=null || btAuto || gpsAuto; } public final boolean hasAutoSpeaker() { return autoSpeaker; } public final String phoneNumber () { return phoneNumber; } public boolean changeRinger(int ring, int vibr) { return changeRinger(ring, vibr, audioMan.getRingerMode(), audioMan.getVibrateSetting(AudioManager.VIBRATE_TYPE_RINGER)); } public final void updateHeadsetBt(boolean on) { if(wiredHeadsetOn) return; // if wired headset are on, then the new bt device connected is NOT headset one! updateHeadset(on, volBt); } //---- private api private void initCall() { //A.logd("PhoneListener.initCall: begin"); proximSensor = A.sensorProxim(); if(proximSensor == null) { proximDisable = proximEnable = proximReverse = autoSpeaker = false; } else { autoSpeaker = A.is(K.SPEAKER_AUTO); speakerDelay = A.geti(K.SPEAKER_DELAY); speakerOnCount = A.geti(K.SPEAKER_ON_COUNT); speakerOffCount = A.geti(K.SPEAKER_OFF_COUNT); speakerOn = speakerOnCount >= 0; speakerOff = speakerOffCount >= 0; proximReverse = A.is(K.REVERSE_PROXIMITY); proximDisable = A.is(K.DISABLE_PROXIMITY); if(!proximDisable) proximEnable = false; else { proximEnable = A.is(K. ENABLE_PROXIMITY); disableDelay = A.geti(K.DISABLE_DELAY); enableDelay = A.geti(K.ENABLE_DELAY); if(enableDelay < 0) enableDelay = disableDelay; } } speakerCall = A.geti(K.SPEAKER_CALL); speakerCallDelay = A.geti(K.SPEAKER_CALL_DELAY); screenOff = Admin.isActive() && A.is(K.SCREEN_OFF); screenOn = A.is(K.SCREEN_ON); rec = A.is(K.REC); volRestore = -1; volSpeaker = A.geti(K.SPEAKER_VOL); volPhone = A.geti(K.VOL_PHONE); volWired = A.geti(K.VOL_WIRED); volBt = A.geti(K.VOL_BT); volSolo = A.is(K.VOL_SOLO); volFlags = A.is(K.NOTIFY_VOLUME) ? AudioManager.FLAG_SHOW_UI : 0; gpsAuto = A.is(K.AUTO_GPS) && Dev.isGpsOn(); boolean mobd = (!gpsAuto || !A.is(K.SKIP_MOBDATA)) && A.is(K.AUTO_MOBDATA) && Dev.isMobDataOn(); boolean wifi = A.is(K.AUTO_WIFI) && A.wifiMan().isWifiEnabled(); if((wifi || mobd) && A.is(K.SKIP_HOTSPOT) && Dev.isHotspotOn()) wifi = mobd = false; else if(mobd && A.is(K.SKIP_TETHER) && Dev.isTetheringOn()) mobd = false; wifiTrack = wifi? new WifiTracker() : null; mobdTrack = mobd? new MobDataTracker() : null; if(Dev.isBtOn()) { btAuto = A.is(K.AUTO_BT); btReverse = false; skipBtConn = A.is(K.SKIP_BT); } else { btAuto = false; btReverse = !headsetOn && A.is(K.REVERSE_BT); } if(autoSpeaker) { taskSpeakerOn = new Task(){ public void run(){ autoSpeaker(true ); }}; taskSpeakerOff = new Task(){ public void run(){ autoSpeaker(false); }}; } if(!hasAutoDev()) { proximEnable = proximDisable = false; } else { taskDevsOn = new Task(){ public void run(){ enableDevs(true ); }}; taskDevsOff = new Task(){ public void run(){ enableDevs(false); }}; notifyEnable = A.is(K.NOTIFY_ENABLE); notifyDisable = A.is(K.NOTIFY_DISABLE); } regProximity(); regHeadset(); if(rec) RecService.start(this); //A.logd("PhoneListener.initCall: end"); } private void updateHeadset(boolean on, int vol) { if(A.is(K.NOTIFY_HEADSET)) { A.notify(A.s(on? R.string.msg_headset_on : R.string.msg_headset_off)); if(rec) A.notifyCanc(); } if(headsetOn == on) return; headsetOn = on; if(!offhook) return; setHeadsetVolume(on, vol); final boolean on2 = on || (lastFar && proximDisable); if(autoSpeaker) autoSpeaker(on2); enableDevs(on2); screenOff(!on2); if(rec) RecService.updateHeadset(on); //A.logd("headset connected: "+on); } // incoming call: phone is ringing private void onRinging() { //A.logd("onRinging"); outgoing = false; final String number = phoneNumber = PhoneReceiver.number; final int ringMode = audioMan.getRingerMode(); final boolean ring = ringMode == AudioManager.RINGER_MODE_NORMAL; final CallFilter cf = CallFilter.instance(); // call blocker final boolean block = cf.includes(number, "block", false) && (!A.is(K.BLOCK_SKIP) || ring); if(block && Blocker.apply(A.geti(K.BLOCK_MODE))) return; // urgent call if(!block && !ring && !headsetOn && cf.includes(number, "urgent", false)) urgentCall(ringMode); // TODO: change ring volume (if activated) // delay before auto answer final int answer = !block && A.is(K.ANSWER) && (headsetOn || !A.is(K.ANSWER_HEADSET)) && (!A.is(K.ANSWER_SKIP) || ring) && cf.includes(number, "answer", true) ? A.geti(K.ANSWER_DELAY) : -1; // announce call if((answer<0 || answer>3000) && A.is(K.TTS) && (headsetOn || !A.is(K.TTS_HEADSET)) && (!A.is(K.TTS_SKIP) || ring)) tts = new TTS(number, true, true, false); initCall(); btAdjust(); // auto answer if(answer >= 0) new Task(){ public void run(){ Dev.answerCall(); }}.exec(answer); } // we have a call! private void onOffhook() { //A.logd("onOffhook: begin"); //if(tts != null) tts.stop(); if(tts != null) { tts.shutdown(); tts = null; } if(Blocker.onOffhook()) return; offhook = true; Dev.enableLock(false); if(outgoing) initCall(); volSolo(true); audioMan.setMicrophoneMute(false); // FIX: remove???? if(headsetOn) { setHeadsetVolume(true, wiredHeadsetOn? volWired : volBt); } else { if(volPhone >= 0) setVolume(volPhone); if( speakerCall == SPEAKER_CALL_INCOMING) { if( outgoing) speakerCall = 0; } else if(speakerCall == SPEAKER_CALL_OUTGOING) { if(!outgoing) speakerCall = 0; } if(speakerCall > 0) speakerOnFar(); } if(!proximDisable) enableDevs(false); if(outgoing) { phoneNumber = PhoneReceiver.number; btAdjust(); } if(rec) RecService.checkAutoRec(); if(!lastFar) screenOff(true); //A.logd("onOffhook: end"); } // call completed: restore & shutdown private void onIdle() { shutdown = true; //A.logd("onIdle: begin"); PickupService.stop(); if(offhook) { Task.stop(TASK_SPEAKER); if(!headsetOn && A.is(K.SPEAKER_SILENT_END)) audioMan.setSpeakerphoneOn(false); } unregProximity(); unregHeadset(); PhoneReceiver.number = null; if(tts != null) { tts.shutdown(); tts = null; } if(rec) RecService.stop(); Task.stopAll(); if(offhook) { if(volRestore >= 0) setVolume(volRestore); Dev.enableLock(true); volSolo(false); screenOff(false); if(A.is(K.VIBRATE_END)) Vibra.vibra(); } restoreRinger(); try { A.telMan().listen(this, LISTEN_NONE); } catch(Exception e) {} Blocker.shutdown(); CallFilter.shutdown(); if(offhook) enableDevs(true); if(rec) Alarmer.runService(Alarmer.ACT_ONIDLE, null); //if(rec) RecService.cron(); if(mobdTrack != null) { mobdTrack.shutdown(); mobdTrack = null; } if(wifiTrack != null) { wifiTrack.shutdown(); wifiTrack = null; } Task.shutdownWait(); final boolean btOn = Dev.isBtOn(); if((btReverse || A.is(K.BT_OFF)) && btOn) { Dev.enableBt(false); A.putc(K.BT_COUNT, 0); } else if(!btOn) A.putc(K.BT_COUNT, 0); // recheck to avoid false counter offhook = false; phoneNumber = null; MainService.stop(); activeInst = null; //A.logd("onIdle: end"); } private synchronized void enableDevs(boolean enable) { if(!enable && headsetOn) return; final long now = SystemClock.elapsedRealtime(); final long diff = now - devsLastTime; if(diff < Conf.DEVS_MIN_RETRY) { if(enable == devsLastEnable) return; final Task task = enable? taskDevsOn : taskDevsOff; if(task != null) task.exec(TASK_DEVS, Math.max(500, (int)diff)); return; } boolean done = false; if(gpsAuto && enable!=Dev.isGpsOn()) { Dev.toggleGps(); done = true; } if(wifiTrack!=null && enable!=wifiTrack.willOn()) { wifiTrack.enable(enable); done = true; } if(mobdTrack!=null && enable!=mobdTrack.willOn()) { mobdTrack.enable(enable); done = true; } if(btAuto && !btReverse && (!skipBtConn||btCount<1) && enable!=Dev.isBtOn()) { Dev.enableBt(enable); done = true; } //A.logd("enableDevs: "+enable+", done = "+done); if(!done) return; devsLastTime = now; devsLastEnable = enable; if(enable) { if(notifyEnable ) { A.notify(A.s(R.string.msg_devs_enabled )); if(rec) A.notifyCanc(); }} else { if(notifyDisable) { A.notify(A.s(R.string.msg_devs_disabled)); if(rec) A.notifyCanc(); }} } private synchronized void autoSpeaker(boolean far) { if(headsetOn || far==audioMan.isSpeakerphoneOn()) return; if(far && volSpeaker>=0 && volRestore<0) volRestore = volPhone<0 ? audioMan.getStreamVolume(AudioManager.STREAM_VOICE_CALL) : volPhone; // retrieve current call volume (to restore later) audioMan.setSpeakerphoneOn(far); //A.logd("auto set speaker: far="+far); if(volSpeaker >= 0) { if(far) { setVolume(volSpeaker); //A.logd("auto set speaker volume"); } else if(volRestore >= 0) { setVolume(volRestore); //A.logd("restore from speaker volume to "+volRestore); //volRestore = -1; } } if(speakerListener != null) speakerListener.onSpeakerChanged(far); } private void speakerOnFar() { if(taskSpeakerOnFar == null) taskSpeakerOnFar = new Task(){ public void run(){ if(!lastFar) return; autoSpeaker(true); }}; if(speakerCallDelay <= 0) { taskSpeakerOnFar.exec(0); if(!outgoing) return; } // enable speaker after a little delay (some phones auto-disable speaker on offhook!!) if(outgoing && speakerCallDelay<FORCE_AUTOSPEAKER_DELAY) speakerCallDelay = FORCE_AUTOSPEAKER_DELAY; taskSpeakerOnFar.exec(TASK_SPEAKER, speakerCallDelay); //A.logd("speakerOnFar: delay="+speakerCallDelay); } private void setVolume(int vol) { audioMan.setStreamVolume(AudioManager.STREAM_VOICE_CALL, vol, volFlags); } private void setHeadsetVolume(boolean on, int vol) { if(vol < 0) return; if(!on) vol = volPhone; else if(volPhone < 0) volPhone = volRestore<0 ? audioMan.getStreamVolume(AudioManager.STREAM_VOICE_CALL) : volRestore; setVolume(vol); //A.logd((on?"preferred":"restored")+" volume set to level "+vol); } private synchronized void screenOff(boolean off) { if(off) { if(!screenOff) return; Dev.lock(); } else { if(!screenOn) return; Dev.wakeScreen(); } //A.logd("screenOff: "+off); } private void volSolo(boolean enable) { if(!volSolo) return; audioMan.setStreamMute(AudioManager.STREAM_SYSTEM , enable); audioMan.setStreamMute(AudioManager.STREAM_ALARM , enable); audioMan.setStreamMute(AudioManager.STREAM_NOTIFICATION, enable); if(!rec) audioMan.setStreamMute(AudioManager.STREAM_MUSIC, enable); // if we mute media, we cannot record phone call! //A.logd("set volume solo: "+enable); } private void btAdjust() { if(headsetOn) return; boolean btOn; if(!btReverse) btOn = Dev.isBtOn(); else { btOn = true; Dev.enableBt(true); final int timeout = A.geti(K.REVERSE_BT_TIMEOUT); if(timeout > 0) new Task(){ public void run(){ if(headsetOn || !btReverse) return; Dev.enableBt(false); btReverse = false; }}.exec(timeout); } if(btOn) { audioMan.setBluetoothScoOn(true); //headsetOn |= audioMan.isBluetoothScoOn(); // FIX: remove??? } } private void urgentCall(int ringMode) { final int vibrMode = audioMan.getVibrateSetting(AudioManager.VIBRATE_TYPE_RINGER); final boolean vibrOnly = ringMode==AudioManager.RINGER_MODE_VIBRATE || vibrMode==AudioManager.VIBRATE_SETTING_ONLY_SILENT; if(vibrOnly && A.is(K.URGENT_SKIP)) return; int ring, vibr; switch(A.geti(K.URGENT_MODE)) { case 1: ring = AudioManager.RINGER_MODE_NORMAL ; vibr = AudioManager.VIBRATE_SETTING_OFF; break; case 2: ring = AudioManager.RINGER_MODE_VIBRATE; vibr = AudioManager.VIBRATE_SETTING_ONLY_SILENT; break; case 3: ring = AudioManager.RINGER_MODE_NORMAL ; vibr = AudioManager.VIBRATE_SETTING_ON; break; default: return; } changeRinger(ring, vibr, ringMode, vibrMode); } private boolean changeRinger(int ringNew, int vibrNew, int ringCur, int vibrCur) { boolean changed = false; if(ringCur != ringNew) { audioMan.setRingerMode(ringNew); if(ringMode == -1) ringMode = ringCur; changed = true; } if(vibrCur != vibrNew) { audioMan.setVibrateSetting(AudioManager.VIBRATE_TYPE_RINGER, vibrNew); if(vibrMode == -1) vibrMode = vibrCur; changed = true; } return changed; } private void restoreRinger() { if(ringMode >= 0) { ModeReceiver.skip = true; // FIX?????? audioMan.setRingerMode(ringMode); } if(vibrMode >= 0) audioMan.setVibrateSetting(AudioManager.VIBRATE_TYPE_RINGER, vibrMode); } private void regProximity() { proximRegistered = proximSensor!=null && (autoSpeaker || screenOff || screenOn || (proximDisable && hasAutoDev())); if(!proximRegistered) return; final float range = proximSensor.getMaximumRange(); proximFar = Math.max(Conf.PROXIM_MIN, Math.min(Conf.PROXIM_MAX, Math.abs(range))) - 0.1f; // register proximity sensor A.sensorMan().registerListener(this, proximSensor, SensorManager.SENSOR_DELAY_NORMAL); // register screen off intents for automatic screen on if(screenOn) A.app().registerReceiver(screenOffReceiver = new BroadcastReceiver() { @Override public void onReceive(Context ctx, Intent i) { if(lastFar) Dev.wakeScreen(); } }, new IntentFilter(Intent.ACTION_SCREEN_OFF)); // register screen on intents for automatic screen off if(screenOff) A.app().registerReceiver(screenOnReceiver = new BroadcastReceiver() { @Override public void onReceive(Context ctx, Intent i) { if(!lastFar) Dev.lock(); } }, new IntentFilter(Intent.ACTION_SCREEN_ON)); } private void unregProximity() { if(!proximRegistered) return; proximRegistered = false; try { A.sensorMan().unregisterListener(this); } catch(Exception e) {} if(screenOff) try { A.app().unregisterReceiver(screenOnReceiver ); } catch(Exception e) {} if(screenOn ) try { A.app().unregisterReceiver(screenOffReceiver); } catch(Exception e) {} } private void regHeadset() { headsetRegistered = true; A.app().registerReceiver(headsetWiredReceiver, new IntentFilter(Intent.ACTION_HEADSET_PLUG)); } private void unregHeadset() { if(!headsetRegistered) return; headsetRegistered = false; try { A.app().unregisterReceiver(headsetWiredReceiver); } catch(Exception e) {} } //---- PhoneStateListener implementation @Override public void onCallForwardingIndicatorChanged(boolean cfi) { if(!offhook || headsetOn) return; if(!lastFar) new Task(){ public void run(){ screenOff(true); }}.exec(500); //if(!lastFar) screenOff(true); else if(outgoing && speakerCall>0) speakerOnFar(); //screenOff(!lastFar); // FIX: remove???? //A.logd("onCallForwardingIndicatorChanged: "+cfi); } @Override public void onCallStateChanged(int state, String num) { switch(state) { case TelephonyManager.CALL_STATE_RINGING: if(lastCallState != CALL_STATE_NONE) break; // to skip multiple/concurrent calls //A.logd("onRinging"); onRinging(); break; case TelephonyManager.CALL_STATE_OFFHOOK: if(offhook) break; // check against "offhook" to skip multiple/concurrent calls //A.logd("onOffhook"); onOffhook(); break; case TelephonyManager.CALL_STATE_IDLE: //A.logd("onIdle"); onIdle(); break; } //A.logd("onCallStateChanged: state="+state+", lastCallState="+lastCallState); lastCallState = state; } //---- SensorEventListener implementation @Override public void onAccuracyChanged(Sensor sensor, int accuracy) { } @Override public void onSensorChanged(SensorEvent evt) { if(shutdown) return; final float val = evt.values[0]; final boolean far = proximReverse? val<proximFar : val>=proximFar; //A.logd("proximity sensor: value="+val+", far="+far+", lastFar="+lastFar); //if(far == lastFar) return; if(offhook && !headsetOn) { // auto switch handsfree if(autoSpeaker) { if(far) { if(speakerOn) { taskSpeakerOn.exec(TASK_SPEAKER, speakerDelay); speakerOn = --speakerOnCount != 0; } } else { if(speakerOff) { taskSpeakerOff.exec(TASK_SPEAKER, 0); speakerOff = --speakerOffCount != 0; } } } // auto switch devices if(far) { if(proximEnable ) taskDevsOn .exec(TASK_DEVS, enableDelay); } else { if(proximDisable) taskDevsOff.exec(TASK_DEVS, disableDelay); } // auto switch screen screenOff(!far); } // save last proximity state lastFar = far; } }