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             }
148         } catch(SecurityException e) {
149             ProvisionLogger.loge("Access denied to SUW NetworkInterceptService", e);
150         }
151     }
152 
153     /**
154      * Reset network interception to its original state.
155      */
dpcFinished()156     public void dpcFinished() {
157         if (mNetworkInterceptServiceBindingInitiated && mNetworkInterceptWasInitiallyEnabled) {
158             enableNetworkIntentIntercept();
159         }
160         mDpcState = DPC_STATE_FINISHED;
161     }
162 
163     /**
164      * Unbind from the SUW NetworkInterceptService.  This should be called whenever the activity
165      * that hosts this service connection is destroyed, in order to avoid leaking the service
166      * connection.
167      */
unbind(Context context)168     public void unbind(Context context) {
169         if (mNetworkInterceptServiceBindingInitiated) {
170             context.unbindService(this);
171             mNetworkInterceptServiceBindingInitiated = false;
172         }
173 
174         mNetworkInterceptService = null;
175     }
176 
177     @Override
onServiceConnected(ComponentName className, IBinder service)178     public void onServiceConnected(ComponentName className, IBinder service) {
179         ProvisionLogger.logi("Connection to SUW NetworkInterceptService established");
180 
181         mNetworkInterceptService = INetworkInterceptService.Stub.asInterface(service);
182 
183         // If the service disconnects and reconnects, don't cache the initial network intercept
184         // setting again, since we changed this when we made the first connection.
185         if (mDpcState != DPC_STATE_NOT_STARTED) {
186             return;
187         }
188 
189         cacheInitialNetworkInterceptSetting();
190 
191         if (mNetworkInterceptWasInitiallyEnabled) {
192             disableNetworkIntentIntercept();
193         }
194 
195         sendDpcIntentIfNotAlreadySent();
196     }
197 
198     @Override
onServiceDisconnected(ComponentName name)199     public void onServiceDisconnected(ComponentName name) {
200         ProvisionLogger.logw("Connection to SUW NetworkInterceptService lost");
201         mNetworkInterceptService = null;
202     }
203 
204     @Override
onBindingDied(ComponentName name)205     public void onBindingDied(ComponentName name) {
206         ProvisionLogger.logw("Connection to SUW NetworkInterceptService died");
207         mNetworkInterceptService = null;
208     }
209 
210     @Override
onNullBinding(ComponentName name)211     public void onNullBinding(ComponentName name) {
212         ProvisionLogger.loge("Binding to SUW NetworkInterceptService returned null");
213         sendDpcIntentIfNotAlreadySent();
214     }
215 
sendDpcIntentIfNotAlreadySent()216     private void sendDpcIntentIfNotAlreadySent() {
217         if (mDpcState == DPC_STATE_NOT_STARTED && mDpcIntentSender != null) {
218             mDpcIntentSender.run();
219             mDpcState = DPC_STATE_STARTED;
220             // Release any reference that mDpcIntentSender has to the host activity
221             mDpcIntentSender = null;
222         }
223     }
224 
cacheInitialNetworkInterceptSetting()225     private void cacheInitialNetworkInterceptSetting() {
226         if (mNetworkInterceptService == null) {
227             ProvisionLogger.loge("Attempt to cache network interception when service is null");
228             return;
229         }
230         try {
231             mNetworkInterceptWasInitiallyEnabled =
232                     mNetworkInterceptService.isNetworkIntentIntercepted();
233         } catch (Exception e) {
234             ProvisionLogger.loge("Exception from SUW NetworkInterceptService", e);
235         }
236     }
237 
disableNetworkIntentIntercept()238     private void disableNetworkIntentIntercept() {
239         if (mNetworkInterceptService == null) {
240             ProvisionLogger.loge("Attempt to disable network interception when service is null");
241             return;
242         }
243         try {
244             if (!mNetworkInterceptService.disableNetworkIntentIntercept()) {
245                 ProvisionLogger.loge(
246                         "Service call to disable SUW network intent interception failed");
247             }
248         } catch (Exception e) {
249             ProvisionLogger.loge("Exception from SUW NetworkInterceptService", e);
250         }
251     }
252 
enableNetworkIntentIntercept()253     private void enableNetworkIntentIntercept() {
254         if (mNetworkInterceptService == null) {
255             ProvisionLogger.logw(
256                     "Attempt to re-enable SUW network intent interception when service is null");
257             return;
258         }
259 
260         try {
261             if (!mNetworkInterceptService.enableNetworkIntentIntercept()) {
262                 ProvisionLogger.logw(
263                         "Service call to re-enable SUW network intent interception failed");
264             }
265         } catch (Exception e) {
266             ProvisionLogger.logw("Exception from SUW NetworkInterceptService", e);
267         }
268     }
269 }
270