| /* |
| * Copyright (C) 2019 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.internal.telephony.dataconnection; |
| |
| import android.annotation.NonNull; |
| import android.annotation.Nullable; |
| import android.net.KeepalivePacketData; |
| import android.net.LinkProperties; |
| import android.net.NattKeepalivePacketData; |
| import android.net.NetworkAgent; |
| import android.net.NetworkAgentConfig; |
| import android.net.NetworkCapabilities; |
| import android.net.NetworkInfo; |
| import android.net.NetworkProvider; |
| import android.net.SocketKeepalive; |
| import android.net.Uri; |
| import android.os.Message; |
| import android.telephony.AccessNetworkConstants; |
| import android.telephony.AccessNetworkConstants.TransportType; |
| import android.telephony.AnomalyReporter; |
| import android.telephony.TelephonyManager; |
| import android.util.ArrayMap; |
| import android.util.LocalLog; |
| import android.util.SparseArray; |
| |
| import com.android.internal.telephony.DctConstants; |
| import com.android.internal.telephony.Phone; |
| import com.android.internal.telephony.RILConstants; |
| import com.android.internal.telephony.metrics.TelephonyMetrics; |
| import com.android.internal.util.IndentingPrintWriter; |
| import com.android.telephony.Rlog; |
| |
| import java.io.FileDescriptor; |
| import java.io.PrintWriter; |
| import java.time.Duration; |
| import java.util.Map; |
| import java.util.Objects; |
| import java.util.UUID; |
| |
| /** |
| * This class represents a network agent which is communication channel between |
| * {@link DataConnection} and {@link com.android.server.ConnectivityService}. The agent is |
| * created when data connection enters {@link DataConnection.DcActiveState} until it exits that |
| * state. |
| * |
| * Note that in IWLAN handover scenario, this agent could be transferred to the new |
| * {@link DataConnection} so for a short window of time this object might be accessed by two |
| * different {@link DataConnection}. Thus each method in this class needs to be synchronized. |
| */ |
| public class DcNetworkAgent extends NetworkAgent { |
| private final String mTag; |
| |
| private final int mId; |
| |
| private Phone mPhone; |
| |
| private int mTransportType; |
| |
| private NetworkCapabilities mNetworkCapabilities; |
| |
| public final DcKeepaliveTracker keepaliveTracker = new DcKeepaliveTracker(); |
| |
| private DataConnection mDataConnection; |
| |
| private final LocalLog mNetCapsLocalLog = new LocalLog(50); |
| |
| private NetworkInfo mNetworkInfo; |
| |
| // For interface duplicate detection. Key is the net id, value is the interface name in string. |
| private static Map<Integer, String> sInterfaceNames = new ArrayMap<>(); |
| |
| DcNetworkAgent(DataConnection dc, Phone phone, NetworkInfo ni, int score, |
| NetworkAgentConfig config, NetworkProvider networkProvider, int transportType) { |
| super(phone.getContext(), dc.getHandler().getLooper(), "DcNetworkAgent", |
| dc.getNetworkCapabilities(), dc.getLinkProperties(), score, config, |
| networkProvider); |
| register(); |
| mId = getNetwork().netId; |
| mTag = "DcNetworkAgent" + "-" + mId; |
| mPhone = phone; |
| mNetworkCapabilities = dc.getNetworkCapabilities(); |
| mTransportType = transportType; |
| mDataConnection = dc; |
| mNetworkInfo = new NetworkInfo(ni); |
| setLegacySubtype(ni.getSubtype(), ni.getSubtypeName()); |
| setLegacyExtraInfo(ni.getExtraInfo()); |
| if (dc.getLinkProperties() != null) { |
| checkDuplicateInterface(mId, dc.getLinkProperties().getInterfaceName()); |
| logd("created for data connection " + dc.getName() + ", " |
| + dc.getLinkProperties().getInterfaceName()); |
| } else { |
| loge("The connection does not have a valid link properties."); |
| } |
| } |
| |
| private void checkDuplicateInterface(int netId, @Nullable String interfaceName) { |
| for (Map.Entry<Integer, String> entry: sInterfaceNames.entrySet()) { |
| if (Objects.equals(interfaceName, entry.getValue())) { |
| String message = "Duplicate interface " + interfaceName |
| + " is detected. DcNetworkAgent-" + entry.getKey() |
| + " already used this interface name."; |
| loge(message); |
| // Using fixed UUID to avoid duplicate bugreport notification |
| AnomalyReporter.reportAnomaly( |
| UUID.fromString("02f3d3f6-4613-4415-b6cb-8d92c8a938a6"), |
| message); |
| return; |
| } |
| } |
| sInterfaceNames.put(netId, interfaceName); |
| } |
| |
| /** |
| * @return The tag |
| */ |
| String getTag() { |
| return mTag; |
| } |
| |
| /** |
| * Set the data connection that owns this network agent. |
| * |
| * @param dc Data connection owning this network agent. |
| * @param transportType Transport that this data connection is on. |
| */ |
| public synchronized void acquireOwnership(@NonNull DataConnection dc, |
| @TransportType int transportType) { |
| mDataConnection = dc; |
| mTransportType = transportType; |
| logd(dc.getName() + " acquired the ownership of this agent."); |
| } |
| |
| /** |
| * Release the ownership of network agent. |
| */ |
| public synchronized void releaseOwnership(DataConnection dc) { |
| if (mDataConnection == null) { |
| loge("releaseOwnership called on no-owner DcNetworkAgent!"); |
| return; |
| } else if (mDataConnection != dc) { |
| loge("releaseOwnership: This agent belongs to " |
| + mDataConnection.getName() + ", ignored the request from " + dc.getName()); |
| return; |
| } |
| logd("Data connection " + mDataConnection.getName() + " released the ownership."); |
| mDataConnection = null; |
| } |
| |
| /** |
| * @return The data connection that owns this agent |
| */ |
| public synchronized DataConnection getDataConnection() { |
| return mDataConnection; |
| } |
| |
| @Override |
| public synchronized void onNetworkUnwanted() { |
| if (mDataConnection == null) { |
| loge("onNetworkUnwanted found called on no-owner DcNetworkAgent!"); |
| return; |
| } |
| |
| logd("onNetworkUnwanted called. Now tear down the data connection " |
| + mDataConnection.getName()); |
| mDataConnection.tearDownAll(Phone.REASON_RELEASED_BY_CONNECTIVITY_SERVICE, |
| DcTracker.RELEASE_TYPE_DETACH, null); |
| } |
| |
| @Override |
| public synchronized void onBandwidthUpdateRequested() { |
| if (mDataConnection == null) { |
| loge("onBandwidthUpdateRequested called on no-owner DcNetworkAgent!"); |
| return; |
| } |
| |
| if (mPhone.getLceStatus() == RILConstants.LCE_ACTIVE // active LCE service |
| && mTransportType == AccessNetworkConstants.TRANSPORT_TYPE_WWAN) { |
| mPhone.mCi.pullLceData(mDataConnection.obtainMessage( |
| DataConnection.EVENT_BW_REFRESH_RESPONSE)); |
| } |
| } |
| |
| @Override |
| public synchronized void onValidationStatus(int status, Uri redirectUri) { |
| if (mDataConnection == null) { |
| loge("onValidationStatus called on no-owner DcNetworkAgent!"); |
| return; |
| } |
| |
| logd("validation status: " + status + " with redirection URL: " + redirectUri); |
| DcTracker dct = mPhone.getDcTracker(mTransportType); |
| if (dct != null) { |
| Message msg = dct.obtainMessage(DctConstants.EVENT_NETWORK_STATUS_CHANGED, |
| status, mDataConnection.getCid(), redirectUri.toString()); |
| msg.sendToTarget(); |
| } |
| } |
| |
| private synchronized boolean isOwned(DataConnection dc, String reason) { |
| if (mDataConnection == null) { |
| loge(reason + " called on no-owner DcNetworkAgent!"); |
| return false; |
| } else if (mDataConnection != dc) { |
| loge(reason + ": This agent belongs to " |
| + mDataConnection.getName() + ", ignored the request from " + dc.getName()); |
| return false; |
| } |
| return true; |
| } |
| |
| /** |
| * Set the network capabilities. |
| * |
| * @param networkCapabilities The network capabilities. |
| * @param dc The data connection that invokes this method. |
| */ |
| public synchronized void sendNetworkCapabilities(NetworkCapabilities networkCapabilities, |
| DataConnection dc) { |
| if (!isOwned(dc, "sendNetworkCapabilities")) return; |
| |
| if (!networkCapabilities.equals(mNetworkCapabilities)) { |
| String logStr = "Changed from " + mNetworkCapabilities + " to " |
| + networkCapabilities + ", Data RAT=" |
| + mPhone.getServiceState().getRilDataRadioTechnology() |
| + ", dc=" + mDataConnection.getName(); |
| logd(logStr); |
| mNetCapsLocalLog.log(logStr); |
| if (networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)) { |
| // only log metrics for DataConnection with NET_CAPABILITY_INTERNET |
| if (mNetworkCapabilities == null |
| || networkCapabilities.hasCapability( |
| NetworkCapabilities.NET_CAPABILITY_TEMPORARILY_NOT_METERED) |
| != mNetworkCapabilities.hasCapability( |
| NetworkCapabilities.NET_CAPABILITY_TEMPORARILY_NOT_METERED)) { |
| TelephonyMetrics.getInstance().writeNetworkCapabilitiesChangedEvent( |
| mPhone.getPhoneId(), networkCapabilities); |
| } |
| } |
| mNetworkCapabilities = networkCapabilities; |
| } |
| sendNetworkCapabilities(networkCapabilities); |
| } |
| |
| /** |
| * Set the link properties |
| * |
| * @param linkProperties The link properties |
| * @param dc The data connection that invokes this method. |
| */ |
| public synchronized void sendLinkProperties(@NonNull LinkProperties linkProperties, |
| DataConnection dc) { |
| if (!isOwned(dc, "sendLinkProperties")) return; |
| |
| sInterfaceNames.put(mId, dc.getLinkProperties().getInterfaceName()); |
| sendLinkProperties(linkProperties); |
| } |
| |
| /** |
| * Set the network score. |
| * |
| * @param score The network score. |
| * @param dc The data connection that invokes this method. |
| */ |
| public synchronized void sendNetworkScore(int score, DataConnection dc) { |
| if (!isOwned(dc, "sendNetworkScore")) return; |
| sendNetworkScore(score); |
| } |
| |
| /** |
| * Unregister the network agent from connectivity service. |
| * |
| * @param dc The data connection that invokes this method. |
| */ |
| public synchronized void unregister(DataConnection dc) { |
| if (!isOwned(dc, "unregister")) return; |
| |
| logd("Unregister from connectivity service. " + sInterfaceNames.get(mId) + " removed."); |
| sInterfaceNames.remove(mId); |
| super.unregister(); |
| } |
| |
| /** |
| * Set the network info. |
| * |
| * @param networkInfo The network info. |
| * @param dc The data connection that invokes this method. |
| */ |
| public synchronized void sendNetworkInfo(NetworkInfo networkInfo, DataConnection dc) { |
| if (!isOwned(dc, "sendNetworkInfo")) return; |
| final NetworkInfo.State oldState = mNetworkInfo.getState(); |
| final NetworkInfo.State state = networkInfo.getState(); |
| if (mNetworkInfo.getExtraInfo() != networkInfo.getExtraInfo()) { |
| setLegacyExtraInfo(networkInfo.getExtraInfo()); |
| } |
| int subType = networkInfo.getSubtype(); |
| if (mNetworkInfo.getSubtype() != subType) { |
| setLegacySubtype(subType, TelephonyManager.getNetworkTypeName(subType)); |
| } |
| if ((oldState == NetworkInfo.State.SUSPENDED || oldState == NetworkInfo.State.CONNECTED) |
| && state == NetworkInfo.State.DISCONNECTED) { |
| unregister(dc); |
| } |
| mNetworkInfo = new NetworkInfo(networkInfo); |
| } |
| |
| /** |
| * Get the latest sent network info. |
| * |
| * @return network info |
| */ |
| public synchronized NetworkInfo getNetworkInfo() { |
| return mNetworkInfo; |
| } |
| |
| @Override |
| public synchronized void onStartSocketKeepalive(int slot, @NonNull Duration interval, |
| @NonNull KeepalivePacketData packet) { |
| if (mDataConnection == null) { |
| loge("onStartSocketKeepalive called on no-owner DcNetworkAgent!"); |
| return; |
| } |
| |
| if (packet instanceof NattKeepalivePacketData) { |
| mDataConnection.obtainMessage(DataConnection.EVENT_KEEPALIVE_START_REQUEST, |
| slot, (int) interval.getSeconds(), packet).sendToTarget(); |
| } else { |
| sendSocketKeepaliveEvent(slot, SocketKeepalive.ERROR_UNSUPPORTED); |
| } |
| } |
| |
| @Override |
| public synchronized void onStopSocketKeepalive(int slot) { |
| if (mDataConnection == null) { |
| loge("onStopSocketKeepalive called on no-owner DcNetworkAgent!"); |
| return; |
| } |
| |
| mDataConnection.obtainMessage(DataConnection.EVENT_KEEPALIVE_STOP_REQUEST, slot) |
| .sendToTarget(); |
| } |
| |
| @Override |
| public String toString() { |
| return "DcNetworkAgent-" |
| + mId |
| + " mDataConnection=" |
| + ((mDataConnection != null) ? mDataConnection.getName() : null) |
| + " mTransportType=" |
| + AccessNetworkConstants.transportTypeToString(mTransportType) |
| + " " + ((mDataConnection != null) ? mDataConnection.getLinkProperties() : null) |
| + " mNetworkCapabilities=" + mNetworkCapabilities; |
| } |
| |
| /** |
| * Dump the state of transport manager |
| * |
| * @param fd File descriptor |
| * @param printWriter Print writer |
| * @param args Arguments |
| */ |
| public void dump(FileDescriptor fd, PrintWriter printWriter, String[] args) { |
| IndentingPrintWriter pw = new IndentingPrintWriter(printWriter, " "); |
| pw.println(toString()); |
| pw.increaseIndent(); |
| pw.println("Net caps logs:"); |
| mNetCapsLocalLog.dump(fd, pw, args); |
| pw.decreaseIndent(); |
| } |
| |
| /** |
| * Log with debug level |
| * |
| * @param s is string log |
| */ |
| private void logd(String s) { |
| Rlog.d(mTag, s); |
| } |
| |
| /** |
| * Log with error level |
| * |
| * @param s is string log |
| */ |
| private void loge(String s) { |
| Rlog.e(mTag, s); |
| } |
| |
| class DcKeepaliveTracker { |
| private class KeepaliveRecord { |
| public int slotId; |
| public int currentStatus; |
| |
| KeepaliveRecord(int slotId, int status) { |
| this.slotId = slotId; |
| this.currentStatus = status; |
| } |
| } |
| |
| private final SparseArray<KeepaliveRecord> mKeepalives = new SparseArray(); |
| |
| int getHandleForSlot(int slotId) { |
| for (int i = 0; i < mKeepalives.size(); i++) { |
| KeepaliveRecord kr = mKeepalives.valueAt(i); |
| if (kr.slotId == slotId) return mKeepalives.keyAt(i); |
| } |
| return -1; |
| } |
| |
| int keepaliveStatusErrorToPacketKeepaliveError(int error) { |
| switch(error) { |
| case KeepaliveStatus.ERROR_NONE: |
| return SocketKeepalive.SUCCESS; |
| case KeepaliveStatus.ERROR_UNSUPPORTED: |
| return SocketKeepalive.ERROR_UNSUPPORTED; |
| case KeepaliveStatus.ERROR_NO_RESOURCES: |
| return SocketKeepalive.ERROR_INSUFFICIENT_RESOURCES; |
| case KeepaliveStatus.ERROR_UNKNOWN: |
| default: |
| return SocketKeepalive.ERROR_HARDWARE_ERROR; |
| } |
| } |
| |
| void handleKeepaliveStarted(final int slot, KeepaliveStatus ks) { |
| switch (ks.statusCode) { |
| case KeepaliveStatus.STATUS_INACTIVE: |
| DcNetworkAgent.this.sendSocketKeepaliveEvent(slot, |
| keepaliveStatusErrorToPacketKeepaliveError(ks.errorCode)); |
| break; |
| case KeepaliveStatus.STATUS_ACTIVE: |
| DcNetworkAgent.this.sendSocketKeepaliveEvent( |
| slot, SocketKeepalive.SUCCESS); |
| // fall through to add record |
| case KeepaliveStatus.STATUS_PENDING: |
| logd("Adding keepalive handle=" |
| + ks.sessionHandle + " slot = " + slot); |
| mKeepalives.put(ks.sessionHandle, |
| new KeepaliveRecord( |
| slot, ks.statusCode)); |
| break; |
| default: |
| logd("Invalid KeepaliveStatus Code: " + ks.statusCode); |
| break; |
| } |
| } |
| |
| void handleKeepaliveStatus(KeepaliveStatus ks) { |
| final KeepaliveRecord kr; |
| kr = mKeepalives.get(ks.sessionHandle); |
| |
| if (kr == null) { |
| // If there is no slot for the session handle, we received an event |
| // for a different data connection. This is not an error because the |
| // keepalive session events are broadcast to all listeners. |
| loge("Discarding keepalive event for different data connection:" + ks); |
| return; |
| } |
| // Switch on the current state, to see what we do with the status update |
| switch (kr.currentStatus) { |
| case KeepaliveStatus.STATUS_INACTIVE: |
| logd("Inactive Keepalive received status!"); |
| DcNetworkAgent.this.sendSocketKeepaliveEvent( |
| kr.slotId, SocketKeepalive.ERROR_HARDWARE_ERROR); |
| break; |
| case KeepaliveStatus.STATUS_PENDING: |
| switch (ks.statusCode) { |
| case KeepaliveStatus.STATUS_INACTIVE: |
| DcNetworkAgent.this.sendSocketKeepaliveEvent(kr.slotId, |
| keepaliveStatusErrorToPacketKeepaliveError(ks.errorCode)); |
| kr.currentStatus = KeepaliveStatus.STATUS_INACTIVE; |
| mKeepalives.remove(ks.sessionHandle); |
| break; |
| case KeepaliveStatus.STATUS_ACTIVE: |
| logd("Pending Keepalive received active status!"); |
| kr.currentStatus = KeepaliveStatus.STATUS_ACTIVE; |
| DcNetworkAgent.this.sendSocketKeepaliveEvent( |
| kr.slotId, SocketKeepalive.SUCCESS); |
| break; |
| case KeepaliveStatus.STATUS_PENDING: |
| loge("Invalid unsolicied Keepalive Pending Status!"); |
| break; |
| default: |
| loge("Invalid Keepalive Status received, " + ks.statusCode); |
| } |
| break; |
| case KeepaliveStatus.STATUS_ACTIVE: |
| switch (ks.statusCode) { |
| case KeepaliveStatus.STATUS_INACTIVE: |
| logd("Keepalive received stopped status!"); |
| DcNetworkAgent.this.sendSocketKeepaliveEvent( |
| kr.slotId, SocketKeepalive.SUCCESS); |
| |
| kr.currentStatus = KeepaliveStatus.STATUS_INACTIVE; |
| mKeepalives.remove(ks.sessionHandle); |
| break; |
| case KeepaliveStatus.STATUS_PENDING: |
| case KeepaliveStatus.STATUS_ACTIVE: |
| loge("Active Keepalive received invalid status!"); |
| break; |
| default: |
| loge("Invalid Keepalive Status received, " + ks.statusCode); |
| } |
| break; |
| default: |
| loge("Invalid Keepalive Status received, " + kr.currentStatus); |
| } |
| } |
| } |
| } |