/* * Copyright 2020 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.google.android.iwlan; import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR; import static android.net.NetworkCapabilities.TRANSPORT_WIFI; import android.content.Context; import android.content.Intent; import android.net.ConnectivityManager; import android.net.LinkAddress; import android.net.LinkProperties; import android.net.Network; import android.net.NetworkCapabilities; import android.os.Handler; import android.os.HandlerThread; import android.os.IBinder; import android.os.Looper; import android.os.Message; import android.support.annotation.GuardedBy; import android.support.annotation.IntRange; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.telephony.AccessNetworkConstants.AccessNetworkType; import android.telephony.DataFailCause; import android.telephony.TelephonyManager; import android.telephony.data.ApnSetting; import android.telephony.data.DataCallResponse; import android.telephony.data.DataProfile; import android.telephony.data.DataService; import android.telephony.data.DataServiceCallback; import android.telephony.data.NetworkSliceInfo; import android.telephony.data.TrafficDescriptor; import android.util.Log; import com.android.internal.annotations.VisibleForTesting; import com.google.android.iwlan.epdg.EpdgSelector; import com.google.android.iwlan.epdg.EpdgTunnelManager; import com.google.android.iwlan.epdg.TunnelLinkProperties; import com.google.android.iwlan.epdg.TunnelSetupRequest; import java.io.FileDescriptor; import java.io.PrintWriter; import java.net.Inet4Address; import java.net.Inet6Address; import java.net.InetAddress; import java.net.UnknownHostException; import java.util.ArrayList; import java.util.Calendar; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.LongSummaryStatistics; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; public class IwlanDataService extends DataService { private static final String TAG = IwlanDataService.class.getSimpleName(); private static Context mContext; private IwlanNetworkMonitorCallback mNetworkMonitorCallback; private HandlerThread mNetworkCallbackHandlerThread; private static boolean sNetworkConnected = false; private static Network sNetwork = null; // TODO: Change this to a hashmap as there is only one provider per slot private static List sIwlanDataServiceProviderList = new ArrayList(); @VisibleForTesting enum Transport { UNSPECIFIED_NETWORK, MOBILE, WIFI; } private static Transport sDefaultDataTransport = Transport.UNSPECIFIED_NETWORK; enum LinkProtocolType { UNKNOWN, IPV4, IPV6, IPV4V6; } private static LinkProtocolType sLinkProtocolType = LinkProtocolType.UNKNOWN; // TODO: see if network monitor callback impl can be shared between dataservice and // networkservice static class IwlanNetworkMonitorCallback extends ConnectivityManager.NetworkCallback { /** Called when the framework connects and has declared a new network ready for use. */ @Override public void onAvailable(Network network) { Log.d(TAG, "onAvailable: " + network); } /** * Called when the network is about to be lost, typically because there are no outstanding * requests left for it. This may be paired with a {@link NetworkCallback#onAvailable} call * with the new replacement network for graceful handover. This method is not guaranteed to * be called before {@link NetworkCallback#onLost} is called, for example in case a network * is suddenly disconnected. */ @Override public void onLosing(Network network, int maxMsToLive) { Log.d(TAG, "onLosing: maxMsToLive: " + maxMsToLive + " network: " + network); } /** * Called when a network disconnects or otherwise no longer satisfies this request or * callback. */ @Override public void onLost(Network network) { Log.d(TAG, "onLost: " + network); IwlanDataService.setNetworkConnected(false, network, Transport.UNSPECIFIED_NETWORK); } /** Called when the network corresponding to this request changes {@link LinkProperties}. */ @Override public void onLinkPropertiesChanged(Network network, LinkProperties linkProperties) { Log.d(TAG, "onLinkPropertiesChanged: " + linkProperties); if (isLinkProtocolTypeChanged(linkProperties)) { for (IwlanDataServiceProvider dp : sIwlanDataServiceProviderList) { dp.dnsPrefetchCheck(); } } } /** Called when access to the specified network is blocked or unblocked. */ @Override public void onBlockedStatusChanged(Network network, boolean blocked) { // TODO: check if we need to handle this Log.d(TAG, "onBlockedStatusChanged: " + network + " BLOCKED:" + blocked); } @Override public void onCapabilitiesChanged( Network network, NetworkCapabilities networkCapabilities) { // onCapabilitiesChanged is guaranteed to be called immediately after onAvailable per // API Log.d(TAG, "onCapabilitiesChanged: " + network + " " + networkCapabilities); if (networkCapabilities != null) { if (networkCapabilities.hasTransport(TRANSPORT_CELLULAR)) { Log.d(TAG, "Network " + network + " connected using transport MOBILE"); IwlanDataService.setNetworkConnected(true, network, Transport.MOBILE); } else if (networkCapabilities.hasTransport(TRANSPORT_WIFI)) { Log.d(TAG, "Network " + network + " connected using transport WIFI"); IwlanDataService.setNetworkConnected(true, network, Transport.WIFI); } else { Log.w(TAG, "Network does not have cellular or wifi capability"); } } } } @VisibleForTesting class IwlanDataServiceProvider extends DataService.DataServiceProvider { private static final int CALLBACK_TYPE_SETUP_DATACALL_COMPLETE = 1; private static final int CALLBACK_TYPE_DEACTIVATE_DATACALL_COMPLETE = 2; private static final int CALLBACK_TYPE_GET_DATACALL_LIST_COMPLETE = 3; private final String SUB_TAG; private final IwlanDataService mIwlanDataService; private final IwlanTunnelCallback mIwlanTunnelCallback; private HandlerThread mHandlerThread; @VisibleForTesting Handler mHandler; private boolean mWfcEnabled = false; private boolean mCarrierConfigReady = false; private EpdgSelector mEpdgSelector; private IwlanDataTunnelStats mTunnelStats; // apn to TunnelState // Lock this at public entry and exit points if: // 1) the function changes mTunnelStateForApn // 2) Makes decisions based on contents of mTunnelStateForApn @GuardedBy("mTunnelStateForApn") private Map mTunnelStateForApn = new ConcurrentHashMap<>(); // Holds the state of a tunnel (for an APN) @VisibleForTesting class TunnelState { // this should be ideally be based on path MTU discovery. 1280 is the minimum packet // size ipv6 routers have to handle so setting it to 1280 is the safest approach. // ideally it should be 1280 - tunnelling overhead ? private static final int LINK_MTU = 1280; // TODO: need to substract tunnelling overhead? static final int TUNNEL_DOWN = 1; static final int TUNNEL_IN_BRINGUP = 2; static final int TUNNEL_UP = 3; static final int TUNNEL_IN_BRINGDOWN = 4; static final int TUNNEL_IN_FORCE_CLEAN_WAS_IN_BRINGUP = 5; private DataServiceCallback dataServiceCallback; private int mState; private int mPduSessionId; private TunnelLinkProperties mTunnelLinkProperties; private boolean mIsHandover; private Date mBringUpStateTime = null; private Date mUpStateTime = null; public int getPduSessionId() { return mPduSessionId; } public void setPduSessionId(int mPduSessionId) { this.mPduSessionId = mPduSessionId; } public int getProtocolType() { return mProtocolType; } public int getLinkMtu() { return LINK_MTU; // TODO: need to substract tunnelling overhead } public void setProtocolType(int protocolType) { mProtocolType = protocolType; } private int mProtocolType; // from DataProfile public TunnelLinkProperties getTunnelLinkProperties() { return mTunnelLinkProperties; } public void setTunnelLinkProperties(TunnelLinkProperties tunnelLinkProperties) { mTunnelLinkProperties = tunnelLinkProperties; } public DataServiceCallback getDataServiceCallback() { return dataServiceCallback; } public void setDataServiceCallback(DataServiceCallback dataServiceCallback) { this.dataServiceCallback = dataServiceCallback; } public TunnelState(DataServiceCallback callback) { dataServiceCallback = callback; mState = TUNNEL_DOWN; } public int getState() { return mState; } /** @param state (TunnelState.TUNNEL_DOWN|TUNNEL_UP|TUNNEL_DOWN) */ public void setState(int state) { mState = state; if (mState == TunnelState.TUNNEL_IN_BRINGUP) { mBringUpStateTime = Calendar.getInstance().getTime(); } if (mState == TunnelState.TUNNEL_UP) { mUpStateTime = Calendar.getInstance().getTime(); } } public void setIsHandover(boolean isHandover) { mIsHandover = isHandover; } public boolean getIsHandover() { return mIsHandover; } public Date getBringUpStateTime() { return mBringUpStateTime; } public Date getUpStateTime() { return mUpStateTime; } @Override public String toString() { StringBuilder sb = new StringBuilder(); String tunnelState = "UNKNOWN"; switch (mState) { case TUNNEL_DOWN: tunnelState = "DOWN"; break; case TUNNEL_IN_BRINGUP: tunnelState = "IN BRINGUP"; break; case TUNNEL_UP: tunnelState = "UP"; break; case TUNNEL_IN_BRINGDOWN: tunnelState = "IN BRINGDOWN"; break; case TUNNEL_IN_FORCE_CLEAN_WAS_IN_BRINGUP: tunnelState = "IN FORCE CLEAN WAS IN BRINGUP"; break; } sb.append("\tCurrent State of this tunnel: " + mState + " " + tunnelState); sb.append("\n\tTunnel state is in Handover: " + mIsHandover); if (mBringUpStateTime != null) { sb.append("\n\tTunnel bring up initiated at: " + mBringUpStateTime); } else { sb.append("\n\tPotential leak. Null mBringUpStateTime"); } if (mUpStateTime != null) { sb.append("\n\tTunnel is up at: " + mUpStateTime); } if (mUpStateTime != null && mBringUpStateTime != null) { long tunnelUpTime = mUpStateTime.getTime() - mBringUpStateTime.getTime(); sb.append("\n\tTime taken for the tunnel to come up in ms: " + tunnelUpTime); } return sb.toString(); } } @VisibleForTesting class IwlanTunnelCallback implements EpdgTunnelManager.TunnelCallback { DataServiceProvider mDataServiceProvider; public IwlanTunnelCallback(DataServiceProvider dsp) { mDataServiceProvider = dsp; } // TODO: full implementation public void onOpened(String apnName, TunnelLinkProperties linkProperties) { Log.d( SUB_TAG, "Tunnel opened!. APN: " + apnName + "linkproperties: " + linkProperties); synchronized (mTunnelStateForApn) { TunnelState tunnelState = mTunnelStateForApn.get(apnName); // tunnelstate should not be null, design violation. // if its null, we should crash and debug. tunnelState.setTunnelLinkProperties(linkProperties); tunnelState.setState(TunnelState.TUNNEL_UP); mTunnelStats.reportTunnelSetupSuccess(apnName, tunnelState); deliverCallback( CALLBACK_TYPE_SETUP_DATACALL_COMPLETE, DataServiceCallback.RESULT_SUCCESS, tunnelState.getDataServiceCallback(), apnTunnelStateToDataCallResponse(apnName)); } } public void onClosed(String apnName, IwlanError error) { Log.d(SUB_TAG, "Tunnel closed!. APN: " + apnName + " Error: " + error); // this is called, when a tunnel that is up, is closed. // the expectation is error==NO_ERROR for user initiated/normal close. synchronized (mTunnelStateForApn) { TunnelState tunnelState = mTunnelStateForApn.get(apnName); mTunnelStats.reportTunnelDown(apnName, tunnelState); mTunnelStateForApn.remove(apnName); if (tunnelState.getState() == TunnelState.TUNNEL_IN_BRINGUP || tunnelState.getState() == TunnelState.TUNNEL_IN_FORCE_CLEAN_WAS_IN_BRINGUP) { DataCallResponse.Builder respBuilder = new DataCallResponse.Builder(); respBuilder .setId(apnName.hashCode()) .setProtocolType(tunnelState.getProtocolType()); if (tunnelState.getIsHandover()) { respBuilder.setHandoverFailureMode( DataCallResponse .HANDOVER_FAILURE_MODE_NO_FALLBACK_RETRY_HANDOVER); } else { respBuilder.setHandoverFailureMode( DataCallResponse .HANDOVER_FAILURE_MODE_NO_FALLBACK_RETRY_SETUP_NORMAL); } if (tunnelState.getState() == TunnelState.TUNNEL_IN_BRINGUP) { respBuilder.setCause( ErrorPolicyManager.getInstance(mContext, getSlotIndex()) .getDataFailCause(apnName)); respBuilder.setRetryDurationMillis( ErrorPolicyManager.getInstance(mContext, getSlotIndex()) .getCurrentRetryTimeMs(apnName)); } else if (tunnelState.getState() == TunnelState.TUNNEL_IN_FORCE_CLEAN_WAS_IN_BRINGUP) { respBuilder.setCause(DataFailCause.IWLAN_NETWORK_FAILURE); respBuilder.setRetryDurationMillis(5000); } deliverCallback( CALLBACK_TYPE_SETUP_DATACALL_COMPLETE, DataServiceCallback.RESULT_SUCCESS, tunnelState.getDataServiceCallback(), respBuilder.build()); return; } // iwlan service triggered teardown if (tunnelState.getState() == TunnelState.TUNNEL_IN_BRINGDOWN) { // IO exception happens when IKE library fails to retransmit requests. // This can happen for multiple reasons: // 1. Network disconnection due to wifi off. // 2. Epdg server does not respond. // 3. Socket send/receive fails. // Ignore this during tunnel bring down. if (error.getErrorType() != IwlanError.NO_ERROR && error.getErrorType() != IwlanError.IKE_INTERNAL_IO_EXCEPTION) { Log.e(SUB_TAG, "Unexpected error during tunnel bring down: " + error); } deliverCallback( CALLBACK_TYPE_DEACTIVATE_DATACALL_COMPLETE, DataServiceCallback.RESULT_SUCCESS, tunnelState.getDataServiceCallback(), null); return; } // just update list of data calls. No way to send error up notifyDataCallListChanged(getCallList()); } } } private final class DSPHandler extends Handler { private final String TAG = IwlanDataService.class.getSimpleName() + DSPHandler.class.getSimpleName() + getSlotIndex(); @Override public void handleMessage(Message msg) { Log.d(TAG, "msg.what = " + msg.what); switch (msg.what) { case IwlanEventListener.CARRIER_CONFIG_CHANGED_EVENT: Log.d(TAG, "On CARRIER_CONFIG_CHANGED_EVENT"); mCarrierConfigReady = true; dnsPrefetchCheck(); break; case IwlanEventListener.CARRIER_CONFIG_UNKNOWN_CARRIER_EVENT: Log.d(TAG, "On CARRIER_CONFIG_UNKNOWN_CARRIER_EVENT"); mCarrierConfigReady = false; break; case IwlanEventListener.WIFI_CALLING_ENABLE_EVENT: Log.d(TAG, "On WIFI_CALLING_ENABLE_EVENT"); mWfcEnabled = true; dnsPrefetchCheck(); break; case IwlanEventListener.WIFI_CALLING_DISABLE_EVENT: Log.d(TAG, "On WIFI_CALLING_DISABLE_EVENT"); mWfcEnabled = false; break; default: Log.d(TAG, "Unknown message received!"); break; } } DSPHandler(Looper looper) { super(looper); } } /** Holds all tunnel related time and count statistics for this IwlanDataServiceProvider */ @VisibleForTesting class IwlanDataTunnelStats { // represents the start time from when the following events are recorded private Date mStartTime; // Stats for TunnelSetup Success time (BRING_UP -> UP state) @VisibleForTesting Map mTunnelSetupSuccessStats = new HashMap(); // Count for Tunnel Setup failures onClosed when in BRING_UP @VisibleForTesting Map mTunnelSetupFailureCounts = new HashMap(); // Count for unsol tunnel down onClosed when in UP without deactivate @VisibleForTesting Map mUnsolTunnelDownCounts = new HashMap(); // Stats for how long the tunnel is in up state onClosed when in UP @VisibleForTesting Map mTunnelUpStats = new HashMap(); private long statCount; private final long COUNT_MAX = 1000; private final int APN_COUNT_MAX = 10; public IwlanDataTunnelStats() { mStartTime = Calendar.getInstance().getTime(); statCount = 0L; } public void reportTunnelSetupSuccess(String apn, TunnelState tunnelState) { if (statCount > COUNT_MAX || maxApnReached()) { reset(); } statCount++; Date bringUpTime = tunnelState.getBringUpStateTime(); Date upTime = tunnelState.getUpStateTime(); if (bringUpTime != null && upTime != null) { long tunnelUpTime = upTime.getTime() - bringUpTime.getTime(); if (!mTunnelSetupSuccessStats.containsKey(apn)) { mTunnelSetupSuccessStats.put(apn, new LongSummaryStatistics()); } LongSummaryStatistics stats = mTunnelSetupSuccessStats.get(apn); stats.accept(tunnelUpTime); mTunnelSetupSuccessStats.put(apn, stats); } } public void reportTunnelDown(String apn, TunnelState tunnelState) { if (statCount > COUNT_MAX || maxApnReached()) { reset(); } statCount++; // Setup fail if (tunnelState.getState() == TunnelState.TUNNEL_IN_BRINGUP) { if (!mTunnelSetupFailureCounts.containsKey(apn)) { mTunnelSetupFailureCounts.put(apn, 0L); } long count = mTunnelSetupFailureCounts.get(apn); mTunnelSetupFailureCounts.put(apn, ++count); return; } // Unsolicited tunnel down as tunnel has to be in BRINGDOWN if // there is a deactivate call associated with this. if (tunnelState.getState() == TunnelState.TUNNEL_UP) { if (!mUnsolTunnelDownCounts.containsKey(apn)) { mUnsolTunnelDownCounts.put(apn, 0L); } long count = mUnsolTunnelDownCounts.get(apn); mUnsolTunnelDownCounts.put(apn, ++count); } Date currentTime = Calendar.getInstance().getTime(); Date upTime = tunnelState.getUpStateTime(); if (upTime != null) { if (!mTunnelUpStats.containsKey(apn)) { mTunnelUpStats.put(apn, new LongSummaryStatistics()); } LongSummaryStatistics stats = mTunnelUpStats.get(apn); stats.accept(currentTime.getTime() - upTime.getTime()); mTunnelUpStats.put(apn, stats); } } boolean maxApnReached() { if (mTunnelSetupSuccessStats.size() >= APN_COUNT_MAX || mTunnelSetupFailureCounts.size() >= APN_COUNT_MAX || mUnsolTunnelDownCounts.size() >= APN_COUNT_MAX || mTunnelUpStats.size() >= APN_COUNT_MAX) { return true; } return false; } @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append("IwlanDataTunnelStats:"); sb.append("\n\tmStartTime: " + mStartTime); sb.append("\n\ttunnelSetupSuccessStats:"); for (Map.Entry entry : mTunnelSetupSuccessStats.entrySet()) { sb.append("\n\t Apn: " + entry.getKey()); sb.append("\n\t " + entry.getValue()); } sb.append("\n\ttunnelUpStats:"); for (Map.Entry entry : mTunnelUpStats.entrySet()) { sb.append("\n\t Apn: " + entry.getKey()); sb.append("\n\t " + entry.getValue()); } sb.append("\n\ttunnelSetupFailureCounts: "); for (Map.Entry entry : mTunnelSetupFailureCounts.entrySet()) { sb.append("\n\t Apn: " + entry.getKey()); sb.append("\n\t counts: " + entry.getValue()); } sb.append("\n\tunsolTunnelDownCounts: "); for (Map.Entry entry : mTunnelSetupFailureCounts.entrySet()) { sb.append("\n\t Apn: " + entry.getKey()); sb.append("\n\t counts: " + entry.getValue()); } sb.append("\n\tendTime: " + Calendar.getInstance().getTime()); return sb.toString(); } private void reset() { mStartTime = Calendar.getInstance().getTime(); mTunnelSetupSuccessStats = new HashMap(); mTunnelUpStats = new HashMap(); mTunnelSetupFailureCounts = new HashMap(); mUnsolTunnelDownCounts = new HashMap(); statCount = 0L; } } Looper getLooper() { mHandlerThread = new HandlerThread("DSPHandlerThread"); mHandlerThread.start(); return mHandlerThread.getLooper(); } /** * Constructor * * @param slotIndex SIM slot index the data service provider associated with. */ public IwlanDataServiceProvider(int slotIndex, IwlanDataService iwlanDataService) { super(slotIndex); SUB_TAG = TAG + "[" + slotIndex + "]"; // TODO: // get reference carrier config for this sub // get reference to resolver mIwlanDataService = iwlanDataService; mIwlanTunnelCallback = new IwlanTunnelCallback(this); mEpdgSelector = EpdgSelector.getSelectorInstance(mContext, slotIndex); mTunnelStats = new IwlanDataTunnelStats(); // Register IwlanEventListener initHandler(); List events = new ArrayList(); events.add(IwlanEventListener.CARRIER_CONFIG_CHANGED_EVENT); events.add(IwlanEventListener.CARRIER_CONFIG_UNKNOWN_CARRIER_EVENT); events.add(IwlanEventListener.WIFI_CALLING_ENABLE_EVENT); events.add(IwlanEventListener.WIFI_CALLING_DISABLE_EVENT); IwlanEventListener.getInstance(mContext, slotIndex).addEventListener(events, mHandler); } void initHandler() { mHandler = new DSPHandler(getLooper()); } @VisibleForTesting EpdgTunnelManager getTunnelManager() { return EpdgTunnelManager.getInstance(mContext, getSlotIndex()); } // creates a DataCallResponse for an apn irrespective of state private DataCallResponse apnTunnelStateToDataCallResponse(String apn) { TunnelState tunnelState = mTunnelStateForApn.get(apn); if (tunnelState == null) { return null; } DataCallResponse.Builder responseBuilder = new DataCallResponse.Builder(); responseBuilder .setId(apn.hashCode()) .setProtocolType(tunnelState.getProtocolType()) .setCause(DataFailCause.NONE); if (tunnelState.getState() != TunnelState.TUNNEL_UP) { // no need to fill additional params return responseBuilder.setLinkStatus(DataCallResponse.LINK_STATUS_UNKNOWN).build(); } // fill wildcard address for gatewayList (used by DataConnection to add routes) List gatewayList = new ArrayList<>(); List linkAddrList = tunnelState.getTunnelLinkProperties().internalAddresses(); if (linkAddrList.stream().anyMatch(t -> t.isIpv4())) { try { gatewayList.add(Inet4Address.getByName("0.0.0.0")); } catch (UnknownHostException e) { // should never happen for static string 0.0.0.0 } } if (linkAddrList.stream().anyMatch(t -> t.isIpv6())) { try { gatewayList.add(Inet6Address.getByName("::")); } catch (UnknownHostException e) { // should never happen for static string :: } } if (tunnelState.getTunnelLinkProperties().sliceInfo().isPresent()) { responseBuilder.setSliceInfo( tunnelState.getTunnelLinkProperties().sliceInfo().get()); } return responseBuilder .setAddresses(linkAddrList) .setDnsAddresses(tunnelState.getTunnelLinkProperties().dnsAddresses()) .setPcscfAddresses(tunnelState.getTunnelLinkProperties().pcscfAddresses()) .setInterfaceName(tunnelState.getTunnelLinkProperties().ifaceName()) .setGatewayAddresses(gatewayList) .setLinkStatus(DataCallResponse.LINK_STATUS_ACTIVE) .setMtu(tunnelState.getLinkMtu()) .setMtuV4(tunnelState.getLinkMtu()) .setMtuV6(tunnelState.getLinkMtu()) .setPduSessionId(tunnelState.getPduSessionId()) .build(); // underlying n/w is same } private List getCallList() { List dcList = new ArrayList<>(); for (String key : mTunnelStateForApn.keySet()) { DataCallResponse dcRsp = apnTunnelStateToDataCallResponse(key); if (dcRsp != null) { Log.d(SUB_TAG, "Apn: " + key + "Link state: " + dcRsp.getLinkStatus()); dcList.add(dcRsp); } } return dcList; } private void deliverCallback( int callbackType, int result, DataServiceCallback callback, DataCallResponse rsp) { if (callback == null) { Log.d(SUB_TAG, "deliverCallback: callback is null. callbackType:" + callbackType); return; } Log.d( SUB_TAG, "Delivering callbackType:" + callbackType + " result:" + result + " rsp:" + rsp); switch (callbackType) { case CALLBACK_TYPE_DEACTIVATE_DATACALL_COMPLETE: callback.onDeactivateDataCallComplete(result); // always update current datacalllist notifyDataCallListChanged(getCallList()); break; case CALLBACK_TYPE_SETUP_DATACALL_COMPLETE: if (result == DataServiceCallback.RESULT_SUCCESS && rsp == null) { Log.d(SUB_TAG, "Warning: null rsp for success case"); } callback.onSetupDataCallComplete(result, rsp); // always update current datacalllist notifyDataCallListChanged(getCallList()); break; case CALLBACK_TYPE_GET_DATACALL_LIST_COMPLETE: callback.onRequestDataCallListComplete(result, getCallList()); // TODO: add code for the rest of the cases } } /** * Setup a data connection. * * @param accessNetworkType Access network type that the data call will be established on. * Must be one of {@link android.telephony.AccessNetworkConstants.AccessNetworkType}. * @param dataProfile Data profile used for data call setup. See {@link DataProfile} * @param isRoaming True if the device is data roaming. * @param allowRoaming True if data roaming is allowed by the user. * @param reason The reason for data setup. Must be {@link #REQUEST_REASON_NORMAL} or {@link * #REQUEST_REASON_HANDOVER}. * @param linkProperties If {@code reason} is {@link #REQUEST_REASON_HANDOVER}, this is the * link properties of the existing data connection, otherwise null. * @param pduSessionId The pdu session id to be used for this data call. The standard range * of values are 1-15 while 0 means no pdu session id was attached to this call. * Reference: 3GPP TS 24.007 section 11.2.3.1b. * @param sliceInfo The slice info related to this data call. * @param trafficDescriptor TrafficDescriptor for which data connection needs to be * established. It is used for URSP traffic matching as described in 3GPP TS 24.526 * Section 4.2.2. It includes an optional DNN which, if present, must be used for * traffic matching; it does not specify the end point to be used for the data call. * @param matchAllRuleAllowed Indicates if using default match-all URSP rule for this * request is allowed. If false, this request must not use the match-all URSP rule and * if a non-match-all rule is not found (or if URSP rules are not available) then {@link * DataCallResponse#getCause()} is {@link * android.telephony.DataFailCause#MATCH_ALL_RULE_NOT_ALLOWED}. This is needed as some * requests need to have a hard failure if the intention cannot be met, for example, a * zero-rating slice. * @param callback The result callback for this request. */ @Override public void setupDataCall( int accessNetworkType, @NonNull DataProfile dataProfile, boolean isRoaming, boolean allowRoaming, int reason, @Nullable LinkProperties linkProperties, @IntRange(from = 0, to = 15) int pduSessionId, @Nullable NetworkSliceInfo sliceInfo, @Nullable TrafficDescriptor trafficDescriptor, boolean matchAllRuleAllowed, @NonNull DataServiceCallback callback) { Log.d( SUB_TAG, "Setup data call with network: " + accessNetworkType + ", DataProfile: " + dataProfile + ", isRoaming:" + isRoaming + ", allowRoaming: " + allowRoaming + ", reason: " + reason + ", linkProperties: " + linkProperties + ", pduSessionId: " + pduSessionId); // Framework will never call bringup on the same APN back 2 back. // but add a safety check if ((accessNetworkType != AccessNetworkType.IWLAN) || (dataProfile == null) || (linkProperties == null && reason == DataService.REQUEST_REASON_HANDOVER)) { deliverCallback( CALLBACK_TYPE_SETUP_DATACALL_COMPLETE, DataServiceCallback.RESULT_ERROR_INVALID_ARG, callback, null); return; } synchronized (mTunnelStateForApn) { boolean isDDS = IwlanHelper.isDefaultDataSlot(mContext, getSlotIndex()); boolean isCSTEnabled = IwlanHelper.isCrossSimCallingEnabled(mContext, getSlotIndex()); boolean networkConnected = isNetworkConnected(isDDS, isCSTEnabled); Log.d( SUB_TAG, "isDds: " + isDDS + ", isCstEnabled: " + isCSTEnabled + ", transport: " + sDefaultDataTransport); if (networkConnected == false || mTunnelStateForApn.get(dataProfile.getApn()) != null) { deliverCallback( CALLBACK_TYPE_SETUP_DATACALL_COMPLETE, 5 /* DataServiceCallback.RESULT_ERROR_TEMPORARILY_UNAVAILABLE */, callback, null); return; } TunnelSetupRequest.Builder tunnelReqBuilder = TunnelSetupRequest.builder() .setApnName(dataProfile.getApn()) .setNetwork(sNetwork) .setIsRoaming(isRoaming) .setPduSessionId(pduSessionId) .setApnIpProtocol( isRoaming ? dataProfile.getRoamingProtocolType() : dataProfile.getProtocolType()); if (reason == DataService.REQUEST_REASON_HANDOVER) { // for now assume that, at max, only one address of eachtype (v4/v6). // TODO: Check if multiple ips can be sent in ike tunnel setup for (LinkAddress lAddr : linkProperties.getLinkAddresses()) { if (lAddr.isIpv4()) { tunnelReqBuilder.setSrcIpv4Address(lAddr.getAddress()); } else if (lAddr.isIpv6()) { tunnelReqBuilder.setSrcIpv6Address(lAddr.getAddress()); tunnelReqBuilder.setSrcIpv6AddressPrefixLength(lAddr.getPrefixLength()); } } } int apnTypeBitmask = dataProfile.getSupportedApnTypesBitmask(); boolean isIMS = (apnTypeBitmask & ApnSetting.TYPE_IMS) == ApnSetting.TYPE_IMS; boolean isEmergency = (apnTypeBitmask & ApnSetting.TYPE_EMERGENCY) == ApnSetting.TYPE_EMERGENCY; tunnelReqBuilder.setRequestPcscf(isIMS || isEmergency); tunnelReqBuilder.setIsEmergency(isEmergency); setTunnelState( dataProfile, callback, TunnelState.TUNNEL_IN_BRINGUP, null, (reason == DataService.REQUEST_REASON_HANDOVER), pduSessionId); boolean result = getTunnelManager() .bringUpTunnel(tunnelReqBuilder.build(), getIwlanTunnelCallback()); Log.d(SUB_TAG, "bringup Tunnel with result:" + result); if (!result) { deliverCallback( CALLBACK_TYPE_SETUP_DATACALL_COMPLETE, DataServiceCallback.RESULT_ERROR_INVALID_ARG, callback, null); return; } } } /** * Deactivate a data connection. The data service provider must implement this method to * support data connection tear down. When completed or error, the service must invoke the * provided callback to notify the platform. * * @param cid Call id returned in the callback of {@link * DataServiceProvider#setupDataCall(int, DataProfile, boolean, boolean, int, * LinkProperties, DataServiceCallback)}. * @param reason The reason for data deactivation. Must be {@link #REQUEST_REASON_NORMAL}, * {@link #REQUEST_REASON_SHUTDOWN} or {@link #REQUEST_REASON_HANDOVER}. * @param callback The result callback for this request. Null if the client does not care */ @Override public void deactivateDataCall(int cid, int reason, DataServiceCallback callback) { Log.d( SUB_TAG, "Deactivate data call " + " reason: " + reason + " cid: " + cid + "callback: " + callback); synchronized (mTunnelStateForApn) { for (String apnName : mTunnelStateForApn.keySet()) { if (apnName.hashCode() == cid) { /* No need to check state since dataconnection in framework serializes setup and deactivate calls using callId/cid. */ mTunnelStateForApn.get(apnName).setState(TunnelState.TUNNEL_IN_BRINGDOWN); mTunnelStateForApn.get(apnName).setDataServiceCallback(callback); boolean isConnected = isNetworkConnected( IwlanHelper.isDefaultDataSlot(mContext, getSlotIndex()), IwlanHelper.isCrossSimCallingEnabled( mContext, getSlotIndex())); getTunnelManager().closeTunnel(apnName, !isConnected); return; } } deliverCallback( CALLBACK_TYPE_DEACTIVATE_DATACALL_COMPLETE, DataServiceCallback.RESULT_ERROR_INVALID_ARG, callback, null); } } public void forceCloseTunnelsInDeactivatingState() { synchronized (mTunnelStateForApn) { for (Map.Entry entry : mTunnelStateForApn.entrySet()) { TunnelState tunnelState = entry.getValue(); if (tunnelState.getState() == TunnelState.TUNNEL_IN_BRINGDOWN) { getTunnelManager().closeTunnel(entry.getKey(), true); } } } } void forceCloseTunnels() { synchronized (mTunnelStateForApn) { for (Map.Entry entry : mTunnelStateForApn.entrySet()) { getTunnelManager().closeTunnel(entry.getKey(), true); } } } /** * Get the active data call list. * * @param callback The result callback for this request. */ @Override public void requestDataCallList(DataServiceCallback callback) { deliverCallback( CALLBACK_TYPE_GET_DATACALL_LIST_COMPLETE, DataServiceCallback.RESULT_SUCCESS, callback, null); } @VisibleForTesting void setTunnelState( DataProfile dataProfile, DataServiceCallback callback, int tunnelStatus, TunnelLinkProperties linkProperties, boolean isHandover, int pduSessionId) { TunnelState tunnelState = new TunnelState(callback); tunnelState.setState(tunnelStatus); tunnelState.setProtocolType(dataProfile.getProtocolType()); tunnelState.setTunnelLinkProperties(linkProperties); tunnelState.setIsHandover(isHandover); tunnelState.setPduSessionId(pduSessionId); mTunnelStateForApn.put(dataProfile.getApn(), tunnelState); } @VisibleForTesting public IwlanTunnelCallback getIwlanTunnelCallback() { return mIwlanTunnelCallback; } @VisibleForTesting IwlanDataTunnelStats getTunnelStats() { return mTunnelStats; } private void updateNetwork(Network network) { if (network != null) { synchronized (mTunnelStateForApn) { for (Map.Entry entry : mTunnelStateForApn.entrySet()) { TunnelState tunnelState = entry.getValue(); if (tunnelState.getState() == TunnelState.TUNNEL_IN_BRINGUP) { // force close tunnels in bringup since IKE lib only supports // updating network for tunnels that are already up. // This may not result in actual closing of Ike Session since // epdg selection may not be complete yet. tunnelState.setState(TunnelState.TUNNEL_IN_FORCE_CLEAN_WAS_IN_BRINGUP); getTunnelManager().closeTunnel(entry.getKey(), true); } else { if (mIwlanDataService.isNetworkConnected( IwlanHelper.isDefaultDataSlot(mContext, getSlotIndex()), IwlanHelper.isCrossSimCallingEnabled( mContext, getSlotIndex()))) { getTunnelManager().updateNetwork(network, entry.getKey()); } } } } } } private void dnsPrefetchCheck() { boolean networkConnected = mIwlanDataService.isNetworkConnected( IwlanHelper.isDefaultDataSlot(mContext, getSlotIndex()), IwlanHelper.isCrossSimCallingEnabled(mContext, getSlotIndex())); /* Check if we need to do prefecting */ synchronized (mTunnelStateForApn) { if (networkConnected == true && mCarrierConfigReady == true && mWfcEnabled == true && mTunnelStateForApn.isEmpty()) { // Get roaming status TelephonyManager telephonyManager = mContext.getSystemService(TelephonyManager.class); telephonyManager = telephonyManager.createForSubscriptionId( IwlanHelper.getSubId(mContext, getSlotIndex())); boolean isRoaming = telephonyManager.isNetworkRoaming(); Log.d(TAG, "Trigger EPDG prefetch. Roaming=" + isRoaming); prefetchEpdgServerList(mIwlanDataService.sNetwork, isRoaming); } } } private void prefetchEpdgServerList(Network network, boolean isRoaming) { mEpdgSelector.getValidatedServerList( 0, EpdgSelector.PROTO_FILTER_IPV4V6, isRoaming, false, network, null); mEpdgSelector.getValidatedServerList( 0, EpdgSelector.PROTO_FILTER_IPV4V6, isRoaming, true, network, null); } /** * Called when the instance of data service is destroyed (e.g. got unbind or binder died) or * when the data service provider is removed. */ @Override public void close() { // TODO: call epdgtunnelmanager.releaseInstance or equivalent mIwlanDataService.removeDataServiceProvider(this); IwlanEventListener.getInstance(mContext, getSlotIndex()).removeEventListener(mHandler); mHandlerThread.quit(); } public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { pw.println("---- IwlanDataServiceProvider[" + getSlotIndex() + "] ----"); boolean isDDS = IwlanHelper.isDefaultDataSlot(mContext, getSlotIndex()); boolean isCSTEnabled = IwlanHelper.isCrossSimCallingEnabled(mContext, getSlotIndex()); pw.println("isDefaultDataSub: " + isDDS + " isCrossSimEnabled: " + isCSTEnabled); pw.println( "isNetworkConnected: " + isNetworkConnected(isDDS, isCSTEnabled) + " Wfc enabled: " + mWfcEnabled); synchronized (mTunnelStateForApn) { for (Map.Entry entry : mTunnelStateForApn.entrySet()) { pw.println("Tunnel state for APN: " + entry.getKey()); pw.println(entry.getValue()); } } pw.println(mTunnelStats); EpdgTunnelManager.getInstance(mContext, getSlotIndex()).dump(fd, pw, args); ErrorPolicyManager.getInstance(mContext, getSlotIndex()).dump(fd, pw, args); pw.println("-------------------------------------"); } } @VisibleForTesting static synchronized boolean isNetworkConnected(boolean isDds, boolean isCstEnabled) { if (!isDds && isCstEnabled) { // Only Non-DDS sub with CST enabled, can use any transport. return sNetworkConnected; } else { // For all other cases, only wifi transport can be used. return ((sDefaultDataTransport == Transport.WIFI) && sNetworkConnected); } } @VisibleForTesting /* Note: this api should have valid transport if networkConnected==true */ // Only synchronize on IwlanDataService.class for changes being made to static variables // Calls to DataServiceProvider object methods (or any objects in the future) should // not be made within synchronized block protected by IwlanDataService.class static void setNetworkConnected( boolean networkConnected, Network network, Transport transport) { boolean hasNetworkChanged = false; boolean hasTransportChanged = false; boolean hasNetworkConnectedChanged = false; synchronized (IwlanDataService.class) { if (sNetworkConnected == networkConnected && network.equals(sNetwork) && sDefaultDataTransport == transport) { // Nothing changed return; } // safety check if (networkConnected && transport == Transport.UNSPECIFIED_NETWORK) { Log.e(TAG, "setNetworkConnected: Network connected but transport unspecified"); return; } if (!network.equals(sNetwork)) { Log.e(TAG, "setNetworkConnected NW changed from: " + sNetwork + " TO: " + network); hasNetworkChanged = true; } if (transport != sDefaultDataTransport) { Log.d( TAG, "Transport was changed from " + sDefaultDataTransport.name() + " to " + transport.name()); hasTransportChanged = true; } if (sNetworkConnected != networkConnected) { Log.d( TAG, "Network connected state change from " + sNetworkConnected + " to " + networkConnected); hasNetworkConnectedChanged = true; } sNetworkConnected = networkConnected; sDefaultDataTransport = transport; sNetwork = network; if (!networkConnected) { // reset link protocol type sLinkProtocolType = LinkProtocolType.UNKNOWN; } } if (networkConnected) { if (hasTransportChanged) { // Perform forceClose for tunnels in bringdown. // let framework handle explicit teardown for (IwlanDataServiceProvider dp : sIwlanDataServiceProviderList) { dp.forceCloseTunnelsInDeactivatingState(); } } if (transport == Transport.WIFI && hasNetworkConnectedChanged) { IwlanEventListener.onWifiConnected(mContext); } // only prefetch dns and updateNetwork if Network has changed if (hasNetworkChanged) { for (IwlanDataServiceProvider dp : sIwlanDataServiceProviderList) { dp.dnsPrefetchCheck(); dp.updateNetwork(sNetwork); } } } else { for (IwlanDataServiceProvider dp : sIwlanDataServiceProviderList) { // once network is disconnected, even NAT KA offload fails // But we should still let framework do an explicit teardown // so as to not affect an ongoing handover // only force close tunnels in bring down state dp.forceCloseTunnelsInDeactivatingState(); } } } static boolean isLinkProtocolTypeChanged(LinkProperties linkProperties) { boolean hasIPV4 = false; boolean hasIPV6 = false; LinkProtocolType linkProtocolType = null; if (linkProperties != null) { for (LinkAddress linkAddress : linkProperties.getLinkAddresses()) { InetAddress inetaddr = linkAddress.getAddress(); // skip linklocal and loopback addresses if (!inetaddr.isLoopbackAddress() && !inetaddr.isLinkLocalAddress()) { if (inetaddr instanceof Inet4Address) { hasIPV4 = true; } else if (inetaddr instanceof Inet6Address) { hasIPV6 = true; } } } if (hasIPV4 && hasIPV6) { linkProtocolType = LinkProtocolType.IPV4V6; } else if (hasIPV4) { linkProtocolType = LinkProtocolType.IPV4; } else if (hasIPV6) { linkProtocolType = LinkProtocolType.IPV6; } if (sLinkProtocolType != linkProtocolType) { Log.d( TAG, "LinkProtocolType was changed from " + sLinkProtocolType + " to " + linkProtocolType); sLinkProtocolType = linkProtocolType; return true; } return false; } Log.w(TAG, "linkProperties is NULL."); return false; } /** * Get the DataServiceProvider associated with the slotId * * @param slotId slot index * @return DataService.DataServiceProvider associated with the slot */ public static DataService.DataServiceProvider getDataServiceProvider(int slotId) { DataServiceProvider ret = null; if (!sIwlanDataServiceProviderList.isEmpty()) { for (IwlanDataServiceProvider provider : sIwlanDataServiceProviderList) { if (provider.getSlotIndex() == slotId) { ret = provider; break; } } } return ret; } public static Context getContext() { return mContext; } @Override public DataServiceProvider onCreateDataServiceProvider(int slotIndex) { // TODO: validity check on slot index Log.d(TAG, "Creating provider for " + slotIndex); if (mNetworkMonitorCallback == null) { // start monitoring network mNetworkCallbackHandlerThread = new HandlerThread(IwlanNetworkService.class.getSimpleName()); mNetworkCallbackHandlerThread.start(); Looper looper = mNetworkCallbackHandlerThread.getLooper(); Handler handler = new Handler(looper); // register for default network callback ConnectivityManager connectivityManager = mContext.getSystemService(ConnectivityManager.class); mNetworkMonitorCallback = new IwlanNetworkMonitorCallback(); connectivityManager.registerDefaultNetworkCallback(mNetworkMonitorCallback, handler); Log.d(TAG, "Registered with Connectivity Service"); } IwlanDataServiceProvider dp = new IwlanDataServiceProvider(slotIndex, this); addIwlanDataServiceProvider(dp); return dp; } public void removeDataServiceProvider(IwlanDataServiceProvider dp) { sIwlanDataServiceProviderList.remove(dp); if (sIwlanDataServiceProviderList.isEmpty()) { // deinit network related stuff ConnectivityManager connectivityManager = mContext.getSystemService(ConnectivityManager.class); connectivityManager.unregisterNetworkCallback(mNetworkMonitorCallback); mNetworkCallbackHandlerThread.quit(); // no need to quitSafely mNetworkCallbackHandlerThread = null; mNetworkMonitorCallback = null; } } @VisibleForTesting void addIwlanDataServiceProvider(IwlanDataServiceProvider dp) { sIwlanDataServiceProviderList.add(dp); } @VisibleForTesting void setAppContext(Context appContext) { mContext = appContext; } @VisibleForTesting IwlanNetworkMonitorCallback getNetworkMonitorCallback() { return mNetworkMonitorCallback; } @Override public void onCreate() { setAppContext(getApplicationContext()); IwlanBroadcastReceiver.startListening(mContext); } @Override public void onDestroy() { IwlanBroadcastReceiver.stopListening(mContext); } @Override public IBinder onBind(Intent intent) { Log.d(TAG, "Iwlanservice onBind"); return super.onBind(intent); } @Override public boolean onUnbind(Intent intent) { Log.d(TAG, "IwlanService onUnbind"); // force close all the tunnels when there are no clients // active for (IwlanDataServiceProvider dp : sIwlanDataServiceProviderList) { dp.forceCloseTunnels(); } return super.onUnbind(intent); } @Override public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { String transport = "UNSPECIFIED"; if (sDefaultDataTransport == Transport.MOBILE) { transport = "CELLULAR"; } else if (sDefaultDataTransport == Transport.WIFI) { transport = "WIFI"; } pw.println("Default transport: " + transport); for (IwlanDataServiceProvider provider : sIwlanDataServiceProviderList) { pw.println(); provider.dump(fd, pw, args); pw.println(); pw.println(); } } }