1 /*
2  * Copyright (C) 2023 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.server.wifi;
18 
19 import android.annotation.Nullable;
20 import android.util.Log;
21 
22 import com.android.server.wifi.hal.WifiChip;
23 
24 import org.json.JSONArray;
25 import org.json.JSONException;
26 import org.json.JSONObject;
27 
28 import java.time.Instant;
29 import java.util.ArrayList;
30 import java.util.List;
31 
32 /**
33  * This class stores information about the response of the AFC Server to a query
34  */
35 public class AfcServerResponse {
36     private static final String TAG = "WifiAfcServerResponse";
37     static final int SUCCESS_AFC_RESPONSE_CODE = 0;
38     static final int SUCCESS_HTTP_RESPONSE_CODE = 200;
39 
40     /*
41      * The value represented by the AFC response code is mapped out as follows, taken from the AFC
42      * server specs:
43      * -1: General Failure
44      * 0: SUCCESS
45      * 100 – 199: General errors related to the protocol
46      * 300 – 399: Error events specific to message exchanges for the Available Spectrum Inquiry
47      */
48     private int mAfcResponseCode;
49     // HTTP response code is 200 if the request succeeded, otherwise the request failed
50     private int mHttpResponseCode;
51     private String mAfcResponseDescription;
52     private WifiChip.AfcChannelAllowance mAfcChannelAllowance;
53 
54     /**
55      * Parses the spectrum inquiry response object received from the AFC server into an
56      * AfcServerResponse object. Returns null if the available spectrum inquiry response is
57      * incorrectly formatted.
58      *
59      * @param httpResponseCode the HTTP response code of the AFC server.
60      * @param spectrumInquiryResponse the available spectrum inquiry response to parse.
61      * @return the parsed response object as an AfcServerResponse, or null if the input JSON is
62      * incorrectly formatted.
63      */
fromSpectrumInquiryResponse(int httpResponseCode, JSONObject spectrumInquiryResponse)64     public static AfcServerResponse fromSpectrumInquiryResponse(int httpResponseCode,
65             JSONObject spectrumInquiryResponse) {
66         AfcServerResponse afcServerResponse = new AfcServerResponse();
67 
68         if (spectrumInquiryResponse == null) {
69             return null;
70         }
71 
72         afcServerResponse.setHttpResponseCode(httpResponseCode);
73         afcServerResponse.setAfcResponseCode(parseAfcResponseCode(spectrumInquiryResponse));
74         afcServerResponse.setAfcResponseDescription(getResponseShortDescriptionFromJSON(
75                 spectrumInquiryResponse));
76 
77         // no need to keep parsing if either the AFC or HTTP codes indicate a failed request
78         if (afcServerResponse.mAfcResponseCode != SUCCESS_AFC_RESPONSE_CODE
79                 || afcServerResponse.mHttpResponseCode != SUCCESS_HTTP_RESPONSE_CODE) {
80             return afcServerResponse;
81         }
82 
83         // parse the available frequencies and channels, and their expiration time
84         try {
85             WifiChip.AfcChannelAllowance afcChannelAllowance = new WifiChip.AfcChannelAllowance();
86             afcChannelAllowance.availableAfcFrequencyInfos = parseAvailableFrequencyInfo(
87                     spectrumInquiryResponse.optJSONArray("availableFrequencyInfo"));
88             afcChannelAllowance.availableAfcChannelInfos = parseAvailableChannelInfo(
89                     spectrumInquiryResponse.optJSONArray("availableChannelInfo"));
90 
91             // expiry time is required as we know the response was successful
92             String availabilityExpireTimeString = spectrumInquiryResponse.getString(
93                     "availabilityExpireTime");
94             afcChannelAllowance.availabilityExpireTimeMs =
95                     convertExpireTimeStringToTimestamp(availabilityExpireTimeString);
96 
97             afcServerResponse.setAfcChannelAllowance(afcChannelAllowance);
98         } catch (JSONException | NullPointerException e) {
99             Log.e(TAG, e.toString());
100             return null;
101         }
102         return afcServerResponse;
103     }
104 
105     /**
106      * Parses the available frequencies of an AfcServerResponse.
107      */
parseAvailableFrequencyInfo( JSONArray availableFrequencyInfoJSON)108     private static List<WifiChip.AvailableAfcFrequencyInfo> parseAvailableFrequencyInfo(
109             JSONArray availableFrequencyInfoJSON) {
110         if (availableFrequencyInfoJSON != null) {
111             List<WifiChip.AvailableAfcFrequencyInfo> availableFrequencyInfo = new ArrayList<>();
112 
113             try {
114                 for (int i = 0; i < availableFrequencyInfoJSON.length(); ++i) {
115                     JSONObject frequencyRange = availableFrequencyInfoJSON.getJSONObject(i)
116                             .getJSONObject("frequencyRange");
117 
118                     WifiChip.AvailableAfcFrequencyInfo availableFrequency = new WifiChip
119                             .AvailableAfcFrequencyInfo();
120                     availableFrequency.startFrequencyMhz =
121                             frequencyRange.getInt("lowFrequency");
122                     availableFrequency.endFrequencyMhz =
123                             frequencyRange.getInt("highFrequency");
124                     availableFrequency.maxPsdDbmPerMhz =
125                             availableFrequencyInfoJSON.getJSONObject(i).getInt("maxPsd");
126 
127                     availableFrequencyInfo.add(availableFrequency);
128                 }
129                 return availableFrequencyInfo;
130             } catch (JSONException | NullPointerException e) {
131                 Log.e(TAG, "Error occurred when parsing available AFC frequency info.");
132             }
133         }
134         return null;
135     }
136 
137     /**
138      * Parses the available channels of an AfcServerResponse.
139      */
parseAvailableChannelInfo( JSONArray availableChannelInfoJSON)140     private static List<WifiChip.AvailableAfcChannelInfo> parseAvailableChannelInfo(
141             JSONArray availableChannelInfoJSON) {
142         if (availableChannelInfoJSON != null) {
143             List<WifiChip.AvailableAfcChannelInfo> availableChannelInfo = new ArrayList<>();
144 
145             try {
146                 for (int i = 0; i < availableChannelInfoJSON.length(); ++i) {
147                     int globalOperatingClass = availableChannelInfoJSON
148                             .getJSONObject(i).getInt("globalOperatingClass");
149 
150                     for (int j = 0; j < availableChannelInfoJSON.getJSONObject(i).getJSONArray(
151                             "channelCfi").length(); ++j) {
152                         WifiChip.AvailableAfcChannelInfo availableChannel = new WifiChip
153                                 .AvailableAfcChannelInfo();
154 
155                         availableChannel.globalOperatingClass = globalOperatingClass;
156                         availableChannel.channelCfi = availableChannelInfoJSON.getJSONObject(i)
157                                         .getJSONArray("channelCfi").getInt(j);
158                         availableChannel.maxEirpDbm = availableChannelInfoJSON.getJSONObject(i)
159                                         .getJSONArray("maxEirp").getInt(j);
160                         availableChannelInfo.add(availableChannel);
161                     }
162                 }
163                 return availableChannelInfo;
164             } catch (JSONException | NullPointerException e) {
165                 Log.e(TAG, "Error occurred when parsing available AFC channel info.");
166             }
167         }
168         return null;
169     }
170 
171     /**
172      * Converts a date string in the format: YYYY-MM-DDThh:mm:ssZ to a Unix timestamp.
173      *
174      * @param availabilityExpireTime the expiration time of the AFC frequency and channel
175      * availability.
176      * @return the expiration time as a Unix timestamp in milliseconds.
177      */
convertExpireTimeStringToTimestamp(String availabilityExpireTime)178     static long convertExpireTimeStringToTimestamp(String availabilityExpireTime) {
179         // Sometimes the expiration time is incorrectly formatted and includes appended
180         // milliseconds after the seconds section, so we want to remove these before converting to
181         // a timestamp
182         if (availabilityExpireTime.length() > 20) {
183             availabilityExpireTime = availabilityExpireTime.substring(0, 19) + "Z";
184         }
185 
186         return Instant.parse(availabilityExpireTime).toEpochMilli();
187     }
188 
189     /**
190      * Returns the AFC response code for an available spectrum inquiry response or Integer.MIN_VALUE
191      * if it does not exist.
192      */
parseAfcResponseCode(JSONObject spectrumInquiryResponse)193     static int parseAfcResponseCode(JSONObject spectrumInquiryResponse) {
194         try {
195             // parse the response code and return it. Throws an error if a field does not exist.
196             int responseCode = spectrumInquiryResponse.getJSONObject("response")
197                     .getInt("responseCode");
198 
199             return responseCode;
200         } catch (JSONException | NullPointerException e) {
201             Log.e(TAG, "The available spectrum inquiry response does not contain a response "
202                     + "code field.");
203 
204             // Currently the internal Google AFC server doesn't provide an AFC response code of 0
205             // for successful queries.
206             // TODO (b/294594249): If the server is modified to correctly provide an AFC response
207             //  code of 0 upon a successful query, handle this case accordingly.
208             return 0;
209         }
210     }
211 
212     /**
213      * Returns the short description of an available spectrum inquiry response, or an empty string
214      * if it does not exist.
215      */
getResponseShortDescriptionFromJSON(JSONObject spectrumInquiryResponse)216     static String getResponseShortDescriptionFromJSON(JSONObject spectrumInquiryResponse) {
217         try {
218             // parse and return the response description. Throws an error if a field does not exist.
219             return spectrumInquiryResponse.getJSONObject("response").getString("shortDescription");
220         } catch (JSONException | NullPointerException e) {
221             return "";
222         }
223     }
224 
225     /**
226      * Sets the AFC response code associated with an available spectrum inquiry response.
227      */
setAfcResponseCode(int afcResponseCode)228     public void setAfcResponseCode(int afcResponseCode) {
229         mAfcResponseCode = afcResponseCode;
230     }
231 
232     /**
233      * Sets the description associated with an available spectrum inquiry response.
234      */
setAfcResponseDescription(String afcResponseDescription)235     public void setAfcResponseDescription(String afcResponseDescription) {
236         mAfcResponseDescription = afcResponseDescription;
237     }
238 
239     /**
240      * Sets the AFC channel and frequency allowance.
241      */
setAfcChannelAllowance(WifiChip.AfcChannelAllowance afcChannelAllowance)242     public void setAfcChannelAllowance(WifiChip.AfcChannelAllowance afcChannelAllowance) {
243         mAfcChannelAllowance = afcChannelAllowance;
244     }
245 
246     /**
247      * Sets the AFC server HTTP response code.
248      */
setHttpResponseCode(int httpResponseCode)249     public void setHttpResponseCode(int httpResponseCode) {
250         mHttpResponseCode = httpResponseCode;
251     }
252 
253     /**
254      * Returns the AFC response code of the available spectrum inquiry response.
255      */
getAfcResponseCode()256     public int getAfcResponseCode() {
257         return mAfcResponseCode;
258     }
259 
260     /**
261      * Returns the HTTP response code of the AFC server's response.
262      */
getHttpResponseCode()263     public int getHttpResponseCode() {
264         return mHttpResponseCode;
265     }
266 
267     /**
268      * Returns the AFC response description.
269      */
getAfcResponseDescription()270     public String getAfcResponseDescription() {
271         return mAfcResponseDescription;
272     }
273 
274     /**
275      * Returns the available channels and frequencies received from the AFC query.
276      */
277     @Nullable
getAfcChannelAllowance()278     public WifiChip.AfcChannelAllowance getAfcChannelAllowance() {
279         return mAfcChannelAllowance;
280     }
281 
282     @Override
toString()283     public String toString() {
284         StringBuilder sbuf = new StringBuilder();
285 
286         if (mAfcChannelAllowance != null) {
287             // print frequency info
288             if (mAfcChannelAllowance.availableAfcFrequencyInfos != null) {
289                 sbuf.append("Available frequencies:\n");
290 
291                 for (WifiChip.AvailableAfcFrequencyInfo frequency :
292                         mAfcChannelAllowance.availableAfcFrequencyInfos) {
293                     sbuf.append("   [" + frequency.startFrequencyMhz + ", "
294                             + frequency.endFrequencyMhz + "], maxPsd: " + frequency.maxPsdDbmPerMhz
295                             + "dBm per MHZ\n");
296                 }
297             }
298 
299             // print channel info
300             if (mAfcChannelAllowance.availableAfcChannelInfos != null) {
301                 sbuf.append("Available channels:\n");
302 
303                 for (WifiChip.AvailableAfcChannelInfo channel :
304                         mAfcChannelAllowance.availableAfcChannelInfos) {
305                     sbuf.append("   Global operating class: " + channel.globalOperatingClass
306                             + ", Cfi: " + channel.channelCfi + " maxEirp: " + channel.maxEirpDbm
307                             + "dBm\n");
308                 }
309             }
310 
311             sbuf.append("Availability expiration time (ms): "
312                     + mAfcChannelAllowance.availabilityExpireTimeMs + "\n");
313         }
314 
315         sbuf.append("HTTP response code: " + mHttpResponseCode + "\n");
316         sbuf.append("AFC response code: " + mAfcResponseCode + "\n");
317         sbuf.append("AFC response short description: " + mAfcResponseDescription + "\n");
318         return sbuf.toString();
319     }
320 }
321