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.plugin.impl;
18
19import android.content.ComponentName;
20import android.content.Context;
21import android.content.Intent;
22import android.content.ServiceConnection;
23import android.content.pm.ResolveInfo;
24import android.os.IBinder;
25import android.os.RemoteException;
26import android.text.TextUtils;
27import android.util.Log;
28
29import com.android.omadm.plugin.DmtData;
30import com.android.omadm.plugin.DmtException;
31import com.android.omadm.plugin.DmtPluginNode;
32import com.android.omadm.plugin.ErrorCodes;
33import com.android.omadm.plugin.IDmtPlugin;
34
35import java.util.ArrayList;
36import java.util.Arrays;
37import java.util.HashMap;
38import java.util.List;
39import java.util.Map;
40
41/**
42 * This class does not manage DMT plugins. It is a proxy between native plugin and java plugins.
43 * An instance of the class is created for each plugin.
44 */
45public final class DmtPluginManager {
46
47    private static final String TAG = "DM_DmtPluginManager";
48    private static final boolean DBG = false;
49
50    private static Context sContext;
51
52    /* Parameters of the plug-in */
53    private String mPath;
54    private String mUid;
55    private String mServerID;   // FIXME: mServerID is never set, remove?
56    private Map<String, String> mParameters;
57
58    private final class DmtServiceConnection implements ServiceConnection {
59
60        DmtServiceConnection() {
61        }
62
63        @Override
64        public synchronized void onServiceConnected(ComponentName className, IBinder service) {
65            logd("Enter onServiceConnected...");
66            logd("Class:" + className + " service:" + service);
67
68            mPluginConnection = IDmtPlugin.Stub.asInterface(service);
69
70            try {
71                notifyAll();
72            } catch (Exception e) {
73                loge("onServiceConnected(): exception", e);
74            }
75        }
76
77        @Override
78        public void onServiceDisconnected(ComponentName className) {
79            logd("Plug-in service disconnected. className:" + className);
80            mPluginConnection = null;
81        }
82
83        public synchronized void waitForConnection() {
84            try {
85                Intent intent = new Intent(mUid);
86                intent.putExtra("rootPath", mPath);
87                boolean currentPackageMatch = false;
88                List<ResolveInfo> intentServices = sContext.getPackageManager()
89                        .queryIntentServices(intent, 0);
90                if (intentServices != null) {
91                    for (ResolveInfo resolveInfo : intentServices) {
92                        logd("getPackageName is: " + sContext.getPackageName());
93                        logd("resolveInfo.serviceInfo.packageName is: "
94                                + resolveInfo.serviceInfo.packageName);
95                        if(resolveInfo.serviceInfo.packageName.equals(sContext.getPackageName())) {
96                            try {
97                                Class.forName(mUid);
98                            } catch (ClassNotFoundException e1) {
99                                loge("ClassNotFoundException in waitForConnection", e1);
100                                currentPackageMatch = true;
101                            }
102                        }
103                    }
104                }
105
106                if (currentPackageMatch) {
107                    return;
108                }
109
110                if (DBG) logd("Calling bindService: uid=\"" + mUid + "\" path=\"" + mPath + '"');
111                sContext.bindService(intent, mConnector,Context.BIND_AUTO_CREATE);
112
113                wait(10000);        // FIXME: wait not in loop!
114                if (DBG) logd("Waiting is finished");
115            } catch (Exception e) {
116                loge("exception in waitForConnection", e);
117            }
118        }
119    }
120
121    private final DmtServiceConnection mConnector = new DmtServiceConnection();
122
123    private IDmtPlugin mPluginConnection;
124
125    public DmtPluginManager() {
126        logd("DmtPluginManager.java constructor...");
127    }
128
129    public static void setContext(Context context) {
130        if (DBG) logd("Enter setContext(" + context + ')');
131
132        if (context == null) {
133            logd("Context is null!");
134            return;
135        }
136
137        Context appContext = context.getApplicationContext();
138
139        if (DBG) logd("app context is: " + appContext);
140
141        sContext = appContext;
142    }
143
144    /**
145     * Initialize Java plugin. Called from JNI in:
146     *  engine/javaplugin/nativelib/src/DmtJavaPluginManager.cc
147     *
148     * @param path the root path of the plug-in.
149     * @param parameters initial parameters of the plug-in, as an array of strings.
150     * @return {@link ErrorCodes#SYNCML_DM_SUCCESS} on success, error code on failure.
151     */
152    public boolean initJavaPlugin(String path, String[] parameters) {
153        if (DBG) logd("Enter initJavaPlugin: path = " + path);
154        if (DBG) logd("parameters are: " + Arrays.toString(parameters));
155
156        if (TextUtils.isEmpty(path)) {
157            loge("Invalid path!....");
158            return false;
159        }
160
161        if (mPath != null) {
162            loge("Plugin already loaded!....");
163            return false;
164        }
165
166        if (DBG) logd("Parameters count is " + parameters.length);
167        if (parameters.length % 2 != 0) {
168            loge("Parameter count is not right.");
169            return false;
170        }
171
172        // FIXME: replace HashTable with HashMap
173        Map<String, String> params = new HashMap<String, String>();
174        for (int i = 0; i < parameters.length; i += 2 ) {
175            params.put(parameters[i], parameters[i + 1]);
176
177            if ("_uid".equals(parameters[i])) {
178                mUid = parameters[i + 1];
179            }
180        }
181
182        if (TextUtils.isEmpty(mUid)) {
183            loge("uid is empty...");
184            return false;
185        }
186
187        mParameters = params;
188        mPath       = path;
189
190        return bindPluginService();
191    }
192
193    /**
194     * Performs "Execute" command on a plug-in node.
195     * Called from JNI.
196     *
197     * @param args exec plug-in arguments.
198     * @param correlator correlator.
199     * @return {@link ErrorCodes#SYNCML_DM_SUCCESS} on success, error code on failure.
200     */
201    public int executeNode(String args, String correlator) {
202        if (DBG) logd("Enter executeNode(\"" + args + "\", \"" + correlator + "\")");
203
204        if (mPluginConnection == null) {
205            loge("There is no bound plug-in");
206            return ErrorCodes.SYNCML_DM_FAIL;
207        }
208
209        try {
210            return mPluginConnection.exec(mPath, args, correlator);
211        } catch (Exception e) {
212            loge("Exception in executeNode", e);
213            return ErrorCodes.SYNCML_DM_FAIL;
214        }
215    }
216
217    /**
218     * Performs commit operation under current DMT.
219     * Called from JNI.
220     *
221     * @return {@link ErrorCodes#SYNCML_DM_SUCCESS} on success, error code on failure.
222     */
223    public int commit() {
224        loge("Enter commit... " + mPath);
225
226        if (mPluginConnection == null) {
227            loge("There is no bound plug-in");
228            return ErrorCodes.SYNCML_DM_FAIL;
229        }
230
231        try {
232            return mPluginConnection.commit();
233        } catch (Exception e) {
234            loge("Exception in commit", e);
235            return ErrorCodes.SYNCML_DM_FAIL;
236        }
237    }
238
239    /**
240     * Sets Server ID of the plug-in.
241     * Called from JNI.
242     *
243     * @param serverID service ID.
244     */
245    public void setServerID(String serverID) {
246        if (DBG) logd("Enter setServerID(\"" + serverID + "\")");
247
248        if (mPluginConnection == null) {
249            loge("There is no bound plug-in");
250            return;
251        }
252
253        try {
254            mPluginConnection.setServerID(serverID);
255        } catch (Exception e) {
256            loge("Exception in setServerID", e);
257       }
258    }
259
260    /**
261     * Creates an interior node in the tree for the specified path.
262     * Called from JNI.
263     *
264     * @param path full path to the node.
265     * @return {@link ErrorCodes#SYNCML_DM_SUCCESS} on success, error code on failure.
266     */
267    public int createInteriorNode(String path) {
268        if (DBG) logd("Enter createInteriorNode(\"" + path + "\")");
269
270        if (mPluginConnection == null) {
271            loge("There is no bound plug-in");
272            return ErrorCodes.SYNCML_DM_FAIL;
273        }
274
275        try {
276            return mPluginConnection.createInteriorNode(getFullPath(path));
277        } catch (Exception e) {
278            loge("Exception in createInteriorNode", e);
279            return ErrorCodes.SYNCML_DM_FAIL;
280        }
281    }
282
283    /**
284     * Creates a leaf node in the tree by given path.
285     * Called from JNI.
286     *
287     * @param path full path to the node.
288     * @param type type of node, defined as constants in {@link DmtData}.
289     * @param value the new value to set.
290     * @return {@link ErrorCodes#SYNCML_DM_SUCCESS} on success, error code on failure.
291     */
292    public int createLeafNode(String path, int type, String value) {
293        if (DBG) logd("Enter createLeafNode(\"" + path + "\", " + type + ", \"" + value + "\")");
294
295        if (mPluginConnection == null) {
296            loge("There is no bound plug-in");
297            return ErrorCodes.SYNCML_DM_FAIL;
298        }
299
300        if (type < DmtData.NULL || type >= DmtData.NODE) {
301            loge("Invalid data type: " + type);
302            return ErrorCodes.SYNCML_DM_FAIL;
303        }
304
305        DmtData data = new DmtData(value, type);
306
307        try {
308            return mPluginConnection.createLeafNode(getFullPath(path), data);
309        } catch (Exception e) {
310            loge("Exception in createLeafNode", e);
311            return ErrorCodes.SYNCML_DM_FAIL;
312        }
313    }
314
315    /**
316     * Renames a node.
317     * Called from JNI.
318     *
319     * NOTE: This is an optional command of the OMA DM protocol.
320     *       Currently java plug-ins do not support the command.
321     *
322     * @param path full path to node to be renamed.
323     * @param newNodeName new node name.
324     * @return {@link ErrorCodes#SYNCML_DM_SUCCESS} on success, error code on failure.
325     */
326    public int renameNode(String path, String newNodeName) {
327        if (DBG) logd("Enter renameNode(\"" + path + "\", \"" + newNodeName + "\")");
328
329        if (mPluginConnection == null) {
330            loge("There is no bound plug-in");
331            return ErrorCodes.SYNCML_DM_FAIL;
332        }
333
334        if (TextUtils.isEmpty(newNodeName)) {
335            loge("Invalid new node name!");
336            return ErrorCodes.SYNCML_DM_FAIL;
337        }
338
339        try {
340            return mPluginConnection.renameNode(getFullPath(path), newNodeName);
341        } catch (Exception e) {
342            loge("Exception in renameNode", e);
343            return ErrorCodes.SYNCML_DM_FAIL;
344        }
345    }
346
347    /**
348     * Deletes a node with the specified path.
349     * Called from JNI.
350     *
351     * @param path full path to the node.
352     * @return {@link ErrorCodes#SYNCML_DM_SUCCESS} on success, error code on failure.
353     */
354    public int deleteNode(String path) {
355        if (DBG) logd("Enter deleteNode(\"" + path + "\")");
356
357        if (mPluginConnection == null) {
358            loge("There is no bound plug-in");
359            return ErrorCodes.SYNCML_DM_FAIL;
360        }
361
362        try {
363            return mPluginConnection.deleteNode(getFullPath(path));
364        } catch (Exception e) {
365            loge("Exception in deleteNode", e);
366            return ErrorCodes.SYNCML_DM_FAIL;
367        }
368    }
369
370    /**
371     * Set new value for the specified node.
372     * Called from JNI.
373     *
374     * @param path full path to the node.
375     * @param type type of node, defined as constants in {@link DmtData}.
376     * @param value the new value to set.
377     * @return {@link ErrorCodes#SYNCML_DM_SUCCESS} on success, error code on failure.
378     */
379    public int setNodeValue(String path, int type, String value) {
380        if (DBG) logd("Enter setNodeValue(\"" + path + "\", " + type + ", \"" + value + "\")");
381
382        if (mPluginConnection == null) {
383            loge("There is no bound plug-in");
384            return ErrorCodes.SYNCML_DM_FAIL;
385        }
386
387        if (type < DmtData.NULL || type >= DmtData.NODE) {
388            loge("Invalid data type: " + type);
389            return ErrorCodes.SYNCML_DM_FAIL;
390        }
391
392        DmtData data = new DmtData(value, type);
393
394        try {
395            logd("Update leaf node: path = " + path + ", data = " + data.getString());
396            return mPluginConnection.updateLeafNode(getFullPath(path), data);
397        } catch (Exception e) {
398            loge("Exception in setNodeValue", e);
399            return ErrorCodes.SYNCML_DM_FAIL;
400        }
401    }
402
403    /**
404     * Returns value of leaf node for the specified path.
405     * Called from JNI.
406     *
407     * @param path path to the leaf node.
408     * @return String array where the first element is value type and the second is the value.
409     * @throws DmtException in case of error.
410     */
411    public String[] getNodeValue(String path) throws DmtException {
412        if (DBG) logd("Enter getNodeValue(\"" + path + "\")");
413
414        if (mPluginConnection == null) {
415            loge("There is no bound plug-in");
416            throw new DmtException("There is no bound plug-in");
417        }
418
419        DmtData data;
420        try {
421            data = mPluginConnection.getNodeValue(getFullPath(path));
422        } catch (Exception e) {
423            loge("Exception in getNodeValue", e);
424            throw new DmtException(e.getMessage());
425        }
426
427        if (data == null) {
428            int retcode;
429            try {
430                retcode = mPluginConnection.getOperationResult();
431            } catch (Exception e) {
432                loge("Exception in getNodeValue", e);
433                throw new DmtException(e.getMessage());
434            }
435
436            if (retcode == ErrorCodes.SYNCML_DM_SUCCESS) {
437                loge("Invalid plug-in implementation!");
438                throw new DmtException("Invalid plug-in implementation!");
439            }
440            /*
441             * Added this block to return Error code to DM Engine since
442             * throwing an exception is causing a VM error and aborting
443             * the app.
444             */
445            else if (retcode == ErrorCodes.SYNCML_DM_UNSUPPORTED_OPERATION) {
446                loge("Get feature not implemented on this node");
447                String[] errString = new String[1];
448                errString[0] = Integer.toString(retcode);
449                return errString;
450            }
451
452            else {
453                loge("Error occurred while doing a get on this node");
454                String[] errString = new String[1];
455                errString[0] = Integer.toString(retcode);
456                return errString;
457            }
458
459            //throw new DmtException(retcode, "Value is not set");
460        }
461
462        int dataType = data.getType();
463
464        switch (dataType) {
465            case DmtData.NULL:
466            case DmtData.STRING:
467            case DmtData.INT:
468            case DmtData.BOOL:
469            case DmtData.BIN:
470            case DmtData.DATE:
471            case DmtData.TIME:
472            case DmtData.FLOAT:
473                String value = data.getString();
474                String[] resStrArr = new String[2];
475                resStrArr[0] = Integer.toString(dataType);
476                resStrArr[1] = TextUtils.isEmpty(value) ? "" : value;
477                return resStrArr;
478
479            case DmtData.NODE:
480                throw new DmtException("Operation not allowed for interior node!");
481
482            default:
483                throw new DmtException("Invalid node type");
484        }
485    }
486
487    /**
488     * Gets a set of nodes for the specified path.
489     * Called from JNI.
490     *
491     * If the plug-in cannot return the set, null value shall be returned by
492     * the method and DM engine will use the getOperationResult() method
493     * to get result of the operation.
494     *
495     * @return a String array containing triples of (key, type, value) as Strings
496     * @throws DmtException on any exception
497     */
498    public String[] getNodes() throws DmtException {
499        if (DBG) logd("Enter getNodes...");
500
501        if (mPluginConnection == null) {
502            loge("There is no bound plug-in");
503            throw new DmtException("There is no bound plug-in");
504        }
505
506        Map<String, DmtPluginNode> pluginNodes;
507        try {
508            pluginNodes = (Map<String, DmtPluginNode>) mPluginConnection.getNodes(mPath);
509        } catch (RemoteException e) {
510            loge("RemoteException in getNodes", e);
511            throw new DmtException(e.getMessage());
512        }
513
514        if (pluginNodes == null) {
515            int retcode;
516            try {
517                retcode = mPluginConnection.getOperationResult();
518            } catch (Exception e) {
519                loge("Exception in getNodes", e);
520                throw new DmtException(e.getMessage());
521            }
522
523            if (retcode == ErrorCodes.SYNCML_DM_SUCCESS) {
524                loge("Invalid plug-in implementation!");
525                throw new DmtException("Invalid plug-in implementation!");
526            }
527
528            throw new DmtException(retcode, "Value is not set");
529        }
530
531        if (pluginNodes.isEmpty()) {
532            // FIXME: zero-length array constructed
533            return new String[0];
534        }
535
536        if (DBG) logd("Data plugin has " + pluginNodes.size() + " nodes.");
537        String[] resStrArr = new String[pluginNodes.size() * 3];
538
539        int i = 0;
540        for (String key : pluginNodes.keySet()) {
541            if (DBG) logd("No." + i + " : " + key);
542            DmtPluginNode tmpNode = pluginNodes.get(key);
543            if (tmpNode == null) {
544                throw new DmtException("Invalid map of all nodes");
545            }
546            resStrArr[i] = getRelativePath(key);
547            resStrArr[i + 1] = Integer.toString(tmpNode.getType());
548            switch (tmpNode.getType()) {
549                case DmtData.NODE:
550                    StringBuilder tmpSB = new StringBuilder("");
551                    DmtData data = tmpNode.getValue();
552                    if (data != null) {
553                        Map<String, DmtData> childNodes = data.getChildNodeMap();
554                        for (String subNodeName : childNodes.keySet()) {
555                            if (TextUtils.isEmpty(subNodeName)) {
556                                loge("invalid interior node value for " + subNodeName + "!!!");
557                                throw new DmtException("Invalid interior node value");
558                            } else {
559                                tmpSB.append(subNodeName).append('\n');
560                            }
561                        }
562                    }
563                    resStrArr[i + 2] = tmpSB.toString();
564                    break;
565
566                case DmtData.NULL:
567                case DmtData.STRING:
568                case DmtData.INT:
569                case DmtData.BOOL:
570                case DmtData.BIN:
571                case DmtData.DATE:
572                case DmtData.TIME:
573                case DmtData.FLOAT:
574                    resStrArr[i + 2] = "";
575                    break;
576
577                default:
578                    loge("invalid node type " + tmpNode.getType() + "!!!!");
579                    throw new DmtException("Invalid node type");
580            }
581            i += 3;
582        }
583        return resStrArr;
584    }
585
586    public void release() {
587        if (DBG) logd("Enter release... " + mPath);
588
589        if (mPluginConnection == null) {
590            return;
591        }
592
593        try {
594            mPluginConnection.release();
595        } catch (Exception e) {
596            loge("exception releasing plugin", e);
597        }
598
599        mPluginConnection = null;
600
601        try {
602            sContext.unbindService(mConnector);
603        } catch (Exception e) {
604            loge("unbindService exception in release", e);
605        }
606    }
607
608    private boolean bindPluginService() {
609        if (DBG) logd("Enter bindPluginService...");
610
611        if (sContext == null) {
612            loge("Undefined context!");
613            return false;
614        }
615
616        if (mPluginConnection != null) {
617            loge("Already bound!");
618            return true;
619        }
620
621        try {
622            logd("uid      = " + mUid);
623            logd("rootPath = " + mPath);
624
625            mConnector.waitForConnection();
626
627            if (mPluginConnection == null) {
628                loge("Impossible to bind to plug-in!...");
629                sContext.unbindService(mConnector);
630                return false;
631            }
632
633            if (DBG) logd("context  = " + sContext.toString());
634
635            // FIXME: mServerID is never set, remove this?
636            if (mServerID != null) {
637                mPluginConnection.setServerID(mServerID);
638            }
639
640            return mPluginConnection.init(mPath, mParameters);
641        } catch (Exception e) {
642            loge("bindPluginService: Unable to get service " + mUid, e);
643            return false;
644        }
645    }
646
647    private String getRelativePath(String path) {
648        if (TextUtils.isEmpty(path) || path.equals(mPath)) {
649            return "";
650        }
651
652        if (path.startsWith(mPath + '/')) {
653            return path.substring(mPath.length() + 1);
654        }
655
656        return path;
657    }
658
659    private String getFullPath(String path) {
660        if (TextUtils.isEmpty(path)) {
661            return mPath;
662        }
663
664        if (path.startsWith("./")) {
665            return path;
666        }
667
668        return mPath + '/' + path;
669    }
670
671    private static void logd(String msg) {
672        Log.d(TAG, msg);
673    }
674
675    private static void loge(String msg) {
676        Log.e(TAG, msg);
677    }
678
679    private static void loge(String msg, Throwable tr) {
680        Log.e(TAG, msg, tr);
681    }
682}
683