summaryrefslogtreecommitdiff
path: root/packages/Tethering/src/com/android/server/connectivity/tethering/EntitlementManager.java
diff options
context:
space:
mode:
Diffstat (limited to 'packages/Tethering/src/com/android/server/connectivity/tethering/EntitlementManager.java')
-rw-r--r--packages/Tethering/src/com/android/server/connectivity/tethering/EntitlementManager.java678
1 files changed, 678 insertions, 0 deletions
diff --git a/packages/Tethering/src/com/android/server/connectivity/tethering/EntitlementManager.java b/packages/Tethering/src/com/android/server/connectivity/tethering/EntitlementManager.java
new file mode 100644
index 000000000000..6b0f1de7ce85
--- /dev/null
+++ b/packages/Tethering/src/com/android/server/connectivity/tethering/EntitlementManager.java
@@ -0,0 +1,678 @@
+/*
+ * 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() {
+ // If provisioning is required and EntitlementManager don't know any downstream,
+ // cellular upstream should not be allowed.
+ final TetheringConfiguration config = mFetcher.fetchTetheringConfiguration();
+ if (mCurrentTethers.size() == 0 && isTetherProvisioningRequired(config)) {
+ return false;
+ }
+ 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);
+ }
+ }
+}