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