Home | History | Annotate | Download | only in service
      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 
     17 package com.android.omadm.service;
     18 
     19 import android.app.IntentService;
     20 import android.content.Context;
     21 import android.content.Intent;
     22 import android.content.res.AssetManager;
     23 import android.os.AsyncTask;
     24 import android.os.Handler;
     25 import android.os.IBinder;
     26 import android.os.PowerManager;
     27 import android.os.PowerManager.WakeLock;
     28 import android.os.RemoteException;
     29 import android.text.TextUtils;
     30 import android.util.Log;
     31 
     32 import com.android.omadm.plugin.DmtData;
     33 import com.android.omadm.plugin.DmtException;
     34 import com.android.omadm.plugin.IDMClientService;
     35 import com.android.omadm.plugin.impl.DmtPluginManager;
     36 
     37 import net.jcip.annotations.GuardedBy;
     38 
     39 import java.io.File;
     40 import java.io.FileNotFoundException;
     41 import java.io.FileOutputStream;
     42 import java.io.IOException;
     43 import java.io.InputStream;
     44 import java.io.OutputStream;
     45 import java.util.Map;
     46 import java.util.concurrent.ExecutionException;
     47 import java.util.concurrent.TimeUnit;
     48 import java.util.concurrent.TimeoutException;
     49 import 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  */
     55 public 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