1 /* 2 * Copyright (C) 2022 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.content.BroadcastReceiver; 20 import android.content.Context; 21 import android.content.Intent; 22 import android.content.IntentFilter; 23 import android.content.res.Resources; 24 import android.net.MacAddress; 25 import android.net.wifi.WifiConfiguration; 26 import android.net.wifi.WifiContext; 27 import android.net.wifi.WifiSsid; 28 import android.os.Handler; 29 import android.text.TextUtils; 30 import android.util.ArrayMap; 31 import android.util.ArraySet; 32 import android.util.Log; 33 import android.util.Pair; 34 35 import androidx.annotation.NonNull; 36 import androidx.annotation.Nullable; 37 import androidx.annotation.VisibleForTesting; 38 39 import com.android.wifi.resources.R; 40 41 import java.io.PrintWriter; 42 import java.nio.ByteBuffer; 43 import java.nio.charset.CharacterCodingException; 44 import java.nio.charset.Charset; 45 import java.nio.charset.CharsetDecoder; 46 import java.nio.charset.CharsetEncoder; 47 import java.nio.charset.CodingErrorAction; 48 import java.nio.charset.StandardCharsets; 49 import java.util.ArrayList; 50 import java.util.Arrays; 51 import java.util.HashMap; 52 import java.util.List; 53 import java.util.Locale; 54 import java.util.Map; 55 import java.util.Set; 56 57 /** 58 * Utility class to translate between non-UTF-8 SSIDs in the Native layer and UTF-8 SSIDs in the 59 * Framework for SSID Translation. 60 * 61 * SSID Translation is intended to provide backwards compatibility with legacy apps that do not 62 * recognize non-UTF-8 SSIDs. Translating non-UTF-8 SSIDs from Native->Framework into UTF-8 63 * (and back) will effectively switch all non-UTF-8 APs into UTF-8 APs from the perspective of the 64 * Framework and apps. 65 * 66 * The list of alternate non-UTF-8 character sets to translate is defined in 67 * R.string.config_wifiCharsetsForSsidTranslation. 68 * 69 * This class is thread-safe. 70 */ 71 public class SsidTranslator { 72 private static final String TAG = "SsidTranslator"; 73 private static final String LOCALE_LANGUAGE_ALL = "all"; 74 @VisibleForTesting static final long BSSID_CACHE_TIMEOUT_MS = 30_000; 75 private final @NonNull WifiContext mWifiContext; 76 private final @NonNull Handler mWifiHandler; 77 78 private @Nullable Charset mCurrentLocaleAlternateCharset = null; 79 private @NonNull Map<String, Charset> mCharsetsPerLocaleLanguage = new HashMap<>(); 80 private @NonNull Map<String, Charset> mMockCharsetsPerLocaleLanguage = new HashMap<>(); 81 82 // Maps a translated SSID to all of its BSSIDs using the alternate Charset. 83 private @NonNull Map<WifiSsid, Set<MacAddress>> mTranslatedBssids = new ArrayMap<>(); 84 // Maps a translated SSID to all of its BSSIDs not using the alternate Charset. 85 private @NonNull Map<WifiSsid, Set<MacAddress>> mUntranslatedBssids = new ArrayMap<>(); 86 private final Map<Pair<WifiSsid, MacAddress>, Runnable> mUntranslatedBssidTimeoutRunnables = 87 new ArrayMap<>(); 88 private final Map<Pair<WifiSsid, MacAddress>, Runnable> mTranslatedBssidTimeoutRunnables = 89 new ArrayMap<>(); 90 private final Map<String, WifiSsid> mTranslatedSsidForStaIface = new ArrayMap<>(); 91 SsidTranslator(@onNull WifiContext wifiContext, @NonNull Handler wifiHandler)92 public SsidTranslator(@NonNull WifiContext wifiContext, @NonNull Handler wifiHandler) { 93 mWifiContext = wifiContext; 94 mWifiHandler = wifiHandler; 95 } 96 97 /** 98 * Initializes SsidTranslator after boot completes to get boot-dependent resources. 99 */ handleBootCompleted()100 public synchronized void handleBootCompleted() { 101 Resources res = mWifiContext.getResources(); 102 if (res == null) { 103 Log.e(TAG, "Boot completed but could not get resources!"); 104 return; 105 } 106 String[] charsetCsvs = res.getStringArray( 107 R.array.config_wifiCharsetsForSsidTranslation); 108 if (charsetCsvs == null) { 109 return; 110 } 111 for (String charsetCsv : charsetCsvs) { 112 String[] charsetNames = charsetCsv.split(","); 113 if (charsetNames.length != 2) { 114 continue; 115 } 116 String localeLanguage = charsetNames[0]; 117 Charset charset; 118 try { 119 charset = Charset.forName(charsetNames[1]); 120 } catch (IllegalArgumentException e) { 121 Log.e(TAG, "Could not find Charset with name " + charsetNames[1]); 122 continue; 123 } 124 mCharsetsPerLocaleLanguage.put(localeLanguage, charset); 125 } 126 mWifiContext.registerReceiver(new BroadcastReceiver() { 127 @Override 128 public void onReceive(Context context, Intent intent) { 129 if (!Intent.ACTION_LOCALE_CHANGED.equals(intent.getAction())) { 130 return; 131 } 132 updateCurrentLocaleCharset(); 133 } 134 }, new IntentFilter(Intent.ACTION_LOCALE_CHANGED), null, mWifiHandler); 135 updateCurrentLocaleCharset(); 136 } 137 138 /** Updates mCurrentLocaleCharset to the alternate charset of the current Locale language. */ updateCurrentLocaleCharset()139 private synchronized void updateCurrentLocaleCharset() { 140 // Clear existing Charset mappings. 141 for (Runnable runnable : mTranslatedBssidTimeoutRunnables.values()) { 142 mWifiHandler.removeCallbacks(runnable); 143 } 144 mTranslatedBssidTimeoutRunnables.clear(); 145 mTranslatedBssids.clear(); 146 for (Runnable runnable : mUntranslatedBssidTimeoutRunnables.values()) { 147 mWifiHandler.removeCallbacks(runnable); 148 } 149 mUntranslatedBssidTimeoutRunnables.clear(); 150 mUntranslatedBssids.clear(); 151 mCurrentLocaleAlternateCharset = null; 152 // Try to find the Charset for the specific language. 153 String language = null; 154 Resources res = mWifiContext.getResources(); 155 if (res != null) { 156 Locale locale = res.getConfiguration().getLocales().get(0); 157 if (locale != null) { 158 language = locale.getLanguage(); 159 } else { 160 Log.e(TAG, "Current Locale is null!"); 161 } 162 } else { 163 Log.e(TAG, "Could not get resources to update locale!"); 164 } 165 if (language != null) { 166 mCurrentLocaleAlternateCharset = mMockCharsetsPerLocaleLanguage.get(language); 167 if (mCurrentLocaleAlternateCharset == null) { 168 mCurrentLocaleAlternateCharset = mCharsetsPerLocaleLanguage.get(language); 169 } 170 } 171 // No Charset for the specific language, use the "all" charset if it exists. 172 if (mCurrentLocaleAlternateCharset == null) { 173 mCurrentLocaleAlternateCharset = 174 mMockCharsetsPerLocaleLanguage.get(LOCALE_LANGUAGE_ALL); 175 } 176 if (mCurrentLocaleAlternateCharset == null) { 177 mCurrentLocaleAlternateCharset = mCharsetsPerLocaleLanguage.get(LOCALE_LANGUAGE_ALL); 178 } 179 Log.i(TAG, "Locale language changed to " + language + ", alternate charset " 180 + "is now " + mCurrentLocaleAlternateCharset); 181 } 182 183 /** Translates an SSID from a source Charset to a target Charset */ translateSsid(@onNull WifiSsid ssid, @NonNull Charset sourceCharset, @NonNull Charset targetCharset)184 private WifiSsid translateSsid(@NonNull WifiSsid ssid, 185 @NonNull Charset sourceCharset, 186 @NonNull Charset targetCharset) { 187 CharsetDecoder decoder = sourceCharset.newDecoder() 188 .onMalformedInput(CodingErrorAction.REPORT) 189 .onUnmappableCharacter(CodingErrorAction.REPORT); 190 CharsetEncoder encoder = targetCharset.newEncoder() 191 .onMalformedInput(CodingErrorAction.REPORT) 192 .onUnmappableCharacter(CodingErrorAction.REPORT); 193 try { 194 ByteBuffer buffer = encoder.encode(decoder.decode(ByteBuffer.wrap(ssid.getBytes()))); 195 byte[] bytes = new byte[buffer.limit()]; 196 buffer.get(bytes); 197 return WifiSsid.fromBytes(bytes); 198 } catch (CharacterCodingException | IllegalArgumentException e) { 199 // Could not translate to a valid SSID. 200 Log.e(TAG, "Could not translate SSID " + ssid + ": " + e); 201 return null; 202 } 203 } 204 205 /** 206 * Translate an SSID to UTF-8 if it is not already UTF-8 and is encoded with the alternate 207 * Charset of the current Locale language. 208 * 209 * @param ssid SSID to translate. 210 * @return translated SSID, or the given SSID if it should not be translated. 211 */ getTranslatedSsid(@onNull WifiSsid ssid)212 public synchronized @NonNull WifiSsid getTranslatedSsid(@NonNull WifiSsid ssid) { 213 return getTranslatedSsidAndRecordBssidCharset(ssid, null); 214 } 215 216 /** 217 * Gets the translated SSID used for a STA iface. This may be different from the default 218 * translation if the untranslated SSID has an ambiguous encoding. 219 */ getTranslatedSsidForStaIface( @onNull WifiSsid untranslated, @NonNull String staIface)220 public synchronized @NonNull WifiSsid getTranslatedSsidForStaIface( 221 @NonNull WifiSsid untranslated, @NonNull String staIface) { 222 WifiSsid translated = mTranslatedSsidForStaIface.get(staIface); 223 if (translated == null || !getAllPossibleOriginalSsids(translated).contains(untranslated)) { 224 // No recorded translation for the iface, use the default translation. 225 return getTranslatedSsid(untranslated); 226 } 227 // Return the recorded translation. 228 return translated; 229 } 230 231 /** 232 * Record the actual translated SSID used for a STA iface in case the untranslated SSID 233 * has an ambiguous encoding. 234 */ setTranslatedSsidForStaIface( @onNull WifiSsid translated, @NonNull String staIface)235 public synchronized void setTranslatedSsidForStaIface( 236 @NonNull WifiSsid translated, @NonNull String staIface) { 237 mTranslatedSsidForStaIface.put(staIface, translated); 238 } 239 240 /** 241 * Translate an SSID to UTF-8 if it is not already UTF-8 and is encoded with the alternate 242 * Charset of the current Locale language, and record the BSSID as translated. If the SSID is 243 * already in UTF-8 or is not encoded with the alternate Charset, then the SSID will not be 244 * translated and the BSSID will be recorded as untranslated. 245 * 246 * @param ssid SSID to translate. 247 * @param bssid BSSID to record the Charset of. 248 * @return translated SSID, or the given SSID if it should not be translated. 249 */ getTranslatedSsidAndRecordBssidCharset( @onNull WifiSsid ssid, @Nullable MacAddress bssid)250 public synchronized @NonNull WifiSsid getTranslatedSsidAndRecordBssidCharset( 251 @NonNull WifiSsid ssid, @Nullable MacAddress bssid) { 252 if (mCurrentLocaleAlternateCharset == null) { 253 return ssid; 254 } 255 if (ssid.getUtf8Text() == null) { 256 WifiSsid translatedSsid = 257 translateSsid(ssid, mCurrentLocaleAlternateCharset, StandardCharsets.UTF_8); 258 if (translatedSsid != null) { 259 if (bssid != null) { 260 mTranslatedBssids.computeIfAbsent(translatedSsid, k -> new ArraySet<>()) 261 .add(bssid); 262 Pair<WifiSsid, MacAddress> ssidBssidPair = new Pair<>(translatedSsid, bssid); 263 Runnable oldRunnable = mTranslatedBssidTimeoutRunnables.remove(ssidBssidPair); 264 if (oldRunnable != null) { 265 mWifiHandler.removeCallbacks(oldRunnable); 266 } 267 Runnable timeoutRunnable = new Runnable() { 268 @Override 269 public void run() { 270 handleTranslatedBssidTimeout(translatedSsid, bssid, this); 271 } 272 }; 273 mTranslatedBssidTimeoutRunnables.put(ssidBssidPair, timeoutRunnable); 274 mWifiHandler.postDelayed(timeoutRunnable, BSSID_CACHE_TIMEOUT_MS); 275 } 276 return translatedSsid; 277 } 278 } 279 if (bssid != null) { 280 mUntranslatedBssids.computeIfAbsent(ssid, k -> new ArraySet<>()).add(bssid); 281 Pair<WifiSsid, MacAddress> ssidBssidPair = new Pair<>(ssid, bssid); 282 Runnable oldRunnable = mUntranslatedBssidTimeoutRunnables.remove(ssidBssidPair); 283 if (oldRunnable != null) { 284 mWifiHandler.removeCallbacks(oldRunnable); 285 } 286 Runnable timeoutRunnable = new Runnable() { 287 @Override 288 public void run() { 289 handleUntranslatedBssidTimeout(ssid, bssid, this); 290 } 291 }; 292 mUntranslatedBssidTimeoutRunnables.put(ssidBssidPair, timeoutRunnable); 293 mWifiHandler.postDelayed(timeoutRunnable, BSSID_CACHE_TIMEOUT_MS); 294 } 295 296 return ssid; 297 } 298 299 /** Removes a timed out translated ssid/bssid mapping */ handleTranslatedBssidTimeout( WifiSsid ssid, MacAddress bssid, Runnable runnable)300 private synchronized void handleTranslatedBssidTimeout( 301 WifiSsid ssid, MacAddress bssid, Runnable runnable) { 302 Pair<WifiSsid, MacAddress> mapping = new Pair<>(ssid, bssid); 303 if (mTranslatedBssidTimeoutRunnables.get(mapping) != runnable) { 304 // This runnable isn't the active runnable anymore. Ignore. 305 return; 306 } 307 mTranslatedBssidTimeoutRunnables.remove(mapping); 308 Set<MacAddress> bssids = mTranslatedBssids.get(ssid); 309 if (bssids == null) { 310 return; 311 } 312 bssids.remove(bssid); 313 if (bssids.isEmpty()) { 314 mTranslatedBssids.remove(ssid); 315 } 316 } 317 318 /** Removes a timed out untranslated ssid/bssid mapping */ handleUntranslatedBssidTimeout( WifiSsid ssid, MacAddress bssid, Runnable runnable)319 private synchronized void handleUntranslatedBssidTimeout( 320 WifiSsid ssid, MacAddress bssid, Runnable runnable) { 321 Pair<WifiSsid, MacAddress> mapping = new Pair<>(ssid, bssid); 322 if (mUntranslatedBssidTimeoutRunnables.get(mapping) != runnable) { 323 // This runnable isn't the active runnable anymore. Ignore. 324 return; 325 } 326 mUntranslatedBssidTimeoutRunnables.remove(mapping); 327 Set<MacAddress> bssids = mUntranslatedBssids.get(ssid); 328 if (bssids == null) { 329 return; 330 } 331 bssids.remove(bssid); 332 if (bssids.isEmpty()) { 333 mUntranslatedBssids.remove(ssid); 334 } 335 } 336 337 /** 338 * Converts the specified translated SSID back to its original Charset if the BSSID is recorded 339 * as translated, or there are translated BSSIDs but no untranslated BSSIDs for this SSID. 340 * 341 * If the BSSID has not been recorded at all, then we will return the SSID as-is. 342 * 343 * @param translatedSsid translated SSID. 344 * @param bssid optional BSSID to look up the Charset. 345 * @return original SSID. May be null if there are no valid translations back to the alternate 346 * Charset and the translated SSID is not a valid SSID. 347 */ getOriginalSsid( @onNull WifiSsid translatedSsid, @Nullable MacAddress bssid)348 public synchronized @Nullable WifiSsid getOriginalSsid( 349 @NonNull WifiSsid translatedSsid, @Nullable MacAddress bssid) { 350 if (mCurrentLocaleAlternateCharset == null) { 351 return translatedSsid.getBytes().length <= 32 ? translatedSsid : null; 352 } 353 boolean ssidWasTranslatedForSomeBssids = mTranslatedBssids.containsKey(translatedSsid); 354 boolean ssidWasTranslatedForThisBssid = ssidWasTranslatedForSomeBssids 355 && mTranslatedBssids.get(translatedSsid).contains(bssid); 356 boolean ssidNotTranslatedForSomeBssids = mUntranslatedBssids.containsKey(translatedSsid); 357 if (ssidWasTranslatedForThisBssid 358 || (ssidWasTranslatedForSomeBssids && !ssidNotTranslatedForSomeBssids)) { 359 // Try to get the SSID in the alternate Charset. 360 WifiSsid altCharsetSsid = translateSsid( 361 translatedSsid, StandardCharsets.UTF_8, mCurrentLocaleAlternateCharset); 362 if (altCharsetSsid == null || altCharsetSsid.getBytes().length > 32) { 363 Log.e(TAG, "Could not translate " + translatedSsid + " back to " 364 + mCurrentLocaleAlternateCharset + " for BSSID " + bssid); 365 } else { 366 return altCharsetSsid; 367 } 368 } 369 // Use the translated SSID as-is 370 if (translatedSsid.getBytes().length > 32) { 371 return null; 372 } 373 return translatedSsid; 374 } 375 376 /** 377 * Gets the original SSID of a WifiConfiguration based on its network selection BSSID or 378 * candidate BSSID. 379 */ getOriginalSsid(@onNull WifiConfiguration config)380 public synchronized @Nullable WifiSsid getOriginalSsid(@NonNull WifiConfiguration config) { 381 WifiConfiguration.NetworkSelectionStatus networkSelectionStatus = 382 config.getNetworkSelectionStatus(); 383 String networkSelectionBssid = networkSelectionStatus.getNetworkSelectionBSSID(); 384 String candidateBssid = networkSelectionStatus.getCandidate() != null 385 ? networkSelectionStatus.getCandidate().BSSID : null; 386 MacAddress selectedBssid = null; 387 if (!TextUtils.isEmpty(networkSelectionBssid) && !TextUtils.equals( 388 networkSelectionBssid, ClientModeImpl.SUPPLICANT_BSSID_ANY)) { 389 selectedBssid = MacAddress.fromString(networkSelectionBssid); 390 } else if (!TextUtils.isEmpty(candidateBssid) && !TextUtils.equals( 391 candidateBssid, ClientModeImpl.SUPPLICANT_BSSID_ANY)) { 392 selectedBssid = MacAddress.fromString(candidateBssid); 393 } 394 return getOriginalSsid(WifiSsid.fromString(config.SSID), selectedBssid); 395 } 396 397 /** 398 * Returns a list of all possible original SSIDs for the specified translated SSID. This will 399 * include all charsets declared for the current Locale language, as well as the UTF-8 SSID. 400 * 401 * @param translatedSsid translated SSID. 402 * @return list of untranslated SSIDs. May be empty if there are no valid reverse translations. 403 */ getAllPossibleOriginalSsids( @onNull WifiSsid translatedSsid)404 public synchronized @NonNull List<WifiSsid> getAllPossibleOriginalSsids( 405 @NonNull WifiSsid translatedSsid) { 406 List<WifiSsid> untranslatedSsids = new ArrayList<>(); 407 // Add the translated SSID first (UTF-8 or unknown character set) 408 if (translatedSsid.getBytes().length <= 32) { 409 untranslatedSsids.add(translatedSsid); 410 } 411 if (mCurrentLocaleAlternateCharset != null) { 412 WifiSsid altCharsetSsid = translateSsid(translatedSsid, 413 StandardCharsets.UTF_8, mCurrentLocaleAlternateCharset); 414 if (altCharsetSsid != null && !altCharsetSsid.equals(translatedSsid) 415 && altCharsetSsid.getBytes().length <= 32) { 416 untranslatedSsids.add(altCharsetSsid); 417 } 418 } 419 return untranslatedSsids; 420 } 421 422 /** 423 * Dump of {@link SsidTranslator}. 424 */ dump(PrintWriter pw)425 public synchronized void dump(PrintWriter pw) { 426 pw.println("Dump of SsidTranslator"); 427 pw.println("mCurrentLocaleCharset: " + mCurrentLocaleAlternateCharset); 428 pw.println("mCharsetsPerLocaleLanguage Begin ---"); 429 for (Map.Entry<String, Charset> entry : mCharsetsPerLocaleLanguage.entrySet()) { 430 pw.println(entry.getKey() + ": " + entry.getValue()); 431 } 432 pw.println("mCharsetsPerLocaleLanguage End ---"); 433 pw.println("mTranslatedBssids Begin ---"); 434 for (Map.Entry<WifiSsid, Set<MacAddress>> translatedBssidsEntry 435 : mTranslatedBssids.entrySet()) { 436 pw.println("Translated SSID: " + translatedBssidsEntry.getKey() + ", BSSIDS: " 437 + Arrays.toString(translatedBssidsEntry.getValue().toArray())); 438 } 439 pw.println("mTranslatedBssids End ---"); 440 pw.println("mUntranslatedBssids Begin ---"); 441 for (Map.Entry<WifiSsid, Set<MacAddress>> untranslatedBssidsEntry 442 : mUntranslatedBssids.entrySet()) { 443 pw.println("Translated SSID: " + untranslatedBssidsEntry.getKey() + ", BSSIDS: " 444 + Arrays.toString(untranslatedBssidsEntry.getValue().toArray())); 445 } 446 pw.println("mUntranslatedBssids End ---"); 447 } 448 449 /** 450 * Sets a mock Charset for the specified Locale language. 451 * Use {@link #clearMockLocaleCharsets()} to clear the mock list. 452 */ setMockLocaleCharset( @onNull String localeLanguage, @NonNull Charset charset)453 public synchronized void setMockLocaleCharset( 454 @NonNull String localeLanguage, @NonNull Charset charset) { 455 Log.i(TAG, "Setting mock alternate charset for " + localeLanguage + ": " + charset); 456 mMockCharsetsPerLocaleLanguage.put(localeLanguage, charset); 457 updateCurrentLocaleCharset(); 458 } 459 460 /** 461 * Clears all mocked Charsets set by {@link #setMockLocaleCharset(String, Charset)}. 462 */ clearMockLocaleCharsets()463 public synchronized void clearMockLocaleCharsets() { 464 Log.i(TAG, "Clearing mock charsets"); 465 mMockCharsetsPerLocaleLanguage.clear(); 466 updateCurrentLocaleCharset(); 467 } 468 469 /** 470 * Indicates whether SSID translation is currently enabled. 471 */ isSsidTranslationEnabled()472 public synchronized boolean isSsidTranslationEnabled() { 473 return mCurrentLocaleAlternateCharset != null; 474 } 475 } 476