Android 6.0 之后開始支持修改默認電話應用完沪,剛好最近有個相關的需求,于是記錄下自己探索之旅藕届。
00 Android Telecom framework
從 API 21 開始,谷歌添加了 TelecomManager
用于提供對電話通訊狀態(tài)的監(jiān)聽,而 API 23 之后讽坏,又開放了 Telecom framework,這個框架允許第三方應用開發(fā)者編寫應用來替換系統(tǒng)默認電話應用例证,而其中大部分接口就添加在 android.telecom
包下路呜。
Telecom framework 其實提供了兩個方面的 API,一個是 ConnectionService 用于實現(xiàn)通訊(比如通過電信服務商提供的電話連接服務)织咧,另一個就是我們這次的需要用到的 InCallService胀葱,它主要負責提供 UI 來管理電話。一般系統(tǒng)自帶的電話應用同樣也是實現(xiàn)這套 API 來提供電話通信的交互界面的笙蒙。
01 替代系統(tǒng)默認電話應用
整個實現(xiàn)過程其實分為兩步抵屿,首先是新增一個 Service 繼承 InCallService
并實現(xiàn)其中你感興趣的方法,然后再添加一個 Activity 用于提供用戶界面捅位。
首先我們來看下 Service 部分轧葛。
實現(xiàn) InCallService
繼承 InCallService
后我們需要實現(xiàn)兩個方法 onCallAdded
和 onCallRemoved
,分別代表電話進來與斷開時會被調用艇搀,一般我們會在 onCallAdded
中注冊電話狀態(tài)監(jiān)聽尿扯,并在 onCallRemoved
中解除監(jiān)聽。
public class PhoneCallService extends InCallService {
private Call.Callback callback = new Call.Callback() {
@Override
public void onStateChanged(Call call, int state) {
super.onStateChanged(call, state);
switch (state) {
case Call.STATE_ACTIVE: {
break; // 通話中
}
case Call.STATE_DISCONNECTED: {
break; // 通話結束
}
}
}
};
@Override
public void onCallAdded(Call call) {
super.onCallAdded(call);
call.registerCallback(callback);
}
@Override
public void onCallRemoved(Call call) {
super.onCallRemoved(call);
call.unregisterCallback(callback);
}
}
當然焰雕,對于通話 Service 在 menifest 中注冊的方法肯定也和普通的 service 有點區(qū)別:
<service
android:name=".PhoneCallService"
android:permission="android.permission.BIND_INCALL_SERVICE">
<meta-data
android:name="android.telecom.IN_CALL_SERVICE_UI"
android:value="true" />
<intent-filter>
<action android:name="android.telecom.InCallService" />
</intent-filter>
</service>
代替系統(tǒng)默認通話應用并不需要添加特殊的權限衷笋,但是要在你實現(xiàn)的 InCallService
上聲明 android.permission.BIND_INCALL_SERVICE
權限,另外還要加上 <meta-data> 用于表明我們的應用提供了接聽電話的 UI淀散,android.telecom.InCallService
的 <intent-filter> 當然就是用于在電話撥出或打進的時候右莱,讓系統(tǒng)發(fā)送的廣播能夠直接啟動我們的電話服務蚜锨。
實現(xiàn)電話接聽的 UI
這部分比較簡單,只要創(chuàng)建一個 Activity 然后在 menifest 中注冊就可以了慢蜓,不過注冊時至少需要添加這兩個 <intent-filter>:
<!-- provides ongoing call UI -->
<intent-filter>
<action android:name="android.intent.action.DIAL" />
<data android:scheme="tel" />
</intent-filter>
<!-- provides dial UI -->
<intent-filter>
<action android:name="android.intent.action.DIAL" />
</intent-filter>
看起來好像有點奇怪亚再,因為好像這兩個 <intent-filter> 幾乎一樣,但其實第一個 <intent-filter> 是用來提供打電話UI的 晨抡,而第二個用于提供撥號功能氛悬,至于為什么要分開呢?可以參考 DefaultDialerManager耘柱,這是安卓源碼里的一個隱藏的類如捅。方法 getInstalledDialerApplications()
上的注釋寫的很清楚,想要讓系統(tǒng)檢測到可供撥號的應用就必須至少要添加這兩個 <intent-filter>调煎。
管理電話的接打
接下來我們需要定義一個接打電話的管理類 PhoneCallManager
镜遣,用于給 activity 提供一些電話相關的操作:
public class PhoneCallManager {
public static Call call;
/**
* 接聽電話
*/
public void answer() {
if (call != null) {
call.answer(VideoProfile.STATE_AUDIO_ONLY);
}
}
/**
* 斷開電話,包括來電時的拒接以及接聽后的掛斷
*/
public void disconnect() {
if (call != null) {
call.disconnect();
}
}
}
可以看到士袄,PhoneCallManger
包含接聽和斷開電話的方法調用悲关,我們要做的只是在合適的地方將 call 對象傳進去,最合適的地方當然是在 InCallService
里添加與移除 Call
的時候了娄柳。
另外寓辱,在添加電話的時候,我們要記得啟動我們作為電話界面的 activity 去提供接打的操作赤拒,這里我們可以根據(jù) call 的 state 來判斷電話是打進的還是撥出的秫筏。
@Override
public void onCallAdded(Call call) {
super.onCallAdded(call);
call.registerCallback(callback);
PhoneCallManager.call = call; // 傳入call
CallType callType = null;
if (call.getState() == Call.STATE_RINGING) {
callType = CallType.CALL_IN;
} else if (call.getState() == Call.STATE_CONNECTING) {
callType = CallType.CALL_OUT;
}
if (callType != null) {
Call.Details details = call.getDetails();
String phoneNumber = details.getHandle().toString().substring(4)
.replaceAll("%20", ""); // 去除撥出電話中的空格
PhoneCallActivity.actionStart(this, phoneNumber, callType);
}
}
@Override
public void onCallRemoved(Call call) {
super.onCallRemoved(call);
call.unregisterCallback(callback);
PhoneCallManager.call = null;
}
還記得之前的我們創(chuàng)建的 call 的回調嗎,我們監(jiān)聽了電話狀態(tài)的改變挎挖,所以可以在電話斷開的時候退出電話界面:
case Call.STATE_DISCONNECTED: {
// 通話結束
ActivityStack.getInstance().finishActivity(PhoneCallActivity.class);
break;
}
至于 UI 界面如何實現(xiàn)这敬,取決于你的需求了,這里我只是提供了接聽和掛斷的按鈕用于演示肋乍,來看下效果圖:
最后鹅颊,既然要替換系統(tǒng)默認電話應用,當然要提醒用戶將我們的應用設置為默認應用了:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// Android M 以上的系統(tǒng)則發(fā)起將本應用設為默認電話應用的請求
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
Intent intent = new Intent(TelecomManager.ACTION_CHANGE_DEFAULT_DIALER);
intent.putExtra(TelecomManager.EXTRA_CHANGE_DEFAULT_DIALER_PACKAGE_NAME,
getPackageName());
startActivity(intent);
}
}
}
這段代碼會在我們的 Actiivty 中啟動這樣一個 Dialog墓造,如果用戶點擊確定后我們的應用就變成了默認電話應用啦╭(??????)??
02 電話狀態(tài)監(jiān)聽
以上實現(xiàn)的功能只能在 Android M 以上的系統(tǒng)才有效堪伍,那么 Android M 以下怎么辦呢?其實我們可以通過監(jiān)聽電話狀態(tài)然后做一些操作觅闽。
之前說過從 API 21 開始 Google 添加了 TelecomManager
類帝雇,所以我們可以借助這個類來監(jiān)聽電話狀態(tài)。
private void initPhoneStateListener() {
phoneStateListener = new PhoneStateListener() {
@Override
public void onCallStateChanged(int state, String incomingNumber) {
super.onCallStateChanged(state, incomingNumber);
switch (state) {
case TelephonyManager.CALL_STATE_IDLE: // 待機蛉拙,即無電話時尸闸,掛斷時觸發(fā)
break;
case TelephonyManager.CALL_STATE_RINGING: // 響鈴,來電時觸發(fā)
break;
case TelephonyManager.CALL_STATE_OFFHOOK: // 摘機,接聽或打電話時觸發(fā)
break;
default:
break;
}
}
};
telephonyManager = (TelephonyManager) getSystemService(TELEPHONY_SERVICE);
if (telephonyManager != null) {
// 設置來電監(jiān)聽
telephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_CALL_STATE);
}
}
我們可以把監(jiān)聽放到 Service 中吮廉,然后再注冊一個電話變化的廣播接收器來啟動這個 Service苞尝,這樣無論何時電話狀態(tài)發(fā)生變化我們都可以接收到并做出一些操作。比如這里我監(jiān)聽了來電并彈出一個頂級彈框覆蓋默認的電話應用宦芦,然后提供一個按鈕用于打開App宙址。
具體代碼請看我寫的 Demo,Github 地址:aJIEw/PhoneCallApp
Demo 包括兩部分调卑,代替默認電話應用以及電話監(jiān)聽抡砂。
這里記錄一個小坑,一般情況下恬涧,Android M 以下只要在 manifest
中聲明權限就可以顯示頂級彈框了注益,但是在某些國產(chǎn) Rom 上頂級彈框默認是禁止的,需要在懸浮窗管理中允許后才可以溯捆。所以這里有個三方庫就非常值得推薦了:czy1121/SettingsCompact
用法也相當簡單:
// 檢查是否開啟了權限
if (!SettingsCompat.canDrawOverlays(this)) {
if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// Android M 以上引導用戶去系統(tǒng)設置中打開允許懸浮窗
// 使用反射是為了用盡可能少的代碼保證在大部分機型上都可用
try {
Context context = this;
Class clazz = Settings.class;
Field field = clazz.getDeclaredField("ACTION_MANAGE_OVERLAY_PERMISSION");
Intent intent = new Intent(field.get(null).toString());
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setData(Uri.parse("package:" + context.getPackageName()));
context.startActivity(intent);
} catch (Exception e) {
Toast.makeText(this, "請在懸浮窗管理中打開權限", Toast.LENGTH_LONG).show();
}
} else {
// 6.0 以下則直接使用 SettingsCompat 中提供的接口
SettingsCompat.manageDrawOverlays(act);
}
}
03 后記
這篇文章拖了一個禮拜才寫完丑搔,論拖延的功底也是沒誰了。其實本來應該上周日發(fā)的现使,但是后來把時間花在了研究各種錄屏軟件的使用低匙,然后為了一個視頻截取的功能又開始研究各種視頻軟件。推薦一個TED視頻:拖延癥人群的內(nèi)心世界碳锈,可以說把拖延癥人群的心理描述得相當?shù)轿涣恕M涎硬豢膳缕劭梗膳碌氖峭铣隽晳T了售碳,完全不拖延的人不存在,所以我們每個人都應該問問自己:我有沒有在那些會影響到自己未來的事上拖延绞呈?