content: /*
   * Copyright (C) 2018 The Android Open Source Project
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *      http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  
  package com.android.server.connectivity.tethering;
  
  import static android.net.ConnectivityManager.EXTRA_ADD_TETHER_TYPE;
  import static android.net.ConnectivityManager.EXTRA_PROVISION_CALLBACK;
  import static android.net.ConnectivityManager.EXTRA_RUN_PROVISION;
  import static android.net.ConnectivityManager.TETHERING_BLUETOOTH;
  import static android.net.ConnectivityManager.TETHERING_INVALID;
  import static android.net.ConnectivityManager.TETHERING_USB;
  import static android.net.ConnectivityManager.TETHERING_WIFI;
  import static android.net.ConnectivityManager.TETHER_ERROR_ENTITLEMENT_UNKONWN;
  import static android.net.ConnectivityManager.TETHER_ERROR_NO_ERROR;
  import static android.net.ConnectivityManager.TETHER_ERROR_PROVISION_FAILED;
  
  import static com.android.internal.R.string.config_wifi_tether_enable;
  
  import android.app.AlarmManager;
  import android.app.PendingIntent;
  import android.content.BroadcastReceiver;
  import android.content.ComponentName;
  import android.content.Context;
  import android.content.Intent;
  import android.content.IntentFilter;
  import android.content.res.Resources;
  import android.net.util.SharedLog;
  import android.os.Binder;
  import android.os.Bundle;
  import android.os.Handler;
  import android.os.Looper;
  import android.os.Message;
  import android.os.Parcel;
  import android.os.PersistableBundle;
  import android.os.ResultReceiver;
  import android.os.SystemClock;
  import android.os.UserHandle;
  import android.provider.Settings;
  import android.telephony.CarrierConfigManager;
  import android.util.ArraySet;
  import android.util.SparseIntArray;
  
  import com.android.internal.annotations.VisibleForTesting;
  import com.android.internal.util.StateMachine;
  import com.android.server.connectivity.MockableSystemProperties;
  
  import java.io.PrintWriter;
  
  /**
   * Re-check tethering provisioning for enabled downstream tether types.
   * Reference ConnectivityManager.TETHERING_{@code *} for each tether type.
   *
   * All methods of this class must be accessed from the thread of tethering
   * state machine.
   * @hide
   */
  public class EntitlementManager {
      private static final String TAG = EntitlementManager.class.getSimpleName();
      private static final boolean DBG = false;
  
      @VisibleForTesting
      protected static final String DISABLE_PROVISIONING_SYSPROP_KEY = "net.tethering.noprovisioning";
      private static final String ACTION_PROVISIONING_ALARM =
              "com.android.server.connectivity.tethering.PROVISIONING_RECHECK_ALARM";
      private static final String EXTRA_SUBID = "subId";
  
      // {@link ComponentName} of the Service used to run tether provisioning.
      private static final ComponentName TETHER_SERVICE = ComponentName.unflattenFromString(
              Resources.getSystem().getString(config_wifi_tether_enable));
      private static final int MS_PER_HOUR = 60 * 60 * 1000;
      private static final int EVENT_START_PROVISIONING = 0;
      private static final int EVENT_STOP_PROVISIONING = 1;
      private static final int EVENT_UPSTREAM_CHANGED = 2;
      private static final int EVENT_MAYBE_RUN_PROVISIONING = 3;
      private static final int EVENT_GET_ENTITLEMENT_VALUE = 4;
  
  
      // The ArraySet contains enabled downstream types, ex:
      // {@link ConnectivityManager.TETHERING_WIFI}
      // {@link ConnectivityManager.TETHERING_USB}
      // {@link ConnectivityManager.TETHERING_BLUETOOTH}
      private final ArraySet<Integer> mCurrentTethers;
      private final Context mContext;
      private final int mPermissionChangeMessageCode;
      private final MockableSystemProperties mSystemProperties;
      private final SharedLog mLog;
      private final SparseIntArray mEntitlementCacheValue;
      private final EntitlementHandler mHandler;
      private final StateMachine mTetherMasterSM;
      // Key: ConnectivityManager.TETHERING_*(downstream).
      // Value: ConnectivityManager.TETHER_ERROR_{NO_ERROR or PROVISION_FAILED}(provisioning result).
      private final SparseIntArray mCellularPermitted;
      private PendingIntent mProvisioningRecheckAlarm;
      private boolean mCellularUpstreamPermitted = true;
      private boolean mUsingCellularAsUpstream = false;
      private boolean mNeedReRunProvisioningUi = false;
      private OnUiEntitlementFailedListener mListener;
      private TetheringConfigurationFetcher mFetcher;
  
      public EntitlementManager(Context ctx, StateMachine tetherMasterSM, SharedLog log,
              int permissionChangeMessageCode, MockableSystemProperties systemProperties) {
  
          mContext = ctx;
          mLog = log.forSubComponent(TAG);
          mCurrentTethers = new ArraySet<Integer>();
          mCellularPermitted = new SparseIntArray();
          mSystemProperties = systemProperties;
          mEntitlementCacheValue = new SparseIntArray();
          mTetherMasterSM = tetherMasterSM;
          mPermissionChangeMessageCode = permissionChangeMessageCode;
          final Handler masterHandler = tetherMasterSM.getHandler();
          // Create entitlement's own handler which is associated with TetherMaster thread
          // let all entitlement processes run in the same thread.
          mHandler = new EntitlementHandler(masterHandler.getLooper());
          mContext.registerReceiver(mReceiver, new IntentFilter(ACTION_PROVISIONING_ALARM),
                  null, mHandler);
      }
  
      public void setOnUiEntitlementFailedListener(final OnUiEntitlementFailedListener listener) {
          mListener = listener;
      }
  
      /** Callback fired when UI entitlement failed. */
      public interface OnUiEntitlementFailedListener {
          /**
           * Ui entitlement check fails in |downstream|.
           *
           * @param downstream  tethering type from ConnectivityManager.TETHERING_{@code *}.
           */
          void onUiEntitlementFailed(int downstream);
      }
  
      public void setTetheringConfigurationFetcher(final TetheringConfigurationFetcher fetcher) {
          mFetcher = fetcher;
      }
  
      /** Interface to fetch TetheringConfiguration. */
      public interface TetheringConfigurationFetcher {
          /**
           * Fetch current tethering configuration. This will be called to ensure whether entitlement
           * check is needed.
           * @return TetheringConfiguration instance.
           */
          TetheringConfiguration fetchTetheringConfiguration();
      }
  
      /**
       * Check if cellular upstream is permitted.
       */
      public boolean isCellularUpstreamPermitted() {
          return mCellularUpstreamPermitted;
      }
  
      /**
       * This is called when tethering starts.
       * Launch provisioning app if upstream is cellular.
       *
       * @param downstreamType tethering type from ConnectivityManager.TETHERING_{@code *}
       * @param showProvisioningUi a boolean indicating whether to show the
       *        provisioning app UI if there is one.
       */
      public void startProvisioningIfNeeded(int downstreamType, boolean showProvisioningUi) {
          mHandler.sendMessage(mHandler.obtainMessage(EVENT_START_PROVISIONING,
                  downstreamType, encodeBool(showProvisioningUi)));
      }
  
      private void handleStartProvisioningIfNeeded(int type, boolean showProvisioningUi) {
          if (!isValidDownstreamType(type)) return;
  
          if (!mCurrentTethers.contains(type)) mCurrentTethers.add(type);
  
          final TetheringConfiguration config = mFetcher.fetchTetheringConfiguration();
          if (isTetherProvisioningRequired(config)) {
              // If provisioning is required and the result is not available yet,
              // cellular upstream should not be allowed.
              if (mCellularPermitted.size() == 0) {
                  mCellularUpstreamPermitted = false;
              }
              // If upstream is not cellular, provisioning app would not be launched
              // till upstream change to cellular.
              if (mUsingCellularAsUpstream) {
                  if (showProvisioningUi) {
                      runUiTetherProvisioning(type, config.subId);
                  } else {
                      runSilentTetherProvisioning(type, config.subId);
                  }
                  mNeedReRunProvisioningUi = false;
              } else {
                  mNeedReRunProvisioningUi |= showProvisioningUi;
              }
          } else {
              mCellularUpstreamPermitted = true;
          }
      }
  
      /**
       * Tell EntitlementManager that a given type of tethering has been disabled
       *
       * @param type tethering type from ConnectivityManager.TETHERING_{@code *}
       */
      public void stopProvisioningIfNeeded(int type) {
          mHandler.sendMessage(mHandler.obtainMessage(EVENT_STOP_PROVISIONING, type, 0));
      }
  
      private void handleStopProvisioningIfNeeded(int type) {
          if (!isValidDownstreamType(type)) return;
  
          mCurrentTethers.remove(type);
          // There are lurking bugs where the notion of "provisioning required" or
          // "tethering supported" may change without without tethering being notified properly.
          // Remove the mapping all the time no matter provisioning is required or not.
          removeDownstreamMapping(type);
      }
  
      /**
       * Notify EntitlementManager if upstream is cellular or not.
       *
       * @param isCellular whether tethering upstream is cellular.
       */
      public void notifyUpstream(boolean isCellular) {
          mHandler.sendMessage(mHandler.obtainMessage(
                  EVENT_UPSTREAM_CHANGED, encodeBool(isCellular), 0));
      }
  
      private void handleNotifyUpstream(boolean isCellular) {
          if (DBG) {
              mLog.i("notifyUpstream: " + isCellular
                      + ", mCellularUpstreamPermitted: " + mCellularUpstreamPermitted
                      + ", mNeedReRunProvisioningUi: " + mNeedReRunProvisioningUi);
          }
          mUsingCellularAsUpstream = isCellular;
  
          if (mUsingCellularAsUpstream) {
              final TetheringConfiguration config = mFetcher.fetchTetheringConfiguration();
              handleMaybeRunProvisioning(config);
          }
      }
  
      /** Run provisioning if needed */
      public void maybeRunProvisioning() {
          mHandler.sendMessage(mHandler.obtainMessage(EVENT_MAYBE_RUN_PROVISIONING));
      }
  
      private void handleMaybeRunProvisioning(final TetheringConfiguration config) {
          if (mCurrentTethers.size() == 0 || !isTetherProvisioningRequired(config)) {
              return;
          }
  
          // Whenever any entitlement value changes, all downstreams will re-evaluate whether they
          // are allowed. Therefore even if the silent check here ends in a failure and the UI later
          // yields success, then the downstream that got a failure will re-evaluate as a result of
          // the change and get the new correct value.
          for (Integer downstream : mCurrentTethers) {
              if (mCellularPermitted.indexOfKey(downstream) < 0) {
                  if (mNeedReRunProvisioningUi) {
                      mNeedReRunProvisioningUi = false;
                      runUiTetherProvisioning(downstream, config.subId);
                  } else {
                      runSilentTetherProvisioning(downstream, config.subId);
                  }
              }
          }
      }
  
      /**
       * Check if the device requires a provisioning check in order to enable tethering.
       *
       * @param config an object that encapsulates the various tethering configuration elements.
       * @return a boolean - {@code true} indicating tether provisioning is required by the carrier.
       */
      @VisibleForTesting
      protected boolean isTetherProvisioningRequired(final TetheringConfiguration config) {
          if (mSystemProperties.getBoolean(DISABLE_PROVISIONING_SYSPROP_KEY, false)
                  || config.provisioningApp.length == 0) {
              return false;
          }
          if (carrierConfigAffirmsEntitlementCheckNotRequired(config)) {
              return false;
          }
          return (config.provisioningApp.length == 2);
      }
  
      /**
       * Re-check tethering provisioning for all enabled tether types.
       * Reference ConnectivityManager.TETHERING_{@code *} for each tether type.
       *
       * @param config an object that encapsulates the various tethering configuration elements.
       * Note: this method is only called from TetherMaster on the handler thread.
       * If there are new callers from different threads, the logic should move to
       * masterHandler to avoid race conditions.
       */
      public void reevaluateSimCardProvisioning(final TetheringConfiguration config) {
          if (DBG) mLog.i("reevaluateSimCardProvisioning");
  
          if (!mHandler.getLooper().isCurrentThread()) {
              // Except for test, this log should not appear in normal flow.
              mLog.log("reevaluateSimCardProvisioning() don't run in TetherMaster thread");
          }
          mEntitlementCacheValue.clear();
          mCellularPermitted.clear();
  
          // TODO: refine provisioning check to isTetherProvisioningRequired() ??
          if (!config.hasMobileHotspotProvisionApp()
                  || carrierConfigAffirmsEntitlementCheckNotRequired(config)) {
              evaluateCellularPermission(config);
              return;
          }
  
          if (mUsingCellularAsUpstream) {
              handleMaybeRunProvisioning(config);
          }
      }
  
      /**
       * Get carrier configuration bundle.
       * @param config an object that encapsulates the various tethering configuration elements.
       * */
      public PersistableBundle getCarrierConfig(final TetheringConfiguration config) {
          final CarrierConfigManager configManager = (CarrierConfigManager) mContext
                  .getSystemService(Context.CARRIER_CONFIG_SERVICE);
          if (configManager == null) return null;
  
          final PersistableBundle carrierConfig = configManager.getConfigForSubId(config.subId);
  
          if (CarrierConfigManager.isConfigForIdentifiedCarrier(carrierConfig)) {
              return carrierConfig;
          }
  
          return null;
      }
  
      // The logic here is aimed solely at confirming that a CarrierConfig exists
      // and affirms that entitlement checks are not required.
      //
      // TODO: find a better way to express this, or alter the checking process
      // entirely so that this is more intuitive.
      private boolean carrierConfigAffirmsEntitlementCheckNotRequired(
              final TetheringConfiguration config) {
          // Check carrier config for entitlement checks
          final PersistableBundle carrierConfig = getCarrierConfig(config);
          if (carrierConfig == null) return false;
  
          // A CarrierConfigManager was found and it has a config.
          final boolean isEntitlementCheckRequired = carrierConfig.getBoolean(
                  CarrierConfigManager.KEY_REQUIRE_ENTITLEMENT_CHECKS_BOOL);
          return !isEntitlementCheckRequired;
      }
  
      /**
       * Run no UI tethering provisioning check.
       * @param type tethering type from ConnectivityManager.TETHERING_{@code *}
       * @param subId default data subscription ID.
       */
      @VisibleForTesting
      protected void runSilentTetherProvisioning(int type, int subId) {
          if (DBG) mLog.i("runSilentTetherProvisioning: " + type);
          // For silent provisioning, settings would stop tethering when entitlement fail.
          ResultReceiver receiver = buildProxyReceiver(type, false/* notifyFail */, null);
  
          Intent intent = new Intent();
          intent.putExtra(EXTRA_ADD_TETHER_TYPE, type);
          intent.putExtra(EXTRA_RUN_PROVISION, true);
          intent.putExtra(EXTRA_PROVISION_CALLBACK, receiver);
          intent.putExtra(EXTRA_SUBID, subId);
          intent.setComponent(TETHER_SERVICE);
          final long ident = Binder.clearCallingIdentity();
          try {
              mContext.startServiceAsUser(intent, UserHandle.CURRENT);
          } finally {
              Binder.restoreCallingIdentity(ident);
          }
      }
  
      private void runUiTetherProvisioning(int type, int subId) {
          ResultReceiver receiver = buildProxyReceiver(type, true/* notifyFail */, null);
          runUiTetherProvisioning(type, subId, receiver);
      }
  
      /**
       * Run the UI-enabled tethering provisioning check.
       * @param type tethering type from ConnectivityManager.TETHERING_{@code *}
       * @param subId default data subscription ID.
       * @param receiver to receive entitlement check result.
       */
      @VisibleForTesting
      protected void runUiTetherProvisioning(int type, int subId, ResultReceiver receiver) {
          if (DBG) mLog.i("runUiTetherProvisioning: " + type);
  
          Intent intent = new Intent(Settings.ACTION_TETHER_PROVISIONING);
          intent.putExtra(EXTRA_ADD_TETHER_TYPE, type);
          intent.putExtra(EXTRA_PROVISION_CALLBACK, receiver);
          intent.putExtra(EXTRA_SUBID, subId);
          intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
          final long ident = Binder.clearCallingIdentity();
          try {
              mContext.startActivityAsUser(intent, UserHandle.CURRENT);
          } finally {
              Binder.restoreCallingIdentity(ident);
          }
      }
  
      // Not needed to check if this don't run on the handler thread because it's private.
      private void scheduleProvisioningRechecks(final TetheringConfiguration config) {
          if (mProvisioningRecheckAlarm == null) {
              final int period = config.provisioningCheckPeriod;
              if (period <= 0) return;
  
              Intent intent = new Intent(ACTION_PROVISIONING_ALARM);
              mProvisioningRecheckAlarm = PendingIntent.getBroadcast(mContext, 0, intent, 0);
              AlarmManager alarmManager = (AlarmManager) mContext.getSystemService(
                      Context.ALARM_SERVICE);
              long periodMs = period * MS_PER_HOUR;
              long firstAlarmTime = SystemClock.elapsedRealtime() + periodMs;
              alarmManager.setRepeating(AlarmManager.ELAPSED_REALTIME, firstAlarmTime, periodMs,
                      mProvisioningRecheckAlarm);
          }
      }
  
      private void cancelTetherProvisioningRechecks() {
          if (mProvisioningRecheckAlarm != null) {
              AlarmManager alarmManager = (AlarmManager) mContext.getSystemService(
                      Context.ALARM_SERVICE);
              alarmManager.cancel(mProvisioningRecheckAlarm);
              mProvisioningRecheckAlarm = null;
          }
      }
  
      private void evaluateCellularPermission(final TetheringConfiguration config) {
          final boolean oldPermitted = mCellularUpstreamPermitted;
          mCellularUpstreamPermitted = (!isTetherProvisioningRequired(config)
                  || mCellularPermitted.indexOfValue(TETHER_ERROR_NO_ERROR) > -1);
  
          if (DBG) {
              mLog.i("Cellular permission change from " + oldPermitted
                      + " to " + mCellularUpstreamPermitted);
          }
  
          if (mCellularUpstreamPermitted != oldPermitted) {
              mLog.log("Cellular permission change: " + mCellularUpstreamPermitted);
              mTetherMasterSM.sendMessage(mPermissionChangeMessageCode);
          }
          // Only schedule periodic re-check when tether is provisioned
          // and the result is ok.
          if (mCellularUpstreamPermitted && mCellularPermitted.size() > 0) {
              scheduleProvisioningRechecks(config);
          } else {
              cancelTetherProvisioningRechecks();
          }
      }
  
      /**
       * Add the mapping between provisioning result and tethering type.
       * Notify UpstreamNetworkMonitor if Cellular permission changes.
       *
       * @param type tethering type from ConnectivityManager.TETHERING_{@code *}
       * @param resultCode Provisioning result
       */
      protected void addDownstreamMapping(int type, int resultCode) {
          mLog.i("addDownstreamMapping: " + type + ", result: " + resultCode
                  + " ,TetherTypeRequested: " + mCurrentTethers.contains(type));
          if (!mCurrentTethers.contains(type)) return;
  
          mCellularPermitted.put(type, resultCode);
          final TetheringConfiguration config = mFetcher.fetchTetheringConfiguration();
          evaluateCellularPermission(config);
      }
  
      /**
       * Remove the mapping for input tethering type.
       * @param type tethering type from ConnectivityManager.TETHERING_{@code *}
       */
      protected void removeDownstreamMapping(int type) {
          mLog.i("removeDownstreamMapping: " + type);
          mCellularPermitted.delete(type);
          final TetheringConfiguration config = mFetcher.fetchTetheringConfiguration();
          evaluateCellularPermission(config);
      }
  
      private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
          @Override
          public void onReceive(Context context, Intent intent) {
              if (ACTION_PROVISIONING_ALARM.equals(intent.getAction())) {
                  mLog.log("Received provisioning alarm");
                  final TetheringConfiguration config = mFetcher.fetchTetheringConfiguration();
                  reevaluateSimCardProvisioning(config);
              }
          }
      };
  
      private class EntitlementHandler extends Handler {
          EntitlementHandler(Looper looper) {
              super(looper);
          }
  
          @Override
          public void handleMessage(Message msg) {
              switch (msg.what) {
                  case EVENT_START_PROVISIONING:
                      handleStartProvisioningIfNeeded(msg.arg1, toBool(msg.arg2));
                      break;
                  case EVENT_STOP_PROVISIONING:
                      handleStopProvisioningIfNeeded(msg.arg1);
                      break;
                  case EVENT_UPSTREAM_CHANGED:
                      handleNotifyUpstream(toBool(msg.arg1));
                      break;
                  case EVENT_MAYBE_RUN_PROVISIONING:
                      final TetheringConfiguration config = mFetcher.fetchTetheringConfiguration();
                      handleMaybeRunProvisioning(config);
                      break;
                  case EVENT_GET_ENTITLEMENT_VALUE:
                      handleGetLatestTetheringEntitlementValue(msg.arg1, (ResultReceiver) msg.obj,
                              toBool(msg.arg2));
                      break;
                  default:
                      mLog.log("Unknown event: " + msg.what);
                      break;
              }
          }
      }
  
      private static boolean toBool(int encodedBoolean) {
          return encodedBoolean != 0;
      }
  
      private static int encodeBool(boolean b) {
          return b ? 1 : 0;
      }
  
      private static boolean isValidDownstreamType(int type) {
          switch (type) {
              case TETHERING_BLUETOOTH:
              case TETHERING_USB:
              case TETHERING_WIFI:
                  return true;
              default:
                  return false;
          }
      }
  
      /**
       * Dump the infromation of EntitlementManager.
       * @param pw {@link PrintWriter} is used to print formatted
       */
      public void dump(PrintWriter pw) {
          pw.print("mCellularUpstreamPermitted: ");
          pw.println(mCellularUpstreamPermitted);
          for (Integer type : mCurrentTethers) {
              pw.print("Type: ");
              pw.print(typeString(type));
              if (mCellularPermitted.indexOfKey(type) > -1) {
                  pw.print(", Value: ");
                  pw.println(errorString(mCellularPermitted.get(type)));
              } else {
                  pw.println(", Value: empty");
              }
          }
      }
  
      private static String typeString(int type) {
          switch (type) {
              case TETHERING_BLUETOOTH: return "TETHERING_BLUETOOTH";
              case TETHERING_INVALID: return "TETHERING_INVALID";
              case TETHERING_USB: return "TETHERING_USB";
              case TETHERING_WIFI: return "TETHERING_WIFI";
              default:
                  return String.format("TETHERING UNKNOWN TYPE (%d)", type);
          }
      }
  
      private static String errorString(int value) {
          switch (value) {
              case TETHER_ERROR_ENTITLEMENT_UNKONWN: return "TETHER_ERROR_ENTITLEMENT_UNKONWN";
              case TETHER_ERROR_NO_ERROR: return "TETHER_ERROR_NO_ERROR";
              case TETHER_ERROR_PROVISION_FAILED: return "TETHER_ERROR_PROVISION_FAILED";
              default:
                  return String.format("UNKNOWN ERROR (%d)", value);
          }
      }
  
      private ResultReceiver buildProxyReceiver(int type, boolean notifyFail,
              final ResultReceiver receiver) {
          ResultReceiver rr = new ResultReceiver(mHandler) {
              @Override
              protected void onReceiveResult(int resultCode, Bundle resultData) {
                  int updatedCacheValue = updateEntitlementCacheValue(type, resultCode);
                  addDownstreamMapping(type, updatedCacheValue);
                  if (updatedCacheValue == TETHER_ERROR_PROVISION_FAILED && notifyFail) {
                      mListener.onUiEntitlementFailed(type);
                  }
                  if (receiver != null) receiver.send(updatedCacheValue, null);
              }
          };
  
          return writeToParcel(rr);
      }
  
      // Instances of ResultReceiver need to be public classes for remote processes to be able
      // to load them (otherwise, ClassNotFoundException). For private classes, this method
      // performs a trick : round-trip parceling any instance of ResultReceiver will return a
      // vanilla instance of ResultReceiver sharing the binder token with the original receiver.
      // The binder token has a reference to the original instance of the private class and will
      // still call its methods, and can be sent over. However it cannot be used for anything
      // else than sending over a Binder call.
      // While round-trip parceling is not great, there is currently no other way of generating
      // a vanilla instance of ResultReceiver because all its fields are private.
      private ResultReceiver writeToParcel(final ResultReceiver receiver) {
          Parcel parcel = Parcel.obtain();
          receiver.writeToParcel(parcel, 0);
          parcel.setDataPosition(0);
          ResultReceiver receiverForSending = ResultReceiver.CREATOR.createFromParcel(parcel);
          parcel.recycle();
          return receiverForSending;
      }
  
      /**
       * Update the last entitlement value to internal cache
       *
       * @param type tethering type from ConnectivityManager.TETHERING_{@code *}
       * @param resultCode last entitlement value
       * @return the last updated entitlement value
       */
      private int updateEntitlementCacheValue(int type, int resultCode) {
          if (DBG) {
              mLog.i("updateEntitlementCacheValue: " + type + ", result: " + resultCode);
          }
          if (resultCode == TETHER_ERROR_NO_ERROR) {
              mEntitlementCacheValue.put(type, resultCode);
              return resultCode;
          } else {
              mEntitlementCacheValue.put(type, TETHER_ERROR_PROVISION_FAILED);
              return TETHER_ERROR_PROVISION_FAILED;
          }
      }
  
      /** Get the last value of the tethering entitlement check. */
      public void getLatestTetheringEntitlementResult(int downstream, ResultReceiver receiver,
              boolean showEntitlementUi) {
          mHandler.sendMessage(mHandler.obtainMessage(EVENT_GET_ENTITLEMENT_VALUE,
                  downstream, encodeBool(showEntitlementUi), receiver));
  
      }
  
      private void handleGetLatestTetheringEntitlementValue(int downstream, ResultReceiver receiver,
              boolean showEntitlementUi) {
  
          final TetheringConfiguration config = mFetcher.fetchTetheringConfiguration();
          if (!isTetherProvisioningRequired(config)) {
              receiver.send(TETHER_ERROR_NO_ERROR, null);
              return;
          }
  
          final int cacheValue = mEntitlementCacheValue.get(
                  downstream, TETHER_ERROR_ENTITLEMENT_UNKONWN);
          if (cacheValue == TETHER_ERROR_NO_ERROR || !showEntitlementUi) {
              receiver.send(cacheValue, null);
          } else {
              ResultReceiver proxy = buildProxyReceiver(downstream, false/* notifyFail */, receiver);
              runUiTetherProvisioning(downstream, config.subId, proxy);
          }
      }
  }
  