/* * Copyright (C) 2024 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.car.wifi; import static android.car.settings.CarSettings.Global.ENABLE_PERSISTENT_TETHERING; import static android.net.TetheringManager.TETHERING_WIFI; import static android.net.wifi.WifiManager.WIFI_AP_STATE_DISABLED; import static android.net.wifi.WifiManager.WIFI_AP_STATE_ENABLED; import static android.net.wifi.WifiManager.WIFI_AP_STATE_FAILED; import static com.android.car.CarServiceUtils.getHandlerThread; import static com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport.DUMP_INFO; import android.car.Car; import android.car.builtin.util.Slogf; import android.car.feature.FeatureFlags; import android.car.feature.FeatureFlagsImpl; import android.car.hardware.power.CarPowerManager; import android.car.hardware.power.ICarPowerStateListener; import android.car.wifi.ICarWifi; import android.content.Context; import android.content.SharedPreferences; import android.database.ContentObserver; import android.net.TetheringManager; import android.net.TetheringManager.StartTetheringCallback; import android.net.wifi.SoftApConfiguration; import android.net.wifi.WifiManager; import android.net.wifi.WifiManager.SoftApCallback; import android.os.Handler; import android.os.HandlerThread; import android.provider.Settings; import android.text.TextUtils; import android.util.proto.ProtoOutputStream; import com.android.car.CarLocalServices; import com.android.car.CarLog; import com.android.car.CarServiceBase; import com.android.car.CarServiceUtils; import com.android.car.R; import com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport; import com.android.car.internal.util.IndentingPrintWriter; import com.android.car.power.CarPowerManagementService; import com.android.car.user.CarUserService; import com.android.internal.annotations.GuardedBy; /** * CarWifiService manages Wi-Fi functionality. */ public final class CarWifiService extends ICarWifi.Stub implements CarServiceBase { private static final String TAG = CarLog.tagFor(CarWifiService.class); private static final String SHARED_PREF_NAME = "com.android.car.wifi.car_wifi_service"; private static final String KEY_PERSIST_TETHERING_ENABLED_LAST = "persist_tethering_enabled_last"; private final Object mLock = new Object(); private final Context mContext; private final boolean mIsPersistTetheringCapabilitiesEnabled; private final boolean mIsPersistTetheringSettingEnabled; private final WifiManager mWifiManager; private final TetheringManager mTetheringManager; private final CarPowerManagementService mCarPowerManagementService; private final CarUserService mCarUserService; private final HandlerThread mHandlerThread = getHandlerThread(getClass().getSimpleName()); private final Handler mHandler = new Handler(mHandlerThread.getLooper()); private final FeatureFlags mFeatureFlags = new FeatureFlagsImpl(); private final ICarPowerStateListener mCarPowerStateListener = new ICarPowerStateListener.Stub() { @Override public void onStateChanged(int state, long expirationTimeMs) { if (state == CarPowerManager.STATE_ON) { onStateOn(); } } }; private final SoftApCallback mSoftApCallback = new SoftApCallback() { @Override public void onStateChanged(int state, int failureReason) { switch (state) { case WIFI_AP_STATE_ENABLED -> { Slogf.i(TAG, "AP enabled successfully"); synchronized (mLock) { if (mSharedPreferences != null) { Slogf.i(TAG, "WIFI_AP_STATE_ENABLED received, saving state in " + "SharedPreferences store"); mSharedPreferences .edit() .putBoolean( KEY_PERSIST_TETHERING_ENABLED_LAST, /* value= */ true) .apply(); } } // If the setting is enabled, tethering sessions should remain on even // if no devices are connected to it. if (mIsPersistTetheringCapabilitiesEnabled && mIsPersistTetheringSettingEnabled) { setSoftApAutoShutdownEnabled(/* enable= */ false); } } case WIFI_AP_STATE_DISABLED -> { synchronized (mLock) { if (mSharedPreferences != null) { Slogf.i(TAG, "WIFI_AP_STATE_DISABLED received, saving state in " + "SharedPreferences store"); mSharedPreferences .edit() .putBoolean( KEY_PERSIST_TETHERING_ENABLED_LAST, /* value= */ false) .apply(); } } } case WIFI_AP_STATE_FAILED -> { Slogf.w(TAG, "WIFI_AP_STATE_FAILED state received"); // FAILED state can occur during enabling OR disabling, should keep // previous setting within store. } default -> { } } } }; private final ContentObserver mPersistTetheringObserver = new ContentObserver(mHandler) { @Override public void onChange(boolean selfChange) { Slogf.i(TAG, "%s setting has changed", ENABLE_PERSISTENT_TETHERING); // If the persist tethering setting is turned off, auto shutdown must be // re-enabled. boolean persistTetheringSettingEnabled = mFeatureFlags.persistApSettings() && TextUtils.equals("true", Settings.Global.getString(mContext.getContentResolver(), ENABLE_PERSISTENT_TETHERING)); setSoftApAutoShutdownEnabled(!persistTetheringSettingEnabled); } }; @GuardedBy("mLock") private SharedPreferences mSharedPreferences; public CarWifiService(Context context) { mContext = context; mIsPersistTetheringCapabilitiesEnabled = context.getResources().getBoolean( R.bool.config_enablePersistTetheringCapabilities); mIsPersistTetheringSettingEnabled = mFeatureFlags.persistApSettings() && TextUtils.equals( "true", Settings.Global.getString(context.getContentResolver(), ENABLE_PERSISTENT_TETHERING)); mWifiManager = context.getSystemService(WifiManager.class); mTetheringManager = context.getSystemService(TetheringManager.class); mCarPowerManagementService = CarLocalServices.getService(CarPowerManagementService.class); mCarUserService = CarLocalServices.getService(CarUserService.class); } @Override public void init() { if (!mIsPersistTetheringCapabilitiesEnabled) { Slogf.w(TAG, "Persist tethering capability is not enabled"); return; } // Listeners should only be set if there's capability (also allows for // tethering persisting if setting is enabled, on next boot). mWifiManager.registerSoftApCallback(mHandler::post, mSoftApCallback); mCarUserService.runOnUser0Unlock(this::onSystemUserUnlocked); mCarPowerManagementService.registerListener(mCarPowerStateListener); if (mFeatureFlags.persistApSettings()) { mContext.getContentResolver().registerContentObserver(Settings.Global.getUriFor( ENABLE_PERSISTENT_TETHERING), /* notifyForDescendants= */ false, mPersistTetheringObserver); } } @Override public void release() { if (!mIsPersistTetheringCapabilitiesEnabled) { Slogf.w(TAG, "Persist tethering capability is not enabled"); return; } mWifiManager.unregisterSoftApCallback(mSoftApCallback); mCarPowerManagementService.unregisterListener(mCarPowerStateListener); if (mFeatureFlags.persistApSettings()) { mContext.getContentResolver().unregisterContentObserver(mPersistTetheringObserver); } } @Override @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO) public void dumpProto(ProtoOutputStream proto) { proto.write(CarWifiDumpProto.PERSIST_TETHERING_CAPABILITIES_ENABLED, mIsPersistTetheringCapabilitiesEnabled); proto.write(CarWifiDumpProto.PERSIST_TETHERING_SETTING_ENABLED, mIsPersistTetheringSettingEnabled); proto.write(CarWifiDumpProto.TETHERING_ENABLED, mWifiManager.isWifiApEnabled()); proto.write(CarWifiDumpProto.AUTO_SHUTDOWN_ENABLED, mWifiManager.getSoftApConfiguration().isAutoShutdownEnabled()); } @Override @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO) public void dump(IndentingPrintWriter writer) { writer.println("**CarWifiService**"); writer.println(); writer.println("Persist Tethering"); writer.println("mIsPersistTetheringCapabilitiesEnabled: " + mIsPersistTetheringCapabilitiesEnabled); writer.println("mIsPersistTetheringSettingEnabled: " + mIsPersistTetheringSettingEnabled); writer.println("Tethering enabled: " + mWifiManager.isWifiApEnabled()); writer.println("Auto shutdown enabled: " + mWifiManager.getSoftApConfiguration().isAutoShutdownEnabled()); } /** * Returns {@code true} if the persist tethering settings are able to be changed. */ @Override public boolean canControlPersistTetheringSettings() { CarServiceUtils.assertPermission(mContext, Car.PERMISSION_READ_PERSIST_TETHERING_SETTINGS); return mIsPersistTetheringCapabilitiesEnabled; } /** * Starts tethering if it's currently being persisted and was on last. * Should only be called given that the SharedPreferences store is properly initialized. */ private void startTethering() { if (!mIsPersistTetheringCapabilitiesEnabled || !mIsPersistTetheringSettingEnabled) { return; } if (mWifiManager.isWifiApEnabled()) { return; } synchronized (mLock) { if (mSharedPreferences == null || !mSharedPreferences.getBoolean( KEY_PERSIST_TETHERING_ENABLED_LAST, /* defValue= */ false)) { Slogf.d(TAG, "Tethering was not enabled last"); return; } } mTetheringManager.startTethering(TETHERING_WIFI, mHandler::post, new StartTetheringCallback() { @Override public void onTetheringFailed(int error) { Slogf.e(TAG, "Starting tethering failed: %d", error); } }); } private void onStateOn() { synchronized (mLock) { if (mSharedPreferences == null) { Slogf.d(TAG, "SharedPreferences store has not been initialized"); return; } } // User 0 has been unlocked, SharedPreferences is initialized and accessible. startTethering(); } private void onSystemUserUnlocked() { synchronized (mLock) { // SharedPreferences are shared among different users thus only need initialized once. // They should be initialized after user 0 is unlocked because SharedPreferences in // credential encrypted storage are not available until after user 0 is unlocked. mSharedPreferences = mContext.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE); } if (mCarPowerManagementService.getPowerState() == CarPowerManager.STATE_ON) { startTethering(); } } private void setSoftApAutoShutdownEnabled(boolean enable) { SoftApConfiguration config = new SoftApConfiguration.Builder( mWifiManager.getSoftApConfiguration()) .setAutoShutdownEnabled(enable) .build(); mWifiManager.setSoftApConfiguration(config); } }