blob: ae68ef116ee2a598c404b5a96f2b588e115c9bdf [file] [log] [blame]
Jessica Hummelc4d5d2b2014-02-27 15:37:01 +00001/*
2 * Copyright 2014, 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
17package com.android.managedprovisioning;
18
19import static com.android.managedprovisioning.UserConsentActivity.USER_CONSENT_KEY;
20
21import android.app.Activity;
22import android.app.AlarmManager;
23import android.content.BroadcastReceiver;
24import android.content.ComponentName;
25import android.content.Context;
26import android.content.Intent;
27import android.content.IntentFilter;
28import android.content.pm.PackageManager;
29import android.net.wifi.WifiManager;
30import android.nfc.NdefMessage;
31import android.nfc.NdefRecord;
32import android.nfc.NfcAdapter;
33import android.os.Bundle;
34import android.os.Handler;
35import android.os.Parcelable;
36import android.view.LayoutInflater;
37import android.view.View;
38import android.view.Window;
39import android.view.WindowManager;
40import android.widget.CheckedTextView;
41
42import com.android.internal.app.LocalePicker;
43
44import java.io.IOException;
45import java.io.StringReader;
46import java.util.Enumeration;
47import java.util.HashMap;
48import java.util.Iterator;
49import java.util.Locale;
50import java.util.Map;
51import java.util.Properties;
52
53/**
54 * Handles intents initiating the provisioning process then launches the DeviceProvisioningService
55 * to start the provisioning tasks.
56 *
57 * Provisioning types that are supported are:
58 * - Managed profile: A device that already has a user, but needs to be set up for a
59 * secondary usage purpose (e.g using your personal device as a corporate device).
60 * - Device owner: a new device is set up for a single use case (e.g. a tablet with restricted
61 * usage options, which a company wants to provide for clients.
62 */
63public class DeviceOwnerProvisioningActivity extends Activity {
64
65 public static final String PACKAGE_NAME = "com.android.managedprovisioning";
66
67 private static final String NFC_MIME_TYPE = "application/com.android.managedprovisioning";
68 private static final String BUMP_LAUNCHER_ACTIVITY = ".DeviceOwnerProvisioningActivity";
69
70 // Five minute timeout by default.
71 private static final String TIMEOUT_IN_MS = "300000";
72
73 // Base string for keys that are used to save UI state in onSaveInstanceState.
74 private static final String BASE_STATE_KEY = "stateKey";
75
76 private BroadcastReceiver mStatusReceiver;
77 private Handler mHandler;
78 private Runnable mTimeoutRunnable;
79
80 private boolean mHasLaunchedConfiguration = false;
81 private Preferences mPrefs;
82
83 /**
84 * States that provisioning can have completed. Not all states are reached for each of
85 * the provisioning types.
86 */
87 public static class ProvisioningState {
88 public static final int CONNECTED_NETWORK = 0; // Device owner only
89 public static final int CREATE_PROFILE = 1; // Managed profile only
90 public static final int REGISTERED_DEVICE_POLICY = 1;
91 public static final int SETUP_COMPLETE = 2;
92 public static final int UPDATE = 3;
93 public static final int ERROR = 4;
94 }
95
96 /**
97 * The UI shows a bunch of checkboxes that roughly correspond to states.
98 * If completing a state should flip a checkbox, it is registered here.
99 */
100 private static Map<Integer, Integer> mStateToCheckbox = new HashMap<Integer, Integer>();
101
102 static {
103 mStateToCheckbox.put(ProvisioningState.CONNECTED_NETWORK, R.id.connecting_wifi);
104 mStateToCheckbox.put(ProvisioningState.CREATE_PROFILE, R.id.creating_profile);
105 mStateToCheckbox.put(ProvisioningState.REGISTERED_DEVICE_POLICY, R.id.device_policy);
106 mStateToCheckbox.put(ProvisioningState.SETUP_COMPLETE, R.id.setup_complete);
107 }
108
109 // Catches updates in provisioning process, watching for errors or completion.
110 private class StatusReceiver extends BroadcastReceiver {
111 @Override
112 public void onReceive(Context context, Intent intent) {
113 ProvisionLogger.logd("Received broadcast: " + intent.getAction());
114
115 if (DeviceProvisioningService.PROVISIONING_STATUS_REPORT_ACTION.
116 equals(intent.getAction())) {
117 int state = intent.
118 getIntExtra(DeviceProvisioningService.PROVISIONING_STATUS_REPORT_EXTRA, -1);
119 String stateText = intent.
120 getStringExtra(DeviceProvisioningService.PROVISIONING_STATUS_TEXT_EXTRA);
121 ProvisionLogger.logd("Received state broadcast: " + state);
122
123 if (state != -1) {
124 ProvisionLogger.logd("Received state broadcast: " + state);
125 if (mStateToCheckbox.containsKey(state)) {
126 completeCheckbox(state);
127 }
128
129 switch (state) {
130 case ProvisioningState.SETUP_COMPLETE:
Jessica Hummelf034ca82014-03-04 10:48:38 +0000131 sendBroadcast(new Intent(ManagedProvisioningActivity.ACTION_PROVISIONING_COMPLETE));
Jessica Hummelc4d5d2b2014-02-27 15:37:01 +0000132 cleanupAndFinish();
133 break;
134 case ProvisioningState.ERROR:
135 error(stateText);
136 break;
137 default:
138 break;
139 }
140 }
141 }
142 }
143 }
144
145 @Override
146 public void onCreate(Bundle savedInstanceState) {
147 super.onCreate(savedInstanceState);
148
149 ProvisionLogger.logd("DeviceOwnerProvisioning started");
150 // TODO Find better location/flow for disabling the NFC receiver during provisioning.
151 // Re-enabling currently takes place in the DeviceProvisioningService.
152 // We need to reassess the whole of enabling/disabling and not provisioning twice
153 PackageManager pkgMgr = getPackageManager();
154 pkgMgr.setComponentEnabledSetting(getComponentName(this),
155 PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP);
156
157 SettingsAdapter settingsAdapter = new SettingsAdapter(getContentResolver());
158
159 if (mStatusReceiver == null) {
160 mStatusReceiver = new StatusReceiver();
161 IntentFilter filter = new IntentFilter();
162 filter.addAction(DeviceProvisioningService.PROVISIONING_STATUS_REPORT_ACTION);
163 registerReceiver(mStatusReceiver, filter);
164 }
165
166 mPrefs = new Preferences(this);
167
168 // Avoid that provisioning is done twice.
169 // Sometimes ManagedProvisioning gets killed and restarted, and needs to resume
170 // provisioning already in progress. Provisioning doesn't need to resume if
171 // provisioning has already succeeded, in which case prefs.doesntNeedResume() returns true.
172 // TODO Check if we need the settingsAdapter for tests.
173 // TODO Add double bump checking/handling.
174 if (mPrefs.doesntNeedResume() && (settingsAdapter.isDeviceProvisioned())) {
175 finish();
176 }
177
178 requestWindowFeature(Window.FEATURE_NO_TITLE);
179 getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
180 WindowManager.LayoutParams.FLAG_FULLSCREEN);
181
182 final LayoutInflater inflater = getLayoutInflater();
183 final View contentView = inflater.inflate(R.layout.show_progress, null);
184 setContentView(contentView);
185 }
186
187 @Override
188 public void onResume() {
189 super.onResume();
190 if (!mHasLaunchedConfiguration) {
191 startDeviceOwnerProvisioning();
192 }
193 }
194
195 @Override
196 public void onNewIntent(Intent intent) {
197 setIntent(intent);
198 }
199
200 @Override
201 public void onSaveInstanceState(Bundle savedInstanceState) {
202 Iterator<Integer> stateIterator = mStateToCheckbox.keySet().iterator();
203 while (stateIterator.hasNext()) {
204 int state = stateIterator.next();
205 CheckedTextView cb = (CheckedTextView) findViewById(mStateToCheckbox.get(state));
206 if (cb != null) {
207 savedInstanceState.putBoolean(BASE_STATE_KEY + state, cb.isChecked());
208 }
209 }
210
211 super.onSaveInstanceState(savedInstanceState);
212 }
213
214 @Override
215 public void onRestoreInstanceState(Bundle savedInstanceState) {
216 super.onRestoreInstanceState(savedInstanceState);
217
218 Iterator<Integer> stateIterator = mStateToCheckbox.keySet().iterator();
219 while (stateIterator.hasNext()) {
220 int state = stateIterator.next();
221 CheckedTextView cb = (CheckedTextView) findViewById(mStateToCheckbox.get(state));
222 if (cb != null) {
223 cb.setChecked(savedInstanceState.getBoolean(BASE_STATE_KEY + state));
224 }
225 }
226 }
227
228 @Override
229 public void onBackPressed() {
230 // TODO: Handle this graciously by stopping the provisioning flow and cleaning up.
231 }
232
233 /**
234 * Build the provisioning intent from the NFC properties and start the device owner
235 * provisioning.
236 */
237 private void startDeviceOwnerProvisioning() {
238 Intent intent = getIntent();
239 Intent provisioningIntent = processNfcPayload(intent);
240
241 if (provisioningIntent != null) {
242
243 // TODO: Validate incoming intent. Check that default user name is provided etc.
244
245 initializePreferences(provisioningIntent);
246
247 // Launch DeviceProvisioningService.
248 provisioningIntent.setClass(getApplicationContext(), DeviceProvisioningService.class);
249 initialize(provisioningIntent);
250
251 ProvisionLogger.logd("Starting DeviceProvisioningService");
252 startService(provisioningIntent);
253 mHasLaunchedConfiguration = true;
254 } else {
255 ProvisionLogger.logd("Unknown provisioning intent, exiting.");
256 cleanupAndFinish();
257 }
258 }
259
260 /**
261 * Parses the NDEF Message from the intent and returns it in the form of intent extras.
262 */
263 private Intent processNfcPayload(Intent intent) {
264 ProvisionLogger.logi("Processing NFC Payload.");
265
266 if (intent.hasExtra(NfcAdapter.EXTRA_NDEF_MESSAGES)) {
267 for (Parcelable rawMsg : intent
268 .getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES)) {
269 NdefMessage msg = (NdefMessage) rawMsg;
270 NdefRecord firstRecord = msg.getRecords()[0];
271 String mimeType = new String(firstRecord.getType());
272
273 if (NFC_MIME_TYPE.equals(mimeType)) {
274 return parseNfcProperties(new String(firstRecord.getPayload()));
275 }
276 }
277 }
278 return null;
279 }
280
281 private Intent parseNfcProperties(String data) {
282 try {
283 Properties props = new Properties();
284 props.load(new StringReader(data));
285
286 // Copy properties to intent.
287 Intent provisioningIntent = new Intent();
288 Enumeration<Object> propertyNames = props.keys();
289 while (propertyNames.hasMoreElements()) {
290 String propName = (String) propertyNames.nextElement();
291 provisioningIntent.putExtra(propName, props.getProperty(propName));
292 }
293 provisioningIntent.putExtra(DeviceProvisioningService.ORIGINAL_INTENT_KEY, true);
294 return provisioningIntent;
295 } catch (IOException e) {
296 error("Couldn't load payload", e);
297 return null;
298 }
299 }
300
301 private void initialize(Intent provisioningIntent) {
302 registerErrorTimeout(provisioningIntent);
303 setTimeAndTimezone(provisioningIntent);
304
305 enableWifi();
306 setLanguage(provisioningIntent);
307 }
308
309 private void registerErrorTimeout(Intent provisioningIntent) {
310 mTimeoutRunnable = new Runnable() {
311 @Override
312 public void run() {
313 error("timeout");
314 }
315 };
316
317 mHandler = new Handler();
318 long timeout = Long.parseLong(getStringExtra(provisioningIntent,
319 Preferences.TIMEOUT_KEY, TIMEOUT_IN_MS));
320 mHandler.postDelayed(mTimeoutRunnable, timeout);
321 }
322
323 private void setTimeAndTimezone(Intent provisioningIntent) {
324 String timeZoneId = provisioningIntent.getStringExtra(Preferences.TIME_ZONE_KEY);
325 String localTimeString = provisioningIntent.getStringExtra(Preferences.LOCAL_TIME_KEY);
326 Long localTime = localTimeString != null ? Long.valueOf(localTimeString) : null;
327
328 try {
329 final AlarmManager am = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
330 if (timeZoneId != null) am.setTimeZone(timeZoneId);
331 if (localTime != null) am.setTime(localTime);
332 } catch (Exception e) {
333 ProvisionLogger.loge("Failed to get alarm manager to set the system time/timezone.");
334 }
335 }
336
337 private void enableWifi() {
338 WifiManager wm = (WifiManager) getSystemService(Context.WIFI_SERVICE);
339 if (wm != null) {
340 int wifiState = wm.getWifiState();
341 boolean wifiOn = wifiState == WifiManager.WIFI_STATE_ENABLED;
342 if (!wifiOn) {
343 if (!wm.setWifiEnabled(true)) {
344 error("Unable to enable WiFi");
345 }
346 }
347 }
348 }
349
350 private void setLanguage(Intent provisioningIntent) {
351 String locale = provisioningIntent.getStringExtra(Preferences.LOCALE_KEY);
352 ProvisionLogger.logd("About to set locale: " + locale);
353 if (locale == null || locale.equals(Locale.getDefault().toString())) {
354 return;
355 }
356
357 try {
358 if (locale.length() == 5) {
359 Locale l = new Locale(locale.substring(0, 2), locale.substring(3, 5));
360 LocalePicker.updateLocale(l);
361 }
362 } catch (Exception e) {
363 ProvisionLogger.loge("failed to set the system locale");
364 }
365 }
366
367 @Override
368 public void onDestroy() {
369 super.onDestroy();
370
371 if (mStatusReceiver != null) {
372 unregisterReceiver(mStatusReceiver);
373 mStatusReceiver = null;
374 }
375 }
376
377 /**
378 * Sets preferences that are shared and persisted between activities and services.
379 *
380 * TODO: Refactor Preferences so the state created by this method is clear.
381 */
382 private void initializePreferences(Intent intent) {
383 // Copy most values directly from bump packet to preferences.
384 for (String propertyName : Preferences.propertiesToStore) {
385 mPrefs.setProperty(propertyName, intent.getStringExtra(propertyName));
386 }
387
388 if (mPrefs.getStringProperty(Preferences.WIFI_SSID_KEY) != null) {
389 String hiddenString = intent.getStringExtra(Preferences.WIFI_HIDDEN_KEY);
390 mPrefs.setProperty(Preferences.WIFI_HIDDEN_KEY, Boolean.parseBoolean(hiddenString));
391
392 if (mPrefs.getStringProperty(Preferences.WIFI_PROXY_HOST_KEY) != null) {
393
394 String proxyPortStr = intent.getStringExtra(Preferences.WIFI_PROXY_PORT_STRING_KEY);
395 try {
396 if (proxyPortStr != null) {
397 mPrefs.setProperty(Preferences.WIFI_PROXY_PORT_INT_KEY,
398 Integer.valueOf(proxyPortStr));
399 }
400 } catch (NumberFormatException e) {
401 ProvisionLogger.loge("Proxy port " + proxyPortStr
402 + " could not be parsed as a number.");
403 mPrefs.setProperty(Preferences.WIFI_PROXY_HOST_KEY, null);
404 }
405 }
406 }
407 }
408
409 protected void completeCheckbox(int state) {
410 ProvisionLogger.logd("Setting checkbox for state " + state);
411 Integer id = mStateToCheckbox.get(state);
412 if (id != null) {
413 CheckedTextView check = (CheckedTextView) findViewById(id);
414 if (check != null) {
415 check.setChecked(true);
416 }
417 }
418 }
419
420 private void cleanupAndFinish() {
421 ProvisionLogger.logd("Finishing DeviceOwnerProvisioningActivity");
422
423 if (mHandler != null) {
424 mHandler.removeCallbacks(mTimeoutRunnable);
425 }
426
427 PackageManager pkgMgr = getPackageManager();
428 pkgMgr.setComponentEnabledSetting(
429 DeviceOwnerProvisioningActivity.getComponentName(this),
430 PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
431 PackageManager.DONT_KILL_APP);
432
433 finish();
434 }
435
436 private void error(String logMsg) {
437 error(logMsg, null);
438 }
439
440 private void error(String logMsg, Throwable t) {
441 mPrefs.setError(logMsg.toString());
442 ProvisionLogger.loge("Error: " + logMsg, t);
443 ErrorDialog.showError(DeviceOwnerProvisioningActivity.this);
444 }
445
446 public static ComponentName getComponentName(Context context) {
447 String ourPackage = context.getPackageName();
448 String bumpLauncher = ourPackage + BUMP_LAUNCHER_ACTIVITY;
449 return new ComponentName(ourPackage, bumpLauncher);
450 }
451
452 private String getStringExtra(Intent intent, String key, String defaultValue) {
453 String rtn = intent.getStringExtra(key);
454 return (rtn != null) ? rtn : defaultValue;
455 }
456}