Voice mail
前幾天,在專案重要節點的時候,突然有個VVM(visual voice mail)的問題被列為重點物件; 由於之前一直疏於檢視voice mail相關的程式碼,所以有些手忙腳亂,雖然問題得到解決,但是對於這種比較少用的功能,還是做個記錄,以備不時之需。
這裡只是梳理了一個粗漏的程式碼流程,由於平時很少處理voice mail相關的問題,暫時不對voice mail做深入的學習,所以這裡的內容對於不熟悉這部分程式碼的人可能會有點幫助。如果想深入學習voice mail相關的知識,還是要結合相關協議,仔細研讀程式碼; 下面兩個連線的內容或許有些幫助。
https://www.gsma.com/newsroom/all-documents/omtp-visual-voice-mail-interface-specification-v-1-3/
https://shubs.io/breaking-international-voicemail-security-via-vvm-exploitation/
Android O將voicemail相關的實現從TeleService挪到了Dialer, 所以下面內容所涉及到的code主要在packages/apps/Dialer庫下,此外也涉及到了packages/service/Telephony庫。
主要內容:
1. Voice mail的儲存。
2. OMTP visual voice mail的啟動。
3. Visual voice mail的接收。
4. Visual voice mail的顯示。
5. Visual voice mail的播放。
1. Voice mail的儲存
Voice mail儲存在CallLog.db資料庫裡面,相關表是voicemail_status和calls。voicemail_status表用於儲存voice mail狀態相關的資訊,比如用於voice mail的apk,account,vvm的型別等資訊; calls表用於儲存具體voice mail的資訊, 比如日期,持續時間等。
CallLogProvider執行在程序android.process.acore內,開機後便會被建立,然後就是一系列的操作來建立CallLog.db; 這部分流程就不細說了,可參考TelephonyProvider的建立。相關table的建立可以檢視CallLogDatabaseHelper.java。
VoicemailContract
VoicemailContract.java作為voicemail provider和應用間的紐帶,內部定義了相關的URI和欄位。
由於有兩張表,所以欄位比較多, 就不貼code了,貼兩張截圖吧。
VoicemailContentProvider
VoicemailContentProvider.java用於voice mail相關的查詢,插入等資料庫相關的操作。
由於需要操作兩個表, 所以VoicemailContentProvider.onCreate方法建立了VoicemailContentTable.java和VoicemailStatusTable.java型別的兩個物件,分別用於操作表calls和voicemail_status。
2. OMTP visual voice mail的啟動:
在PhoneApp的AndroidMenifext.xml裡面定義了下面的receiver:
<receiver
android:name="com.android.phone.vvm.VvmSimStateTracker"
android:exported="false"
androidprv:systemUserOnly="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/>
<action android:name="android.telephony.action.CARRIER_CONFIG_CHANGED"/>
<action android:name="android.intent.action.SIM_STATE_CHANGED"/>
</intent-filter>
</receiver>
VvmSimStateTracker在系統裡註冊了三個廣播的監聽,ACTION_CARRIER_CONFIG_CHANGED廣播和啟動關係最大。單單用語言描述這個流程有些困難,畫了一個簡單的時序圖, 如下:
當收到ACTION_CARRIER_CONFIG_CHANGED後,VvmSimStateTracker.onCarrierConfigChanged方法被呼叫,而引數就是根據廣播資訊查詢到的PhoneAccountHandle物件。
private void onCarrierConfigChanged(Context context, PhoneAccountHandle phoneAccountHandle) {
if (!isBootCompleted()) {//判斷系統是否完成了啟動, 如果沒有完成,那麼儲存PhoneAccountHandle資訊後返回。
sPreBootHandles.add(phoneAccountHandle);
return;
}
/*如果完成了啟動,繼續執行下面的code*/
TelephonyManager telephonyManager = getTelephonyManager(context, phoneAccountHandle);
if(telephonyManager == null){
int subId = context.getSystemService(TelephonyManager.class).getSubIdForPhoneAccount(
context.getSystemService(TelecomManager.class)
.getPhoneAccount(phoneAccountHandle));
VvmLog.e(TAG, "Cannot create TelephonyManager from " + phoneAccountHandle + ", subId="
+ subId);
// TODO(b/33945549): investigate more why this is happening. The PhoneAccountHandle was
// just converted from a valid subId so createForPhoneAccountHandle shouldn't really
// return null.
return;
}
if (telephonyManager.getServiceState().getState()
== ServiceState.STATE_IN_SERVICE) {//手機已經註冊上了網路
sendConnected(context, phoneAccountHandle);
sListeners.put(phoneAccountHandle, null);
} else {
listenToAccount(context, phoneAccountHandle);
}
}
sendConnected方法比較簡單,只是呼叫了RemoteVvmTaskManager.startCellServiceConnected, 後者程式碼如下:
public static void startCellServiceConnected(Context context,
PhoneAccountHandle phoneAccountHandle) {
Intent intent = new Intent(ACTION_START_CELL_SERVICE_CONNECTED, null, context,
RemoteVvmTaskManager.class);
intent.putExtra(VisualVoicemailService.DATA_PHONE_ACCOUNT_HANDLE, phoneAccountHandle);
context.startService(intent);
}
RemoteVvmTaskManager繼承了Service類,startCellServiceConnected方法只是啟動了RemoteVvmTaskManager; 相應的onStartCommand方法被呼叫,該方法會呼叫RemoteVvmTaskManager.send方法,第二個引數為VisualVoicemailService.MSG_ON_CELL_SERVICE_CONNECTED(後續會用到)。
下面看看send方法的實現:
private void send(ComponentName remotePackage, int what, Bundle extras) {
Assert.isMainThread();
if (getBroadcastPackage(this) != null) {
/*
* Temporarily use a broadcast to notify dialer VVM events instead of using the
* VisualVoicemailService.
* b/35766990 The VisualVoicemailService is undergoing API changes. The dialer is in
* a different repository so it can not be updated in sync with android SDK. It is also
* hard to make a manifest service to work in the intermittent state.
*/
VvmLog.i(TAG, "sending broadcast " + what + " to " + remotePackage);
Intent intent = new Intent(ACTION_VISUAL_VOICEMAIL_SERVICE_EVENT);
intent.putExtras(extras);
intent.putExtra(EXTRA_WHAT, what);
intent.setComponent(remotePackage);
sendBroadcast(intent);
return;
}
Message message = Message.obtain();//構建Message物件
message.what = what;//將VisualVoicemailService.MSG_ON_CELL_SERVICE_CONNECTED放進Message物件。
message.setData(new Bundle(extras));
if (mConnection == null) {
mConnection = new RemoteServiceConnection();
}
mConnection.enqueue(message);//將Message物件放進佇列。
if (!mConnection.isConnected()) {//首次呼叫,connection還沒有連線,所以會去bind service。
Intent intent = newBindIntent(this);//構建一個action為"android.telephony.VisualVoicemailService"的 Intent物件。
intent.setComponent(remotePackage);
VvmLog.i(TAG, "Binding to " + intent.getComponent());
bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
}
bind 的service是OmtpService,繼承自VisualVoicemailService。VisualVoicemailService.onBind方法比較簡單,只是將成員變數mMessager的binder做為返回值return了。現在返回RemoteVvmTaskManager.RemoteServiceConnection看看service 連線之後做了哪些操作?
public void onServiceConnected(ComponentName className,
IBinder service) {
mRemoteMessenger = new Messenger(service);//這個service就是mMessager的binder物件
mConnected = true;
runQueue();//繼續處理佇列裡面的訊息,我們在前面放了VisualVoicemailService.MSG_ON_CELL_SERVICE_CONNECTED訊息。
}
...
private void runQueue() {
Assert.isMainThread();
Message message = mTaskQueue.poll();
while (message != null) {
message.replyTo = mMessenger;
message.arg1 = getTaskId();
try {
mRemoteMessenger.send(message);//此處send的訊息會在VisualVoicemailService.mMessenger內處理。
} catch (RemoteException e) {
VvmLog.e(TAG, "Error sending message to remote service", e);
}
message = mTaskQueue.poll();
}
}
VisualVoicemailService的mMessenger其實是匿名內部類的物件:
private final Messenger mMessenger = new Messenger(new Handler() {
@Override
public void handleMessage(final Message msg) {
final PhoneAccountHandle handle = msg.getData()
.getParcelable(DATA_PHONE_ACCOUNT_HANDLE);
VisualVoicemailTask task = new VisualVoicemailTask(msg.replyTo, msg.arg1);
switch (msg.what) {
case MSG_ON_CELL_SERVICE_CONNECTED://OmtpService重寫了onCellServiceConnected
onCellServiceConnected(task, handle);
break;
case MSG_ON_SMS_RECEIVED:
VisualVoicemailSms sms = msg.getData().getParcelable(DATA_SMS);
onSmsReceived(task, sms);
break;
case MSG_ON_SIM_REMOVED:
onSimRemoved(task, handle);
break;
case MSG_TASK_STOPPED:
onStopped(task);
break;
default:
super.handleMessage(msg);
break;
}
}
});
總結:bind 完service後,這條邏輯線就走通了。RemoteVvmTaskManager負責傳送任務(SMS reveived, SIM removed),而OmtpService負責處理任務。
OmtpService.onCellServiceConnected方法內會用到OmtpVvmCarrierConfigHelper以及VVM相關的配置資訊,具體資訊看code吧。
3. Visual voice mail的接收
對於VVM的接收,以VisualVoicemailSmsFilter.filer為起點畫了一個時序圖,涵蓋了主要節點。
VisualVoicemailSmsFilter.filer會對VVM按照協議做解析; OmtpMessageReceiver.OnReceive會對收到的mail,按照不同的協議做不同的處理, 主要是更新DB以及和IMAP server通訊。
4. Visual voice mail的顯示
現在針對voice mail已經有很多第三方應用,實現的方式也不盡相同。有些應用可以讓使用者設定顯示的方式(calllog或者應用內部),有些應用直接將顯示放在了第三方應用裡。這裡說下call log部分對於voice mail的顯示。DialtactsActivity啟動(上次關閉時沒有儲存狀態)的時候會建立ListsFragment,ListsFragment.onResume會呼叫CallLogQueryHandler.fetchVoicemailStatus查詢voice mail 的狀態。
public void fetchVoicemailStatus() {
StringBuilder where = new StringBuilder();
List<String> selectionArgs = new ArrayList<>();
VoicemailComponent.get(mContext)
.getVoicemailClient()
.appendOmtpVoicemailStatusSelectionClause(mContext, where, selectionArgs);
if (TelecomUtil.hasReadWriteVoicemailPermissions(mContext)) {
startQuery(
QUERY_VOICEMAIL_STATUS_TOKEN,
null,
Status.CONTENT_URI, //“content://com.android.voicemail/status",VoicemailContentProvider.query方法根據這個URI,會找到voicemail_status表。
VoicemailStatusQuery.getProjection(),
where.toString(),
selectionArgs.toArray(new String[selectionArgs.size()]),
null);
}
}
當獲取查詢結果後, ListsFragment.onVoicemailStatusFetched方法會被呼叫, 下面摘錄了這個方法裡最重要的一句。
public void onVoicemailStatusFetched(Cursor statusCursor) {
....
/*Update hasActiveVoicemailProvider, which controls the number of tabs displayed.*/
boolean hasActiveVoicemailProvider =
mVoicemailStatusHelper.getNumberActivityVoicemailSources(statusCursor) > 0;
...
}
下面看VoicemailStatusHelper的getNumberActivityVoicemailSources方法,這個方法的註釋寫的很清楚:
返回值是已經安裝的有效voicemail sources的數量,而這個數量是通過查詢voicemail_status表獲取的。
/**
* Returns the number of active voicemail sources installed.
*
* <p>The number of sources is counted by querying the voicemail status table.
*
* @param cursor The caller is responsible for the life cycle of the cursor and resetting the
* position
*/
public int getNumberActivityVoicemailSources(Cursor cursor) {
int count = 0;
if (!cursor.moveToFirst()) {
return 0;
}
do {
if (isVoicemailSourceActive(cursor)) {
++count;
}
} while (cursor.moveToNext());
return count;
}
有效的Voicemail sources要滿足下面的條件voicemail_status表裡獲取的package 名字存在,並且configuration state 不是NOT_CONFIGURED。所以如果第三方應用在voicemail_status表裡儲存了這些資訊,那麼call log裡會顯示voice mail相關的UI。
/**
* Returns whether the source status in the cursor corresponds to an active source. A source is
* active if its' configuration state is not NOT_CONFIGURED. For most voicemail sources, only OK
* and NOT_CONFIGURED are used. The OMTP visual voicemail client has the same behavior pre-NMR1.
* NMR1 visual voicemail will only set it to NOT_CONFIGURED when it is deactivated. As soon as
* activation is attempted, it will transition into CONFIGURING then into OK or other error state,
* NOT_CONFIGURED is never set through an error.
*/
private boolean isVoicemailSourceActive(Cursor cursor) {
return cursor.getString(VoicemailStatusQuery.SOURCE_PACKAGE_INDEX) != null
&& cursor.getInt(VoicemailStatusQuery.CONFIGURATION_STATE_INDEX)
!= Status.CONFIGURATION_STATE_NOT_CONFIGURED;
}
5. Visual voice mail的播放
ListsFragment.onCreateView方法會建立DialtactsPagerAdapter,當我們選擇voice mail的tab(TAB_INDEX_VOICEMAIL)的時候,DialtactsPagerAdapter.getItem會返回VisualVoicemailCallLogFragment物件,如果需要,會建立新物件。VisualVoicemailCallLogFragment繼承自CallLogFragment,所以也繼承了很多邏輯實現,只有一部分方法做了重寫。VVM的播放,是從UI操作開始的,對於UI 佈局就不詳細寫了, 寫太多容易精神崩潰,直接從VoicemailPlaybackPresenter.requestContent開始,簡單畫了一個時序圖,可以讓這個流程更清晰些。
VoicemailPlaybackPresenter.requestContent方法裡面有個非同步任務,這個任務在執行的時候會發action為ACTION_FETCH_VOICEMAIL的廣播。
protected boolean requestContent(int code) {
"...省略..."
mAsyncTaskExecutor.submit(
Tasks.SEND_FETCH_REQUEST,
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... voids) {
"...省略..."
// Send voicemail fetch request.
Intent intent = new Intent(VoicemailContract.ACTION_FETCH_VOICEMAIL, mVoicemailUri);
intent.setPackage(sourcePackage);
LogUtil.i(
"VoicemailPlaybackPresenter.requestContent",
"Sending ACTION_FETCH_VOICEMAIL to " + sourcePackage);
mContext.sendBroadcast(intent);
}
return null;
}
});
return true;
}
FetchVoicemailReceiver.java會接收並處理上面的廣播,
@Override
public void onReceive(final Context context, Intent intent) {
if (!VoicemailComponent.get(context).getVoicemailClient().isVoicemailModuleEnabled()) {
return;
}
if (VoicemailContract.ACTION_FETCH_VOICEMAIL.equals(intent.getAction())) {//處理ACTION_FETCH_VOICEMAIL廣播
VvmLog.i(TAG, "ACTION_FETCH_VOICEMAIL received");
mContext = context;
mContentResolver = context.getContentResolver();
mUri = intent.getData();
if (mUri == null) {
VvmLog.w(TAG, VoicemailContract.ACTION_FETCH_VOICEMAIL + " intent sent with no data");
return;
}
if (!context
.getPackageName()
.equals(mUri.getQueryParameter(VoicemailContract.PARAM_KEY_SOURCE_PACKAGE))) {
// Ignore if the fetch request is for a voicemail not from this package.
VvmLog.e(TAG, "ACTION_FETCH_VOICEMAIL from foreign pacakge " + context.getPackageName());
return;
}
/*根據uri,從資料庫獲取對應的phone account資訊*/
Cursor cursor = mContentResolver.query(mUri, PROJECTION, null, null, null);
if (cursor == null) {
VvmLog.i(TAG, "ACTION_FETCH_VOICEMAIL query returned null");
return;
}
try {
if (cursor.moveToFirst()) {
mUid = cursor.getString(SOURCE_DATA);
String accountId = cursor.getString(PHONE_ACCOUNT_ID);
if (TextUtils.isEmpty(accountId)) {
TelephonyManager telephonyManager =
(TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
accountId = telephonyManager.getSimSerialNumber();
if (TextUtils.isEmpty(accountId)) {
VvmLog.e(TAG, "Account null and no default sim found.");
return;
}
}
mPhoneAccount =
new PhoneAccountHandle(
ComponentName.unflattenFromString(cursor.getString(PHONE_ACCOUNT_COMPONENT_NAME)),
cursor.getString(PHONE_ACCOUNT_ID));//構造PhoneAccountHandle物件
TelephonyManager telephonyManager =
context
.getSystemService(TelephonyManager.class)
.createForPhoneAccountHandle(mPhoneAccount);
if (telephonyManager == null) {
// can happen when trying to fetch voicemails from a SIM that is no longer on the
// device
VvmLog.e(TAG, "account no longer valid, cannot retrieve message");
return;
}
if (!VvmAccountManager.isAccountActivated(context, mPhoneAccount)) {
mPhoneAccount = getAccountFromMarshmallowAccount(context, mPhoneAccount);
if (mPhoneAccount == null) {
VvmLog.w(TAG, "Account not registered - cannot retrieve message.");
return;
}
VvmLog.i(TAG, "Fetching voicemail with Marshmallow PhoneAccountHandle");
}
VvmLog.i(TAG, "Requesting network to fetch voicemail");
mNetworkCallback = new fetchVoicemailNetworkRequestCallback(context, mPhoneAccount);
mNetworkCallback.requestNetwork();//請求網路連線
}
} finally {
cursor.close();
}
}
}
fetchVoicemailNetworkRequestCallback繼承自VvmNetworkRequestCallback,後者在構造方法裡便建立了NetworkRequest物件:
/**
* @return NetworkRequest for a proper transport type. Use only cellular network if the carrier
* requires it. Otherwise use whatever available.
*/
private NetworkRequest createNetworkRequest() {
NetworkRequest.Builder builder =
new NetworkRequest.Builder().addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
TelephonyManager telephonyManager =
mContext
.getSystemService(TelephonyManager.class)
.createForPhoneAccountHandle(mPhoneAccount);
// At this point mPhoneAccount should always be valid and telephonyManager will never be null
Assert.isNotNull(telephonyManager);
if (mCarrierConfigHelper.isCellularDataRequired()) {//如果carrier config裡面配置了使用cellular data的要求,那麼就要使用NetworkCapabilities.TRANSPORT_CELLULAR。
VvmLog.d(TAG, "Transport type: CELLULAR");
builder
.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
.setNetworkSpecifier(telephonyManager.getNetworkSpecifier());
} else {
VvmLog.d(TAG, "Transport type: ANY");
}
return builder.build();
}
當網路可用之後fetchVoicemailNetworkRequestCallback.onAvailable方法會被呼叫,該方法會呼叫fetchVoicemailNetworkRequestCallback.fetchVoicemail。
private void fetchVoicemail(final Network network, final VoicemailStatus.Editor status) {
Executor executor = Executors.newCachedThreadPool();
executor.execute(
new Runnable() {
@Override
public void run() {
try {
while (mRetryCount > 0) {//嘗試次數,FetchVoicemailReceiver定義了一個常量NETWORK_RETRY_COUNT,值為3
VvmLog.i(TAG, "fetching voicemail, retry count=" + mRetryCount);
try (ImapHelper imapHelper =
new ImapHelper(mContext, mPhoneAccount, network, status)) {
boolean success =
imapHelper.fetchVoicemailPayload(
new VoicemailFetchedCallback(mContext, mUri, mPhoneAccount), mUid);//這裡就是用來下載的。
if (!success && mRetryCount > 0) {
VvmLog.i(TAG, "fetch voicemail failed, retrying");
mRetryCount--;
} else {
return;
}
} catch (InitializingException e) {
VvmLog.w(TAG, "Can't retrieve Imap credentials ", e);
return;
}
}
} finally {
if (mNetworkCallback != null) {
mNetworkCallback.releaseNetwork();
}
}
}
});
}
fetchVoicemailNetworkRequestCallback.fetchVoicemail方法構造了ImapHelper物件,並呼叫了fetchVoicemailPayload方法,這個方法完成了下載。看似很簡單,但是ImapHelper物件的構造和fetchVoicemailPayload方法的呼叫完成了很多工作。