/*
* Copyright (C) 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.android.server.connectivity;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.net.IQosCallback;
import android.net.Network;
import android.net.QosCallbackException;
import android.net.QosFilter;
import android.net.QosSession;
import android.os.Binder;
import android.os.Handler;
import android.os.IBinder;
import android.telephony.data.EpsBearerQosSessionAttributes;
import android.telephony.data.NrQosSessionAttributes;
import android.util.Log;
import com.android.net.module.util.CollectionUtils;
import com.android.server.ConnectivityService;
import java.util.ArrayList;
import java.util.List;
/**
* Tracks qos callbacks and handles the communication between the network agent and application.
*
* Any method prefixed by handle must be called from the
* {@link com.android.server.ConnectivityService} handler thread.
*
* @hide
*/
public class QosCallbackTracker {
private static final String TAG = QosCallbackTracker.class.getSimpleName();
private static final boolean DBG = true;
@NonNull
private final Handler mConnectivityServiceHandler;
@NonNull
private final ConnectivityService.RequestInfoPerUidCounter mNetworkRequestCounter;
/**
* Each agent gets a unique callback id that is used to proxy messages back to the original
* callback.
*
* Note: The fact that this is initialized to 0 is to ensure that the thread running
* {@link #handleRegisterCallback(IQosCallback, QosFilter, int, NetworkAgentInfo)} sees the
* initialized value. This would not necessarily be the case if the value was initialized to
* the non-default value.
*
* Note: The term previous does not apply to the first callback id that is assigned.
*/
private int mPreviousAgentCallbackId = 0;
@NonNull
private final List mConnections = new ArrayList<>();
/**
*
* @param connectivityServiceHandler must be the same handler used with
* {@link com.android.server.ConnectivityService}
* @param networkRequestCounter keeps track of the number of open requests under a given
* uid
*/
public QosCallbackTracker(@NonNull final Handler connectivityServiceHandler,
final ConnectivityService.RequestInfoPerUidCounter networkRequestCounter) {
mConnectivityServiceHandler = connectivityServiceHandler;
mNetworkRequestCounter = networkRequestCounter;
}
/**
* Registers the callback with the tracker
*
* @param callback the callback to register
* @param filter the filter being registered alongside the callback
*/
public void registerCallback(@NonNull final IQosCallback callback,
@NonNull final QosFilter filter, @NonNull final NetworkAgentInfo networkAgentInfo) {
final int uid = Binder.getCallingUid();
// Enforce that the number of requests under this uid has exceeded the allowed number
mNetworkRequestCounter.incrementCountOrThrow(uid);
mConnectivityServiceHandler.post(
() -> handleRegisterCallback(callback, filter, uid, networkAgentInfo));
}
private void handleRegisterCallback(@NonNull final IQosCallback callback,
@NonNull final QosFilter filter, final int uid,
@NonNull final NetworkAgentInfo networkAgentInfo) {
final QosCallbackAgentConnection ac =
handleRegisterCallbackInternal(callback, filter, uid, networkAgentInfo);
if (ac != null) {
if (DBG) log("handleRegisterCallback: added callback " + ac.getAgentCallbackId());
mConnections.add(ac);
} else {
mNetworkRequestCounter.decrementCount(uid);
}
}
private QosCallbackAgentConnection handleRegisterCallbackInternal(
@NonNull final IQosCallback callback,
@NonNull final QosFilter filter, final int uid,
@NonNull final NetworkAgentInfo networkAgentInfo) {
final IBinder binder = callback.asBinder();
if (CollectionUtils.any(mConnections, c -> c.getBinder().equals(binder))) {
// A duplicate registration would have only made this far due to a programming error.
logwtf("handleRegisterCallback: Callbacks can only be register once.");
return null;
}
mPreviousAgentCallbackId = mPreviousAgentCallbackId + 1;
final int newCallbackId = mPreviousAgentCallbackId;
final QosCallbackAgentConnection ac =
new QosCallbackAgentConnection(this, newCallbackId, callback,
filter, uid, networkAgentInfo);
final int exceptionType = filter.validate();
if (exceptionType != QosCallbackException.EX_TYPE_FILTER_NONE) {
ac.sendEventQosCallbackError(exceptionType);
return null;
}
// Only add to the callback maps if the NetworkAgent successfully registered it
if (!ac.sendCmdRegisterCallback()) {
// There was an issue when registering the agent
if (DBG) log("handleRegisterCallback: error sending register callback");
mNetworkRequestCounter.decrementCount(uid);
return null;
}
return ac;
}
/**
* Unregisters callback
* @param callback callback to unregister
*/
public void unregisterCallback(@NonNull final IQosCallback callback) {
mConnectivityServiceHandler.post(() -> handleUnregisterCallback(callback.asBinder(), true));
}
private void handleUnregisterCallback(@NonNull final IBinder binder,
final boolean sendToNetworkAgent) {
final int connIndex =
CollectionUtils.indexOf(mConnections, c -> c.getBinder().equals(binder));
if (connIndex < 0) {
logw("handleUnregisterCallback: no matching agentConnection");
return;
}
final QosCallbackAgentConnection agentConnection = mConnections.get(connIndex);
if (DBG) {
log("handleUnregisterCallback: unregister "
+ agentConnection.getAgentCallbackId());
}
mNetworkRequestCounter.decrementCount(agentConnection.getUid());
mConnections.remove(agentConnection);
if (sendToNetworkAgent) {
agentConnection.sendCmdUnregisterCallback();
}
agentConnection.unlinkToDeathRecipient();
}
/**
* Called when the NetworkAgent sends the qos session available event for EPS
*
* @param qosCallbackId the callback id that the qos session is now available to
* @param session the qos session that is now available
* @param attributes the qos attributes that are now available on the qos session
*/
public void sendEventEpsQosSessionAvailable(final int qosCallbackId,
final QosSession session,
final EpsBearerQosSessionAttributes attributes) {
runOnAgentConnection(qosCallbackId, "sendEventEpsQosSessionAvailable: ",
ac -> ac.sendEventEpsQosSessionAvailable(session, attributes));
}
/**
* Called when the NetworkAgent sends the qos session available event for NR
*
* @param qosCallbackId the callback id that the qos session is now available to
* @param session the qos session that is now available
* @param attributes the qos attributes that are now available on the qos session
*/
public void sendEventNrQosSessionAvailable(final int qosCallbackId,
final QosSession session,
final NrQosSessionAttributes attributes) {
runOnAgentConnection(qosCallbackId, "sendEventNrQosSessionAvailable: ",
ac -> ac.sendEventNrQosSessionAvailable(session, attributes));
}
/**
* Called when the NetworkAgent sends the qos session lost event
*
* @param qosCallbackId the callback id that lost the qos session
* @param session the corresponding qos session
*/
public void sendEventQosSessionLost(final int qosCallbackId,
final QosSession session) {
runOnAgentConnection(qosCallbackId, "sendEventQosSessionLost: ",
ac -> ac.sendEventQosSessionLost(session));
}
/**
* Called when the NetworkAgent sends the qos session on error event
*
* @param qosCallbackId the callback id that should receive the exception
* @param exceptionType the type of exception that caused the callback to error
*/
public void sendEventQosCallbackError(final int qosCallbackId,
@QosCallbackException.ExceptionType final int exceptionType) {
runOnAgentConnection(qosCallbackId, "sendEventQosCallbackError: ",
ac -> {
ac.sendEventQosCallbackError(exceptionType);
handleUnregisterCallback(ac.getBinder(), false);
});
}
/**
* Unregisters all callbacks associated to this network agent
*
* Note: Must be called on the connectivity service handler thread
*
* @param network the network that was released
*/
public void handleNetworkReleased(@Nullable final Network network) {
// Iterate in reverse order as agent connections will be removed when unregistering
for (int i = mConnections.size() - 1; i >= 0; i--) {
final QosCallbackAgentConnection agentConnection = mConnections.get(i);
if (!agentConnection.getNetwork().equals(network)) continue;
agentConnection.sendEventQosCallbackError(
QosCallbackException.EX_TYPE_FILTER_NETWORK_RELEASED);
// Call unregister workflow w\o sending anything to agent since it is disconnected.
handleUnregisterCallback(agentConnection.getBinder(), false);
}
}
private interface AgentConnectionAction {
void execute(@NonNull QosCallbackAgentConnection agentConnection);
}
@Nullable
private void runOnAgentConnection(final int qosCallbackId,
@NonNull final String logPrefix,
@NonNull final AgentConnectionAction action) {
mConnectivityServiceHandler.post(() -> {
final int acIndex = CollectionUtils.indexOf(mConnections,
c -> c.getAgentCallbackId() == qosCallbackId);
if (acIndex == -1) {
loge(logPrefix + ": " + qosCallbackId + " missing callback id");
return;
}
action.execute(mConnections.get(acIndex));
});
}
private static void log(final String msg) {
Log.d(TAG, msg);
}
private static void logw(final String msg) {
Log.w(TAG, msg);
}
private static void loge(final String msg) {
Log.e(TAG, msg);
}
private static void logwtf(final String msg) {
Log.wtf(TAG, msg);
}
}