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