blob: f3b21cd1c53dbe1c4dbd0e855afb7a0102c2f2a5 [file] [log] [blame]
/*
* Copyright (C) 2022 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.wifi;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.res.Resources;
import android.net.MacAddress;
import android.net.wifi.WifiConfiguration;
import android.net.wifi.WifiContext;
import android.net.wifi.WifiSsid;
import android.os.Handler;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Log;
import android.util.Pair;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import com.android.wifi.resources.R;
import java.io.PrintWriter;
import java.nio.ByteBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CharsetEncoder;
import java.nio.charset.CodingErrorAction;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
/**
* Utility class to translate between non-UTF-8 SSIDs in the Native layer and UTF-8 SSIDs in the
* Framework for SSID Translation.
*
* SSID Translation is intended to provide backwards compatibility with legacy apps that do not
* recognize non-UTF-8 SSIDs. Translating non-UTF-8 SSIDs from Native->Framework into UTF-8
* (and back) will effectively switch all non-UTF-8 APs into UTF-8 APs from the perspective of the
* Framework and apps.
*
* The list of alternate non-UTF-8 character sets to translate is defined in
* R.string.config_wifiCharsetsForSsidTranslation.
*
* This class is thread-safe.
*/
public class SsidTranslator {
private static final String TAG = "SsidTranslator";
private static final String LOCALE_LANGUAGE_ALL = "all";
@VisibleForTesting static final long BSSID_CACHE_TIMEOUT_MS = 30_000;
private final @NonNull WifiContext mWifiContext;
private final @NonNull Handler mWifiHandler;
private @Nullable Charset mCurrentLocaleAlternateCharset = null;
private @NonNull Map<String, Charset> mCharsetsPerLocaleLanguage = new HashMap<>();
private @NonNull Map<String, Charset> mMockCharsetsPerLocaleLanguage = new HashMap<>();
// Maps a translated SSID to all of its BSSIDs using the alternate Charset.
private @NonNull Map<WifiSsid, Set<MacAddress>> mTranslatedBssids = new ArrayMap<>();
// Maps a translated SSID to all of its BSSIDs not using the alternate Charset.
private @NonNull Map<WifiSsid, Set<MacAddress>> mUntranslatedBssids = new ArrayMap<>();
private final Map<Pair<WifiSsid, MacAddress>, Runnable> mUntranslatedBssidTimeoutRunnables =
new ArrayMap<>();
private final Map<Pair<WifiSsid, MacAddress>, Runnable> mTranslatedBssidTimeoutRunnables =
new ArrayMap<>();
public SsidTranslator(@NonNull WifiContext wifiContext, @NonNull Handler wifiHandler) {
mWifiContext = wifiContext;
mWifiHandler = wifiHandler;
}
/**
* Initializes SsidTranslator after boot completes to get boot-dependent resources.
*/
public synchronized void handleBootCompleted() {
Resources res = mWifiContext.getResources();
if (res == null) {
Log.e(TAG, "Boot completed but could not get resources!");
return;
}
String[] charsetCsvs = res.getStringArray(
R.array.config_wifiCharsetsForSsidTranslation);
if (charsetCsvs == null) {
return;
}
for (String charsetCsv : charsetCsvs) {
String[] charsetNames = charsetCsv.split(",");
if (charsetNames.length != 2) {
continue;
}
String localeLanguage = charsetNames[0];
Charset charset;
try {
charset = Charset.forName(charsetNames[1]);
} catch (IllegalArgumentException e) {
Log.e(TAG, "Could not find Charset with name " + charsetNames[1]);
continue;
}
mCharsetsPerLocaleLanguage.put(localeLanguage, charset);
}
mWifiContext.registerReceiver(new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (!Intent.ACTION_LOCALE_CHANGED.equals(intent.getAction())) {
return;
}
updateCurrentLocaleCharset();
}
}, new IntentFilter(Intent.ACTION_LOCALE_CHANGED), null, mWifiHandler);
updateCurrentLocaleCharset();
}
/** Updates mCurrentLocaleCharset to the alternate charset of the current Locale language. */
private synchronized void updateCurrentLocaleCharset() {
// Clear existing Charset mappings.
for (Runnable runnable : mTranslatedBssidTimeoutRunnables.values()) {
mWifiHandler.removeCallbacks(runnable);
}
mTranslatedBssidTimeoutRunnables.clear();
mTranslatedBssids.clear();
for (Runnable runnable : mUntranslatedBssidTimeoutRunnables.values()) {
mWifiHandler.removeCallbacks(runnable);
}
mUntranslatedBssidTimeoutRunnables.clear();
mUntranslatedBssids.clear();
mCurrentLocaleAlternateCharset = null;
// Try to find the Charset for the specific language.
String language = null;
Resources res = mWifiContext.getResources();
if (res != null) {
Locale locale = res.getConfiguration().getLocales().get(0);
if (locale != null) {
language = locale.getLanguage();
} else {
Log.e(TAG, "Current Locale is null!");
}
} else {
Log.e(TAG, "Could not get resources to update locale!");
}
if (language != null) {
mCurrentLocaleAlternateCharset = mMockCharsetsPerLocaleLanguage.get(language);
if (mCurrentLocaleAlternateCharset == null) {
mCurrentLocaleAlternateCharset = mCharsetsPerLocaleLanguage.get(language);
}
}
// No Charset for the specific language, use the "all" charset if it exists.
if (mCurrentLocaleAlternateCharset == null) {
mCurrentLocaleAlternateCharset =
mMockCharsetsPerLocaleLanguage.get(LOCALE_LANGUAGE_ALL);
}
if (mCurrentLocaleAlternateCharset == null) {
mCurrentLocaleAlternateCharset = mCharsetsPerLocaleLanguage.get(LOCALE_LANGUAGE_ALL);
}
Log.i(TAG, "Locale language changed to " + language + ", alternate charset "
+ "is now " + mCurrentLocaleAlternateCharset);
}
/** Translates an SSID from a source Charset to a target Charset */
private WifiSsid translateSsid(@NonNull WifiSsid ssid,
@NonNull Charset sourceCharset,
@NonNull Charset targetCharset) {
CharsetDecoder decoder = sourceCharset.newDecoder()
.onMalformedInput(CodingErrorAction.REPORT)
.onUnmappableCharacter(CodingErrorAction.REPORT);
CharsetEncoder encoder = targetCharset.newEncoder()
.onMalformedInput(CodingErrorAction.REPORT)
.onUnmappableCharacter(CodingErrorAction.REPORT);
try {
ByteBuffer buffer = encoder.encode(decoder.decode(ByteBuffer.wrap(ssid.getBytes())));
byte[] bytes = new byte[buffer.limit()];
buffer.get(bytes);
return WifiSsid.fromBytes(bytes);
} catch (CharacterCodingException | IllegalArgumentException e) {
// Could not translate to a valid SSID.
Log.e(TAG, "Could not translate SSID " + ssid + ": " + e);
return null;
}
}
/**
* Translate an SSID to UTF-8 if it is encoded with the alternate Charset of the current Locale
* language.
*
* @param ssid SSID to translate.
* @return translated SSID, or the given SSID if it should not be translated.
*/
public synchronized @NonNull WifiSsid getTranslatedSsid(@NonNull WifiSsid ssid) {
return getTranslatedSsidAndRecordBssidCharset(ssid, null, false);
}
/**
* Translate an SSID to UTF-8 if it is encoded with the alternate Charset of the current Locale
* language, and record the BSSID as translated. If the SSID is not encoded with the alternate
* Charset, then the SSID will not be translated and the BSSID will be recorded as untranslated.
*
* @param ssid SSID to translate.
* @param bssid BSSID to record the Charset of.
* @param isStrictUtf8 If the SSID was declared as UTF-8 in the beacon extended capabilities.
* @return translated SSID, or the given SSID if it should not be translated.
*/
public synchronized @NonNull WifiSsid getTranslatedSsidAndRecordBssidCharset(
@NonNull WifiSsid ssid, @Nullable MacAddress bssid, boolean isStrictUtf8) {
if (mCurrentLocaleAlternateCharset == null) {
return ssid;
}
// Try translating the SSID from the alternate charset if it hasn't been declared as UTF-8.
if (!isStrictUtf8
&& !(mUntranslatedBssids.containsKey(ssid)
&& mUntranslatedBssids.get(ssid).contains(bssid))) {
WifiSsid translatedSsid =
translateSsid(ssid, mCurrentLocaleAlternateCharset, StandardCharsets.UTF_8);
if (translatedSsid != null) {
if (bssid != null) {
mTranslatedBssids.computeIfAbsent(translatedSsid, k -> new ArraySet<>())
.add(bssid);
Pair<WifiSsid, MacAddress> ssidBssidPair = new Pair<>(translatedSsid, bssid);
Runnable oldRunnable = mTranslatedBssidTimeoutRunnables.remove(ssidBssidPair);
if (oldRunnable != null) {
mWifiHandler.removeCallbacks(oldRunnable);
}
Runnable timeoutRunnable = new Runnable() {
@Override
public void run() {
handleTranslatedBssidTimeout(translatedSsid, bssid, this);
}
};
mTranslatedBssidTimeoutRunnables.put(ssidBssidPair, timeoutRunnable);
mWifiHandler.postDelayed(timeoutRunnable, BSSID_CACHE_TIMEOUT_MS);
}
return translatedSsid;
}
}
// SSID should be used untranslated.
if (bssid != null) {
mUntranslatedBssids.computeIfAbsent(ssid, k -> new ArraySet<>()).add(bssid);
Pair<WifiSsid, MacAddress> ssidBssidPair = new Pair<>(ssid, bssid);
Runnable oldRunnable = mUntranslatedBssidTimeoutRunnables.remove(ssidBssidPair);
if (oldRunnable != null) {
mWifiHandler.removeCallbacks(oldRunnable);
}
Runnable timeoutRunnable = new Runnable() {
@Override
public void run() {
handleUntranslatedBssidTimeout(ssid, bssid, this);
}
};
mUntranslatedBssidTimeoutRunnables.put(ssidBssidPair, timeoutRunnable);
mWifiHandler.postDelayed(timeoutRunnable, BSSID_CACHE_TIMEOUT_MS);
}
return ssid;
}
/** Removes a timed out translated ssid/bssid mapping */
private synchronized void handleTranslatedBssidTimeout(
WifiSsid ssid, MacAddress bssid, Runnable runnable) {
Pair<WifiSsid, MacAddress> mapping = new Pair<>(ssid, bssid);
if (mTranslatedBssidTimeoutRunnables.get(mapping) != runnable) {
// This runnable isn't the active runnable anymore. Ignore.
return;
}
mTranslatedBssidTimeoutRunnables.remove(mapping);
Set<MacAddress> bssids = mTranslatedBssids.get(ssid);
if (bssids == null) {
return;
}
bssids.remove(bssid);
if (bssids.isEmpty()) {
mTranslatedBssids.remove(ssid);
}
}
/** Removes a timed out untranslated ssid/bssid mapping */
private synchronized void handleUntranslatedBssidTimeout(
WifiSsid ssid, MacAddress bssid, Runnable runnable) {
Pair<WifiSsid, MacAddress> mapping = new Pair<>(ssid, bssid);
if (mUntranslatedBssidTimeoutRunnables.get(mapping) != runnable) {
// This runnable isn't the active runnable anymore. Ignore.
return;
}
mUntranslatedBssidTimeoutRunnables.remove(mapping);
Set<MacAddress> bssids = mUntranslatedBssids.get(ssid);
if (bssids == null) {
return;
}
bssids.remove(bssid);
if (bssids.isEmpty()) {
mUntranslatedBssids.remove(ssid);
}
}
/**
* Converts the specified translated SSID back to its original Charset if the BSSID is recorded
* as translated, or there are translated BSSIDs but no untranslated BSSIDs for this SSID.
*
* If the BSSID has not been recorded at all, then we will return the SSID as-is.
*
* @param translatedSsid translated SSID.
* @param bssid optional BSSID to look up the Charset.
* @return original SSID. May be null if there are no valid translations back to the alternate
* Charset and the translated SSID is not a valid SSID.
*/
public synchronized @Nullable WifiSsid getOriginalSsid(
@NonNull WifiSsid translatedSsid, @Nullable MacAddress bssid) {
if (mCurrentLocaleAlternateCharset == null) {
return translatedSsid.getBytes().length <= 32 ? translatedSsid : null;
}
boolean ssidWasTranslatedForSomeBssids = mTranslatedBssids.containsKey(translatedSsid);
boolean ssidWasTranslatedForThisBssid = ssidWasTranslatedForSomeBssids
&& mTranslatedBssids.get(translatedSsid).contains(bssid);
boolean ssidNotTranslatedForSomeBssids = mUntranslatedBssids.containsKey(translatedSsid);
if (ssidWasTranslatedForThisBssid
|| (ssidWasTranslatedForSomeBssids && !ssidNotTranslatedForSomeBssids)) {
// Try to get the SSID in the alternate Charset.
WifiSsid altCharsetSsid = translateSsid(
translatedSsid, StandardCharsets.UTF_8, mCurrentLocaleAlternateCharset);
if (altCharsetSsid == null || altCharsetSsid.getBytes().length > 32) {
Log.e(TAG, "Could not translate " + translatedSsid + " back to "
+ mCurrentLocaleAlternateCharset + " for BSSID " + bssid);
} else {
return altCharsetSsid;
}
}
// Use the translated SSID as-is
if (translatedSsid.getBytes().length > 32) {
return null;
}
return translatedSsid;
}
/**
* Gets the original SSID of a WifiConfiguration based on its network selection BSSID or
* candidate BSSID.
*/
public synchronized @Nullable WifiSsid getOriginalSsid(@NonNull WifiConfiguration config) {
WifiConfiguration.NetworkSelectionStatus networkSelectionStatus =
config.getNetworkSelectionStatus();
String networkSelectionBssid = networkSelectionStatus.getNetworkSelectionBSSID();
String candidateBssid = networkSelectionStatus.getCandidate() != null
? networkSelectionStatus.getCandidate().BSSID : null;
MacAddress selectedBssid = null;
if (!TextUtils.isEmpty(networkSelectionBssid) && !TextUtils.equals(
networkSelectionBssid, ClientModeImpl.SUPPLICANT_BSSID_ANY)) {
selectedBssid = MacAddress.fromString(networkSelectionBssid);
} else if (!TextUtils.isEmpty(candidateBssid) && !TextUtils.equals(
candidateBssid, ClientModeImpl.SUPPLICANT_BSSID_ANY)) {
selectedBssid = MacAddress.fromString(candidateBssid);
}
return getOriginalSsid(WifiSsid.fromString(config.SSID), selectedBssid);
}
/**
* Returns a list of all possible original SSIDs for the specified translated SSID. This will
* include all charsets declared for the current Locale language, as well as the UTF-8 SSID.
*
* @param translatedSsid translated SSID.
* @return list of untranslated SSIDs. May be empty if there are no valid reverse translations.
*/
public synchronized @NonNull List<WifiSsid> getAllPossibleOriginalSsids(
@NonNull WifiSsid translatedSsid) {
List<WifiSsid> untranslatedSsids = new ArrayList<>();
// Add the translated SSID first (UTF-8 or unknown character set)
if (translatedSsid.getBytes().length <= 32) {
untranslatedSsids.add(translatedSsid);
}
if (mCurrentLocaleAlternateCharset != null) {
WifiSsid altCharsetSsid = translateSsid(translatedSsid,
StandardCharsets.UTF_8, mCurrentLocaleAlternateCharset);
if (altCharsetSsid != null && !altCharsetSsid.equals(translatedSsid)
&& altCharsetSsid.getBytes().length <= 32) {
untranslatedSsids.add(altCharsetSsid);
}
}
return untranslatedSsids;
}
/**
* Dump of {@link SsidTranslator}.
*/
public synchronized void dump(PrintWriter pw) {
pw.println("Dump of SsidTranslator");
pw.println("mCurrentLocaleCharset: " + mCurrentLocaleAlternateCharset);
pw.println("mCharsetsPerLocaleLanguage Begin ---");
for (Map.Entry<String, Charset> entry : mCharsetsPerLocaleLanguage.entrySet()) {
pw.println(entry.getKey() + ": " + entry.getValue());
}
pw.println("mCharsetsPerLocaleLanguage End ---");
pw.println("mTranslatedBssids Begin ---");
for (Map.Entry<WifiSsid, Set<MacAddress>> translatedBssidsEntry
: mTranslatedBssids.entrySet()) {
pw.println("Translated SSID: " + translatedBssidsEntry.getKey() + ", BSSIDS: "
+ Arrays.toString(translatedBssidsEntry.getValue().toArray()));
}
pw.println("mTranslatedBssids End ---");
pw.println("mUntranslatedBssids Begin ---");
for (Map.Entry<WifiSsid, Set<MacAddress>> untranslatedBssidsEntry
: mUntranslatedBssids.entrySet()) {
pw.println("Translated SSID: " + untranslatedBssidsEntry.getKey() + ", BSSIDS: "
+ Arrays.toString(untranslatedBssidsEntry.getValue().toArray()));
}
pw.println("mUntranslatedBssids End ---");
}
/**
* Sets a mock Charset for the specified Locale language.
* Use {@link #clearMockLocaleCharsets()} to clear the mock list.
*/
public synchronized void setMockLocaleCharset(
@NonNull String localeLanguage, @NonNull Charset charset) {
Log.i(TAG, "Setting mock alternate charset for " + localeLanguage + ": " + charset);
mMockCharsetsPerLocaleLanguage.put(localeLanguage, charset);
updateCurrentLocaleCharset();
}
/**
* Clears all mocked Charsets set by {@link #setMockLocaleCharset(String, Charset)}.
*/
public synchronized void clearMockLocaleCharsets() {
Log.i(TAG, "Clearing mock charsets");
mMockCharsetsPerLocaleLanguage.clear();
updateCurrentLocaleCharset();
}
}