1 /* 2 * Copyright (C) 2020 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.managedprovisioning.common; 18 19 import static android.content.Context.BIND_AUTO_CREATE; 20 21 import android.annotation.IntDef; 22 import android.content.ComponentName; 23 import android.content.Context; 24 import android.content.Intent; 25 import android.content.ServiceConnection; 26 import android.os.Bundle; 27 import android.os.IBinder; 28 29 import com.android.internal.annotations.VisibleForTesting; 30 31 import com.google.android.setupwizard.util.INetworkInterceptService; 32 33 /** 34 * This wrapper performs system configuration that is necessary for some DPCs to run properly, and 35 * reverts the system to its previous state after the DPC has finished. One call should be made to 36 * {@link #triggerDpcStart(Context, Runnable)} to start the DPC, and one call should be made to 37 * {@link #dpcFinished()} after the DPC activity returns a result. Whenever the activity that 38 * hosts this connection is destroyed, {@link #unbind(Context)} should also be called. 39 */ 40 public class StartDpcInsideSuwServiceConnection implements ServiceConnection { 41 @VisibleForTesting 42 static final String NETWORK_INTERCEPT_SERVICE_ACTION = 43 "com.google.android.setupwizard.NetworkInterceptService.BIND"; 44 45 @VisibleForTesting 46 static final String SETUP_WIZARD_PACKAGE_NAME = "com.google.android.setupwizard"; 47 48 private static final String DPC_STATE_KEY = "dpc_state"; 49 private static final String NETWORK_INTERCEPT_SERVICE_BINDING_INITIATED_KEY = 50 "network_intercept_service_binding_initiated"; 51 private static final String NETWORK_INTERCEPT_WAS_INITIALLY_ENABLED_KEY = 52 "network_intercept_was_initially_enabled"; 53 54 private static final int DPC_STATE_NOT_STARTED = 1; 55 private static final int DPC_STATE_STARTED = 2; 56 private static final int DPC_STATE_FINISHED = 3; 57 58 @IntDef({DPC_STATE_NOT_STARTED, 59 DPC_STATE_STARTED, 60 DPC_STATE_FINISHED}) 61 private @interface DpcState {} 62 63 private Runnable mDpcIntentSender; 64 private INetworkInterceptService mNetworkInterceptService; 65 private @DpcState int mDpcState; 66 private boolean mNetworkInterceptServiceBindingInitiated; 67 private boolean mNetworkInterceptWasInitiallyEnabled; 68 StartDpcInsideSuwServiceConnection()69 public StartDpcInsideSuwServiceConnection() { 70 mDpcState = DPC_STATE_NOT_STARTED; 71 mNetworkInterceptServiceBindingInitiated = false; 72 mNetworkInterceptWasInitiallyEnabled = true; 73 } 74 StartDpcInsideSuwServiceConnection(Context context, Bundle savedInstanceState, Runnable dpcIntentSender)75 public StartDpcInsideSuwServiceConnection(Context context, Bundle savedInstanceState, 76 Runnable dpcIntentSender) { 77 // Three statements are required to assign an int value from a bundle into an IntDef 78 final int savedDpcState = savedInstanceState.getInt(DPC_STATE_KEY, DPC_STATE_NOT_STARTED); 79 final @DpcState int dpcStateAsIntDef = savedDpcState; 80 mDpcState = dpcStateAsIntDef; 81 82 mNetworkInterceptServiceBindingInitiated = savedInstanceState.getBoolean( 83 NETWORK_INTERCEPT_SERVICE_BINDING_INITIATED_KEY, false); 84 mNetworkInterceptWasInitiallyEnabled = savedInstanceState.getBoolean( 85 NETWORK_INTERCEPT_WAS_INITIALLY_ENABLED_KEY, true); 86 87 if (mNetworkInterceptServiceBindingInitiated) { 88 // bindService() succeeded previously, which implies that triggerDpc() was previously 89 // called. Attempt to bind again, so that we have a service connection ready for when 90 // dpcFinished() is called. Additionally, if we never received the onServiceConnected() 91 // callback previously, it means that the DPC was never actually started, so the DPC 92 // will be started when this new service connection is established. 93 mNetworkInterceptServiceBindingInitiated = false; 94 if (mDpcState != DPC_STATE_FINISHED) { 95 if (mDpcState == DPC_STATE_NOT_STARTED) { 96 mDpcIntentSender = dpcIntentSender; 97 } 98 tryBindService(context); 99 } 100 } 101 } 102 103 /** 104 * This should be called when onSaveInstanceState() is invoked on the host activity (the one 105 * that holds this connection). 106 */ saveInstanceState(Bundle outState)107 public void saveInstanceState(Bundle outState) { 108 outState.putInt(DPC_STATE_KEY, mDpcState); 109 outState.putBoolean(NETWORK_INTERCEPT_SERVICE_BINDING_INITIATED_KEY, 110 mNetworkInterceptServiceBindingInitiated); 111 outState.putBoolean(NETWORK_INTERCEPT_WAS_INITIALLY_ENABLED_KEY, 112 mNetworkInterceptWasInitiallyEnabled); 113 } 114 115 /** 116 * Configure Setup Wizard to allow the DPC setup activity to send network intents, then start 117 * the DPC setup activity. This should only be called once - subsequent calls have no effect. 118 */ triggerDpcStart(Context context, Runnable dpcIntentSender)119 public void triggerDpcStart(Context context, Runnable dpcIntentSender) { 120 if (mNetworkInterceptServiceBindingInitiated || mDpcState != DPC_STATE_NOT_STARTED) { 121 ProvisionLogger.loge("Duplicate calls to triggerDpcStart() - ignoring"); 122 return; 123 } 124 125 mDpcIntentSender = dpcIntentSender; 126 127 tryBindService(context); 128 129 // If binding to NetworkInterceptService succeeds, DPC will be started from one of the 130 // ServiceConnection's callbacks. If binding failed, we need to start the DPC ourselves 131 // here. Without the service binding, the DPC setup activity may fail, in which case it is 132 // the DPC's responsibility to report what happened and decide how to handle the failure. 133 if (!mNetworkInterceptServiceBindingInitiated) { 134 sendDpcIntentIfNotAlreadySent(); 135 } 136 } 137 tryBindService(Context context)138 private void tryBindService(Context context) { 139 final Intent networkInterceptServiceIntent = new Intent(NETWORK_INTERCEPT_SERVICE_ACTION); 140 networkInterceptServiceIntent.setPackage(SETUP_WIZARD_PACKAGE_NAME); 141 142 try { 143 if (context.bindService(networkInterceptServiceIntent, this, BIND_AUTO_CREATE)) { 144 mNetworkInterceptServiceBindingInitiated = true; 145 } else { 146 ProvisionLogger.loge("Failed to bind to SUW NetworkInterceptService"); 147 // Unbind even when binding failed to avoid leaking ServiceConnection 148 try { 149 context.unbindService(this); 150 } catch (IllegalArgumentException e) { 151 ProvisionLogger.loge("unbindService failed after failed bindService", e); 152 } 153 } 154 } catch(SecurityException e) { 155 ProvisionLogger.loge("Access denied to SUW NetworkInterceptService", e); 156 } 157 } 158 159 /** 160 * Reset network interception to its original state. 161 */ dpcFinished()162 public void dpcFinished() { 163 if (mNetworkInterceptServiceBindingInitiated && mNetworkInterceptWasInitiallyEnabled) { 164 enableNetworkIntentIntercept(); 165 } 166 mDpcState = DPC_STATE_FINISHED; 167 } 168 169 /** 170 * Unbind from the SUW NetworkInterceptService. This should be called whenever the activity 171 * that hosts this service connection is destroyed, in order to avoid leaking the service 172 * connection. 173 */ unbind(Context context)174 public void unbind(Context context) { 175 if (mNetworkInterceptServiceBindingInitiated) { 176 context.unbindService(this); 177 mNetworkInterceptServiceBindingInitiated = false; 178 } 179 180 mNetworkInterceptService = null; 181 } 182 183 @Override onServiceConnected(ComponentName className, IBinder service)184 public void onServiceConnected(ComponentName className, IBinder service) { 185 ProvisionLogger.logi("Connection to SUW NetworkInterceptService established"); 186 187 mNetworkInterceptService = INetworkInterceptService.Stub.asInterface(service); 188 189 // If the service disconnects and reconnects, don't cache the initial network intercept 190 // setting again, since we changed this when we made the first connection. 191 if (mDpcState != DPC_STATE_NOT_STARTED) { 192 return; 193 } 194 195 cacheInitialNetworkInterceptSetting(); 196 197 if (mNetworkInterceptWasInitiallyEnabled) { 198 disableNetworkIntentIntercept(); 199 } 200 201 sendDpcIntentIfNotAlreadySent(); 202 } 203 204 @Override onServiceDisconnected(ComponentName name)205 public void onServiceDisconnected(ComponentName name) { 206 ProvisionLogger.logw("Connection to SUW NetworkInterceptService lost"); 207 mNetworkInterceptService = null; 208 } 209 210 @Override onBindingDied(ComponentName name)211 public void onBindingDied(ComponentName name) { 212 ProvisionLogger.logw("Connection to SUW NetworkInterceptService died"); 213 mNetworkInterceptService = null; 214 } 215 216 @Override onNullBinding(ComponentName name)217 public void onNullBinding(ComponentName name) { 218 ProvisionLogger.loge("Binding to SUW NetworkInterceptService returned null"); 219 sendDpcIntentIfNotAlreadySent(); 220 } 221 sendDpcIntentIfNotAlreadySent()222 private void sendDpcIntentIfNotAlreadySent() { 223 if (mDpcState == DPC_STATE_NOT_STARTED && mDpcIntentSender != null) { 224 mDpcIntentSender.run(); 225 mDpcState = DPC_STATE_STARTED; 226 // Release any reference that mDpcIntentSender has to the host activity 227 mDpcIntentSender = null; 228 } 229 } 230 cacheInitialNetworkInterceptSetting()231 private void cacheInitialNetworkInterceptSetting() { 232 if (mNetworkInterceptService == null) { 233 ProvisionLogger.loge("Attempt to cache network interception when service is null"); 234 return; 235 } 236 try { 237 mNetworkInterceptWasInitiallyEnabled = 238 mNetworkInterceptService.isNetworkIntentIntercepted(); 239 } catch (Exception e) { 240 ProvisionLogger.loge("Exception from SUW NetworkInterceptService", e); 241 } 242 } 243 disableNetworkIntentIntercept()244 private void disableNetworkIntentIntercept() { 245 if (mNetworkInterceptService == null) { 246 ProvisionLogger.loge("Attempt to disable network interception when service is null"); 247 return; 248 } 249 try { 250 if (!mNetworkInterceptService.disableNetworkIntentIntercept()) { 251 ProvisionLogger.loge( 252 "Service call to disable SUW network intent interception failed"); 253 } 254 } catch (Exception e) { 255 ProvisionLogger.loge("Exception from SUW NetworkInterceptService", e); 256 } 257 } 258 enableNetworkIntentIntercept()259 private void enableNetworkIntentIntercept() { 260 if (mNetworkInterceptService == null) { 261 ProvisionLogger.logw( 262 "Attempt to re-enable SUW network intent interception when service is null"); 263 return; 264 } 265 266 try { 267 if (!mNetworkInterceptService.enableNetworkIntentIntercept()) { 268 ProvisionLogger.logw( 269 "Service call to re-enable SUW network intent interception failed"); 270 } 271 } catch (Exception e) { 272 ProvisionLogger.logw("Exception from SUW NetworkInterceptService", e); 273 } 274 } 275 } 276