1/*
2 * Copyright (C) 2015 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License
15 */
16package com.android.voicemail.impl;
17
18import android.app.PendingIntent;
19import android.content.Context;
20import android.content.pm.PackageManager.NameNotFoundException;
21import android.os.Bundle;
22import android.os.PersistableBundle;
23import android.support.annotation.NonNull;
24import android.support.annotation.Nullable;
25import android.support.annotation.VisibleForTesting;
26import android.telecom.PhoneAccountHandle;
27import android.telephony.CarrierConfigManager;
28import android.telephony.TelephonyManager;
29import android.telephony.VisualVoicemailSmsFilterSettings;
30import android.text.TextUtils;
31import android.util.ArraySet;
32import com.android.dialer.common.Assert;
33import com.android.voicemail.impl.protocol.VisualVoicemailProtocol;
34import com.android.voicemail.impl.protocol.VisualVoicemailProtocolFactory;
35import com.android.voicemail.impl.sms.StatusMessage;
36import com.android.voicemail.impl.sync.VvmAccountManager;
37import java.util.Collections;
38import java.util.Set;
39
40/**
41 * Manages carrier dependent visual voicemail configuration values. The primary source is the value
42 * retrieved from CarrierConfigManager. If CarrierConfigManager does not provide the config
43 * (KEY_VVM_TYPE_STRING is empty, or "hidden" configs), then the value hardcoded in telephony will
44 * be used (in res/xml/vvm_config.xml)
45 *
46 * <p>Hidden configs are new configs that are planned for future APIs, or miscellaneous settings
47 * that may clutter CarrierConfigManager too much.
48 *
49 * <p>The current hidden configs are: {@link #getSslPort()} {@link #getDisabledCapabilities()}
50 */
51public class OmtpVvmCarrierConfigHelper {
52
53  private static final String TAG = "OmtpVvmCarrierCfgHlpr";
54
55  static final String KEY_VVM_TYPE_STRING = CarrierConfigManager.KEY_VVM_TYPE_STRING;
56  static final String KEY_VVM_DESTINATION_NUMBER_STRING =
57      CarrierConfigManager.KEY_VVM_DESTINATION_NUMBER_STRING;
58  static final String KEY_VVM_PORT_NUMBER_INT = CarrierConfigManager.KEY_VVM_PORT_NUMBER_INT;
59  static final String KEY_CARRIER_VVM_PACKAGE_NAME_STRING =
60      CarrierConfigManager.KEY_CARRIER_VVM_PACKAGE_NAME_STRING;
61  static final String KEY_CARRIER_VVM_PACKAGE_NAME_STRING_ARRAY =
62      "carrier_vvm_package_name_string_array";
63  static final String KEY_VVM_PREFETCH_BOOL = CarrierConfigManager.KEY_VVM_PREFETCH_BOOL;
64  static final String KEY_VVM_CELLULAR_DATA_REQUIRED_BOOL =
65      CarrierConfigManager.KEY_VVM_CELLULAR_DATA_REQUIRED_BOOL;
66
67  /** @see #getSslPort() */
68  static final String KEY_VVM_SSL_PORT_NUMBER_INT = "vvm_ssl_port_number_int";
69
70  /** @see #isLegacyModeEnabled() */
71  static final String KEY_VVM_LEGACY_MODE_ENABLED_BOOL = "vvm_legacy_mode_enabled_bool";
72
73  /**
74   * Ban a capability reported by the server from being used. The array of string should be a subset
75   * of the capabilities returned IMAP CAPABILITY command.
76   *
77   * @see #getDisabledCapabilities()
78   */
79  static final String KEY_VVM_DISABLED_CAPABILITIES_STRING_ARRAY =
80      "vvm_disabled_capabilities_string_array";
81
82  static final String KEY_VVM_CLIENT_PREFIX_STRING = "vvm_client_prefix_string";
83
84  private final Context mContext;
85  private final PersistableBundle mCarrierConfig;
86  private final String mVvmType;
87  private final VisualVoicemailProtocol mProtocol;
88  private final PersistableBundle mTelephonyConfig;
89
90  private PhoneAccountHandle mPhoneAccountHandle;
91
92  public OmtpVvmCarrierConfigHelper(Context context, @Nullable PhoneAccountHandle handle) {
93    mContext = context;
94    mPhoneAccountHandle = handle;
95    TelephonyManager telephonyManager =
96        context
97            .getSystemService(TelephonyManager.class)
98            .createForPhoneAccountHandle(mPhoneAccountHandle);
99    if (telephonyManager == null) {
100      VvmLog.e(TAG, "PhoneAccountHandle is invalid");
101      mCarrierConfig = null;
102      mTelephonyConfig = null;
103      mVvmType = null;
104      mProtocol = null;
105      return;
106    }
107
108    mCarrierConfig = getCarrierConfig(telephonyManager);
109    mTelephonyConfig =
110        new TelephonyVvmConfigManager(context).getConfig(telephonyManager.getSimOperator());
111
112    mVvmType = getVvmType();
113    mProtocol = VisualVoicemailProtocolFactory.create(mContext.getResources(), mVvmType);
114  }
115
116  @VisibleForTesting
117  OmtpVvmCarrierConfigHelper(
118      Context context, PersistableBundle carrierConfig, PersistableBundle telephonyConfig) {
119    mContext = context;
120    mCarrierConfig = carrierConfig;
121    mTelephonyConfig = telephonyConfig;
122    mVvmType = getVvmType();
123    mProtocol = VisualVoicemailProtocolFactory.create(mContext.getResources(), mVvmType);
124  }
125
126  public Context getContext() {
127    return mContext;
128  }
129
130  @Nullable
131  public PhoneAccountHandle getPhoneAccountHandle() {
132    return mPhoneAccountHandle;
133  }
134
135  /**
136   * return whether the carrier's visual voicemail is supported, with KEY_VVM_TYPE_STRING set as a
137   * known protocol.
138   */
139  public boolean isValid() {
140    return mProtocol != null;
141  }
142
143  @Nullable
144  public String getVvmType() {
145    return (String) getValue(KEY_VVM_TYPE_STRING);
146  }
147
148  @Nullable
149  public VisualVoicemailProtocol getProtocol() {
150    return mProtocol;
151  }
152
153  /** @returns arbitrary String stored in the config file. Used for protocol specific values. */
154  @Nullable
155  public String getString(String key) {
156    Assert.checkArgument(isValid());
157    return (String) getValue(key);
158  }
159
160  @Nullable
161  public Set<String> getCarrierVvmPackageNames() {
162    Assert.checkArgument(isValid());
163    Set<String> names = getCarrierVvmPackageNames(mCarrierConfig);
164    if (names != null) {
165      return names;
166    }
167    return getCarrierVvmPackageNames(mTelephonyConfig);
168  }
169
170  private static Set<String> getCarrierVvmPackageNames(@Nullable PersistableBundle bundle) {
171    if (bundle == null) {
172      return null;
173    }
174    Set<String> names = new ArraySet<>();
175    if (bundle.containsKey(KEY_CARRIER_VVM_PACKAGE_NAME_STRING)) {
176      names.add(bundle.getString(KEY_CARRIER_VVM_PACKAGE_NAME_STRING));
177    }
178    if (bundle.containsKey(KEY_CARRIER_VVM_PACKAGE_NAME_STRING_ARRAY)) {
179      String[] vvmPackages = bundle.getStringArray(KEY_CARRIER_VVM_PACKAGE_NAME_STRING_ARRAY);
180      if (vvmPackages != null && vvmPackages.length > 0) {
181        Collections.addAll(names, vvmPackages);
182      }
183    }
184    if (names.isEmpty()) {
185      return null;
186    }
187    return names;
188  }
189
190  /**
191   * For checking upon sim insertion whether visual voicemail should be enabled. This method does so
192   * by checking if the carrier's voicemail app is installed.
193   */
194  public boolean isEnabledByDefault() {
195    if (!isValid()) {
196      return false;
197    }
198
199    Set<String> carrierPackages = getCarrierVvmPackageNames();
200    if (carrierPackages == null) {
201      return true;
202    }
203    for (String packageName : carrierPackages) {
204      try {
205        mContext.getPackageManager().getPackageInfo(packageName, 0);
206        return false;
207      } catch (NameNotFoundException e) {
208        // Do nothing.
209      }
210    }
211    return true;
212  }
213
214  public boolean isCellularDataRequired() {
215    Assert.checkArgument(isValid());
216    return (boolean) getValue(KEY_VVM_CELLULAR_DATA_REQUIRED_BOOL, false);
217  }
218
219  public boolean isPrefetchEnabled() {
220    Assert.checkArgument(isValid());
221    return (boolean) getValue(KEY_VVM_PREFETCH_BOOL, true);
222  }
223
224  public int getApplicationPort() {
225    Assert.checkArgument(isValid());
226    return (int) getValue(KEY_VVM_PORT_NUMBER_INT, 0);
227  }
228
229  @Nullable
230  public String getDestinationNumber() {
231    Assert.checkArgument(isValid());
232    return (String) getValue(KEY_VVM_DESTINATION_NUMBER_STRING);
233  }
234
235  /** @return Port to start a SSL IMAP connection directly. */
236  public int getSslPort() {
237    Assert.checkArgument(isValid());
238    return (int) getValue(KEY_VVM_SSL_PORT_NUMBER_INT, 0);
239  }
240
241  /**
242   * Hidden Config.
243   *
244   * <p>Sometimes the server states it supports a certain feature but we found they have bug on the
245   * server side. For example, in b/28717550 the server reported AUTH=DIGEST-MD5 capability but
246   * using it to login will cause subsequent response to be erroneous.
247   *
248   * @return A set of capabilities that is reported by the IMAP CAPABILITY command, but determined
249   *     to have issues and should not be used.
250   */
251  @Nullable
252  public Set<String> getDisabledCapabilities() {
253    Assert.checkArgument(isValid());
254    Set<String> disabledCapabilities = getDisabledCapabilities(mCarrierConfig);
255    if (disabledCapabilities != null) {
256      return disabledCapabilities;
257    }
258    return getDisabledCapabilities(mTelephonyConfig);
259  }
260
261  @Nullable
262  private static Set<String> getDisabledCapabilities(@Nullable PersistableBundle bundle) {
263    if (bundle == null) {
264      return null;
265    }
266    if (!bundle.containsKey(KEY_VVM_DISABLED_CAPABILITIES_STRING_ARRAY)) {
267      return null;
268    }
269    String[] disabledCapabilities =
270        bundle.getStringArray(KEY_VVM_DISABLED_CAPABILITIES_STRING_ARRAY);
271    if (disabledCapabilities != null && disabledCapabilities.length > 0) {
272      ArraySet<String> result = new ArraySet<>();
273      Collections.addAll(result, disabledCapabilities);
274      return result;
275    }
276    return null;
277  }
278
279  public String getClientPrefix() {
280    Assert.checkArgument(isValid());
281    String prefix = (String) getValue(KEY_VVM_CLIENT_PREFIX_STRING);
282    if (prefix != null) {
283      return prefix;
284    }
285    return "//VVM";
286  }
287
288  /**
289   * Should legacy mode be used when the OMTP VVM client is disabled?
290   *
291   * <p>Legacy mode is a mode that on the carrier side visual voicemail is still activated, but on
292   * the client side all network operations are disabled. SMSs are still monitored so a new message
293   * SYNC SMS will be translated to show a message waiting indicator, like traditional voicemails.
294   *
295   * <p>This is for carriers that does not support VVM deactivation so voicemail can continue to
296   * function without the data cost.
297   */
298  public boolean isLegacyModeEnabled() {
299    Assert.checkArgument(isValid());
300    return (boolean) getValue(KEY_VVM_LEGACY_MODE_ENABLED_BOOL, false);
301  }
302
303  public void startActivation() {
304    Assert.checkArgument(isValid());
305    PhoneAccountHandle phoneAccountHandle = getPhoneAccountHandle();
306    if (phoneAccountHandle == null) {
307      // This should never happen
308      // Error logged in getPhoneAccountHandle().
309      return;
310    }
311
312    if (mVvmType == null || mVvmType.isEmpty()) {
313      // The VVM type is invalid; we should never have gotten here in the first place since
314      // this is loaded initially in the constructor, and callers should check isValid()
315      // before trying to start activation anyways.
316      VvmLog.e(TAG, "startActivation : vvmType is null or empty for account " + phoneAccountHandle);
317      return;
318    }
319
320    if (mProtocol != null) {
321      ActivationTask.start(mContext, mPhoneAccountHandle, null);
322    }
323  }
324
325  public void activateSmsFilter() {
326    Assert.checkArgument(isValid());
327    TelephonyMangerCompat.setVisualVoicemailSmsFilterSettings(
328        mContext,
329        getPhoneAccountHandle(),
330        new VisualVoicemailSmsFilterSettings.Builder().setClientPrefix(getClientPrefix()).build());
331  }
332
333  public void startDeactivation() {
334    Assert.checkArgument(isValid());
335    VvmLog.i(TAG, "startDeactivation");
336    if (!isLegacyModeEnabled()) {
337      // SMS should still be filtered in legacy mode
338      TelephonyMangerCompat.setVisualVoicemailSmsFilterSettings(
339          mContext, getPhoneAccountHandle(), null);
340      VvmLog.i(TAG, "filter disabled");
341    }
342    if (mProtocol != null) {
343      mProtocol.startDeactivation(this);
344    }
345    VvmAccountManager.removeAccount(mContext, getPhoneAccountHandle());
346  }
347
348  public boolean supportsProvisioning() {
349    Assert.checkArgument(isValid());
350    return mProtocol.supportsProvisioning();
351  }
352
353  public void startProvisioning(
354      ActivationTask task,
355      PhoneAccountHandle phone,
356      VoicemailStatus.Editor status,
357      StatusMessage message,
358      Bundle data) {
359    Assert.checkArgument(isValid());
360    mProtocol.startProvisioning(task, phone, this, status, message, data);
361  }
362
363  public void requestStatus(@Nullable PendingIntent sentIntent) {
364    Assert.checkArgument(isValid());
365    mProtocol.requestStatus(this, sentIntent);
366  }
367
368  public void handleEvent(VoicemailStatus.Editor status, OmtpEvents event) {
369    Assert.checkArgument(isValid());
370    VvmLog.i(TAG, "OmtpEvent:" + event);
371    mProtocol.handleEvent(mContext, this, status, event);
372  }
373
374  @Override
375  public String toString() {
376    StringBuilder builder = new StringBuilder("OmtpVvmCarrierConfigHelper [");
377    builder
378        .append("phoneAccountHandle: ")
379        .append(mPhoneAccountHandle)
380        .append(", carrierConfig: ")
381        .append(mCarrierConfig != null)
382        .append(", telephonyConfig: ")
383        .append(mTelephonyConfig != null)
384        .append(", type: ")
385        .append(getVvmType())
386        .append(", destinationNumber: ")
387        .append(getDestinationNumber())
388        .append(", applicationPort: ")
389        .append(getApplicationPort())
390        .append(", sslPort: ")
391        .append(getSslPort())
392        .append(", isEnabledByDefault: ")
393        .append(isEnabledByDefault())
394        .append(", isCellularDataRequired: ")
395        .append(isCellularDataRequired())
396        .append(", isPrefetchEnabled: ")
397        .append(isPrefetchEnabled())
398        .append(", isLegacyModeEnabled: ")
399        .append(isLegacyModeEnabled())
400        .append("]");
401    return builder.toString();
402  }
403
404  @Nullable
405  private PersistableBundle getCarrierConfig(@NonNull TelephonyManager telephonyManager) {
406    CarrierConfigManager carrierConfigManager =
407        (CarrierConfigManager) mContext.getSystemService(Context.CARRIER_CONFIG_SERVICE);
408    if (carrierConfigManager == null) {
409      VvmLog.w(TAG, "No carrier config service found.");
410      return null;
411    }
412
413    PersistableBundle config = telephonyManager.getCarrierConfig();
414
415    if (TextUtils.isEmpty(config.getString(CarrierConfigManager.KEY_VVM_TYPE_STRING))) {
416      return null;
417    }
418    return config;
419  }
420
421  @Nullable
422  private Object getValue(String key) {
423    return getValue(key, null);
424  }
425
426  @Nullable
427  private Object getValue(String key, Object defaultValue) {
428    Object result;
429    if (mCarrierConfig != null) {
430      result = mCarrierConfig.get(key);
431      if (result != null) {
432        return result;
433      }
434    }
435    if (mTelephonyConfig != null) {
436      result = mTelephonyConfig.get(key);
437      if (result != null) {
438        return result;
439      }
440    }
441    return defaultValue;
442  }
443}
444