1/*
2 * Copyright (C) 2009 Myriad Group AG.
3 * Copyright (C) 2009 The Android Open Source Project
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
6 * use this file except in compliance with the License. You may obtain a copy of
7 * the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14 * License for the specific language governing permissions and limitations under
15 * the License.
16 */
17
18package com.android.im.app;
19
20import java.util.ArrayList;
21import java.util.List;
22import java.util.Map;
23
24import android.content.ContentResolver;
25import android.content.ContentUris;
26import android.content.ContentValues;
27import android.content.Context;
28import android.content.Intent;
29import android.content.pm.PackageManager;
30import android.content.pm.ResolveInfo;
31import android.content.pm.ServiceInfo;
32import android.database.Cursor;
33import android.database.sqlite.SQLiteFullException;
34import android.net.Uri;
35import android.os.Bundle;
36import android.text.TextUtils;
37import android.util.Log;
38
39import com.android.im.plugin.ImConfigNames;
40import com.android.im.plugin.ImPlugin;
41import com.android.im.plugin.ImPluginConstants;
42import com.android.im.plugin.ImPluginInfo;
43import com.android.im.provider.Imps;
44
45public class ImPluginHelper {
46
47    private static final String TAG = "ImPluginUtils";
48
49    private Context mContext;
50    private ArrayList<ImPluginInfo> mPluginsInfo;
51    private ArrayList<ImPlugin> mPluginObjects;
52    private boolean mLoaded;
53
54    private static ImPluginHelper sInstance;
55    public static ImPluginHelper getInstance(Context context) {
56        if (sInstance == null) {
57            sInstance = new ImPluginHelper(context);
58        }
59        return sInstance;
60    }
61
62    private ImPluginHelper(Context context) {
63        mContext = context;
64        mPluginsInfo = new ArrayList<ImPluginInfo>();
65        mPluginObjects = new ArrayList<ImPlugin>();
66    }
67
68    public ArrayList<ImPluginInfo> getPluginsInfo() {
69        if (!mLoaded) {
70            loadAvaiablePlugins();
71        }
72        return mPluginsInfo;
73    }
74
75    public ArrayList<ImPlugin> getPluginObjects() {
76        if (!mLoaded) {
77            loadAvaiablePlugins();
78        }
79        return mPluginObjects;
80    }
81
82    public void loadAvaiablePlugins() {
83        if (mLoaded) {
84            return;
85        }
86
87        PackageManager pm = mContext.getPackageManager();
88        List<ResolveInfo> plugins = pm.queryIntentServices(
89                new Intent(ImPluginConstants.PLUGIN_ACTION_NAME), PackageManager.GET_META_DATA);
90        for (ResolveInfo info : plugins) {
91            Log.d(TAG, "Found plugin " + info);
92
93            ServiceInfo serviceInfo = info.serviceInfo;
94            if (serviceInfo == null) {
95                Log.e(TAG, "Ignore bad IM plugin: " + info);
96                continue;
97            }
98            String providerName = null;
99            String providerFullName = null;
100            String signUpUrl = null;
101            Bundle metaData = serviceInfo.metaData;
102            if (metaData != null) {
103                providerName = metaData.getString(ImPluginConstants.METADATA_PROVIDER_NAME);
104                providerFullName = metaData.getString(ImPluginConstants.METADATA_PROVIDER_FULL_NAME);
105                signUpUrl = metaData.getString(ImPluginConstants.METADATA_SIGN_UP_URL);
106            }
107            if (TextUtils.isEmpty(providerName) || TextUtils.isEmpty(providerFullName)) {
108                Log.e(TAG, "Ignore bad IM plugin: " + info + ". Lack of required meta data");
109                continue;
110            }
111
112            if (isPluginDuplicated(providerName)) {
113                Log.e(TAG, "Ignore duplicated IM plugin: " + info);
114                continue;
115            }
116
117            if (!serviceInfo.packageName.equals(mContext.getPackageName())) {
118                Log.e(TAG, "Ignore plugin in package: " + serviceInfo.packageName);
119                continue;
120            }
121            ImPluginInfo pluginInfo = new ImPluginInfo(providerName, serviceInfo.packageName,
122                    serviceInfo.name, serviceInfo.applicationInfo.sourceDir);
123
124            ImPlugin plugin = loadPlugin(pluginInfo);
125            if (plugin == null) {
126                Log.e(TAG, "Ignore bad IM plugin");
127                continue;
128            }
129
130            try {
131                updateProviderDb(plugin, pluginInfo,providerFullName, signUpUrl);
132            } catch (SQLiteFullException e) {
133                Log.e(TAG, "Storage full", e);
134                return;
135            }
136            mPluginsInfo.add(pluginInfo);
137            mPluginObjects.add(plugin);
138        }
139        mLoaded = true;
140    }
141
142    private boolean isPluginDuplicated(String providerName) {
143        for (ImPluginInfo plugin : mPluginsInfo) {
144            if (plugin.mProviderName.equals(providerName)) {
145                return true;
146            }
147        }
148        return false;
149    }
150
151    private ImPlugin loadPlugin(ImPluginInfo pluginInfo) {
152        // XXX Load the plug-in implementation directly from the apk rather than
153        // binding to the service and call through IPC Binder API. This is much
154        // more effective since we don't need to start the service in other
155        // process. We can not run the plug-in service in the same process as a
156        // local service because that the interface is defined in a shared
157        // library in order to compile the plug-in separately. In this case, the
158        // interface will be loaded by two class loader separately and a
159        // ClassCastException will be thrown if we cast the binder to the
160        // interface.
161        ClassLoader loader = mContext.getClassLoader();
162        try {
163            Class<?> cls = loader.loadClass(pluginInfo.mClassName);
164            return (ImPlugin) cls.newInstance();
165        } catch (ClassNotFoundException e) {
166            Log.e(TAG, "Could not find plugin class", e);
167        } catch (IllegalAccessException e) {
168            Log.e(TAG, "Could not create plugin instance", e);
169        } catch (InstantiationException e) {
170            Log.e(TAG, "Could not create plugin instance", e);
171        } catch (SecurityException e) {
172            Log.e(TAG, "Could not load plugin", e);
173        } catch (IllegalArgumentException e) {
174            Log.e(TAG, "Could not load plugin", e);
175        }
176        return null;
177    }
178
179    private long updateProviderDb(ImPlugin plugin, ImPluginInfo info,
180            String providerFullName, String signUpUrl) {
181        Map<String, String> config = loadConfiguration(plugin, info);
182        if (config == null) {
183            return 0;
184        }
185
186        long providerId = 0;
187        ContentResolver cr = mContext.getContentResolver();
188        String where = Imps.Provider.NAME + "=?";
189        String[] selectionArgs = new String[]{info.mProviderName};
190        Cursor c = cr.query(Imps.Provider.CONTENT_URI,
191                null /* projection */,
192                where,
193                selectionArgs,
194                null /* sort order */);
195
196        boolean pluginChanged;
197        try {
198            if (c.moveToFirst()) {
199                providerId = c.getLong(c.getColumnIndexOrThrow(Imps.Provider._ID));
200                pluginChanged = isPluginChanged(cr, providerId, config);
201                if (pluginChanged) {
202                    // Update the full name, signup url and category each time when the plugin change
203                    // instead of specific version change because this is called only once.
204                    // It's ok to update them even the values are not changed.
205                    // Note that we don't update the provider name because it's used as
206                    // identifier at some place and the plugin should never change it.
207                    ContentValues values = new ContentValues(3);
208                    values.put(Imps.Provider.FULLNAME, providerFullName);
209                    values.put(Imps.Provider.SIGNUP_URL, signUpUrl);
210                    values.put(Imps.Provider.CATEGORY, ImApp.IMPS_CATEGORY);
211                    Uri uri = ContentUris.withAppendedId(Imps.Provider.CONTENT_URI, providerId);
212                    cr.update(uri, values, null, null);
213                }
214            } else {
215                ContentValues values = new ContentValues(3);
216                values.put(Imps.Provider.NAME, info.mProviderName);
217                values.put(Imps.Provider.FULLNAME, providerFullName);
218                values.put(Imps.Provider.CATEGORY, ImApp.IMPS_CATEGORY);
219                values.put(Imps.Provider.SIGNUP_URL, signUpUrl);
220
221                Uri result = cr.insert(Imps.Provider.CONTENT_URI, values);
222                providerId = ContentUris.parseId(result);
223                pluginChanged = true;
224            }
225        } finally {
226            if (c != null) {
227                c.close();
228            }
229        }
230
231        if (pluginChanged) {
232            // Remove all the old settings
233            cr.delete(ContentUris.withAppendedId(
234                    Imps.ProviderSettings.CONTENT_URI, providerId),
235                    null, /*where*/
236                    null /*selectionArgs*/);
237
238            ContentValues[] settingValues = new ContentValues[config.size()];
239
240            int index = 0;
241            for (Map.Entry<String, String> entry : config.entrySet()) {
242                ContentValues settingValue = new ContentValues();
243                settingValue.put(Imps.ProviderSettings.PROVIDER, providerId);
244                settingValue.put(Imps.ProviderSettings.NAME, entry.getKey());
245                settingValue.put(Imps.ProviderSettings.VALUE, entry.getValue());
246                settingValues[index++] = settingValue;
247            }
248            cr.bulkInsert(Imps.ProviderSettings.CONTENT_URI, settingValues);
249        }
250
251        return providerId;
252    }
253
254    private Map<String, String> loadConfiguration(ImPlugin plugin,
255            ImPluginInfo info) {
256        Map<String, String> config = null;
257
258            config = plugin.getProviderConfig();
259
260        if (config != null) {
261            config.put(ImConfigNames.PLUGIN_PATH, info.mSrcPath);
262            config.put(ImConfigNames.PLUGIN_CLASS, info.mClassName);
263        }
264        return config;
265    }
266
267    private boolean isPluginChanged(ContentResolver cr, long providerId,
268            Map<String, String> config) {
269        String origVersion = Imps.ProviderSettings.getStringValue(cr, providerId,
270                ImConfigNames.PLUGIN_VERSION);
271
272        if (origVersion == null) {
273            return true;
274        }
275        String newVersion = config.get(ImConfigNames.PLUGIN_VERSION);
276        return !origVersion.equals(newVersion);
277    }
278}
279