/* * Copyright (C) 2020 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.managedprovisioning.common; import static android.content.Context.BIND_AUTO_CREATE; import android.annotation.IntDef; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; import android.os.Bundle; import android.os.IBinder; import com.android.internal.annotations.VisibleForTesting; import com.google.android.setupwizard.util.INetworkInterceptService; /** * This wrapper performs system configuration that is necessary for some DPCs to run properly, and * reverts the system to its previous state after the DPC has finished. One call should be made to * {@link #triggerDpcStart(Context, Runnable)} to start the DPC, and one call should be made to * {@link #dpcFinished()} after the DPC activity returns a result. Whenever the activity that * hosts this connection is destroyed, {@link #unbind(Context)} should also be called. */ public class StartDpcInsideSuwServiceConnection implements ServiceConnection { @VisibleForTesting static final String NETWORK_INTERCEPT_SERVICE_ACTION = "com.google.android.setupwizard.NetworkInterceptService.BIND"; @VisibleForTesting static final String SETUP_WIZARD_PACKAGE_NAME = "com.google.android.setupwizard"; private static final String DPC_STATE_KEY = "dpc_state"; private static final String NETWORK_INTERCEPT_SERVICE_BINDING_INITIATED_KEY = "network_intercept_service_binding_initiated"; private static final String NETWORK_INTERCEPT_WAS_INITIALLY_ENABLED_KEY = "network_intercept_was_initially_enabled"; private static final int DPC_STATE_NOT_STARTED = 1; private static final int DPC_STATE_STARTED = 2; private static final int DPC_STATE_FINISHED = 3; @IntDef({DPC_STATE_NOT_STARTED, DPC_STATE_STARTED, DPC_STATE_FINISHED}) private @interface DpcState {} private Runnable mDpcIntentSender; private INetworkInterceptService mNetworkInterceptService; private @DpcState int mDpcState; private boolean mNetworkInterceptServiceBindingInitiated; private boolean mNetworkInterceptWasInitiallyEnabled; public StartDpcInsideSuwServiceConnection() { mDpcState = DPC_STATE_NOT_STARTED; mNetworkInterceptServiceBindingInitiated = false; mNetworkInterceptWasInitiallyEnabled = true; } public StartDpcInsideSuwServiceConnection(Context context, Bundle savedInstanceState, Runnable dpcIntentSender) { // Three statements are required to assign an int value from a bundle into an IntDef final int savedDpcState = savedInstanceState.getInt(DPC_STATE_KEY, DPC_STATE_NOT_STARTED); final @DpcState int dpcStateAsIntDef = savedDpcState; mDpcState = dpcStateAsIntDef; mNetworkInterceptServiceBindingInitiated = savedInstanceState.getBoolean( NETWORK_INTERCEPT_SERVICE_BINDING_INITIATED_KEY, false); mNetworkInterceptWasInitiallyEnabled = savedInstanceState.getBoolean( NETWORK_INTERCEPT_WAS_INITIALLY_ENABLED_KEY, true); if (mNetworkInterceptServiceBindingInitiated) { // bindService() succeeded previously, which implies that triggerDpc() was previously // called. Attempt to bind again, so that we have a service connection ready for when // dpcFinished() is called. Additionally, if we never received the onServiceConnected() // callback previously, it means that the DPC was never actually started, so the DPC // will be started when this new service connection is established. mNetworkInterceptServiceBindingInitiated = false; if (mDpcState != DPC_STATE_FINISHED) { if (mDpcState == DPC_STATE_NOT_STARTED) { mDpcIntentSender = dpcIntentSender; } tryBindService(context); } } } /** * This should be called when onSaveInstanceState() is invoked on the host activity (the one * that holds this connection). */ public void saveInstanceState(Bundle outState) { outState.putInt(DPC_STATE_KEY, mDpcState); outState.putBoolean(NETWORK_INTERCEPT_SERVICE_BINDING_INITIATED_KEY, mNetworkInterceptServiceBindingInitiated); outState.putBoolean(NETWORK_INTERCEPT_WAS_INITIALLY_ENABLED_KEY, mNetworkInterceptWasInitiallyEnabled); } /** * Configure Setup Wizard to allow the DPC setup activity to send network intents, then start * the DPC setup activity. This should only be called once - subsequent calls have no effect. */ public void triggerDpcStart(Context context, Runnable dpcIntentSender) { if (mNetworkInterceptServiceBindingInitiated || mDpcState != DPC_STATE_NOT_STARTED) { ProvisionLogger.loge("Duplicate calls to triggerDpcStart() - ignoring"); return; } mDpcIntentSender = dpcIntentSender; tryBindService(context); // If binding to NetworkInterceptService succeeds, DPC will be started from one of the // ServiceConnection's callbacks. If binding failed, we need to start the DPC ourselves // here. Without the service binding, the DPC setup activity may fail, in which case it is // the DPC's responsibility to report what happened and decide how to handle the failure. if (!mNetworkInterceptServiceBindingInitiated) { sendDpcIntentIfNotAlreadySent(); } } private void tryBindService(Context context) { final Intent networkInterceptServiceIntent = new Intent(NETWORK_INTERCEPT_SERVICE_ACTION); networkInterceptServiceIntent.setPackage(SETUP_WIZARD_PACKAGE_NAME); try { if (context.bindService(networkInterceptServiceIntent, this, BIND_AUTO_CREATE)) { mNetworkInterceptServiceBindingInitiated = true; } else { ProvisionLogger.loge("Failed to bind to SUW NetworkInterceptService"); // Unbind even when binding failed to avoid leaking ServiceConnection try { context.unbindService(this); } catch (IllegalArgumentException e) { ProvisionLogger.loge("unbindService failed after failed bindService", e); } } } catch(SecurityException e) { ProvisionLogger.loge("Access denied to SUW NetworkInterceptService", e); } } /** * Reset network interception to its original state. */ public void dpcFinished() { if (mNetworkInterceptServiceBindingInitiated && mNetworkInterceptWasInitiallyEnabled) { enableNetworkIntentIntercept(); } mDpcState = DPC_STATE_FINISHED; } /** * Unbind from the SUW NetworkInterceptService. This should be called whenever the activity * that hosts this service connection is destroyed, in order to avoid leaking the service * connection. */ public void unbind(Context context) { if (mNetworkInterceptServiceBindingInitiated) { context.unbindService(this); mNetworkInterceptServiceBindingInitiated = false; } mNetworkInterceptService = null; } @Override public void onServiceConnected(ComponentName className, IBinder service) { ProvisionLogger.logi("Connection to SUW NetworkInterceptService established"); mNetworkInterceptService = INetworkInterceptService.Stub.asInterface(service); // If the service disconnects and reconnects, don't cache the initial network intercept // setting again, since we changed this when we made the first connection. if (mDpcState != DPC_STATE_NOT_STARTED) { return; } cacheInitialNetworkInterceptSetting(); if (mNetworkInterceptWasInitiallyEnabled) { disableNetworkIntentIntercept(); } sendDpcIntentIfNotAlreadySent(); } @Override public void onServiceDisconnected(ComponentName name) { ProvisionLogger.logw("Connection to SUW NetworkInterceptService lost"); mNetworkInterceptService = null; } @Override public void onBindingDied(ComponentName name) { ProvisionLogger.logw("Connection to SUW NetworkInterceptService died"); mNetworkInterceptService = null; } @Override public void onNullBinding(ComponentName name) { ProvisionLogger.loge("Binding to SUW NetworkInterceptService returned null"); sendDpcIntentIfNotAlreadySent(); } private void sendDpcIntentIfNotAlreadySent() { if (mDpcState == DPC_STATE_NOT_STARTED && mDpcIntentSender != null) { mDpcIntentSender.run(); mDpcState = DPC_STATE_STARTED; // Release any reference that mDpcIntentSender has to the host activity mDpcIntentSender = null; } } private void cacheInitialNetworkInterceptSetting() { if (mNetworkInterceptService == null) { ProvisionLogger.loge("Attempt to cache network interception when service is null"); return; } try { mNetworkInterceptWasInitiallyEnabled = mNetworkInterceptService.isNetworkIntentIntercepted(); } catch (Exception e) { ProvisionLogger.loge("Exception from SUW NetworkInterceptService", e); } } private void disableNetworkIntentIntercept() { if (mNetworkInterceptService == null) { ProvisionLogger.loge("Attempt to disable network interception when service is null"); return; } try { if (!mNetworkInterceptService.disableNetworkIntentIntercept()) { ProvisionLogger.loge( "Service call to disable SUW network intent interception failed"); } } catch (Exception e) { ProvisionLogger.loge("Exception from SUW NetworkInterceptService", e); } } private void enableNetworkIntentIntercept() { if (mNetworkInterceptService == null) { ProvisionLogger.logw( "Attempt to re-enable SUW network intent interception when service is null"); return; } try { if (!mNetworkInterceptService.enableNetworkIntentIntercept()) { ProvisionLogger.logw( "Service call to re-enable SUW network intent interception failed"); } } catch (Exception e) { ProvisionLogger.logw("Exception from SUW NetworkInterceptService", e); } } }