1 /*
2  * Copyright (C) 2016 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 static com.android.server.wifi.WifiSettingsConfigStore.WIFI_DEFAULT_COUNTRY_CODE;
20 
21 import android.annotation.NonNull;
22 import android.annotation.Nullable;
23 import android.content.Context;
24 import android.os.SystemProperties;
25 import android.telephony.TelephonyManager;
26 import android.text.TextUtils;
27 import android.util.ArrayMap;
28 import android.util.Log;
29 
30 import com.android.internal.annotations.VisibleForTesting;
31 import com.android.modules.utils.build.SdkLevel;
32 import com.android.wifi.resources.R;
33 
34 import java.io.FileDescriptor;
35 import java.io.PrintWriter;
36 import java.text.SimpleDateFormat;
37 import java.util.ArrayList;
38 import java.util.Date;
39 import java.util.List;
40 import java.util.Locale;
41 import java.util.Map;
42 import java.util.Set;
43 
44 /**
45  * Provide functions for making changes to WiFi country code.
46  * This Country Code is from MCC or phone default setting. This class sends Country Code
47  * to driver through wpa_supplicant when ClientModeImpl marks current state as ready
48  * using setReadyForChange(true).
49  */
50 public class WifiCountryCode {
51     private static final String TAG = "WifiCountryCode";
52     private static final String BOOT_DEFAULT_WIFI_COUNTRY_CODE = "ro.boot.wificountrycode";
53     private final Context mContext;
54     private final TelephonyManager mTelephonyManager;
55     private final ActiveModeWarden mActiveModeWarden;
56     private final WifiNative mWifiNative;
57     private final WifiSettingsConfigStore mSettingsConfigStore;
58     private List<ChangeListener> mListeners = new ArrayList<>();
59     private boolean DBG = false;
60     /**
61      * Map of active ClientModeManager instance to whether it is ready for country code change.
62      *
63      * - When a new ClientModeManager instance is created, it is added to this map and starts out
64      * ready for any country code changes (value = true).
65      * - When the ClientModeManager instance starts a connection attempt, it is marked not ready for
66      * country code changes (value = false).
67      * - When the ClientModeManager instance ends the connection, it is again marked ready for
68      * country code changes (value = true).
69      * - When the ClientModeManager instance is destroyed, it is removed from this map.
70      */
71     private final Map<ActiveModeManager, Boolean> mAmmToReadyForChangeMap =
72             new ArrayMap<>();
73     private static final SimpleDateFormat FORMATTER = new SimpleDateFormat("MM-dd HH:mm:ss.SSS");
74 
75     private String mTelephonyCountryCode = null;
76     private String mOverrideCountryCode = null;
77     private String mDriverCountryCode = null;
78     private String mReceivedDriverCountryCode = null;
79     private String mTelephonyCountryTimestamp = null;
80     private String mDriverCountryTimestamp = null;
81     private String mReadyTimestamp = null;
82 
83     private class ModeChangeCallbackInternal implements ActiveModeWarden.ModeChangeCallback {
84         @Override
onActiveModeManagerAdded(@onNull ActiveModeManager activeModeManager)85         public void onActiveModeManagerAdded(@NonNull ActiveModeManager activeModeManager) {
86             if (activeModeManager.getRole() instanceof ActiveModeManager.ClientRole
87                     || activeModeManager instanceof SoftApManager) {
88                 // Add this CMM for tracking. Interface is up and HAL is initialized at this point.
89                 // If this device runs the 1.5 HAL version, use the IWifiChip.setCountryCode()
90                 // to set the country code.
91                 mAmmToReadyForChangeMap.put(activeModeManager, true);
92                 evaluateAllCmmStateAndApplyIfAllReady();
93             }
94         }
95 
96         @Override
onActiveModeManagerRemoved(@onNull ActiveModeManager activeModeManager)97         public void onActiveModeManagerRemoved(@NonNull ActiveModeManager activeModeManager) {
98             if (mAmmToReadyForChangeMap.remove(activeModeManager) != null) {
99                 // Remove this CMM from tracking.
100                 evaluateAllCmmStateAndApplyIfAllReady();
101             }
102         }
103 
104         @Override
onActiveModeManagerRoleChanged(@onNull ActiveModeManager activeModeManager)105         public void onActiveModeManagerRoleChanged(@NonNull ActiveModeManager activeModeManager) {
106             if (activeModeManager.getRole() == ActiveModeManager.ROLE_CLIENT_PRIMARY) {
107                 // Set this CMM ready for change. This is needed to handle the transition from
108                 // ROLE_CLIENT_SCAN_ONLY to ROLE_CLIENT_PRIMARY on devices running older HAL
109                 // versions (since the IWifiChip.setCountryCode() was only added in the 1.5 HAL
110                 // version, before that we need to wait till supplicant is up for country code
111                 // change.
112                 mAmmToReadyForChangeMap.put(activeModeManager, true);
113                 evaluateAllCmmStateAndApplyIfAllReady();
114             }
115         }
116     }
117 
118     private class ClientModeListenerInternal implements ClientModeImplListener {
119         @Override
onConnectionStart(@onNull ConcreteClientModeManager clientModeManager)120         public void onConnectionStart(@NonNull ConcreteClientModeManager clientModeManager) {
121             if (mAmmToReadyForChangeMap.get(clientModeManager) == null) {
122                 Log.wtf(TAG, "Connection start received from unknown client mode manager");
123             }
124             // connection start. CMM not ready for country code change.
125             mAmmToReadyForChangeMap.put(clientModeManager, false);
126             evaluateAllCmmStateAndApplyIfAllReady();
127         }
128 
129         @Override
onConnectionEnd(@onNull ConcreteClientModeManager clientModeManager)130         public void onConnectionEnd(@NonNull ConcreteClientModeManager clientModeManager) {
131             if (mAmmToReadyForChangeMap.get(clientModeManager) == null) {
132                 Log.wtf(TAG, "Connection end received from unknown client mode manager");
133             }
134             // connection end. CMM ready for country code change.
135             mAmmToReadyForChangeMap.put(clientModeManager, true);
136             evaluateAllCmmStateAndApplyIfAllReady();
137         }
138 
139     }
140 
141     private class CountryChangeListenerInternal implements ChangeListener {
142         @Override
onDriverCountryCodeChanged(String country)143         public void onDriverCountryCodeChanged(String country) {
144             if (TextUtils.equals(country, mReceivedDriverCountryCode)) {
145                 return;
146             }
147             Log.i(TAG, "Receive onDriverCountryCodeChanged " + country);
148             mReceivedDriverCountryCode = country;
149             updateDriverCountryCodeAndNotifyListener(country);
150         }
151 
152         @Override
onSetCountryCodeSucceeded(String country)153         public void onSetCountryCodeSucceeded(String country) {
154             Log.i(TAG, "Receive onSetCountryCodeSucceeded " + country);
155             // The driver country code updated, don't need to trigger again.
156             if (TextUtils.equals(country, mReceivedDriverCountryCode)) {
157                 return;
158             }
159             updateDriverCountryCodeAndNotifyListener(country);
160         }
161     }
162 
WifiCountryCode( Context context, ActiveModeWarden activeModeWarden, ClientModeImplMonitor clientModeImplMonitor, WifiNative wifiNative, @NonNull WifiSettingsConfigStore settingsConfigStore)163     public WifiCountryCode(
164             Context context,
165             ActiveModeWarden activeModeWarden,
166             ClientModeImplMonitor clientModeImplMonitor,
167             WifiNative wifiNative,
168             @NonNull WifiSettingsConfigStore settingsConfigStore) {
169         mContext = context;
170         mTelephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
171         mActiveModeWarden = activeModeWarden;
172         mWifiNative = wifiNative;
173         mSettingsConfigStore = settingsConfigStore;
174 
175         mActiveModeWarden.registerModeChangeCallback(new ModeChangeCallbackInternal());
176         clientModeImplMonitor.registerListener(new ClientModeListenerInternal());
177         mWifiNative.registerCountryCodeEventListener(new CountryChangeListenerInternal());
178 
179         Log.d(TAG, "Default country code from system property "
180                 + BOOT_DEFAULT_WIFI_COUNTRY_CODE + " is " + getOemDefaultCountryCode());
181     }
182 
183     /**
184      * Default country code stored in system property
185      * @return Country code if available, null otherwise.
186      */
getOemDefaultCountryCode()187     public static String getOemDefaultCountryCode() {
188         String country = SystemProperties.get(BOOT_DEFAULT_WIFI_COUNTRY_CODE);
189         return WifiCountryCode.isValid(country) ? country.toUpperCase(Locale.US) : null;
190     }
191 
192     /**
193      * Is this a valid country code
194      * @param countryCode A 2-Character alphanumeric country code.
195      * @return true if the countryCode is valid, false otherwise.
196      */
isValid(String countryCode)197     public static boolean isValid(String countryCode) {
198         return countryCode != null && countryCode.length() == 2
199                 && countryCode.chars().allMatch(Character::isLetterOrDigit);
200     }
201 
202     /**
203      * The class for country code related change listener
204      */
205     public interface ChangeListener {
206         /**
207          * Called when receiving country code changed from driver.
208          */
onDriverCountryCodeChanged(String countryCode)209         void onDriverCountryCodeChanged(String countryCode);
210 
211         /**
212          * Called when country code set to native layer successful, framework sends event to
213          * force country code changed.
214          *
215          * Reason: The country code change listener from wificond rely on driver supported
216          * NL80211_CMD_REG_CHANGE/NL80211_CMD_WIPHY_REG_CHANGE. Trigger update country code
217          * to listener here for non-supported platform.
218          */
onSetCountryCodeSucceeded(String country)219         default void onSetCountryCodeSucceeded(String country) {}
220     }
221 
222     /**
223      * Register Country code changed listener.
224      */
registerListener(@onNull ChangeListener listener)225     public void registerListener(@NonNull ChangeListener listener) {
226         mListeners.add(listener);
227         if (mDriverCountryCode != null) {
228             listener.onDriverCountryCodeChanged(mDriverCountryCode);
229         }
230     }
231 
232     /**
233      * Enable verbose logging for WifiCountryCode.
234      */
enableVerboseLogging(boolean verbose)235     public void enableVerboseLogging(boolean verbose) {
236         DBG = verbose;
237     }
238 
initializeTelephonyCountryCodeIfNeeded()239     private void initializeTelephonyCountryCodeIfNeeded() {
240         // If we don't have telephony country code set yet, poll it.
241         if (mTelephonyCountryCode == null) {
242             Log.d(TAG, "Reading country code from telephony");
243             setTelephonyCountryCode(mTelephonyManager.getNetworkCountryIso());
244         }
245     }
246 
247     /**
248      * We call native code to request country code changes only if all {@link ClientModeManager}
249      * instances are ready for country code change. Country code is a chip level configuration and
250      * results in all the connections on the chip being disrupted.
251      *
252      * @return true if there are active CMM's and all are ready for country code change.
253      */
isReady()254     private boolean isReady() {
255         return !mAmmToReadyForChangeMap.isEmpty()
256                 && mAmmToReadyForChangeMap.values().stream().allMatch(r -> r);
257     }
258 
259     /**
260      * Check all active CMM instances and apply country code change if ready.
261      */
evaluateAllCmmStateAndApplyIfAllReady()262     private void evaluateAllCmmStateAndApplyIfAllReady() {
263         Log.d(TAG, "evaluateAllCmmStateAndApplyIfAllReady: " + mAmmToReadyForChangeMap);
264         if (isReady()) {
265             mReadyTimestamp = FORMATTER.format(new Date(System.currentTimeMillis()));
266             // We are ready to set country code now.
267             // We need to post pending country code request.
268             initializeTelephonyCountryCodeIfNeeded();
269             updateCountryCode();
270         }
271     }
272 
273     /**
274      * This call will override any existing country code.
275      * This is for test purpose only and we should disallow any update from
276      * telephony in this mode.
277      * @param countryCode A 2-Character alphanumeric country code.
278      */
setOverrideCountryCode(String countryCode)279     public synchronized void setOverrideCountryCode(String countryCode) {
280         if (TextUtils.isEmpty(countryCode)) {
281             Log.d(TAG, "Fail to override country code because"
282                     + "the received country code is empty");
283             return;
284         }
285         mOverrideCountryCode = countryCode.toUpperCase(Locale.US);
286 
287         // If wpa_supplicant is ready we set the country code now, otherwise it will be
288         // set once wpa_supplicant is ready.
289         if (isReady()) {
290             updateCountryCode();
291         } else {
292             Log.d(TAG, "skip update supplicant not ready yet");
293         }
294     }
295 
296     /**
297      * This is for clearing the country code previously set through #setOverrideCountryCode() method
298      */
clearOverrideCountryCode()299     public synchronized void clearOverrideCountryCode() {
300         mOverrideCountryCode = null;
301 
302         // If wpa_supplicant is ready we set the country code now, otherwise it will be
303         // set once wpa_supplicant is ready.
304         if (isReady()) {
305             updateCountryCode();
306         } else {
307             Log.d(TAG, "skip update supplicant not ready yet");
308         }
309     }
310 
setTelephonyCountryCode(String countryCode)311     private void setTelephonyCountryCode(String countryCode) {
312         Log.d(TAG, "Set telephony country code to: " + countryCode);
313         mTelephonyCountryTimestamp = FORMATTER.format(new Date(System.currentTimeMillis()));
314 
315         // Empty country code.
316         if (TextUtils.isEmpty(countryCode)) {
317             if (mContext.getResources()
318                         .getBoolean(R.bool.config_wifi_revert_country_code_on_cellular_loss)) {
319                 Log.d(TAG, "Received empty country code, reset to default country code");
320                 mTelephonyCountryCode = null;
321             }
322         } else {
323             mTelephonyCountryCode = countryCode.toUpperCase(Locale.US);
324         }
325     }
326 
327     /**
328      * Handle country code change request.
329      * @param countryCode The country code intended to set.
330      * This is supposed to be from Telephony service.
331      * otherwise we think it is from other applications.
332      * @return Returns true if the country code passed in is acceptable.
333      */
setTelephonyCountryCodeAndUpdate(String countryCode)334     public boolean setTelephonyCountryCodeAndUpdate(String countryCode) {
335         setTelephonyCountryCode(countryCode);
336         if (mOverrideCountryCode != null) {
337             Log.d(TAG, "Skip Telephony Country code update due to override country code set");
338             return false;
339         }
340         // If wpa_supplicant is ready we set the country code now, otherwise it will be
341         // set once wpa_supplicant is ready.
342         if (isReady()) {
343             updateCountryCode();
344         } else {
345             Log.d(TAG, "skip update supplicant not ready yet");
346         }
347 
348         return true;
349     }
350 
351     /**
352      * Method to get the Country Code that was sent to wpa_supplicant.
353      *
354      * @return Returns the local copy of the Country Code that was sent to the driver upon
355      * setReadyForChange(true).
356      * If wpa_supplicant was never started, this may be null even if Telephony reported a valid
357      * country code.
358      * Returns null if no Country Code was sent to driver.
359      */
360     @VisibleForTesting
getCountryCodeSentToDriver()361     public synchronized String getCountryCodeSentToDriver() {
362         return mDriverCountryCode;
363     }
364 
365     /**
366      * Method to return the currently reported Country Code resolved from various sources:
367      * e.g. default country code, cellular network country code, country code override, etc.
368      *
369      * @return The current Wifi Country Code resolved from various sources. Returns null when there
370      * is no Country Code available.
371      */
372     @Nullable
getCountryCode()373     public synchronized String getCountryCode() {
374         initializeTelephonyCountryCodeIfNeeded();
375         return pickCountryCode();
376     }
377 
378     /**
379      * set default country code
380      * @param countryCode A 2-Character alphanumeric country code.
381      */
setDefaultCountryCode(String countryCode)382     public synchronized void setDefaultCountryCode(String countryCode) {
383         if (TextUtils.isEmpty(countryCode)) {
384             Log.d(TAG, "Fail to set default country code because the country code is empty");
385             return;
386         }
387 
388         mSettingsConfigStore.put(WIFI_DEFAULT_COUNTRY_CODE,
389                 countryCode.toUpperCase(Locale.US));
390         Log.i(TAG, "Default country code updated in config store: " + countryCode);
391 
392         // If wpa_supplicant is ready we set the country code now, otherwise it will be
393         // set once wpa_supplicant is ready.
394         if (isReady()) {
395             updateCountryCode();
396         } else {
397             Log.d(TAG, "skip update supplicant not ready yet");
398         }
399     }
400 
401     /**
402      * Method to dump the current state of this WifiCounrtyCode object.
403      */
dump(FileDescriptor fd, PrintWriter pw, String[] args)404     public synchronized void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
405         pw.println("mRevertCountryCodeOnCellularLoss: "
406                 + mContext.getResources().getBoolean(
407                         R.bool.config_wifi_revert_country_code_on_cellular_loss));
408         pw.println("DefaultCountryCode(system property): " + getOemDefaultCountryCode());
409         pw.println("DefaultCountryCode(config store): "
410                 + mSettingsConfigStore.get(WIFI_DEFAULT_COUNTRY_CODE));
411         pw.println("mDriverCountryCode: " + mDriverCountryCode);
412         pw.println("mTelephonyCountryCode: " + mTelephonyCountryCode);
413         pw.println("mTelephonyCountryTimestamp: " + mTelephonyCountryTimestamp);
414         pw.println("mOverrideCountryCode: " + mOverrideCountryCode);
415         pw.println("mDriverCountryTimestamp: " + mDriverCountryTimestamp);
416         pw.println("mReadyTimestamp: " + mReadyTimestamp);
417         pw.println("isReady: " + isReady());
418         pw.println("mAmmToReadyForChangeMap: " + mAmmToReadyForChangeMap);
419     }
420 
updateCountryCode()421     private void updateCountryCode() {
422         String country = pickCountryCode();
423         Log.d(TAG, "updateCountryCode to " + country);
424 
425         // We do not check if the country code equals the current one.
426         // There are two reasons:
427         // 1. Wpa supplicant may silently modify the country code.
428         // 2. If Wifi restarted therefore wpa_supplicant also restarted,
429         // the country code counld be reset to '00' by wpa_supplicant.
430         if (country != null) {
431             setCountryCodeNative(country);
432         }
433         // We do not set country code if there is no candidate. This is reasonable
434         // because wpa_supplicant usually starts with an international safe country
435         // code setting: '00'.
436     }
437 
pickCountryCode()438     private String pickCountryCode() {
439         if (mOverrideCountryCode != null) {
440             return mOverrideCountryCode;
441         }
442         if (mTelephonyCountryCode != null) {
443             return mTelephonyCountryCode;
444         }
445         return mSettingsConfigStore.get(WIFI_DEFAULT_COUNTRY_CODE);
446     }
447 
setCountryCodeNative(String country)448     private boolean setCountryCodeNative(String country) {
449         Set<ActiveModeManager> amms = mAmmToReadyForChangeMap.keySet();
450         boolean isConcreteClientModeManagerUpdated = false;
451         boolean anyAmmConfigured = false;
452         for (ActiveModeManager am : amms) {
453             if (!isConcreteClientModeManagerUpdated && am instanceof ConcreteClientModeManager) {
454                 // Set the country code using one of the active mode managers. Since
455                 // country code is a chip level global setting, it can be set as long
456                 // as there is at least one active interface to communicate to Wifi chip
457                 ConcreteClientModeManager cm = (ConcreteClientModeManager) am;
458                 if (!cm.setCountryCode(country)) {
459                     Log.d(TAG, "Failed to set country code (ConcreteClientModeManager) to "
460                             + country);
461                 } else {
462                     isConcreteClientModeManagerUpdated = true;
463                     anyAmmConfigured = true;
464                     // Start from S, frameworks support country code callback from wificond,
465                     // move "notify the lister" to CountryChangeListenerInternal.
466                     if (!SdkLevel.isAtLeastS()) {
467                         updateDriverCountryCodeAndNotifyListener(country);
468                     }
469                 }
470             } else if (am instanceof SoftApManager) {
471                 // The API:updateCountryCode in SoftApManager is asynchronous, it requires a new
472                 // callback support in S to trigger "updateDriverCountryCodeAndNotifyListener" for
473                 // the new S API: SoftApCapability#getSupportedChannelList(band).
474                 // It requires:
475                 // 1. a new overlay configuration which is introduced from S.
476                 // 2. wificond support in S for S API: SoftApCapability#getSupportedChannelList
477                 // Any case if device supported to set country code in R,
478                 // the new S API: SoftApCapability#getSupportedChannelList(band) still doesn't work
479                 // normally in R build when wifi disabled.
480                 SoftApManager sm = (SoftApManager) am;
481                 if (!sm.updateCountryCode(country)) {
482                     Log.d(TAG, "Can't set country code (SoftApManager) to "
483                             + country + " (Device doesn't support it)");
484                 } else {
485                     anyAmmConfigured = true;
486                 }
487             }
488         }
489         return anyAmmConfigured;
490     }
491 
updateDriverCountryCodeAndNotifyListener(String country)492     private void updateDriverCountryCodeAndNotifyListener(String country) {
493         mDriverCountryTimestamp = FORMATTER.format(new Date(System.currentTimeMillis()));
494         mDriverCountryCode = country;
495         for (ChangeListener listener : mListeners) {
496             listener.onDriverCountryCodeChanged(country);
497         }
498     }
499 }
500 
501