1. 程式人生 > >Voice mail

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_statuscalls。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了,貼兩張截圖吧。
calls表voicemail_status表

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方法的呼叫完成了很多工作。

結束!