Представим себе следующую ситуацию: мы разрабатываем продукт, который требует очень специфические свойства которые или не существуют или недоступны в инвентаре Андроид. Например требуется кард-ридер.
Да, я знаю о внешних ридерах, но мы ведь серьезные разработчики и решили сделать его внутренним чтобы финальное устройство выглядело более целостным. Например как это:
Такие устройства скорее всего должны предоставлять специальные службы обработки платежей для сторонних разработчиков.
В этой статье я хочу описать процесс расширения Android API на примере создания нового сервиса для кастомизации строки навигации.
Формулировка задачи
Мы собираемся разработать API что даст возможность пользовательской настройки (кастомизации) строки навигации путем добавления туда новых элементов. Стандартная строка навигации выглядит по разному на разных устройствах, и даже может отсутствовать на устройствах с аппаратными кнопками навигации.
Давайте возьмем Nexus 9:
Строка навигации — это черный прямоугольник внизу с кнопками навигации. Он показывается пользователю все время (кроме случая когда какое-либо приложение работает в полно-экранном режиме) так что было бы неплохо предоставить возможность разработчикам помещать там свои собственные элементы.
Для того чтоб сделать API максимально гибким, возьмем Android Remote View как основный параметр нашего API. Это позволит пользователю (т.е. другому разработчику) использовать многие стандартные компоненты для кастомизации. Также это предоставит механизм обратной связи от элементов в приложение.
Как точку расширения, возьмем непосредственно Android SDK: мы внесем изменения в AOSP (Android Open Source Project) и соберем собственное Android SDK. Такое SDK подойдет только для устройств работающих под управлением нашей модифицированной прошивки, но нам такое ограничение подходит априори. Как результат наши службы будет очень просто использовать и они будут доступны сразу из Android SDK.
Релизация
Скачайте и настройте среду разработки AOSP
Вся необходимая информация для настройки среды разработки AOSP предоставлена здесь. Выберите соответствующую ветвь AOSP (это влияет на версию Android) совместимую с выбранным устройством. В моем случае это Nexus 9 и ветвь android-7.1.1_r33.
Разработайте новую службу
Забегая вперед следующая схема показывает все задействованные в реализации компоненты:
Идея состоит в том, чтоб предоставить расширенные функции по управлению строкой навигации клиентским приложениям через службу Android. Строка навигации определена в файлах NavigationBarInflaterView.java и {aosp}/frameworks/base/packages/SystemUI/res/layout/navigation_bar.xml. Но мы не можем получить доступ к ним напрямую из клиентского приложения, так как они находятся в системном приложении SystemUI в то время, как основные службы Android располагаются в процессе system_process.
Таким образом наша цель сводится к связи этих компонентов с system_process, где они будут доступны для клиентского кода.
1. Создать прокси сервиса (менеджер) который будет видим из клиентского приложения. Это стандартный Java класс, который делегирует все функции сервису:
package android.os;
/**
* /framework/base/core/java/android/os/NavBarExServiceMgr.java
* It will be available in framework through import android.os.NavBarExServiceMgr;
*/
import android.content.Context;
import android.widget.RemoteViews;
public class NavBarExServiceMgr
{
private static final String TAG = "NavBarExServiceMgr";
private final Context context;
private final INavBarExService navBarExService;
public static NavBarExServiceMgr getInstance(Context context)
{
return (NavBarExServiceMgr) context.getSystemService(Context.NAVBAREX_SERVICE);
}
/**
* Creates a new instance.
*
* @param context The current context in which to operate.
* @param service The backing system service.
* @hide
*/
public NavBarExServiceMgr(Context context, INavBarExService service)
{
this.context = context;
if (service == null) throw new IllegalArgumentException("service is null");
this.navBarExService = service;
}
/**
* Sets the UI component
*
* @param ui - ui component
* @throws RemoteException
* @hide
*/
public void setUI(INavBarExServiceUI ui) throws RemoteException
{
navBarExService.setUI(ui);
}
public String addView(int priority, RemoteViews remoteViews)
{
try { return navBarExService.addView(priority, remoteViews); }
catch (RemoteException ignored) {}
return null;
}
public boolean removeView(String id)
{
try { return navBarExService.removeView(id); }
catch (RemoteException ignored) {}
return false;
}
public boolean replaceView(String id, RemoteViews remoteViews)
{
try { return navBarExService.replaceView(id, remoteViews); }
catch (RemoteException e) {}
return false;
}
public boolean viewExist(String id)
{
try { return navBarExService.viewExist(id); }
catch (RemoteException e) {}
return false;
}
}
Этот класс должен быть зарегистрирован в статической секции класса SystemServiceRegistry:
registerService(Context.NAVBAREX_SERVICE, NavBarExServiceMgr.class,
new CachedServiceFetcher() {
@Override
public NavBarExServiceMgr createService(ContextImpl ctx) {
IBinder b = ServiceManager.getService(Context.NAVBAREX_SERVICE);
INavBarExService service = INavBarExService.Stub.asInterface(b);
if (service == null) {
Log.wtf(TAG, "Failed to get INavBarExService service.");
return null;
}
return new NavBarExServiceMgr(ctx, service);
}});
С клиентской стороны служба может быть доступна следующим образом:
NavBarExServiceMgr navBarExServiceMgr = (NavBarExServiceMgr) getSystemService(Context.NAVBAREX_SERVICE);
// TODO
2. Определить AIDL интерфейс службы:
/*
* aidl file :
* frameworks/base/core/java/android/os/INavBarExService.aidl
* This file contains definitions of functions which are
* exposed by service.
*/
package android.os;
import android.os.INavBarExServiceUI;
import android.widget.RemoteViews;
/** */
interface INavBarExService
{
/**
* @hide
*/
void setUI(INavBarExServiceUI ui);
String addView(in int priority, in RemoteViews remoteViews);
boolean removeView(in String id);
boolean replaceView(in String id, in RemoteViews remoteViews);
boolean viewExist(in String id);
}
И реализовать его:
public class NavBarExService extends INavBarExService.Stub
…
Как видно большинство методов прямолинейны, только setUI выглядит странно. Это внутренний метод используемый классом, который реализует интерфейс INavBarExServiceUI для регистрации себя в NavBarExService. Все сущности помеченные комментарием “@hide” будут вырезаны из финального Android SDK и таким образом не будут видимы из клиентского кода.
Несколько комментариев относительно семантики API:
- Каждый элемент идентифицируется строковым идентификатором. Он генерируется службой при добавлении нового элемента. Таким образом каждое приложение, что добавляет новые элементы через нашу службу должны сохранять возвращаемый идентификатор для того, чтоб иметь возможность работы с элементом в дальнейшем;
- Другие методы принимает идентификатор как параметр который идентифицирует элемент;
- Каждый элемент также имеет параметр «приоритет» который указывает в каком месте разместить элемент. Чем выше значение приоритета, тем левее будет размещение.
Реализация метода setUI:
@Override
public void setUI(INavBarExServiceUI ui)
{
Log.d(TAG, "setUI");
this.ui = ui;
if (ui != null)
{
try
{
for (Pair entry : remoteViewsList.getList())
{
ui.navBarExAddViewAtEnd(entry.first, entry.second);
}
}
catch (Exception e)
{
Log.e(TAG, "Failed to configure UI", e);
}
}
}
Реализация метода addView:
@Override
public String addView(int priority, RemoteViews remoteViews) throws RemoteException
{
String id = UUID.randomUUID().toString();
int pos = remoteViewsList.add(priority, id, remoteViews);
if (ui != null)
{
if (pos == 0)
ui.navBarExAddViewAtStart(id, remoteViews);
else if (pos == remoteViewsList.size() - 1)
ui.navBarExAddViewAtEnd(id, remoteViews);
else
{
// find previous element ID
Pair prevElPair = remoteViewsList.getAt(pos - 1);
ui.navBarExAddViewAfter(prevElPair.first, id, remoteViews);
}
}
return id;
}
remoteViewsList используется чтоб удерживать созданные элементы пока UI не будет подключен (не будет вызван метод setUI). Если он уже подключен (поле ui не равно null) новый элемент добавляется прямо в UI.
3. SystemServer должен зарегистрировать нашу службу в системе:
try {
traceBeginAndSlog("StartNavBarExService");
ServiceManager.addService(Context.NAVBAREX_SERVICE, new NavBarExService(context));
Slog.i(TAG, "NavBarExService Started");
} catch (Throwable e) {
reportWtf("Failure starting NavBarExService Service", e);
}
Trace.traceEnd(Trace.TRACE_TAG_SYSTEM_SERVER);
Добавление новой службы требует правки политик Android SEPolicy.
Добавьте новую запись в файл {aosp}/system/selinux/service.te:
type navbarex_service, app_api_service, system_server_service, service_manager_type;
Добавьте новую запись в файл {aosp}/system/selinux/service_contexts:
navbarex u:object_r:navbarex_service:s0
Имейте ввиду что формат файлов SELinux для Android 7.0 отличается от прежних версий Android.
4. Добавьте константы имени службы в класс Context чтоб клиенты могли его использовать вместо строкового значения:
/**
* Use with {@link #getSystemService} to retrieve a
* {@link android.os.NavBarExServiceMgr} for using NavBarExService
*
* @see #getSystemService
*/
public static final String NAVBAREX_SERVICE = "navbarex";
5. Определите интерфейс INavBarExServiceUI:
/*
* aidl file :
* frameworks/base/core/java/android/os/INavBarExServiceUI.aidl
* This file contains definitions of functions which are provided by UI.
*/
package android.os;
import android.widget.RemoteViews;
/** @hide */
oneway interface INavBarExServiceUI
{
void navBarExAddViewAtStart(in String id, in RemoteViews remoteViews);
void navBarExAddViewAtEnd(in String id, in RemoteViews remoteViews);
void navBarExAddViewBefore(in String targetId, in String id, in RemoteViews remoteViews);
void navBarExAddViewAfter(in String targetId, in String id, in RemoteViews remoteViews);
void navBarExRemoveView(in String id);
void navBarExReplaceView(in String id, in RemoteViews remoteViews);
}
Обратите внимание, что он помечен атрибутом “hide”, что делает его невидимым для клиентов. Также обратите внимание на ключевое слово oneway. Оно делает межпроцессное взаимодействие от system_process к SystemUI быстрее, так как все вызовы будут неблокирующими (это также требует пустого возвращаемого значения в методах).
6. VendorServices это стандартный SystemUI компонент (он наследуется от класса SystemUI), который реализует интерфейс INavBarExServiceUI:
public class VendorServices extends SystemUI
{
private final Handler handler = new Handler();
private NavBarExServiceMgr navBarExServiceMgr;
private volatile PhoneStatusBar statusBar;
private INavBarExServiceUI.Stub navBarExServiceUI = new INavBarExServiceUI.Stub()
{
@Override
public void navBarExAddViewAtStart(final String id, final RemoteViews remoteViews)
{
if (!initStatusBar()) return;
handler.post(new Runnable()
{
@Override
public void run()
{
statusBar.navBarExAddViewAtStart(id, remoteViews);
}
});
}
//…
}
@Override
protected void onBootCompleted()
{
super.onBootCompleted();
navBarExServiceMgr = (NavBarExServiceMgr) mContext.getSystemService(Context.NAVBAREX_SERVICE);
if (navBarExServiceMgr == null)
{
Log.e(TAG, "navBarExServiceMgr=null");
return;
}
try
{
navBarExServiceMgr.setUI(navBarExServiceUI);
}
catch (Exception e)
{
Log.e(TAG, "setUI exception: " + e);
}
}
}
Интересен метод onBootCompleted. Он производит само-регистрацию (вызывает метод setUI) в NavBarExService через NavBarExServiceMgr. Также обратите внимание что процесс SystemUI может быть перезапущен (например из-за падения) в результате чего будут сделаны несколько вызовов setUI. Само собой только последний должен учитываться.
7. Класс PhoneStatusBar это ключевой элемент который связывает NavBarExService и NavigationBarInflaterView. Он содержит ссылку на NavigationBarView, который в свою очередь содержит ссылку на NavigationBarInflaterView. С другой стороны VendorServices получает ссылку на PhoneStatusBar через метод SystemUI.getComponent:
private boolean initStatusBar()
{
if (statusBar == null)
{
synchronized (initLock)
{
if (statusBar == null)
{
statusBar = getComponent(PhoneStatusBar.class);
if (statusBar == null)
{
Log.e(TAG, "statusBar = null");
return false;
}
Log.d(TAG, "statusBar initialized");
}
}
}
return true;
}
Вы заметили странную конструкцию “if (statusBar == null)”? Она называется “Double Checking Locking Pattern” и преследует следующую цель: произвести потоко-безопасную инициализацию объекта, но избежать вхождение в секцию синхронизации когда объект уже проинициализирован. Изменения в PhoneStatusBar и NavigationBarView достаточно простые: они просто делегируют все вызовы в класс NavigationBarInflaterView.
8. Класс NavigationBarInflaterView — это конечный класс который производит непосредственно изменения в UI. Вот его метод navBarExAddViewAtStart:
public void navBarExAddViewAtStart(String id, RemoteViews remoteViews) {
if ((mRot0 == null) || (mRot90 == null)) return;
ViewGroup ends0 = (ViewGroup) mRot0.findViewById(R.id.ends_group);
ViewGroup ends90 = (ViewGroup) mRot90.findViewById(R.id.ends_group);
if ((ends0 == null) || (ends90 == null)) return;
navBarExAddView(0, id, remoteViews, ends0);
navBarExAddView(0, id, remoteViews, ends90);
}
private void navBarExAddView(int index, String id, RemoteViews remoteViews, ViewGroup parent) {
View view = remoteViews.apply(mContext, parent);
view.setTag(navBarExFormatTag(id));
TransitionManager.beginDelayedTransition(parent);
parent.addView(view, index);
}
Этот код полагается на существующую реализацию и использует поля mRot0, mRot90 а также R.id.ends_group как ViewGroup для кастомных элементов. mRot0 и mRot90 представляют разметку для портретного и ландшафтного режимов, так что добавляем наши элементы к обоим из них. Также мы задействовали TransitionManager для проигрывания некоторой анимации.
9. Один нюанс насчет файла Android.mk. Он должен содержать ссылки на наши AIDL файлы. Два файла в секции LOCAL_SRC_FILES:
LOCAL_SRC_FILES +=
core/java/android/os/INavBarExService.aidl
core/java/android/os/INavBarExServiceUI.aidl
...
и INavBarExService.aidl в секции “aidl_files”:
aidl_files :=
frameworks/base/core/java/android/os/INavBarExService.aidl
...
Все вместе
Исходные коды доступны на GitHub. В каталоге “patches” есть два файла: frameworks_base.patch и system_sepolicy.patch. Первый патч должен быть применен к каталогу “{aosp}/frameworks/base”, второй — к “{aosp}/system/sepolicy”.
Каталог NavBarExDemo содержит демонстрационное приложение для системы сборки gradle.
Чтобы проверить реализацию на реальном устройстве нам нужно:
- Скачать стоковые исходные коды Android и настроить среду разработки;
- Применить патчи;
- Собрать кастомный Android SDK;
- Собрать кастомную прошивку и прошить устройство;
- Запустить демонстрационное приложение.
Для сборки Android SDK выполните следующие команды:
. build/envsetup.sh
lunch sdk_x86-eng
make sdk -j16
Результирующий ZIP файл расположен в каталоге {aosp}/out/host/linux-x86/sdk/sdk_x86 (если запущен в Linux). Имейте ввиду что имя файла будет содержать ваше имя пользователя Linux. Для того чтоб изменить это поведение, определите переменную окружения “BUILD_NUMBER” и ее значение будет использовано вместо имени пользователя. Параметр J указывает сколько задач запускать одновременно. Используйте количество процессорных ядер умноженное на 2.
Прошивка может быть создана используя следующие команды (для устройства Nexus 9):
. build/envsetup.sh
lunch aosp_flounder-userdebug
make otapackage -j16
Прошивка может быть прошита используя комманду:
fastboot -w flashall
Устройство должно иметь разблокированный загрузчик.
Чтоб проверить результаты было создано специальное демонстрационное приложение. Оно позволяет управлять элементами строки навигации.
Нажатие на кастомный элемент покажет тост даже если демо приложение не запущено (на самом деле система сама запустит процесс если нужно).
Недочеты
После перезагрузки устройство утратит всю кастомизацию. Так что было бы хорошо сохранить ее где нибудь. Также было бы хорошо добавить параметр «gravity» в API для более точного контроля расположения элементов.
Резюме
Мы расширили Android SDK и внедрили новую службу предназначенную для кастомизации строки навигации Android. Также мы создали кастомную прошивку для планшета Nexus 9 содержащую нашу службу.
В прежней статье мы реализовали стабилизацию экрана для Nexus 7. Здесь эта-же реализация но для Nexus 9:
P.S. На самом деле я также являюсь и автором оригинальной англоязычной версии статьи, которая была опубликована на blog.lemberg.co.uk, так что могу ответить на технические вопросы.
Автор: r_ii