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