1/* 2 * Copyright (C) 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.omadm.service; 18 19import android.app.IntentService; 20import android.content.Context; 21import android.content.Intent; 22import android.content.res.AssetManager; 23import android.os.AsyncTask; 24import android.os.Handler; 25import android.os.IBinder; 26import android.os.PowerManager; 27import android.os.PowerManager.WakeLock; 28import android.os.RemoteException; 29import android.text.TextUtils; 30import android.util.Log; 31 32import com.android.omadm.plugin.DmtData; 33import com.android.omadm.plugin.DmtException; 34import com.android.omadm.plugin.IDMClientService; 35import com.android.omadm.plugin.impl.DmtPluginManager; 36 37import net.jcip.annotations.GuardedBy; 38 39import java.io.File; 40import java.io.FileNotFoundException; 41import java.io.FileOutputStream; 42import java.io.IOException; 43import java.io.InputStream; 44import java.io.OutputStream; 45import java.util.Map; 46import java.util.concurrent.ExecutionException; 47import java.util.concurrent.TimeUnit; 48import java.util.concurrent.TimeoutException; 49import java.util.concurrent.atomic.AtomicBoolean; 50 51/** 52 * This is the OMA DM client service as an IntentService. 53 * FIXME: this should be rewritten as a regular Service with an associated StateMachine. 54 */ 55public class DMClientService extends IntentService { 56 private static final String TAG = "DMClientService"; 57 static final boolean DBG = false; // STOPSHIP: change to false 58 59 // flag "DM session in progress" used from DMIntentReceiver 60 public static boolean sIsDMSessionInProgress; 61 62 private boolean mInitGood; 63 private WakeLock mWakeLock; 64 65 /** Lock object for {@link #mSession} and {@link #mServiceID}. */ 66 private final Object mSessionLock = new Object(); 67 68 @GuardedBy("mSessionLock") 69 private DMSession mSession; 70 71 @GuardedBy("mSessionLock") 72 private long mServiceID; 73 74 @GuardedBy("mSessionTimeOutHandler") 75 private final Handler mSessionTimeOutHandler = new Handler(); 76 77 /** AsyncTask to manage the settings SQLite database. */ 78 private DMConfigureTask mDMConfigureTask; 79 80 /** 81 * Helper class for DM session packages. 82 */ 83 static final class DMSessionPkg { 84 public DMSessionPkg(int type, long gId) { 85 mType = type; 86 mGlobalSID = gId; 87 mobj = null; 88 } 89 90 public final int mType; 91 public final long mGlobalSID; 92 public Object mobj; 93 public Object mobj2; 94 public boolean mbvalue; 95 } 96 97 // Class for clients to access. Because we know this service always runs 98 // in the same process as its clients, we don't need to deal with IPC. 99 public class LocalBinder extends IDMClientService.Stub { 100 @Override 101 public DmtData getDMTree(String path, boolean recursive) 102 throws RemoteException { 103 try { 104 if (DBG) 105 logd("getDMTree(\"" + path + "\", " + recursive + ") called"); 106 synchronized (mSessionLock) { 107 int nodeType = NativeDM.getNodeType(path); 108 String nodeValue = NativeDM.getNodeValue(path); 109 DmtData dmtData = new DmtData(nodeValue, nodeType); 110 if (nodeType == DmtData.NODE && recursive) { 111 addNodeChildren(path, dmtData); 112 } 113 return dmtData; 114 } 115 } catch (Exception e) { 116 loge("caught exception", e); 117 return new DmtData("", DmtData.STRING); 118 } 119 } 120 121 private void addNodeChildren(String path, DmtData node) throws DmtException { 122 for (Map.Entry<String, DmtData> child : node.getChildNodeMap().entrySet()) { 123 String childPath = path + '/' + child.getKey(); 124 125 int nodeType = NativeDM.getNodeType(childPath); 126 String nodeValue = NativeDM.getNodeValue(childPath); 127 128 DmtData newChildNode = new DmtData(nodeValue, nodeType); 129 130 node.addChildNode(child.getKey(), newChildNode); 131 132 if (nodeType == DmtData.NODE) { 133 addNodeChildren(childPath, newChildNode); 134 } 135 } 136 } 137 138 @Override 139 public int startClientSession(String path, String clientCert, String privateKey, 140 String alertType, String redirectURI, String username, String password) 141 throws RemoteException { 142 if (DBG) logd("startClientSession(\"" + path + "\", \"" + clientCert 143 + "\", \"" + privateKey + "\", \"" + alertType + "\", \"" + redirectURI 144 + "\", \"" + username + "\", \"" + password + "\") called"); 145 return 0; 146 } 147 148 @Override 149 public int notifyExecFinished(String path) throws RemoteException { 150 if (DBG) logd("notifyExecFinished(\"" + path + "\") called"); 151 return 0; 152 } 153 154 @Override 155 public int injectSoapPackage(String path, String command, String payload) 156 throws RemoteException { 157 if (DBG) logd("injectSoapPackage(\"" + path + "\", \"" + command 158 + "\", \"" + payload + "\") called"); 159 synchronized (mSessionLock) { 160 //return processSerializedTree(serverId, path, command, payload); // FIXME 161 } 162 return DMResult.SYNCML_DM_FAIL; 163 } 164 } 165 166 /** 167 * Create the IntentService, naming the worker thread DMClientService. 168 */ 169 public DMClientService() { 170 super(TAG); 171 } 172 173 @Override 174 public void onCreate() { 175 super.onCreate(); 176 177 logd("Enter onCreate tid=" + Thread.currentThread().getId()); 178 179 copyFilesFromAssets(); // wait for completion before continuing 180 181 mInitGood = (NativeDM.initialize() == DMResult.SYNCML_DM_SUCCESS); 182 DmtPluginManager.setContext(this); 183 184 PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); 185 WakeLock lock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, getClass().getName()); 186 lock.setReferenceCounted(false); 187 lock.acquire(); 188 logd("XXXXX mWakeLock.acquire() in DMClientService.onCreate() XXXXX"); 189 mWakeLock = lock; 190 191 mDMConfigureTask = new DMConfigureTask(); 192 mDMConfigureTask.execute(this); 193 } 194 195 @Override 196 public void onDestroy() { 197 super.onDestroy(); 198 199 logd("Enter onDestroy tid=" + Thread.currentThread().getId()); 200 201 mAbortSession = null; 202 203 if (mInitGood) NativeDM.destroy(); 204 205 getConfigDB().closeDatabase(); 206 207 logd("XXXXX mWakeLock.release() in DMClientService.onDestroy() XXXXX"); 208 mWakeLock.release(); 209 210 synchronized (mSessionLock) { 211 mSessionTimeOutHandler.removeCallbacks(mAbortSession); 212 } 213 214 if (DBG) logd("leave onDestroy"); 215 } 216 217 /** 218 * AsyncTask to create the DMConfigureDB object on a helper thread. 219 */ 220 private static class DMConfigureTask extends 221 AsyncTask<DMClientService, Void, DMConfigureDB> { 222 DMConfigureTask() {} 223 224 @Override 225 protected DMConfigureDB doInBackground(DMClientService... params) { 226 logd("creating new DMConfigureDB() on tid " 227 + Thread.currentThread().getId()); 228 return new DMConfigureDB(params[0]); 229 } 230 } 231 232 /** 233 * Process message on IntentService worker thread. 234 * @param pkg the parameters to pass from the Intent 235 */ 236 private void processMsg(DMSessionPkg pkg) { 237 // wait for up to 70 seconds for config DB to initialize. 238 if (getConfigDB() == null) { 239 loge("processMsg: getConfigDB() failed. Aborting session"); 240 return; 241 } 242 logd("processMsg: received pkg type " + pkg.mType + "; getConfigDB() succeeded"); 243 244 sIsDMSessionInProgress = true; 245 246 // check if DMT locked by DMSettingsProvider and wait. If DMT is 247 // locked more then 1 minute (error case, means that something 248 // wrong with DMSettingsProvider) we are continuing execution 249 250 try { 251 synchronized (mSessionLock) { 252 mSession = new DMSession(this); 253 mServiceID = pkg.mGlobalSID; 254 } 255 256 int timeOutSecond = 600 * 1000; /* 10 minutes */ 257 int ret = DMResult.SYNCML_DM_SESSION_PARAM_ERR; 258 259 switch (pkg.mType) { 260 case DMIntent.TYPE_PKG0_NOTIFICATION: 261 if (DBG) { 262 logd("Start pkg0 alert session"); 263 } 264 startTimeOutTick(timeOutSecond); 265 synchronized (mSessionLock) { 266 ret = mSession.startPkg0AlertSession((byte[]) pkg.mobj); 267 } 268 break; 269 270 case DMIntent.TYPE_FOTA_CLIENT_SESSION_REQUEST: 271 if (DBG) { 272 logd("Start fota client initialized session"); 273 } 274 startTimeOutTick(timeOutSecond); 275 synchronized (mSessionLock) { 276 ret = mSession.startFotaClientSession( 277 (String) pkg.mobj, (String) pkg.mobj2); 278 } 279 break; 280 281 case DMIntent.TYPE_FOTA_NOTIFY_SERVER: 282 if (DBG) { 283 logd("Start FOTA notify session"); 284 } 285 startTimeOutTick(timeOutSecond); 286 synchronized (mSessionLock) { 287 ret = mSession.fotaNotifyDMServer((FotaNotifyContext) pkg.mobj); 288 } 289 break; 290 291 case DMIntent.TYPE_CLIENT_SESSION_REQUEST: 292 if (DBG) { 293 logd("Start client initialized session:"); 294 } 295 if (pkg.mobj != null) { 296 startTimeOutTick(timeOutSecond); 297 synchronized (mSessionLock) { 298 ret = mSession.startClientSession((String) pkg.mobj); 299 } 300 } 301 break; 302 303 case DMIntent.TYPE_LAWMO_NOTIFY_SESSION: 304 if (DBG) { 305 logd("Start LAWMO notify session"); 306 } 307 startTimeOutTick(timeOutSecond); 308 synchronized (mSessionLock) { 309 ret = mSession 310 .startLawmoNotifySession((FotaNotifyContext) pkg.mobj); 311 } 312 break; 313 } 314 315 logd("DM Session result code=" + ret); 316 317 synchronized (mSessionLock) { 318 mSession = null; 319 } 320 321 Intent intent = new Intent(DMIntent.DM_SERVICE_RESULT_INTENT); 322 intent.putExtra(DMIntent.FIELD_DMRESULT, ret); 323 intent.putExtra(DMIntent.FIELD_REQUEST_ID, pkg.mGlobalSID); 324 sendBroadcast(intent); 325 } finally { 326 //set static flag "DM session in progress" to false. Used from DMIntentReceiver 327 sIsDMSessionInProgress = false; 328 } 329 } 330 331 void cancelSession(long requestID) { 332 synchronized (mSessionLock) { 333 if (requestID == 0 || mServiceID == requestID) { 334 if (mSession != null) { 335 loge("Cancel session with serviceID: " + mServiceID); 336 mSession.cancelSession(); 337 } 338 } 339 } 340 } 341 342 /** 343 * Called on worker thread with the Intent to handle. Calls DMSession directly. 344 * @param intent The intent to handle 345 */ 346 @Override 347 protected void onHandleIntent(Intent intent) { 348 long requestID = intent.getLongExtra(DMIntent.FIELD_REQUEST_ID, 0); 349 int intentType = intent.getIntExtra(DMIntent.FIELD_TYPE, DMIntent.TYPE_UNKNOWN); 350 351 logd("onStart intentType: " + intentType + " requestID: " 352 + requestID); 353 354 // wait for up to 70 seconds for config DB to initialize. 355 if (getConfigDB() == null) { 356 loge("WARNING! getConfigDB() failed. Aborting session"); 357 return; 358 } 359 if (DBG) logd("getConfigDB() succeeded"); 360 361 switch (intentType) { 362 case DMIntent.TYPE_PKG0_NOTIFICATION: { 363 if (DBG) logd("Pkg0 provision received."); 364 365 byte[] pkg0data = intent.getByteArrayExtra(DMIntent.FIELD_PKG0); 366 if (pkg0data == null) { 367 if (DBG) logd("Pkg0 provision received, but no pkg0 data."); 368 return; 369 } 370 DMSessionPkg pkg = new DMSessionPkg(intentType, requestID); 371 pkg.mobj = intent.getByteArrayExtra(DMIntent.FIELD_PKG0); 372 processMsg(pkg); 373 break; 374 } 375 case DMIntent.TYPE_FOTA_CLIENT_SESSION_REQUEST: { 376 if (DBG) logd("Client initiated dm session was received."); 377 378 DMSessionPkg pkg = new DMSessionPkg(intentType, requestID); 379 String serverID = intent.getStringExtra(DMIntent.FIELD_SERVERID); 380 String alertStr = intent.getStringExtra(DMIntent.FIELD_ALERT_STR); 381 382 if (TextUtils.isEmpty(serverID)) { 383 loge("missing server ID, returning"); 384 return; 385 } 386 387 if (TextUtils.isEmpty(alertStr)) { 388 loge("missing alert string, returning"); 389 return; 390 } 391 392 pkg.mobj = serverID; 393 pkg.mobj2 = alertStr; 394 processMsg(pkg); 395 break; 396 } 397 case DMIntent.TYPE_FOTA_NOTIFY_SERVER: { 398 String result = intent.getStringExtra(DMIntent.FIELD_FOTA_RESULT); 399 String pkgURI = intent.getStringExtra(DMIntent.FIELD_PKGURI); 400 String alertType = intent.getStringExtra(DMIntent.FIELD_ALERTTYPE); 401 String serverID = intent.getStringExtra(DMIntent.FIELD_SERVERID); 402 String correlator = intent.getStringExtra(DMIntent.FIELD_CORR); 403 404 if (DBG) logd("FOTA_NOTIFY_SERVER_SESSION Input==>\n" + " Result=" 405 + result + '\n' + " pkgURI=" + pkgURI + '\n' 406 + " alertType=" + alertType + '\n' + " serverID=" 407 + serverID + '\n' + " correlator=" + correlator); 408 409 DMSessionPkg pkg = new DMSessionPkg(intentType, requestID); 410 pkg.mobj = new FotaNotifyContext(result, pkgURI, alertType, 411 serverID, correlator); 412 processMsg(pkg); 413 break; 414 } 415 case DMIntent.TYPE_CLIENT_SESSION_REQUEST: { 416 if (DBG) logd("Client initiated dm session was received."); 417 418 DMSessionPkg pkg = new DMSessionPkg(intentType, requestID); 419 String serverID = intent.getStringExtra(DMIntent.FIELD_SERVERID); 420 int timer = intent.getIntExtra(DMIntent.FIELD_TIMER, 0); 421 422 // XXXXX FIXME this should not be here! 423 synchronized (this) { 424 try { 425 if (DBG) logd("Timeout: " + timer); 426 if (timer > 0) { 427 wait(timer * 1000); 428 } 429 } catch (InterruptedException e) { 430 if (DBG) logd("Waiting has been interrupted."); 431 } 432 } 433 if (DBG) logd("Starting session."); 434 435 if (serverID != null && !serverID.isEmpty()) { 436 pkg.mobj = serverID; 437 processMsg(pkg); 438 } 439 break; 440 } 441 case DMIntent.TYPE_CANCEL_DM_SESSION: { 442 cancelSession(requestID); 443 processMsg(new DMSessionPkg(DMIntent.TYPE_DO_NOTHING, requestID)); 444 break; 445 } 446 case DMIntent.TYPE_LAWMO_NOTIFY_SESSION: { 447 if (DBG) logd("LAWMO Notify DM Session was received"); 448 449 DMSessionPkg pkg = new DMSessionPkg(intentType, requestID); 450 451 String result = intent.getStringExtra(DMIntent.FIELD_LAWMO_RESULT); 452 String pkgURI = intent.getStringExtra(DMIntent.FIELD_PKGURI); 453 String alertType = intent.getStringExtra(DMIntent.FIELD_ALERTTYPE); 454 String correlator = intent.getStringExtra(DMIntent.FIELD_CORR); 455 456 pkg.mobj = new FotaNotifyContext(result, pkgURI, alertType, null, correlator); 457 processMsg(pkg); 458 break; 459 } 460 } 461 } 462 463 public int deleteNode(String node) { 464 if (mInitGood) { 465 return NativeDM.deleteNode(node); 466 } 467 return DMResult.SYNCML_DM_FAIL; 468 } 469 470 public int createInterior(String node) { 471 if (mInitGood) { 472 return NativeDM.createInterior(node); 473 } 474 return DMResult.SYNCML_DM_FAIL; 475 } 476 477 public int createLeaf(String node, String value) { 478 if (mInitGood) { 479 return NativeDM.createLeaf(node, value); 480 } 481 return DMResult.SYNCML_DM_FAIL; 482 } 483 484 public String getNodeInfoSP(String node) { 485 if (mInitGood) { 486 return NativeDM.getNodeInfo(node); 487 } 488 return null; 489 } 490 491 // This is the object that receives interactions from clients. See 492 // RemoteService for a more complete example. 493 private final IBinder mBinder = new LocalBinder(); 494 495 @Override 496 public IBinder onBind(Intent arg0) { 497 if (DBG) logd("entering onBind()"); 498 DMConfigureDB db = getConfigDB(); // wait for configure DB to initialize 499 if (DBG) logd("returning mBinder"); 500 return mBinder; 501 } 502 503 /** 504 * Get the {@code DMConfigureDB} object from the AsyncTask, waiting up to 70 seconds. 505 * @return the {@code DMConfigureDB} object, or null if the AsyncTask failed 506 */ 507 public DMConfigureDB getConfigDB() { 508 try { 509 return mDMConfigureTask.get(70, TimeUnit.SECONDS); 510 } catch (InterruptedException e) { 511 loge("onBind() got InterruptedException waiting for config DB", e); 512 } catch (ExecutionException e) { 513 loge("onBind() got ExecutionException waiting for config DB", e); 514 } catch (TimeoutException e) { 515 loge("onBind() got TimeoutException waiting for config DB", e); 516 } 517 return null; 518 } 519 520 String parseBootstrapServerId(byte[] data, boolean isWbxml) { 521 String retServerId = NativeDM.parseBootstrapServerId(data, isWbxml); 522 if (DBG) logd("parseBootstrapServerId retServerId=" + retServerId); 523 524 if (DBG) { // dump data for debug 525 int logLevel = getConfigDB().getSyncMLLogLevel(); 526 if (logLevel > 0) { 527 try { 528 // FIXME SECURITY: don't open file as world writeable, WTF! 529 FileOutputStream os = openFileOutput("syncml_" + System.currentTimeMillis() 530 + ".dump", MODE_WORLD_WRITEABLE); 531 os.write(data); 532 os.close(); 533 logd("xml/wbxml file saved to " 534 + getApplication().getFilesDir().getAbsolutePath()); 535 536 if (isWbxml && logLevel == 2) { 537 byte[] xml = NativeDM.nativeWbxmlToXml(data); 538 if (xml != null) { 539 // FIXME SECURITY: don't open file as world writeable, WTF! 540 FileOutputStream xmlos = openFileOutput("syncml_" 541 + System.currentTimeMillis() + ".xml", MODE_WORLD_WRITEABLE); 542 xmlos.write(xml); 543 xmlos.close(); 544 logd("wbxml2xml converted successful and saved to file"); 545 } 546 } 547 } 548 catch (FileNotFoundException e) { 549 logd("unable to open file for wbxml, e=" + e.toString()); 550 } 551 catch (IOException e) { 552 logd("unable to write to wbxml file, e=" + e.toString()); 553 } 554 catch(Exception e) { 555 loge("Unexpected exception converting wbxml to xml, e=" + e.toString()); 556 } 557 } 558 } 559 return retServerId; 560 } 561 562 private static int processBootstrapScript(byte[] data, boolean isWbxml, String serverId) { 563 int retcode = NativeDM.processBootstrapScript(data, isWbxml, serverId); 564 if (DBG) logd("processBootstrapScript retcode=" + retcode); 565 return retcode; 566 } 567 568 private Runnable mAbortSession = new Runnable() { 569 @Override 570 public void run() { 571 cancelSession(0); 572 } 573 }; 574 575 // FIXME: only used from SessionThread inner class 576 private void startTimeOutTick(long delayTime) { 577 synchronized (mSessionTimeOutHandler) { 578 mSessionTimeOutHandler.removeCallbacks(mAbortSession); 579 mSessionTimeOutHandler.postDelayed(mAbortSession, delayTime); 580 } 581 } 582 583 private static boolean copyFile(InputStream in, File to) { 584 try { 585 if (!to.exists()) { 586 to.createNewFile(); 587 } 588 OutputStream out = new FileOutputStream(to); 589 byte[] buf = new byte[1024]; 590 int len; 591 while ((len = in.read(buf)) > 0) { 592 out.write(buf, 0, len); 593 } 594 out.close(); 595 } catch (IOException e) { 596 loge("Error: copyFile exception", e); 597 return false; 598 } 599 return true; 600 } 601 602 /** 603 * Copy files from assets folder. 604 * @return true on success; false on any failure 605 */ 606 private boolean copyFilesFromAssets() { 607 // Check files in assets folder 608 String strDes = getFilesDir().getAbsolutePath() + "/dm"; 609 logd("Directory is: " + strDes); 610 File dirDes = new File(strDes); 611 if (dirDes.exists() && dirDes.isDirectory()) { 612 logd("Predefined files already created: " + strDes); 613 return true; 614 } 615 logd("Predefined files not created: " + strDes); 616 if (!dirDes.mkdir()) { 617 logd("Failed to create dir: " + dirDes.getAbsolutePath()); 618 return false; 619 } 620 // Create log directory. 621 File dirLog = new File(dirDes, "log"); 622 // FIXME: don't ignore return value 623 dirLog.mkdir(); 624 if (DBG) logd("read assets"); 625 try { 626 AssetManager am = getAssets(); 627 String[] arrRoot = am.list("dm"); 628 int cnt = arrRoot.length; 629 if (DBG) logd("assets count: " + cnt); 630 for (int i = 0; i < cnt; i++) { 631 if (DBG) logd("Root No. " + i + ':' + arrRoot[i]); 632 File dir2 = new File(dirDes, arrRoot[i]); 633 if (!dir2.mkdir()) { 634 // FIXME: don't ignore return value 635 dirDes.delete(); 636 return false; 637 } 638 String[] arrSub = am.list("dm/" + arrRoot[i]); 639 int cntSub = arrSub.length; 640 if (DBG) logd(arrRoot[i] + " has " + cntSub + " items"); 641 if (cntSub > 0) { 642 for (int j = 0; j < cntSub; j++) { 643 if (DBG) logd("Sub No. " + j + ':' + arrSub[j]); 644 File to2 = new File(dir2, arrSub[j]); 645 String strFrom = "dm/" + arrRoot[i] + '/' + arrSub[j]; 646 InputStream in2 = am.open(strFrom); 647 if (!copyFile(in2, to2)) { 648 // FIXME: don't ignore return value 649 dirDes.delete(); 650 return false; 651 } 652 } 653 } 654 } 655 } catch (IOException e) { 656 loge("error copying file from assets", e); 657 return false; 658 } 659 return true; 660 } 661 662 private static void logd(String msg) { 663 Log.d(TAG, msg); 664 } 665 666 private static void loge(String msg) { 667 Log.e(TAG, msg); 668 } 669 670 private static void loge(String msg, Throwable tr) { 671 Log.e(TAG, msg, tr); 672 } 673} 674