/* DevicePluginManager.java Copyright (c) 2014 NTT DOCOMO,INC. Released under the MIT license http://opensource.org/licenses/mit-license.php */ package org.deviceconnect.android.manager; import android.content.ComponentName; import android.content.Intent; import android.content.pm.ActivityInfo; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.ServiceInfo; import android.content.res.XmlResourceParser; import android.graphics.drawable.Drawable; import android.support.v4.content.res.ResourcesCompat; import android.util.Log; import org.deviceconnect.android.manager.util.DConnectUtil; import org.deviceconnect.android.manager.util.VersionName; import org.deviceconnect.message.DConnectMessage; import org.deviceconnect.message.intent.message.IntentDConnectMessage; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.logging.Logger; /** * デバイスプラグインを管理するクラス. * @author NTT DOCOMO, INC. */ public class DevicePluginManager { /** デバイスプラグインに格納されるプラグイン名称のメタタグ名. */ private static final String PLUGIN_META_PLUGIN_NAME = "org.deviceconnect.android.deviceplugin.name"; /** デバイスプラグインに格納されるアイコンのメタタグ名. */ private static final String PLUGIN_META_PLUGIN_ICON = "org.deviceconnect.android.deviceplugin.icon"; /** ロガー. */ private final Logger mLogger = Logger.getLogger("dconnect.manager"); /** デバイスプラグインSDKに格納されるメタタグ名. */ private static final String PLUGIN_SDK_META_DATA = "org.deviceconnect.android.deviceplugin.sdk"; /** デバイスプラグインに格納されるメタタグ名. */ private static final String PLUGIN_META_DATA = "org.deviceconnect.android.deviceplugin"; /** 再起動用のサービスを表すメタデータの値. */ private static final String VALUE_META_DATA = "enable"; /** デバイスプラグイン一覧. */ private final Map<String, DevicePlugin> mPlugins = new ConcurrentHashMap<String, DevicePlugin>(); /** dConnectManagerのドメイン名. */ private String mDConnectDomain; /** イベントリスナー. */ private DevicePluginEventListener mEventListener; /** アプリケーションクラスインスタンス. */ private final DConnectApplication mApp; /** * コンストラクタ. * @param app DConnectApplicationのインスタンス * @param domain dConnect Managerのドメイン */ public DevicePluginManager(final DConnectApplication app, final String domain) { setDConnectDomain(domain); mApp = app; } /** * イベントリスナーを設定する. * @param listener リスナー */ public void setEventListener(final DevicePluginEventListener listener) { mEventListener = listener; } /** * dConnect Managerのドメイン名を設定する. * @param domain ドメイン名 */ public void setDConnectDomain(final String domain) { mDConnectDomain = domain; } /** * アプリ一覧からデバイスプラグイン一覧を作成する. */ public void createDevicePluginList() { PackageManager pkgMgr = mApp.getPackageManager(); List<PackageInfo> pkgList = pkgMgr.getInstalledPackages(PackageManager.GET_RECEIVERS); if (pkgList != null) { for (PackageInfo pkg : pkgList) { ActivityInfo[] receivers = pkg.receivers; if (receivers != null) { for (int i = 0; i < receivers.length; i++) { String packageName = receivers[i].packageName; String className = receivers[i].name; checkAndAddDevicePlugin(new ComponentName(packageName, className), pkg); } } } } } /** * 指定されたIntentからデバイスプラグインを確認して、リストに追加する. * @param intent 追加するデバイスプラグインのIntent */ public void checkAndAddDevicePlugin(final Intent intent) { checkAndAddDevicePlugin(getPackageName(intent)); } /** * 指定されたパッケージの中にデバイスプラグインが存在するかチェックし追加する. * パッケージの中にデバイスプラグインがない場合には何もしない。 * @param packageName パッケージ名 */ public void checkAndAddDevicePlugin(final String packageName) { if (packageName == null) { throw new IllegalArgumentException("packageName is null."); } PackageManager pkgMgr = mApp.getPackageManager(); try { PackageInfo pkg = pkgMgr.getPackageInfo(packageName, PackageManager.GET_RECEIVERS); if (pkg != null) { ActivityInfo[] receivers = pkg.receivers; if (receivers != null) { for (int i = 0; i < receivers.length; i++) { String pkgName = receivers[i].packageName; String className = receivers[i].name; checkAndAddDevicePlugin(new ComponentName(pkgName, className), pkg); } } } } catch (NameNotFoundException e) { return; } } /** * コンポーネントにデバイスプラグインが存在するかチェックし追加する. * コンポーネントの中にデバイスプラグインがない場合には何もしない。 * @param component コンポーネント */ public void checkAndAddDevicePlugin(final ComponentName component, final PackageInfo pkgInfo) { ApplicationInfo appInfo; ActivityInfo receiverInfo; try { PackageManager pkgMgr = mApp.getPackageManager(); appInfo = pkgMgr.getApplicationInfo(component.getPackageName(), PackageManager.GET_META_DATA); receiverInfo = pkgMgr.getReceiverInfo(component, PackageManager.GET_META_DATA); if (receiverInfo.metaData != null) { Object value = receiverInfo.metaData.get(PLUGIN_META_DATA); if (value != null) { String pluginName = (String) receiverInfo.metaData.getString(PLUGIN_META_PLUGIN_NAME); if (pluginName == null) { pluginName = receiverInfo.applicationInfo.loadLabel(pkgMgr).toString(); } VersionName sdkVersionName = getPluginSDKVersion(appInfo); String packageName = receiverInfo.packageName; String className = receiverInfo.name; String versionName = pkgInfo.versionName; String startClassName = getStartServiceClassName(packageName); String hash = md5(packageName + className); if (hash == null) { throw new RuntimeException("Can't generate md5."); } Drawable icon; Object iconId = receiverInfo.metaData.get(PLUGIN_META_PLUGIN_ICON); if (iconId != null) { icon = ResourcesCompat.getDrawable(mApp.getResources(), (int)iconId, null); } else { try { ApplicationInfo info = pkgMgr.getApplicationInfo(packageName, 0); icon = pkgMgr.getApplicationIcon(info.packageName); } catch (PackageManager.NameNotFoundException e) { icon = null; if (BuildConfig.DEBUG) { Log.d("Manager", "Icon is not found."); } } } mLogger.info("Added DevicePlugin: [" + hash + "]"); mLogger.info(" PackageName: " + packageName); mLogger.info(" className: " + className); mLogger.info(" versionName: " + versionName); mLogger.info(" sdkVersionName: " + sdkVersionName); // MEMO 既に同じ名前のデバイスプラグインが存在した場合の処理 // 現在は警告を表示し、上書きする. if (mPlugins.containsKey(hash)) { mLogger.warning("DevicePlugin[" + hash + "] already exists."); } DevicePlugin plugin = new DevicePlugin(); plugin.setClassName(className); plugin.setPackageName(packageName); plugin.setVersionName(versionName); plugin.setPluginId(hash); plugin.setDeviceName(pluginName); plugin.setStartServiceClassName(startClassName); plugin.setSupportProfiles(checkDevicePluginXML(receiverInfo)); plugin.setPluginSdkVersionName(sdkVersionName); plugin.setPluginIcon(icon); mPlugins.put(hash, plugin); if (mEventListener != null) { mEventListener.onDeviceFound(plugin); } } } } catch (NameNotFoundException e) { return; } } /** * 指定されたIntentのデバイスプラグインを削除する. * @param intent 削除するデバイスプラグインのIntent */ public void checkAndRemoveDevicePlugin(final Intent intent) { checkAndRemoveDevicePlugin(getPackageName(intent)); } /** * 指定されたパッケージ名に対応するデバイスプラグインを削除する. * @param packageName パッケージ名 */ public void checkAndRemoveDevicePlugin(final String packageName) { if (packageName == null) { throw new IllegalArgumentException("packageName is null."); } for (String key : mPlugins.keySet()) { DevicePlugin plugin = mPlugins.get(key); if (plugin.getPackageName().equals(packageName)) { mPlugins.remove(key); if (mEventListener != null) { mEventListener.onDeviceLost(plugin); } } } } /** * 指定されたコンポーネントがデバイスプラグインがチェックを行い、デバイスプラグインの場合には削除を行う. * @param component 削除するコンポーネント */ public void checkAndRemoveDevicePlugin(final ComponentName component) { ActivityInfo receiverInfo = null; try { PackageManager pkgMgr = mApp.getPackageManager(); receiverInfo = pkgMgr.getReceiverInfo(component, PackageManager.GET_META_DATA); if (receiverInfo.metaData != null) { String value = receiverInfo.metaData.getString(PLUGIN_META_DATA); if (value != null) { String packageName = receiverInfo.packageName; String className = receiverInfo.name; String hash = md5(packageName + className); mLogger.info("Removed DevicePlugin: [" + hash + "]"); mLogger.info(" PackageName: " + packageName); mLogger.info(" className: " + className); DevicePlugin plugin = mPlugins.remove(hash); if (plugin != null && mEventListener != null) { mEventListener.onDeviceLost(plugin); } } } } catch (NameNotFoundException e) { return; } } /** * 指定されたサービスIDと一致するデバイスプラグインを取得する. * @param serviceId サービスID * @return デバイスプラグイン */ public DevicePlugin getDevicePlugin(final String serviceId) { return mPlugins.get(serviceId); } /** * 指定されたサービスIDからデバイスプラグインを取得する. * 指定されたserviceIdに対応するデバイスプラグインが存在しない場合にはnullを返却する。 * サービスIDのネーミング規則は以下のようになる。 * [device].[deviceplugin].[dconnect].deviceconnect.org * [dconnect].deviceconnect.org が serviceIdに渡されたときには、 * すべてのプラグインをListに格納して返します。 * @param serviceId パースするサービスID * @return デバイスプラグイン */ public List<DevicePlugin> getDevicePlugins(final String serviceId) { if (serviceId == null) { return null; } String pluginName = serviceId; int idx = pluginName.lastIndexOf(mDConnectDomain); if (idx > 0) { pluginName = pluginName.substring(0, idx - 1); } else { // ここで見つからない場合には、サービスIDとして不正なので // nullを返却する。 return null; } idx = pluginName.lastIndexOf(DConnectMessageService.SEPARATOR); if (idx > 0) { pluginName = pluginName.substring(idx + 1); } if (mPlugins.containsKey(pluginName)) { List<DevicePlugin> plugins = new ArrayList<DevicePlugin>(); plugins.add(mPlugins.get(pluginName)); return plugins; } else { return null; } } /** * すべてのデバイスプラグインを取得する. * @return すべてのデバイスプラグイン */ public List<DevicePlugin> getDevicePlugins() { return new ArrayList<DevicePlugin>(mPlugins.values()); } /** * サービスIDにDevice Connect Managerのドメイン名を追加する. * * サービスIDがnullのときには、サービスIDは無視します。 * * @param plugin デバイスプラグイン * @param serviceId サービスID * @return Device Connect Managerのドメインなどが追加されたサービスID */ public String appendServiceId(final DevicePlugin plugin, final String serviceId) { if (serviceId == null) { return plugin.getPluginId() + DConnectMessageService.SEPARATOR + mDConnectDomain; } else { return serviceId + DConnectMessageService.SEPARATOR + plugin.getPluginId() + DConnectMessageService.SEPARATOR + mDConnectDomain; } } /** * リクエストに含まれるセッションキーを変換する. * * セッションキーにデバイスプラグインIDとreceiverを追加する。 * 下記のように、分解できるようになっている。 * 【セッションキー.デバイスプラグインID@receiver】 * * @param request リクエスト */ public void appendPluginIdToSessionKey(final Intent request) { String serviceId = request.getStringExtra(DConnectMessage.EXTRA_SERVICE_ID); List<DevicePlugin> plugins = getDevicePlugins(serviceId); String sessionKey = request.getStringExtra(DConnectMessage.EXTRA_SESSION_KEY); if (plugins != null && sessionKey != null) { sessionKey = sessionKey + DConnectMessageService.SEPARATOR + plugins.get(0).getPluginId(); ComponentName receiver = (ComponentName) request.getExtras().get(DConnectMessage.EXTRA_RECEIVER); if (receiver != null) { sessionKey = sessionKey + DConnectMessageService.SEPARATOR_SESSION + receiver.flattenToString(); } request.putExtra(DConnectMessage.EXTRA_SESSION_KEY, sessionKey); } } /** * リクエストに含まれるセッションキーを変換する. * * セッションキーにデバイスプラグインIDとreceiverを追加する。 * 下記のように、分解できるようになっている。 * 【セッションキー.デバイスプラグインID@receiver】 * * @param request リクエスト * @param plugin デバイスプラグイン */ public void appendPluginIdToSessionKey(final Intent request, final DevicePlugin plugin) { String sessionKey = request.getStringExtra(DConnectMessage.EXTRA_SESSION_KEY); if (plugin != null && sessionKey != null) { sessionKey = sessionKey + DConnectMessageService.SEPARATOR + plugin.getPluginId(); ComponentName receiver = (ComponentName) request.getExtras().get(DConnectMessage.EXTRA_RECEIVER); if (receiver != null) { sessionKey = sessionKey + DConnectMessageService.SEPARATOR_SESSION + receiver.flattenToString(); } request.putExtra(DConnectMessage.EXTRA_SESSION_KEY, sessionKey); } } /** * 指定されたリクエストのserviceIdからプラグインIDを削除する. * @param request リクエスト */ public void splitPluginIdToServiceId(final Intent request) { String serviceId = request.getStringExtra(DConnectMessage.EXTRA_SERVICE_ID); List<DevicePlugin> plugins = getDevicePlugins(serviceId); // 各デバイスプラグインへ渡すサービスIDを作成 String id = DevicePluginManager.splitServiceId(plugins.get(0), serviceId); request.putExtra(IntentDConnectMessage.EXTRA_SERVICE_ID, id); } /** * サービスIDを分解して、Device Connect Managerのドメイン名を省いた本来のサービスIDにする. * Device Connect Managerのドメインを省いたときに、何もない場合には空文字を返します。 * @param plugin デバイスプラグイン * @param serviceId サービスID * @return Device Connect Managerのドメインが省かれたサービスID */ public static String splitServiceId(final DevicePlugin plugin, final String serviceId) { String p = plugin.getPluginId(); int idx = serviceId.indexOf(p); if (idx > 0) { return serviceId.substring(0, idx - 1); } return ""; } private VersionName getPluginSDKVersion(final ApplicationInfo info) { VersionName versionName = null; if (info.metaData != null && info.metaData.get(PLUGIN_SDK_META_DATA) != null) { PackageManager pkgMgr = mApp.getPackageManager(); XmlResourceParser xpp = info.loadXmlMetaData(pkgMgr, PLUGIN_SDK_META_DATA); try { String str = parsePluginSDKVersionName(xpp); if (str != null) { versionName = VersionName.parse(str); } } catch (XmlPullParserException e) { // NOP } catch (IOException e) { // NOP } } if (versionName != null) { return versionName; } else { return VersionName.parse("1.0.0"); } } private String parsePluginSDKVersionName(final XmlResourceParser xpp) throws XmlPullParserException, IOException { String versionName = null; int eventType = xpp.getEventType(); while (eventType != XmlPullParser.END_DOCUMENT) { final String name = xpp.getName(); switch (eventType) { case XmlPullParser.START_DOCUMENT: break; case XmlPullParser.START_TAG: if (name.equals("version")) { versionName = xpp.nextText(); } break; case XmlPullParser.END_TAG: break; default: break; } eventType = xpp.next(); } return versionName; } /** * deviceplugin.xmlの確認を行う. * @param info receiverのタグ情報 * @return プロファイル一覧 */ public List<String> checkDevicePluginXML(final ActivityInfo info) { PackageManager pkgMgr = mApp.getPackageManager(); XmlResourceParser xpp = info.loadXmlMetaData(pkgMgr, PLUGIN_META_DATA); try { return parseDevicePluginXML(xpp); } catch (XmlPullParserException e) { return null; } catch (IOException e) { return null; } } /** * xml/deviceplugin.xmlの解析を行う. * * @param xpp xmlパーサ * @throws XmlPullParserException xmlの解析に失敗した場合に発生 * @throws IOException xmlの読み込みに失敗した場合 * @return プロファイル一覧 */ private List<String> parseDevicePluginXML(final XmlResourceParser xpp) throws XmlPullParserException, IOException { ArrayList<String> list = new ArrayList<String>(); int eventType = xpp.getEventType(); while (eventType != XmlPullParser.END_DOCUMENT) { final String name = xpp.getName(); switch (eventType) { case XmlPullParser.START_DOCUMENT: break; case XmlPullParser.START_TAG: if (name.equals("profile")) { list.add(xpp.getAttributeValue(null, "name")); } break; case XmlPullParser.END_TAG: break; default: break; } eventType = xpp.next(); } return list; } /** * 指定された文字列をMD5の文字列に変換する. * MD5への変換に失敗した場合にはnullを返却する。 * @param s MD5にする文字列 * @return MD5にされた文字列 */ private String md5(final String s) { try { return DConnectUtil.toMD5(s); } catch (UnsupportedEncodingException e) { mLogger.warning("Not support Charset."); } catch (NoSuchAlgorithmException e) { mLogger.warning("Not support MD5."); } return null; } /** * インストール・アンインストールしたアプリのパッケージ名を取得する. * @param intent パッケージ名を取得するIntent * @return パッケージ名 */ private String getPackageName(final Intent intent) { String pkgName = intent.getDataString(); int idx = pkgName.indexOf(":"); if (idx != -1) { pkgName = pkgName.substring(idx + 1); } return pkgName; } /** * Get a class name of service for start. * @param packageName package name of device plugin * @return class name or null if there are no service for start */ private String getStartServiceClassName(final String packageName) { PackageManager pkgMgr = mApp.getPackageManager(); try { PackageInfo pkg = pkgMgr.getPackageInfo(packageName, PackageManager.GET_SERVICES); ServiceInfo[] slist = pkg.services; if (slist != null) { for (ServiceInfo s : slist) { ComponentName comp = new ComponentName(s.packageName, s.name); ServiceInfo ss = pkgMgr.getServiceInfo(comp, PackageManager.GET_META_DATA); if (ss.metaData != null) { Object value = ss.metaData.get(PLUGIN_META_DATA); if (value != null && value.equals(VALUE_META_DATA)) { return s.name; } } } } return null; } catch (NameNotFoundException e) { return null; } } /** * デバイスプラグインの発見、見失う通知を行うリスナー. * @author NTT DOCOMO, INC. */ public interface DevicePluginEventListener { /** * デバイスプラグインが発見されたことを通知する. * @param plugin 発見されたデバイスプラグイン */ void onDeviceFound(DevicePlugin plugin); /** * デバイスプラグインを見失ったことを通知する. * @param plugin 見失ったデバイスプラグイン */ void onDeviceLost(DevicePlugin plugin); } }