Voice mail

本文解析了Android系统中视觉语音邮箱(VVM)的实现细节,包括Voicemail存储方式、OMTP视觉语音邮箱启动过程、视觉语音邮箱接收及显示机制、以及语音播放流程。通过对核心代码的梳理,为开发者提供了一条清晰的学习路径。

前几天,在项目重要节点的时候,突然有个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方法的调用完成了很多工作。

结束!

以下代码可以实现返回吗package com.android.phone.settings; import android.app.Activity; import android.app.ComponentCaller; import android.app.Dialog; import android.content.DialogInterface; import android.content.Intent; import android.content.pm.PackageManager; import android.database.Cursor; import android.net.Uri; import android.os.AsyncResult; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.os.PersistableBundle; import android.os.UserHandle; import android.os.UserManager; import androidx.preference.Preference; import androidx.preference.PreferenceScreen; import android.provider.ContactsContract.CommonDataKinds; import android.provider.Settings; import android.telecom.PhoneAccountHandle; import android.telephony.CarrierConfigManager; import android.telephony.TelephonyManager; import android.text.BidiFormatter; import android.text.TextDirectionHeuristics; import android.text.TextUtils; import android.util.EventLog; import android.util.Log; import android.view.MenuItem; import android.widget.ListAdapter; import android.widget.Toast; import androidx.activity.OnBackPressedCallback; import com.android.internal.telephony.CallForwardInfo; import com.android.internal.telephony.CommandsInterface; import com.android.internal.telephony.Phone; import com.android.internal.telephony.PhoneConstants; import com.android.internal.telephony.flags.Flags; import com.android.internal.telephony.util.NotificationChannelController; import com.android.phone.EditPhoneNumberPreference; import com.android.phone.PhoneGlobals; import com.android.phone.PhoneUtils; import com.android.phone.R; import com.android.phone.SubscriptionInfoHelper; import com.mediatek.settings.CallSettingUtils; import com.android.settingslib.core.lifecycle.ObservablePreferenceFragment; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.Map; public class VoicemailSettingsFragment extends ObservablePreferenceFragment implements DialogInterface.OnClickListener, Preference.OnPreferenceChangeListener, EditPhoneNumberPreference.OnDialogClosedListener, EditPhoneNumberPreference.GetDefaultNumberListener, PhoneGlobals.SubInfoUpdateListener { private static final String LOG_TAG = VoicemailSettingsActivity.class.getSimpleName(); private static final boolean DBG = true; //(PhoneGlobals.DBG_LEVEL >= 2); /** * Intent action to bring up Voicemail Provider settings * DO NOT RENAME. There are existing apps which use this intent value. */ public static final String ACTION_ADD_VOICEMAIL = "com.android.phone.CallFeaturesSetting.ADD_VOICEMAIL"; /** * Intent action to bring up the {@code VoicemailSettingsActivity}. * DO NOT RENAME. There are existing apps which use this intent value. */ public static final String ACTION_CONFIGURE_VOICEMAIL = "com.android.phone.CallFeaturesSetting.CONFIGURE_VOICEMAIL"; // Extra put in the return from VM provider config containing voicemail number to set public static final String VM_NUMBER_EXTRA = "com.android.phone.VoicemailNumber"; // Extra put in the return from VM provider config containing call forwarding number to set public static final String FWD_NUMBER_EXTRA = "com.android.phone.ForwardingNumber"; // Extra put in the return from VM provider config containing call forwarding number to set public static final String FWD_NUMBER_TIME_EXTRA = "com.android.phone.ForwardingNumberTime"; // If the VM provider returns non null value in this extra we will force the user to // choose another VM provider public static final String SIGNOUT_EXTRA = "com.android.phone.Signout"; /** * String Extra put into ACTION_ADD_VOICEMAIL call to indicate which provider should be hidden * in the list of providers presented to the user. This allows a provider which is being * disabled (e.g. GV user logging out) to force the user to pick some other provider. */ public static final String IGNORE_PROVIDER_EXTRA = "com.android.phone.ProviderToIgnore"; /** * String Extra put into ACTION_ADD_VOICEMAIL to indicate that the voicemail setup screen should * be opened. */ public static final String SETUP_VOICEMAIL_EXTRA = "com.android.phone.SetupVoicemail"; // TODO: Define these preference keys in XML. private static final String BUTTON_VOICEMAIL_KEY = "button_voicemail_key"; private static final String BUTTON_VOICEMAIL_PROVIDER_KEY = "button_voicemail_provider_key"; private static final String BUTTON_VOICEMAIL_SETTING_KEY = "button_voicemail_setting_key"; /** Event for Async voicemail change call */ private static final int EVENT_VOICEMAIL_CHANGED = 500; private static final int EVENT_FORWARDING_CHANGED = 501; private static final int EVENT_FORWARDING_GET_COMPLETED = 502; /** Handle to voicemail pref */ private static final int VOICEMAIL_PREF_ID = 1; private static final int VOICEMAIL_PROVIDER_CFG_ID = 2; /** * Results of reading forwarding settings */ private CallForwardInfo[] mForwardingReadResults = null; /** * Result of forwarding number change. * Keys are reasons (eg. unconditional forwarding). */ private Map<Integer, AsyncResult> mForwardingChangeResults = null; /** * Expected CF read result types. * This set keeps track of the CF types for which we've issued change * commands so we can tell when we've received all of the responses. */ private Collection<Integer> mExpectedChangeResultReasons = null; /** * Result of vm number change */ private AsyncResult mVoicemailChangeResult = null; /** * Previous VM provider setting so we can return to it in case of failure. */ private String mPreviousVMProviderKey = null; /** * Id of the dialog being currently shown. */ private int mCurrentDialogId = 0; /** * Flag indicating that we are invoking settings for the voicemail provider programmatically * due to vm provider change. */ private boolean mVMProviderSettingsForced = false; /** * Flag indicating that we are making changes to vm or fwd numbers * due to vm provider change. */ private boolean mChangingVMorFwdDueToProviderChange = false; /** * True if we are in the process of vm & fwd number change and vm has already been changed. * This is used to decide what to do in case of rollback. */ private boolean mVMChangeCompletedSuccessfully = false; /** * True if we had full or partial failure setting forwarding numbers and so need to roll them * back. */ private boolean mFwdChangesRequireRollback = false; /** * Id of error msg to display to user once we are done reverting the VM provider to the previous * one. */ private int mVMOrFwdSetError = 0; /** string to hold old voicemail number as it is being updated. */ private String mOldVmNumber; // New call forwarding settings and vm number we will be setting // Need to save these since before we get to saving we need to asynchronously // query the existing forwarding settings. private CallForwardInfo[] mNewFwdSettings; private String mNewVMNumber; /** * Used to indicate that the voicemail preference should be shown. */ private boolean mShowVoicemailPreference = false; private boolean mForeground; private boolean mDisallowedConfig = false; private Phone mPhone; private SubscriptionInfoHelper mSubscriptionInfoHelper; private EditPhoneNumberPreference mSubMenuVoicemailSettings = null; private VoicemailProviderListPreference mVoicemailProviders; private PreferenceScreen mVoicemailSettings; private Preference mVoicemailNotificationPreference; private PreferenceScreen mMainPreferenceScreen; //********************************************************************************************* // Preference Activity Methods //********************************************************************************************* @Override public void onCreate(Bundle icicle) { super.onCreate(icicle); getActivity().getWindow().addSystemFlags( android.view.WindowManager.LayoutParams .SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS); // Make sure we are running as the primary user only UserManager userManager = getContext().getApplicationContext().getSystemService(UserManager.class); if (!userManager.isPrimaryUser()) { Toast.makeText(getContext(), R.string.voice_number_setting_primary_user_only, Toast.LENGTH_SHORT).show(); getActivity().finish(); return; } // Handle system back button OnBackPressedCallback callback = new OnBackPressedCallback(true) { @Override public void handleOnBackPressed() { // Check if we're on a sub preference screen if (getPreferenceScreen() != mMainPreferenceScreen && mMainPreferenceScreen != null) { // Return to main preference screen setPreferenceScreen(mMainPreferenceScreen); getActivity().getActionBar().setDisplayHomeAsUpEnabled(false); } else { // Let the activity handle back press getActivity().onBackPressed(); } } }; requireActivity().getOnBackPressedDispatcher().addCallback(this, callback); // Check if mobile network configs are restricted. if (Flags.ensureAccessToCallSettingsIsRestricted() && userManager.hasUserRestriction(UserManager.DISALLOW_CONFIG_MOBILE_NETWORKS)) { mDisallowedConfig = true; Log.i(LOG_TAG, "Mobile network configs are restricted, disabling voicemail " + "settings"); } // Show the voicemail preference in onResume if the calling intent specifies the // ACTION_ADD_VOICEMAIL action. mShowVoicemailPreference = (icicle == null) && TextUtils.equals(getActivity().getIntent().getAction(), ACTION_ADD_VOICEMAIL); PhoneAccountHandle phoneAccountHandle = (PhoneAccountHandle) getActivity().getIntent().getParcelableExtra(TelephonyManager.EXTRA_PHONE_ACCOUNT_HANDLE); if (phoneAccountHandle != null) { getActivity().getIntent().putExtra(SubscriptionInfoHelper.SUB_ID_EXTRA, PhoneUtils.getSubIdForPhoneAccountHandle(phoneAccountHandle)); } mSubscriptionInfoHelper = new SubscriptionInfoHelper(getActivity(), getActivity().getIntent()); /// M: Get the subscription ID. @{ log("onCreate subId: " + mSubscriptionInfoHelper.getSubId()); /// @} mSubscriptionInfoHelper.setActionBarTitle( getActivity().getActionBar(), getResources(), R.string.voicemail_settings_with_label); mPhone = mSubscriptionInfoHelper.getPhone(); mVoicemailNotificationPreference = findPreference(getString(R.string.voicemail_notifications_key)); if (mSubMenuVoicemailSettings == null) { mSubMenuVoicemailSettings = (EditPhoneNumberPreference) findPreference(BUTTON_VOICEMAIL_KEY); } final Intent intent = new Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS); intent.putExtra(Settings.EXTRA_CHANNEL_ID, NotificationChannelController.CHANNEL_ID_VOICE_MAIL); intent.putExtra(Settings.EXTRA_APP_PACKAGE, mPhone.getContext().getPackageName()); mVoicemailNotificationPreference.setIntent(intent); SettingsConstants.setupEdgeToEdge(getActivity()); ///M: For hot swap PhoneGlobals.getInstance().addSubInfoUpdateListener(this); /// M: Get list entry value before dialog onPrepareDialogBuilder called. @{ mVoicemailProviders = (VoicemailProviderListPreference) findPreference( BUTTON_VOICEMAIL_PROVIDER_KEY); mVoicemailProviders.init(mPhone, getActivity().getIntent()); /// @} } //AGUI [yaozhiqing] @Override public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { setPreferencesFromResource(R.xml.voicemail_settings, rootKey); } @Override public void onResume() { super.onResume(); mForeground = true; if (mSubMenuVoicemailSettings != null) { mSubMenuVoicemailSettings.setParentActivity(getActivity(), VOICEMAIL_PREF_ID, this); mSubMenuVoicemailSettings.setDialogOnClosedListener(this); mSubMenuVoicemailSettings.setDialogTitle(R.string.voicemail_settings_number_label); if (!getBooleanCarrierConfig( CarrierConfigManager.KEY_EDITABLE_VOICEMAIL_NUMBER_SETTING_BOOL) || mDisallowedConfig) { mSubMenuVoicemailSettings.setEnabled(false); } } /// M: Get list entry value before dialog onPrepareDialogBuilder called. @{ //mVoicemailProviders = (VoicemailProviderListPreference) findPreference( // BUTTON_VOICEMAIL_PROVIDER_KEY); //mVoicemailProviders.init(mPhone, getIntent()); /// @} mVoicemailProviders.setOnPreferenceChangeListener(this); mPreviousVMProviderKey = mVoicemailProviders.getValue(); // Save main preference screen reference mMainPreferenceScreen = getPreferenceScreen(); mVoicemailSettings = (PreferenceScreen) findPreference(BUTTON_VOICEMAIL_SETTING_KEY); getPreferenceManager().setOnNavigateToScreenListener(screen -> { if (screen == mVoicemailSettings) { onPreferenceTreeClick(mSubMenuVoicemailSettings); setPreferenceScreen(screen); // Enable back button when entering sub-screen getActivity().getActionBar().setDisplayHomeAsUpEnabled(true); if (getListView() != null) { getListView().refreshDrawableState(); } } }); // 😮‍💨 the legacy PreferenceScreen displays a dialog in its onClick. Set a property on the // PreferenceScreen to ensure that it will fit system windows to accommodate for edge to // edge. /*mVoicemailSettings.setDialogFitsSystemWindows(true);*/ maybeHidePublicSettings(); updateVMPreferenceWidgets(mVoicemailProviders.getValue()); // check the intent that started this activity and pop up the voicemail // dialog if we've been asked to. // If we have at least one non default VM provider registered then bring up // the selection for the VM provider, otherwise bring up a VM number dialog. // We only bring up the dialog the first time we are called (not after orientation change) if (mShowVoicemailPreference) { if (DBG) log("ACTION_ADD_VOICEMAIL Intent is thrown"); if (mVoicemailProviders.hasMoreThanOneVoicemailProvider()) { if (DBG) log("Voicemail data has more than one provider."); simulatePreferenceClick(mVoicemailProviders); } else { onPreferenceChange(mVoicemailProviders, VoicemailProviderListPreference.DEFAULT_KEY); mVoicemailProviders.setValue(VoicemailProviderListPreference.DEFAULT_KEY); } mShowVoicemailPreference = false; } updateVoiceNumberField(); mVMProviderSettingsForced = false; /// M: make sure the action bar is disabled @{ final Dialog dialog = null; if (dialog != null) { dialog.getActionBar().setDisplayHomeAsUpEnabled(false); } /// @} } /** * Hides a subset of voicemail settings if required by the intent extra. This is used by the * default dialer to show "advanced" voicemail settings from its own custom voicemail settings * UI. */ private void maybeHidePublicSettings() { if (!getActivity().getIntent().getBooleanExtra(TelephonyManager.EXTRA_HIDE_PUBLIC_SETTINGS, false)) { return; } if (DBG) { log("maybeHidePublicSettings: settings hidden by EXTRA_HIDE_PUBLIC_SETTINGS"); } PreferenceScreen preferenceScreen = getPreferenceScreen(); preferenceScreen.removePreference(mVoicemailNotificationPreference); } @Override public void onPause() { super.onPause(); mForeground = false; } @Override public boolean onOptionsItemSelected(MenuItem item) { if (item.getItemId() == android.R.id.home) { // Check if we're on a sub preference screen, if so return to main if (getPreferenceScreen() != mMainPreferenceScreen && mMainPreferenceScreen != null) { setPreferenceScreen(mMainPreferenceScreen); getActivity().getActionBar().setDisplayHomeAsUpEnabled(false); return true; } getActivity().onBackPressed(); return true; } return super.onOptionsItemSelected(item); } @Override public boolean onPreferenceTreeClick(Preference preference) { if (preference == mSubMenuVoicemailSettings) { return true; } else if (preference.getKey().equals(mVoicemailSettings.getKey())) { // Check key instead of comparing reference because closing the voicemail notification // ringtone dialog invokes onResume(), but leaves the old preference screen up, // TODO: Revert to checking reference after migrating voicemail to its own activity. if (DBG) log("onPreferenceTreeClick: Voicemail Settings Preference is clicked."); /*final Dialog dialog = ((PreferenceScreen) preference).getDialog(); if (dialog != null) { dialog.getActionBar().setDisplayHomeAsUpEnabled(false); }*/ mSubMenuVoicemailSettings = (EditPhoneNumberPreference) findPreference(BUTTON_VOICEMAIL_KEY); mSubMenuVoicemailSettings.setParentActivity(getActivity(), VOICEMAIL_PREF_ID, this); mSubMenuVoicemailSettings.setOnContactPickListener(new EditPhoneNumberPreference.OnContactPickListener() { @Override public void onContactPickRequested(int requestCode, Intent intent) { startActivityForResult(intent, requestCode); } }); mSubMenuVoicemailSettings.setDialogOnClosedListener(this); mSubMenuVoicemailSettings.setDialogTitle(R.string.voicemail_settings_number_label); updateVoiceNumberField(); if (preference.getIntent() != null) { if (DBG) log("Invoking cfg intent " + preference.getIntent().getPackage()); // onActivityResult() will be responsible for resetting some of variables. this.startActivityForResult(preference.getIntent(), VOICEMAIL_PROVIDER_CFG_ID); return true; } else { if (DBG) log("onPreferenceTreeClick(). No intent; use default behavior in xml."); // onActivityResult() will not be called, so reset variables here. mPreviousVMProviderKey = VoicemailProviderListPreference.DEFAULT_KEY; mVMProviderSettingsForced = false; return false; } } return false; } /** * Implemented to support onPreferenceChangeListener to look for preference changes. * * @param preference is the preference to be changed * @param objValue should be the value of the selection, NOT its localized * display value. */ @Override public boolean onPreferenceChange(Preference preference, Object objValue) { if (DBG) log("onPreferenceChange: \"" + preference + "\" changed to \"" + objValue + "\""); if (preference == mVoicemailProviders) { final String newProviderKey = (String) objValue; // If previous provider key and the new one is same, we don't need to handle it. if (mPreviousVMProviderKey.equals(newProviderKey)) { if (DBG) log("No change is made to the VM provider setting."); return true; } updateVMPreferenceWidgets(newProviderKey); final VoicemailProviderSettings newProviderSettings = VoicemailProviderSettingsUtil.load(getActivity(), newProviderKey); // If the user switches to a voice mail provider and we have numbers stored for it we // will automatically change the phone's voice mail and forwarding number to the stored // ones. Otherwise we will bring up provider's configuration UI. if (newProviderSettings == null) { // Force the user into a configuration of the chosen provider Log.w(LOG_TAG, "Saved preferences not found - invoking config"); mVMProviderSettingsForced = true; simulatePreferenceClick(mVoicemailSettings); } else { if (DBG) log("Saved preferences found - switching to them"); // Set this flag so if we get a failure we revert to previous provider mChangingVMorFwdDueToProviderChange = true; saveVoiceMailAndForwardingNumber(newProviderKey, newProviderSettings); } } // Always let the preference setting proceed. return true; } /** * Implemented for EditPhoneNumberPreference.GetDefaultNumberListener. * This method set the default values for the various * EditPhoneNumberPreference dialogs. */ @Override public String onGetDefaultNumber(EditPhoneNumberPreference preference) { if (preference == mSubMenuVoicemailSettings) { // update the voicemail number field, which takes care of the // mSubMenuVoicemailSettings itself, so we should return null. if (DBG) log("updating default for voicemail dialog"); updateVoiceNumberField(); return null; } String vmDisplay = mPhone.getVoiceMailNumber(); if (TextUtils.isEmpty(vmDisplay)) { // if there is no voicemail number, we just return null to // indicate no contribution. return null; } // Return the voicemail number prepended with "VM: " if (DBG) log("updating default for call forwarding dialogs"); return getString(R.string.voicemail_abbreviated) + " " + vmDisplay; } @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { if (DBG) { log("onActivityResult: requestCode: " + requestCode + ", resultCode: " + resultCode + ", data: " + data); } // there are cases where the contact picker may end up sending us more than one // request. We want to ignore the request if we're not in the correct state. if (requestCode == VOICEMAIL_PROVIDER_CFG_ID) { boolean failure = false; // No matter how the processing of result goes lets clear the flag if (DBG) log("mVMProviderSettingsForced: " + mVMProviderSettingsForced); final boolean isVMProviderSettingsForced = mVMProviderSettingsForced; mVMProviderSettingsForced = false; String vmNum = null; if (resultCode != Activity.RESULT_OK) { if (DBG) log("onActivityResult: vm provider cfg result not OK."); failure = true; } else { if (data == null) { if (DBG) log("onActivityResult: vm provider cfg result has no data"); failure = true; } else { if (data.getBooleanExtra(SIGNOUT_EXTRA, false)) { if (DBG) log("Provider requested signout"); if (isVMProviderSettingsForced) { if (DBG) log("Going back to previous provider on signout"); switchToPreviousVoicemailProvider(); } else { final String victim = mVoicemailProviders.getKey(); if (DBG) log("Relaunching activity and ignoring " + victim); Intent i = new Intent(ACTION_ADD_VOICEMAIL); i.putExtra(IGNORE_PROVIDER_EXTRA, victim); i.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); ((Activity) getContext()).startActivityAsUser(i, UserHandle.CURRENT); } return; } vmNum = data.getStringExtra(VM_NUMBER_EXTRA); if (vmNum == null || vmNum.length() == 0) { if (DBG) log("onActivityResult: vm provider cfg result has no vmnum"); failure = true; } } } if (failure) { if (DBG) log("Failure in return from voicemail provider."); if (isVMProviderSettingsForced) { switchToPreviousVoicemailProvider(); } return; } mChangingVMorFwdDueToProviderChange = isVMProviderSettingsForced; final String fwdNum = data.getStringExtra(FWD_NUMBER_EXTRA); // TODO: It would be nice to load the current network setting for this and // send it to the provider when it's config is invoked so it can use this as default final int fwdNumTime = data.getIntExtra(FWD_NUMBER_TIME_EXTRA, 20); /// M: Print voiceNumber @{ // if (DBG) log("onActivityResult: cfg result has forwarding number " + fwdNum); CallSettingUtils.sensitiveLog(LOG_TAG, "onActivityResult: cfg result has forwarding number ", fwdNum); /// }@ saveVoiceMailAndForwardingNumber(mVoicemailProviders.getKey(), new VoicemailProviderSettings(vmNum, fwdNum, fwdNumTime)); return; } if (requestCode == VOICEMAIL_PREF_ID) { if (resultCode != Activity.RESULT_OK) { if (DBG) log("onActivityResult: contact picker result not OK."); return; } Cursor cursor = null; try { // check if the URI returned by the user belongs to the user ComponentCaller currentCaller = getActivity().getCurrentCaller(); if (currentCaller == null) { Log.e(LOG_TAG, "onActivityResult: Current caller is null, cannot check permissions."); return; } Uri contactUri = (data != null) ? data.getData() : null; if (contactUri == null) { Log.w(LOG_TAG, "onActivityResult: Intent data or contact URI is null."); return; } if (currentCaller.checkContentUriPermission( contactUri, Intent.FLAG_GRANT_READ_URI_PERMISSION) == PackageManager.PERMISSION_DENIED) { EventLog.writeEvent(0x534e4554, "337785563", currentCaller.getUid(), "Permission denied, cannot access contact"); throw new SecurityException(String.format( "Permission denial: Caller (uid=%d, pkg=%s) lacks specific permission" + " grant %s to access contact URI %s.", currentCaller.getUid(), currentCaller.getPackage(), "FLAG_GRANT_READ_URI_PERMISSION", contactUri )); } cursor = getActivity().getContentResolver().query(data.getData(), new String[] { CommonDataKinds.Phone.NUMBER }, null, null, null); if ((cursor == null) || (!cursor.moveToFirst())) { if (DBG) log("onActivityResult: bad contact data, no results found."); return; } if (mSubMenuVoicemailSettings != null) { mSubMenuVoicemailSettings.onPickActivityResult(cursor.getString(0)); } else { Log.w(LOG_TAG, "VoicemailSettingsActivity destroyed while setting contacts."); } return; } finally { if (cursor != null) { cursor.close(); } } } super.onActivityResult(requestCode, resultCode, data); } /** * Simulates user clicking on a passed preference. * Usually needed when the preference is a dialog preference and we want to invoke * a dialog for this preference programmatically. * TODO: figure out if there is a cleaner way to cause preference dlg to come up */ private void simulatePreferenceClick(Preference preference) { // Go through settings until we find our setting // and then simulate a click on it to bring up the dialog if (preference != null) { preference.performClick(); } } /** * Get the boolean config from carrier config manager. * * @param key config key defined in CarrierConfigManager * @return boolean value of corresponding key. */ private boolean getBooleanCarrierConfig(String key) { PersistableBundle b = PhoneGlobals.getInstance() .getCarrierConfigForSubId(mPhone.getSubId()); if (b == null) { b = PhoneGlobals.getInstance().getCarrierConfig(); } return b.getBoolean(key); } //********************************************************************************************* // Activity Dialog Methods //********************************************************************************************* protected void onPrepareDialog(int id, Dialog dialog) { mCurrentDialogId = id; } // dialog creation method, called by showDialog() protected Dialog onCreateDialog(int dialogId) { return VoicemailDialogUtil.getDialog(this, dialogId); } @Override public void onDialogClosed(EditPhoneNumberPreference preference, int buttonClicked) { if (DBG) log("onDialogClosed: Button clicked is " + buttonClicked); if (buttonClicked == DialogInterface.BUTTON_NEGATIVE) { return; } if (preference == mSubMenuVoicemailSettings) { VoicemailProviderSettings newSettings = new VoicemailProviderSettings( mSubMenuVoicemailSettings.getPhoneNumber(), VoicemailProviderSettings.NO_FORWARDING); saveVoiceMailAndForwardingNumber(mVoicemailProviders.getKey(), newSettings); } } /** * Wrapper around showDialog() that will silently do nothing if we're * not in the foreground. * * This is useful here because most of the dialogs we display from * this class are triggered by asynchronous events (like * success/failure messages from the telephony layer) and it's * possible for those events to come in even after the user has gone * to a different screen. */ // TODO: this is too brittle: it's still easy to accidentally add new // code here that calls showDialog() directly (which will result in a // WindowManager$BadTokenException if called after the activity has // been stopped.) // // It would be cleaner to do the "if (mForeground)" check in one // central place, maybe by using a single Handler for all asynchronous // events (and have *that* discard events if we're not in the // foreground.) // // Unfortunately it's not that simple, since we sometimes need to do // actual work to handle these events whether or not we're in the // foreground (see the Handler code in mSetOptionComplete for // example.) // // TODO: It's a bit worrisome that we don't do anything in error cases when we're not in the // foreground. Consider displaying a toast instead. private void showDialogIfForeground(int id) { if (mForeground) { getActivity().showDialog(id); } } private void dismissDialogSafely(int id) { try { getActivity().dismissDialog(id); } catch (IllegalArgumentException e) { // This is expected in the case where we were in the background // at the time we would normally have shown the dialog, so we didn't // show it. } } // This is a method implemented for DialogInterface.OnClickListener. // Used with the error dialog to close the app, voicemail dialog to just dismiss. // Close button is mapped to BUTTON_POSITIVE for the errors that close the activity, // while those that are mapped to BUTTON_NEUTRAL only move the preference focus. public void onClick(DialogInterface dialog, int which) { if (DBG) log("onClick: button clicked is " + which); dialog.dismiss(); switch (which) { case DialogInterface.BUTTON_NEGATIVE: if (mCurrentDialogId == VoicemailDialogUtil.FWD_GET_RESPONSE_ERROR_DIALOG) { // We failed to get current forwarding settings and the user // does not wish to continue. switchToPreviousVoicemailProvider(); } break; case DialogInterface.BUTTON_POSITIVE: if (mCurrentDialogId == VoicemailDialogUtil.FWD_GET_RESPONSE_ERROR_DIALOG) { // We failed to get current forwarding settings but the user // wishes to continue changing settings to the new vm provider setVoicemailNumberWithCarrier(); } else { getActivity().finish(); } return; default: // just let the dialog close and go back to the input } // In all dialogs, all buttons except BUTTON_POSITIVE lead to the end of user interaction // with settings UI. If we were called to explicitly configure voice mail then // we finish the settings activity here to come back to whatever the user was doing. final String action = getActivity().getIntent() != null ? getActivity().getIntent().getAction() : null; if (ACTION_ADD_VOICEMAIL.equals(action)) { getActivity().finish(); } } //********************************************************************************************* // Voicemail Methods //********************************************************************************************* /** * TODO: Refactor to make it easier to understand what's done in the different stages. */ private void saveVoiceMailAndForwardingNumber( String key, VoicemailProviderSettings newSettings) { /// M: Print voiceNumber @{ CallSettingUtils.sensitiveLog(LOG_TAG, "saveVoiceMailAndForwardingNumber: ", newSettings.toString()); /// }@ mNewVMNumber = newSettings.getVoicemailNumber(); mNewVMNumber = (mNewVMNumber == null) ? "" : mNewVMNumber; mNewFwdSettings = newSettings.getForwardingSettings(); // Call forwarding is not suppported on CDMA. if (mPhone.getPhoneType() == PhoneConstants.PHONE_TYPE_CDMA) { if (DBG) log("Ignoring forwarding setting since this is CDMA phone"); mNewFwdSettings = VoicemailProviderSettings.NO_FORWARDING; } // Throw a warning if the voicemail is the same and we did not change forwarding. if (mNewVMNumber.equals(mOldVmNumber) && mNewFwdSettings == VoicemailProviderSettings.NO_FORWARDING) { showDialogIfForeground(VoicemailDialogUtil.VM_NOCHANGE_ERROR_DIALOG); return; } VoicemailProviderSettingsUtil.save(getActivity(), key, newSettings); mVMChangeCompletedSuccessfully = false; mFwdChangesRequireRollback = false; mVMOrFwdSetError = 0; if (mNewFwdSettings == VoicemailProviderSettings.NO_FORWARDING || key.equals(mPreviousVMProviderKey)) { if (DBG) log("Set voicemail number. No changes to forwarding number."); setVoicemailNumberWithCarrier(); } else { if (DBG) log("Reading current forwarding settings."); int numSettingsReasons = VoicemailProviderSettings.FORWARDING_SETTINGS_REASONS.length; mForwardingReadResults = new CallForwardInfo[numSettingsReasons]; for (int i = 0; i < mForwardingReadResults.length; i++) { mPhone.getCallForwardingOption( VoicemailProviderSettings.FORWARDING_SETTINGS_REASONS[i], CommandsInterface.SERVICE_CLASS_VOICE, mGetOptionComplete.obtainMessage(EVENT_FORWARDING_GET_COMPLETED, i, 0)); } showDialogIfForeground(VoicemailDialogUtil.VM_FWD_READING_DIALOG); } } private final Handler mGetOptionComplete = new Handler() { @Override public void handleMessage(Message msg) { AsyncResult result = (AsyncResult) msg.obj; switch (msg.what) { case EVENT_FORWARDING_GET_COMPLETED: handleForwardingSettingsReadResult(result, msg.arg1); break; } } }; private void handleForwardingSettingsReadResult(AsyncResult ar, int idx) { if (DBG) Log.d(LOG_TAG, "handleForwardingSettingsReadResult: " + idx); Throwable error = null; if (ar.exception != null) { error = ar.exception; if (DBG) Log.d(LOG_TAG, "FwdRead: ar.exception=" + error.getMessage()); } if (ar.userObj instanceof Throwable) { error = (Throwable) ar.userObj; if (DBG) Log.d(LOG_TAG, "FwdRead: userObj=" + error.getMessage()); } // We may have already gotten an error and decided to ignore the other results. if (mForwardingReadResults == null) { if (DBG) Log.d(LOG_TAG, "Ignoring fwd reading result: " + idx); return; } // In case of error ignore other results, show an error dialog if (error != null) { if (DBG) Log.d(LOG_TAG, "Error discovered for fwd read : " + idx); mForwardingReadResults = null; dismissDialogSafely(VoicemailDialogUtil.VM_FWD_READING_DIALOG); showDialogIfForeground(VoicemailDialogUtil.FWD_GET_RESPONSE_ERROR_DIALOG); return; } // Get the forwarding info. mForwardingReadResults[idx] = CallForwardInfoUtil.getCallForwardInfo( (CallForwardInfo[]) ar.result, VoicemailProviderSettings.FORWARDING_SETTINGS_REASONS[idx]); // Check if we got all the results already boolean done = true; for (int i = 0; i < mForwardingReadResults.length; i++) { if (mForwardingReadResults[i] == null) { done = false; break; } } if (done) { if (DBG) Log.d(LOG_TAG, "Done receiving fwd info"); dismissDialogSafely(VoicemailDialogUtil.VM_FWD_READING_DIALOG); if (mPreviousVMProviderKey.equals(VoicemailProviderListPreference.DEFAULT_KEY)) { VoicemailProviderSettingsUtil.save(mPhone.getContext(), VoicemailProviderListPreference.DEFAULT_KEY, new VoicemailProviderSettings(mOldVmNumber, mForwardingReadResults)); } saveVoiceMailAndForwardingNumberStage2(); } } private void resetForwardingChangeState() { mForwardingChangeResults = new HashMap<Integer, AsyncResult>(); mExpectedChangeResultReasons = new HashSet<Integer>(); } // Called after we are done saving the previous forwarding settings if we needed. private void saveVoiceMailAndForwardingNumberStage2() { mForwardingChangeResults = null; mVoicemailChangeResult = null; resetForwardingChangeState(); for (int i = 0; i < mNewFwdSettings.length; i++) { CallForwardInfo fi = mNewFwdSettings[i]; CallForwardInfo fiForReason = CallForwardInfoUtil.infoForReason(mForwardingReadResults, fi.reason); final boolean doUpdate = CallForwardInfoUtil.isUpdateRequired(fiForReason, fi); if (doUpdate) { if (DBG) log("Setting fwd #: " + i + ": " + fi.toString()); mExpectedChangeResultReasons.add(i); CallForwardInfoUtil.setCallForwardingOption(mPhone, fi, mSetOptionComplete.obtainMessage( EVENT_FORWARDING_CHANGED, fi.reason, 0)); } } showDialogIfForeground(VoicemailDialogUtil.VM_FWD_SAVING_DIALOG); } /** * Callback to handle option update completions */ private final Handler mSetOptionComplete = new Handler() { @Override public void handleMessage(Message msg) { AsyncResult result = (AsyncResult) msg.obj; boolean done = false; switch (msg.what) { case EVENT_VOICEMAIL_CHANGED: mVoicemailChangeResult = result; mVMChangeCompletedSuccessfully = isVmChangeSuccess(); PhoneGlobals.getInstance().refreshMwiIndicator( mSubscriptionInfoHelper.getSubId()); done = true; break; case EVENT_FORWARDING_CHANGED: mForwardingChangeResults.put(msg.arg1, result); if (result.exception != null) { Log.w(LOG_TAG, "Error in setting fwd# " + msg.arg1 + ": " + result.exception.getMessage()); } if (isForwardingCompleted()) { if (isFwdChangeSuccess()) { if (DBG) log("Overall fwd changes completed ok, starting vm change"); setVoicemailNumberWithCarrier(); } else { Log.w(LOG_TAG, "Overall fwd changes completed in failure. " + "Check if we need to try rollback for some settings."); mFwdChangesRequireRollback = false; Iterator<Map.Entry<Integer, AsyncResult>> it = mForwardingChangeResults.entrySet().iterator(); while (it.hasNext()) { Map.Entry<Integer, AsyncResult> entry = it.next(); if (entry.getValue().exception == null) { // If at least one succeeded we have to revert Log.i(LOG_TAG, "Rollback will be required"); mFwdChangesRequireRollback = true; break; } } if (!mFwdChangesRequireRollback) { Log.i(LOG_TAG, "No rollback needed."); } done = true; } } break; default: // TODO: should never reach this, may want to throw exception } if (done) { if (DBG) log("All VM provider related changes done"); if (mForwardingChangeResults != null) { dismissDialogSafely(VoicemailDialogUtil.VM_FWD_SAVING_DIALOG); } handleSetVmOrFwdMessage(); } } }; /** * Callback to handle option revert completions */ private final Handler mRevertOptionComplete = new Handler() { @Override public void handleMessage(Message msg) { AsyncResult result = (AsyncResult) msg.obj; switch (msg.what) { case EVENT_VOICEMAIL_CHANGED: if (DBG) log("VM revert complete msg"); mVoicemailChangeResult = result; break; case EVENT_FORWARDING_CHANGED: if (DBG) log("FWD revert complete msg "); mForwardingChangeResults.put(msg.arg1, result); if (result.exception != null) { if (DBG) log("Error in reverting fwd# " + msg.arg1 + ": " + result.exception.getMessage()); } break; default: // TODO: should never reach this, may want to throw exception } final boolean done = (!mVMChangeCompletedSuccessfully || mVoicemailChangeResult != null) && (!mFwdChangesRequireRollback || isForwardingCompleted()); if (done) { if (DBG) log("All VM reverts done"); dismissDialogSafely(VoicemailDialogUtil.VM_REVERTING_DIALOG); onRevertDone(); } } }; private void setVoicemailNumberWithCarrier() { //if (DBG) log("save voicemail #: " + mNewVMNumber); /// M: Print voiceNumber @{ CallSettingUtils.sensitiveLog(LOG_TAG, "save voicemail #: ", mNewVMNumber); /// }@ mVoicemailChangeResult = null; mPhone.setVoiceMailNumber( mPhone.getVoiceMailAlphaTag().toString(), mNewVMNumber, Message.obtain(mSetOptionComplete, EVENT_VOICEMAIL_CHANGED)); } private void switchToPreviousVoicemailProvider() { if (DBG) log("switchToPreviousVoicemailProvider " + mPreviousVMProviderKey); if (mPreviousVMProviderKey == null) { return; } if (mVMChangeCompletedSuccessfully || mFwdChangesRequireRollback) { showDialogIfForeground(VoicemailDialogUtil.VM_REVERTING_DIALOG); final VoicemailProviderSettings prevSettings = VoicemailProviderSettingsUtil.load(getActivity(), mPreviousVMProviderKey); if (prevSettings == null) { Log.e(LOG_TAG, "VoicemailProviderSettings for the key \"" + mPreviousVMProviderKey + "\" is null but should be loaded."); return; } if (mVMChangeCompletedSuccessfully) { mNewVMNumber = prevSettings.getVoicemailNumber(); Log.i(LOG_TAG, "VM change is already completed successfully." + "Have to revert VM back to " + mNewVMNumber + " again."); mPhone.setVoiceMailNumber( mPhone.getVoiceMailAlphaTag().toString(), mNewVMNumber, Message.obtain(mRevertOptionComplete, EVENT_VOICEMAIL_CHANGED)); } if (mFwdChangesRequireRollback) { Log.i(LOG_TAG, "Requested to rollback forwarding changes."); final CallForwardInfo[] prevFwdSettings = prevSettings.getForwardingSettings(); if (prevFwdSettings != null) { Map<Integer, AsyncResult> results = mForwardingChangeResults; resetForwardingChangeState(); for (int i = 0; i < prevFwdSettings.length; i++) { CallForwardInfo fi = prevFwdSettings[i]; if (DBG) log("Reverting fwd #: " + i + ": " + fi.toString()); // Only revert the settings for which the update succeeded. AsyncResult result = results.get(fi.reason); if (result != null && result.exception == null) { mExpectedChangeResultReasons.add(fi.reason); CallForwardInfoUtil.setCallForwardingOption(mPhone, fi, mRevertOptionComplete.obtainMessage( EVENT_FORWARDING_CHANGED, i, 0)); } } } } } else { if (DBG) log("No need to revert"); onRevertDone(); } } //********************************************************************************************* // Voicemail Handler Helpers //********************************************************************************************* /** * Updates the look of the VM preference widgets based on current VM provider settings. * Note that the provider name is loaded fxrorm the found activity via loadLabel in * {@link VoicemailProviderListPreference#initVoiceMailProviders()} in order for it to be * localizable. */ private void updateVMPreferenceWidgets(String currentProviderSetting) { Log.d("VoicemailSettingsFragment", "进入VoicemailSettingsFragment的updateVMPreferenceWidgets方法"); final String key = currentProviderSetting; final VoicemailProviderListPreference.VoicemailProvider provider = mVoicemailProviders.getVoicemailProvider(key); /* This is the case when we are coming up on a freshly wiped phone and there is no persisted value for the list preference mVoicemailProviders. In this case we want to show the UI asking the user to select a voicemail provider as opposed to silently falling back to default one. */ if (provider == null) { if (DBG) log("updateVMPreferenceWidget: key: " + key + " -> null."); mVoicemailProviders.setSummary(getString(R.string.sum_voicemail_choose_provider)); mVoicemailSettings.setEnabled(false); mVoicemailSettings.setIntent(null); } else { if (DBG) log("updateVMPreferenceWidget: key: " + key + " -> " + provider.toString()); final String providerName = provider.name; mVoicemailProviders.setSummary(providerName); mVoicemailSettings.setEnabled(true); mVoicemailSettings.setIntent(provider.intent); } } /** * Update the voicemail number from what we've recorded on the sim. */ private void updateVoiceNumberField() { //if (DBG) log("updateVoiceNumberField()"); mOldVmNumber = mPhone.getVoiceMailNumber(); /// M: Print voiceNumber @{ CallSettingUtils.sensitiveLog(LOG_TAG, "updateVoiceNumberField(), mOldVmNumber = ", mOldVmNumber); /// }@ if (TextUtils.isEmpty(mOldVmNumber)) { mSubMenuVoicemailSettings.setPhoneNumber(""); mSubMenuVoicemailSettings.setSummary(getString(R.string.voicemail_number_not_set)); } else { mSubMenuVoicemailSettings.setPhoneNumber(mOldVmNumber); mSubMenuVoicemailSettings.setSummary(BidiFormatter.getInstance().unicodeWrap( mOldVmNumber, TextDirectionHeuristics.LTR)); } } private void handleSetVmOrFwdMessage() { if (DBG) log("handleSetVMMessage: set VM request complete"); if (!isFwdChangeSuccess()) { handleVmOrFwdSetError(VoicemailDialogUtil.FWD_SET_RESPONSE_ERROR_DIALOG); } else if (!isVmChangeSuccess()) { handleVmOrFwdSetError(VoicemailDialogUtil.VM_RESPONSE_ERROR_DIALOG); } else { handleVmAndFwdSetSuccess(VoicemailDialogUtil.VM_CONFIRM_DIALOG); } } /** * Called when Voicemail Provider or its forwarding settings failed. Rolls back partly made * changes to those settings and show "failure" dialog. * * @param dialogId ID of the dialog to show for the specific error case. Either * {@link #FWD_SET_RESPONSE_ERROR_DIALOG} or {@link #VM_RESPONSE_ERROR_DIALOG} */ private void handleVmOrFwdSetError(int dialogId) { if (mChangingVMorFwdDueToProviderChange) { mVMOrFwdSetError = dialogId; mChangingVMorFwdDueToProviderChange = false; switchToPreviousVoicemailProvider(); return; } mChangingVMorFwdDueToProviderChange = false; VoicemailDialogUtil.showDialog(getActivity(), this, dialogId); //AGUI[yaozhiqing] /*showDialogIfForeground(dialogId);*/ updateVoiceNumberField(); } /** * Called when Voicemail Provider and its forwarding settings were successfully finished. * This updates a bunch of variables and show "success" dialog. */ private void handleVmAndFwdSetSuccess(int dialogId) { if (DBG) log("handleVmAndFwdSetSuccess: key is " + mVoicemailProviders.getKey()); mPreviousVMProviderKey = mVoicemailProviders.getKey(); mChangingVMorFwdDueToProviderChange = false; VoicemailDialogUtil.showDialog(getActivity(), this, VoicemailDialogUtil.VM_CONFIRM_DIALOG); /*showDialogIfForeground(dialogId);*/ updateVoiceNumberField(); } private void onRevertDone() { if (DBG) log("onRevertDone: Changing provider key back to " + mPreviousVMProviderKey); updateVMPreferenceWidgets(mPreviousVMProviderKey); updateVoiceNumberField(); if (mVMOrFwdSetError != 0) { showDialogIfForeground(mVMOrFwdSetError); mVMOrFwdSetError = 0; } } //********************************************************************************************* // Voicemail State Helpers //********************************************************************************************* /** * Return true if there is a change result for every reason for which we expect a result. */ private boolean isForwardingCompleted() { if (mForwardingChangeResults == null) { return true; } for (Integer reason : mExpectedChangeResultReasons) { if (mForwardingChangeResults.get(reason) == null) { return false; } } return true; } private boolean isFwdChangeSuccess() { if (mForwardingChangeResults == null) { return true; } for (AsyncResult result : mForwardingChangeResults.values()) { Throwable exception = result.exception; if (exception != null) { String msg = exception.getMessage(); msg = (msg != null) ? msg : ""; Log.w(LOG_TAG, "Failed to change forwarding setting. Reason: " + msg); return false; } } return true; } private boolean isVmChangeSuccess() { if (mVoicemailChangeResult.exception != null) { String msg = mVoicemailChangeResult.exception.getMessage(); msg = (msg != null) ? msg : ""; Log.w(LOG_TAG, "Failed to change voicemail. Reason: " + msg); return false; } return true; } private static void log(String msg) { Log.d(LOG_TAG, msg); } // -------------------------------MTK----------------------------- @Override public void onDestroy() { PhoneGlobals.getInstance().removeSubInfoUpdateListener(this); super.onDestroy(); } @Override public void handleSubInfoUpdate() { log("handleSubInfoUpdate");
最新发布
12-16
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值