Плагины в Android или выполняем код другого приложения

от
Android    android packagemanager

В Android есть неплохой набор средств для взаимодействия между приложениями, от вызова стороннего Activity до получения ресурсов из других приложений. Пользуясь этими средствами, можно значительно расширить функционал своих приложений или игр дополнительным контентом.

Получение списка существующих приложений
Основным классом для работы со списком приложений является PackageManager, с ним и будем работать.

Для получения информации о другом приложении, нужно знать имя его пакета (например com.example.app). Если планируется создание конкретного плагина, то можно просто прописать название пакета прямо в коде, но если необходимо вызывать разные плагины, то придётся получать список установленных плагинов и затем вызывать необходимый.

Чтобы понять, какое из приложений является плагином, можно пометить его собственной категорией в манифесте, например:
  1. <activity android:name=".PluginActivity">
  2.   <intent-filter>
  3.     <action android:name="android.intent.action.MAIN" />
  4.  
  5.     <category android:name="android.intent.category.LAUNCHER" />
  6.     <category android:name="com.example.plugin.PLUGIN_APPLICATION" />
  7.   </intent-filter>
  8. </activity>

Затем при помощи PackageManager можно получить все Activity, которые содержат данную категорию.
  1. private static final String CATEGORY_PLUGIN = "com.example.plugin.PLUGIN_APPLICATION";
  2.  
  3. PackageManager pm = getApplicationContext().getPackageManager();
  4. // Фильтруем по ACTION_MAIN и CATEGORY_PLUGIN
  5. Intent queryIntent = new Intent(Intent.ACTION_MAIN);
  6. queryIntent.addCategory(CATEGORY_PLUGIN);
  7. // Получаем все активити, сервисы и ресиверы с заданным критерием отбора
  8. List<ResolveInfo> infos = pm.queryIntentActivities(queryIntent, 0);
  9. // Отбираем только активити, взяв сразу информацию о приложении (ApplicationInfo)
  10. final List<ApplicationInfo> pluginApps = new ArrayList<>();
  11. for (ResolveInfo resolveInfo : infos) {
  12.     if (resolveInfo.activityInfo != null) {
  13.         pluginApps.add(resolveInfo.activityInfo.applicationInfo);
  14.     }
  15. }

В списке pluginApps будет информация о всех приложениях-плагинах, установленных на устройстве. Из этого списка можно получить package, иконку приложения, имя и т.п. Если же список пуст, значит установленных приложений-плагинов нет.

По такому принципу и работают лаунчеры. Они отбирают все запускаемые активити android.intent.category.LAUNCHER.


Получение информации о стороннем приложении
В ApplicationInfo содержится информация из AndroidManifest.xml, например targetSdkVersion, идентификатор ресурса темы, названия, иконка, а также дополнительная информация - путь к директории установки приложения, библиотекам и т.д.
  1. PackageManager pm = getApplicationContext().getPackageManager();
  2. ApplicationInfo appInfo = pluginApps.get(0);
  3. // Имя пакета
  4. String packageName = appInfo.packageName;
  5. // Версия SDK
  6. int targetSdk = appInfo.targetSdkVersion;
  7. // Название приложения
  8. String appName = appInfo.loadLabel(pm);
  9. // Иконка
  10. Drawable icon = appInfo.loadIcon(pm);

Кроме базовой информации, можно получить ресурсы приложения.
  1. Resources res = pm.getResourcesForApplication(appInfo);

Однако, для доступа к ним нужно знать идентификаторы или их названия.
Например, если известно, что в приложении есть текстовый ресурс R.string.plugin_text, мы можем получить его идентификатор и уже по нему взять текст.
  1. int plugin_text = res.getIdentifier("plugin_text", "string", appInfo.packageName);
  2. String pluginText = res.getString(plugin_text);

Точно так же и с остальными ресурсами.
Информация о приложении Сторонняя Activity
Наконец, можно вызвать Activity стороннего приложения, получив Intent для его запуска.
  1. Intent activityIntent = pm.getLaunchIntentForPackage(appInfo.packageName);
  2. startActivity(activityIntent);


Выполнение кода стороннего приложения
При помощи данного метода, можно получить Context стороннего приложения:
  1. public static Context getPackageContext(Context context, String packageName) {
  2.     try {
  3.         return context.getApplicationContext().createPackageContext(packageName,
  4.                 Context.CONTEXT_INCLUDE_CODE | Context.CONTEXT_IGNORE_SECURITY);
  5.     } catch (PackageManager.NameNotFoundException e) {
  6.         return null;
  7.     }
  8. }

CONTEXT_INCLUDE_CODE говорит о том, что будет загружен код приложения, так что можно будет получить доступ к его классам. CONTEXT_IGNORE_SECURITY игнорирует возможные ошибки безопасности при использовании полученного контекста.

Теперь можно получить ClassLoader, передав в метод нужное имя пакета.
  1. Context pluginContext = getPackageContext(this, "com.annimon.plugin");
  2. if (pluginContext == null) return;
  3. ClassLoader classLoader = pluginContext.getClassLoader();

При помощи ClassLoader можно загружать классы, используя Reflection API. Например, в стороннем приложении есть класс com.annimon.plugin.Plugin:
  1. package com.annimon.plugin;
  2.  
  3. public class Plugin {
  4.  
  5.     public static final double GOLDEN_RATIO = 1.61803398875;
  6.  
  7.     public static int sum(int x, int y) {
  8.         return x + y;
  9.     }
  10.  
  11.     public static String reverse(String str) {
  12.         return new StringBuilder(str).reverse().toString();
  13.     }
  14. }

Тогда для получения значения полей и вызова функции можно использовать такой код:
  1. Class<?> pluginClass = classLoader.loadClass("com.annimon.plugin.Plugin");
  2.  
  3. // Получаем поле по имени
  4. Field goldenRatioField = pluginClass.getDeclaredField("GOLDEN_RATIO");
  5. double goldenRatio = goldenRatioField.getDouble(null);
  6.  
  7. // Получаем метод по имени и сигнатуре
  8. // sum(42, 280)
  9. Method sumMethod = pluginClass.getDeclaredMethod("sum", int.class, int.class);
  10. int sum = sumMethod.invoke(null, 42, 280);
  11. // reverse("abcdefgh");
  12. Method reverseMethod = pluginClass.getDeclaredMethod("reverse", String.class);
  13. String reverse = reverseMethod.invoke(null, "abcdefgh");

Получение значения поля и вызов методов прошли успешно
Учтите, для передачи более сложных данных, нужно выполнять сериализацию, поскольку код выполняется в разных ClassLoader'ах.


Выполнение кода неустановленного apk
Ещё один способ вызова плагина - загрузить его динамически, например, из интернета. Этот способ небезопасный, но для общего развития пригодится.

Загружаем приложение во внутреннюю память, после чего получаем ClassLoader вот таким кодом:
  1. DexClassLoader dexClassLoader = new DexClassLoader(
  2.         path, // путь к файлу apk или dex
  3.         getFilesDir().getAbsolutePath(), // рабочая директория
  4.         null, // путь к нативным библиотекам
  5.         context.getClassLoader());  // родительский ClassLoader

Далее, всё тот же Reflection APi. Допустим, есть класс plugin.Info
  1. package plugin;
  2.  
  3. public class Info {
  4.     private final String info;
  5.  
  6.     public Info() {
  7.        this("Default info");
  8.     }
  9.  
  10.     public Info(String info) {
  11.        this.info = info;
  12.     }
  13.  
  14.     public String getInfo() {
  15.         return info;
  16.     }
  17. }

Создадим два экземпляра этого класса, первый раз вызвав конструктор по умолчанию, а второй, передав в конструктор строку.
  1. final Class<?> infoClass = dexClassLoader.loadClass("plugin.Info");
  2.  
  3. // new Info().getInfo()
  4. Object defaultObject = infoClass.newInstance();
  5. Method getInfo = infoClass.getDeclaredMethod("getInfo");
  6. String defaultInfo = (String) getInfo.invoke(defaultObject);
  7.  
  8. // new Info("Test").getInfo()
  9. Constructor constructor = infoClass.getDeclaredConstructor(String.class);
  10. Object testObject = constructor.newInstance("Test");
  11. String testInfo = (String) getInfo.invoke(testObject);

Создание экземпляра класса напрямую из apk


Исходный код: GitHub
Основное приложение: plugin-demo_app.apk
Приложение-плагин: plugin-demo_plugin.apk
  • +9
  • views 8640