1 /* 2 * Copyright (C) 2025 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.networkstack.tethering; 18 19 import static com.android.networkstack.tethering.util.TetheringUtils.createPlaceholderRequest; 20 21 import android.net.TetheringManager.TetheringRequest; 22 import android.net.ip.IpServer; 23 import android.util.ArrayMap; 24 import android.util.Log; 25 26 import androidx.annotation.NonNull; 27 import androidx.annotation.Nullable; 28 29 import com.android.internal.annotations.VisibleForTesting; 30 31 import java.util.ArrayList; 32 import java.util.List; 33 import java.util.Map; 34 35 /** 36 * Helper class to keep track of tethering requests. 37 * The intended usage of this class is 38 * 1) Add a pending request with {@link #addPendingRequest(TetheringRequest)} before asking the link 39 * layer to start. 40 * 2) When the link layer is up, use {@link #getOrCreatePendingRequest(int)} to get a request to 41 * start IP serving with. 42 * 3) Remove pending request with {@link #removePendingRequest(TetheringRequest)}. 43 * Note: This class is not thread-safe. 44 */ 45 public class RequestTracker { 46 private static final String TAG = RequestTracker.class.getSimpleName(); 47 48 @NonNull 49 private final boolean mUseFuzzyMatching; 50 RequestTracker(boolean useFuzzyMatching)51 public RequestTracker(boolean useFuzzyMatching) { 52 mUseFuzzyMatching = useFuzzyMatching; 53 } 54 55 public enum AddResult { 56 /** 57 * Request was successfully added 58 */ 59 SUCCESS, 60 /** 61 * Failure indicating that the request could not be added due to a request of the same type 62 * with conflicting parameters already pending. If so, we must stop tethering for the 63 * pending request before trying to add the result again. Only returned on V-. 64 */ 65 FAILURE_DUPLICATE_REQUEST_RESTART, 66 /** 67 * Failure indicating that the request could not be added due to a fuzzy-matched request 68 * already pending or serving. Only returned on B+. 69 */ 70 FAILURE_DUPLICATE_REQUEST_ERROR, 71 } 72 73 /** 74 * List of pending requests added by {@link #addPendingRequest(TetheringRequest)} 75 * There can be only one per type, since we remove every request of the 76 * same type when we add a request. 77 */ 78 private final List<TetheringRequest> mPendingRequests = new ArrayList<>(); 79 /** 80 * List of serving requests added by 81 * {@link #promoteRequestToServing(IpServer, TetheringRequest)}. 82 */ 83 private final Map<IpServer, TetheringRequest> mServingRequests = new ArrayMap<>(); 84 85 @VisibleForTesting getPendingTetheringRequests()86 List<TetheringRequest> getPendingTetheringRequests() { 87 return new ArrayList<>(mPendingRequests); 88 } 89 90 /** 91 * Adds a pending request or fails with FAILURE_CONFLICTING_REQUEST_FAIL if the request 92 * fuzzy-matches an existing request (either pending or serving). 93 */ addPendingRequestFuzzyMatched(@onNull final TetheringRequest newRequest)94 public AddResult addPendingRequestFuzzyMatched(@NonNull final TetheringRequest newRequest) { 95 List<TetheringRequest> existingRequests = new ArrayList<>(); 96 existingRequests.addAll(mServingRequests.values()); 97 existingRequests.addAll(mPendingRequests); 98 for (TetheringRequest request : existingRequests) { 99 if (request.fuzzyMatches(newRequest)) { 100 Log.i(TAG, "Cannot add pending request due to existing fuzzy-matched " 101 + "request: " + request); 102 return AddResult.FAILURE_DUPLICATE_REQUEST_ERROR; 103 } 104 } 105 106 mPendingRequests.add(newRequest); 107 return AddResult.SUCCESS; 108 } 109 110 /** 111 * Add a pending request and listener. The request should be added before asking the link layer 112 * to start, and should be retrieved with {@link #getNextPendingRequest(int)} once the link 113 * layer comes up. The result of the add operation will be returned as an AddResult code. 114 */ addPendingRequest(@onNull final TetheringRequest newRequest)115 public AddResult addPendingRequest(@NonNull final TetheringRequest newRequest) { 116 if (mUseFuzzyMatching) { 117 return addPendingRequestFuzzyMatched(newRequest); 118 } 119 120 // Check the existing requests to see if it is OK to add the new request. 121 for (TetheringRequest existingRequest : mPendingRequests) { 122 if (existingRequest.getTetheringType() != newRequest.getTetheringType()) { 123 continue; 124 } 125 126 // Can't add request if there's a request of the same type with different 127 // parameters. 128 if (!existingRequest.equalsIgnoreUidPackage(newRequest)) { 129 return AddResult.FAILURE_DUPLICATE_REQUEST_RESTART; 130 } 131 } 132 133 // Remove the existing pending request of the same type. We already filter out for 134 // conflicting parameters above, so these would have been equivalent anyway (except for 135 // UID). 136 removeAllPendingRequests(newRequest.getTetheringType()); 137 mPendingRequests.add(newRequest); 138 return AddResult.SUCCESS; 139 } 140 141 /** 142 * Gets the next pending TetheringRequest of a given type, or creates a placeholder request if 143 * there are none. 144 * Note: There are edge cases where the pending request is absent and we must temporarily 145 * synthesize a placeholder request, such as if stopTethering was called before link 146 * layer went up, or if the link layer goes up without us poking it (e.g. adb shell 147 * cmd wifi start-softap). These placeholder requests only specify the tethering type 148 * and the default connectivity scope. 149 */ 150 @NonNull getOrCreatePendingRequest(int type)151 public TetheringRequest getOrCreatePendingRequest(int type) { 152 TetheringRequest pending = getNextPendingRequest(type); 153 if (pending != null) return pending; 154 155 Log.w(TAG, "No pending TetheringRequest for type " + type + " found, creating a" 156 + " placeholder request"); 157 return createPlaceholderRequest(type); 158 } 159 160 /** 161 * Same as {@link #getOrCreatePendingRequest(int)} but returns {@code null} if there's no 162 * pending request found. 163 * 164 * @param type Tethering type of the pending request 165 * @return pending request or {@code null} if there are none. 166 */ 167 @Nullable getNextPendingRequest(int type)168 public TetheringRequest getNextPendingRequest(int type) { 169 for (TetheringRequest request : mPendingRequests) { 170 if (request.getTetheringType() == type) return request; 171 } 172 return null; 173 } 174 175 /** 176 * Removes all pending requests of the given tethering type. 177 * 178 * @param type Tethering type 179 */ removeAllPendingRequests(final int type)180 public void removeAllPendingRequests(final int type) { 181 mPendingRequests.removeIf(r -> r.getTetheringType() == type); 182 } 183 184 /** 185 * Removes a specific pending request. 186 * 187 * Note: For V-, this will be the same as removeAllPendingRequests to align with historical 188 * behavior. 189 * 190 * @param request Request to be removed 191 */ removePendingRequest(@onNull TetheringRequest request)192 public void removePendingRequest(@NonNull TetheringRequest request) { 193 if (!mUseFuzzyMatching) { 194 // Remove all requests of the same type to match the historical behavior. 195 removeAllPendingRequests(request.getTetheringType()); 196 return; 197 } 198 199 mPendingRequests.removeIf(r -> r.equals(request)); 200 } 201 202 /** 203 * Removes a tethering request from the pending list and promotes it to serving with the 204 * IpServer that is using it. 205 * Note: If mUseFuzzyMatching is false, then the request will be removed from the pending list, 206 * but it will not be added to serving list. 207 */ promoteRequestToServing(@onNull final IpServer ipServer, @NonNull final TetheringRequest tetheringRequest)208 public void promoteRequestToServing(@NonNull final IpServer ipServer, 209 @NonNull final TetheringRequest tetheringRequest) { 210 removePendingRequest(tetheringRequest); 211 if (!mUseFuzzyMatching) return; 212 mServingRequests.put(ipServer, tetheringRequest); 213 } 214 215 216 /** 217 * Returns the serving request tied to the given IpServer, or null if there is none. 218 * Note: If mUseFuzzyMatching is false, then this will always return null. 219 */ 220 @Nullable getServingRequest(@onNull final IpServer ipServer)221 public TetheringRequest getServingRequest(@NonNull final IpServer ipServer) { 222 return mServingRequests.get(ipServer); 223 } 224 225 /** 226 * Removes the serving request tied to the given IpServer. 227 * Note: If mUseFuzzyMatching is false, then this is a no-op since serving requests are unused 228 * for that configuration. 229 */ removeServingRequest(@onNull final IpServer ipServer)230 public void removeServingRequest(@NonNull final IpServer ipServer) { 231 mServingRequests.remove(ipServer); 232 } 233 234 /** 235 * Removes all serving requests of the given tethering type. 236 * 237 * @param type Tethering type 238 */ removeAllServingRequests(final int type)239 public void removeAllServingRequests(final int type) { 240 mServingRequests.entrySet().removeIf(e -> e.getValue().getTetheringType() == type); 241 } 242 243 @VisibleForTesting getServingTetheringRequests()244 List<TetheringRequest> getServingTetheringRequests() { 245 return new ArrayList<>(mServingRequests.values()); 246 } 247 248 /** 249 * Returns an existing (pending or serving) request that fuzzy matches the given request. 250 * Optionally specify matchUid to only return requests with the same uid. 251 */ findFuzzyMatchedRequest( @onNull final TetheringRequest tetheringRequest, boolean matchUid)252 public TetheringRequest findFuzzyMatchedRequest( 253 @NonNull final TetheringRequest tetheringRequest, boolean matchUid) { 254 List<TetheringRequest> allRequests = new ArrayList<>(); 255 allRequests.addAll(getPendingTetheringRequests()); 256 allRequests.addAll(mServingRequests.values()); 257 for (TetheringRequest request : allRequests) { 258 if (!request.fuzzyMatches(tetheringRequest)) continue; 259 if (matchUid && tetheringRequest.getUid() != request.getUid()) continue; 260 return request; 261 } 262 return null; 263 } 264 } 265