Android 11 wprowadza możliwość korzystania z AIDL dla HAL-i w Androidzie, co umożliwia implementowanie części Androida bez HIDL. Przejście na używanie wyłącznie interfejsów HAL z AIDL (gdy interfejsy HAL na wyższych poziomach używają HIDL, należy używać HIDL).
Interfejsy HAL korzystające z AIDL do komunikacji między komponentami platformy, takimi jak te w system.img
, i komponentami sprzętowymi, takimi jak te w vendor.img
, muszą używać stabilnej wersji AIDL. Jednak w ramach partycji, na przykład z jednego HAL do drugiego, nie ma żadnych ograniczeń dotyczących używanego mechanizmu IPC.
Motywacja
Interfejs AIDL istnieje dłużej niż HIDL i jest używany w wielu innych miejscach, np. między komponentami platformy Androida lub w aplikacjach. Teraz, gdy AIDL zapewnia stabilność, można wdrożyć cały pakiet za pomocą pojedynczego środowiska wykonawczego IPC. AIDL ma też lepszy system wersji niż HIDL. Oto kilka zalet AIDL:
- Korzystanie z jednego języka IPC oznacza, że trzeba się uczyć, debugować, optymalizować i chronić tylko jedną rzecz.
- AIDL obsługuje wersjonowanie na miejscu dla właścicieli interfejsu:
- Właściciele mogą dodawać metody na końcu interfejsów lub pola do obiektów Parcelable. Oznacza to, że łatwiej jest wersjonować kod na przestrzeni lat, a także że koszty w ciągu roku są mniejsze (typy można zmieniać na miejscu i nie trzeba tworzyć dodatkowych bibliotek dla każdej wersji interfejsu).
- Interfejsy rozszerzeń można dołączać w czasie wykonywania, a nie w systemie typów, więc nie trzeba ponownie bazować rozszerzeń na nowszych wersjach interfejsów.
- Dotychczasowy interfejs AIDL może być używany bezpośrednio, gdy jego właściciel zdecyduje się na jego stabilizację. Wcześniej trzeba było utworzyć całą kopię interfejsu w HIDL.
Kompilowanie w środowisku wykonawczym AIDL
AIDL ma 3 różne backendy: Java, NDK i CPP. Aby korzystać ze stabilnej wersji AIDL, zawsze używaj kopii systemu libbinder
na stronie system/lib*/libbinder.so
i rozmawiaj na stronie /dev/binder
. W przypadku kodu w obrazu vendor
oznacza to, że nie można używać biblioteki libbinder
(z VNDK), ponieważ ma ona niestabilny interfejs API w C++ i niestabilne wewnętrzne elementy. Zamiast tego natywny kod dostawcy musi używać backendu NDK w AIDL, linkować się do biblioteki libbinder_ndk
(która jest obsługiwana przez system libbinder.so
) oraz do bibliotek NDK utworzonych przez wpisy aidl_interface
. Dokładne nazwy modułów znajdziesz w zasadach nazewnictwa modułów.
Tworzenie interfejsu AIDL HAL
Aby interfejs AIDL mógł być używany przez system i dostawcę, należy wprowadzić w nim 2 zmiany:
- Każda definicja typu musi być opatrzona adnotacją
@VintfStability
. aidl_interface
deklaracja musi zawieraćstability: "vintf",
.
Tylko właściciel interfejsu może wprowadzać te zmiany.
Aby interfejs działał, po wprowadzeniu tych zmian musisz go umieścić w pliku manifestu VINTF. Testuj to (i powiązane wymagania, takie jak weryfikacja, czy opublikowane interfejsy są zamrożone) za pomocą testu VTS vts_treble_vintf_vendor_test
. Możesz używać interfejsu @VintfStability
bez tych wymagań, wywołując AIBinder_forceDowngradeToLocalStability
w backendzie NDK, android::Stability::forceDowngradeToLocalStability
w backendzie C++, android.os.Binder#forceDowngradeToSystemStability
w backendzie Java na obiekcie bindera, zanim zostanie on wysłany do innego procesu.
Aby zapewnić maksymalną przenośność kodu i uniknąć potencjalnych problemów, takich jak niepotrzebne dodatkowe biblioteki, wyłącz backend CPP.
Użycie w tym przykładzie kodu funkcji backends
jest poprawne, ponieważ istnieją 3 backendy (Java, NDK i CPP). Kod pokazuje, jak wyłączyć backend CPP:
aidl_interface: {
...
backends: {
cpp: {
enabled: false,
},
},
}
Znajdowanie interfejsów AIDL HAL
Stabilne interfejsy AIDL AOSP dla HAL znajdują się w folderach aidl
w tych samych katalogach bazy danych co interfejsy HIDL:
hardware/interfaces
to interfejsy zwykle udostępniane przez sprzęt.frameworks/hardware/interfaces
służy do obsługi interfejsów wysokiego poziomu udostępnianych sprzętowi.system/hardware/interfaces
służy do interfejsów niskiego poziomu udostępnianych sprzętowi.
Umieść interfejsy rozszerzeń w innych hardware/interfaces
podkatalogach w vendor
lub hardware
.
Interfejsy rozszerzeń
Android ma zestaw oficjalnych interfejsów AOSP w każdej wersji. Jeśli partnerzy Androida chcą dodać funkcje do tych interfejsów, nie powinni ich zmieniać bezpośrednio, ponieważ spowoduje to niezgodność ich środowiska wykonawczego Androida z środowiskiem wykonawczym AOSP. Unikaj zmiany tych interfejsów, aby obraz GSI mógł nadal działać.
Rozszerzenia mogą się rejestrować na 2 sposoby:
- W czasie wykonywania kodu (patrz Dołączone interfejsy rozszerzeń).
- jako samodzielny system zarejestrowany na całym świecie i w VINTF.
Rozszerzenie jest jednak zarejestrowane, a jeśli komponenty specyficzne dla dostawcy (czyli niebędące częścią wstępnej wersji AOSP) korzystają z interfejsu, nie ma możliwości wystąpienia konfliktów podczas scalania. Jeśli jednak w dodatkowych komponentach wstępnej wersji AOSP zostaną wprowadzone zmiany, mogą wystąpić konflikty podczas scalania. W takich przypadkach zalecamy stosowanie tych strategii:
- Prześlij zmiany interfejsu do AOSP w kolejej wersji.
- Dodatki do interfejsu upstream, które zapewniają większą elastyczność (bez konfliktów podczas scalania) w następnej wersji.
Obiekty parcelable rozszerzenia: ParcelableHolder
ParcelableHolder
to instancja interfejsu Parcelable
, która może zawierać inną instancję Parcelable
.
Głównym zastosowaniem ParcelableHolder
jest umożliwienie rozszerzalności Parcelable
.
Na przykład implementatorzy urządzeń oczekują, że będą mogli rozszerzyć definiowane przez AOSP Parcelable
, AospDefinedParcelable
, aby uwzględnić w nich funkcje zwiększające wartość.
Użyj interfejsu ParcelableHolder
, aby rozszerzyć Parcelable
o funkcje zwiększające wartość. Interfejs ParcelableHolder
zawiera instancję Parcelable
. Jeśli spróbujesz dodać pola bezpośrednio do Parcelable
, spowoduje to błąd:
parcelable AospDefinedParcelable {
int a;
String b;
String x; // ERROR: added by a device implementer
int[] y; // added by a device implementer
}
Jak widać w poprzednim kodzie, ta metoda nie działa, ponieważ pola dodane przez implementatora urządzenia mogą powodować konflikt, gdy Parcelable
zostanie zmienione w kolejnych wersjach Androida.
Za pomocą ParcelableHolder
właściciel obiektu Parcelable może zdefiniować punkt rozszerzenia w instancji Parcelable
:
parcelable AospDefinedParcelable {
int a;
String b;
ParcelableHolder extension;
}
Następnie implementatorzy urządzeń mogą zdefiniować własne wystąpienie Parcelable
dla swojego rozszerzenia:
parcelable OemDefinedParcelable {
String x;
int[] y;
}
Nowe wystąpienie pola Parcelable
można dołączyć do pierwotnego wystąpienia Parcelable
za pomocą pola ParcelableHolder
:
// Java
AospDefinedParcelable ap = ...;
OemDefinedParcelable op = new OemDefinedParcelable();
op.x = ...;
op.y = ...;
ap.extension.setParcelable(op);
...
OemDefinedParcelable op = ap.extension.getParcelable(OemDefinedParcelable.class);
// C++
AospDefinedParcelable ap;
OemDefinedParcelable op;
std::shared_ptr<OemDefinedParcelable> op_ptr = make_shared<OemDefinedParcelable>();
ap.extension.setParcelable(op);
ap.extension.setParcelable(op_ptr);
...
std::shared_ptr<OemDefinedParcelable> op_ptr;
ap.extension.getParcelable(&op_ptr);
// NDK
AospDefinedParcelable ap;
OemDefinedParcelable op;
ap.extension.setParcelable(op);
...
std::optional<OemDefinedParcelable> op;
ap.extension.getParcelable(&op);
// Rust
let mut ap = AospDefinedParcelable { .. };
let op = Rc::new(OemDefinedParcelable { .. });
ap.extension.set_parcelable(Rc::clone(&op));
...
let op = ap.extension.get_parcelable::<OemDefinedParcelable>();
Nazwa instancji serwera AIDL HAL
Zgodnie z konwencją usługi AIDL HAL mają nazwę instancji w formacie $package.$type/$instance
. Na przykład instancja wibratora HAL jest zarejestrowana jako android.hardware.vibrator.IVibrator/default
.
Tworzenie serwera AIDL HAL
@VintfStability
Serwery AIDL muszą być zadeklarowane w pliku manifestu VINTF, na przykład:
<hal format="aidl">
<name>android.hardware.vibrator</name>
<version>1</version>
<fqname>IVibrator/default</fqname>
</hal>
W przeciwnym razie należy zarejestrować usługę AIDL w zwykły sposób. Podczas uruchamiania testów VTS należy mieć dostępne wszystkie zadeklarowane interfejsy API AIDL.
Tworzenie klienta AIDL
Klienci AIDL muszą zadeklarować się w macierz zgodności, na przykład:
<hal format="aidl" optional="true">
<name>android.hardware.vibrator</name>
<version>1-2</version>
<interface>
<name>IVibrator</name>
<instance>default</instance>
</interface>
</hal>
Konwertowanie istniejącego interfejsu HAL z HIDL na AIDL
Aby przekonwertować interfejs HIDL na AIDL, użyj narzędzia hidl2aidl
.
hidl2aidl
funkcji:
- Utwórz pliki AIDL (
.aidl
) na podstawie plików HAL (.hal
) dla danego pakietu. - Utwórz reguły kompilacji dla nowo utworzonego pakietu AIDL ze wszystkimi włączonymi backendami.
- Utwórz metody tłumaczenia w backendach Java, CPP i NDK, aby przekształcać typy HIDL w typy AIDL.
- Utwórz reguły kompilacji dla bibliotek tłumaczenia z wymaganymi zależnościami.
- Utwórz statyczne stwierdzenia, aby mieć pewność, że enumeratory HIDL i AIDL mają te same wartości w backendach CPP i NDK.
Aby przekonwertować pakiet plików HAL na pliki AIDL:
Utwórz narzędzie dostępne w sekcji
system/tools/hidl/hidl2aidl
.Utworzenie tego narzędzia na podstawie najnowszego źródła zapewnia najbardziej kompletne wrażenia. Najnowszej wersji możesz używać do konwertowania interfejsów w starszych gałęziach z poprzednich wersji:
m hidl2aidl
Uruchom narzędzie, podając katalog wyjściowy, a następnie pakiet, który ma zostać przekonwertowany.
Opcjonalnie możesz użyć argumentu
-l
, aby dodać zawartość nowego pliku licencji do górnej części wszystkich wygenerowanych plików. Upewnij się, że używasz prawidłowej licencji i daty:hidl2aidl -o <output directory> -l <file with license> <package>
Przykład:
hidl2aidl -o . -l my_license.txt android.hardware.nfc@1.2
Przejrzyj wygenerowane pliki i rozwiąż problemy z konwersją:
conversion.log
zawiera wszystkie problemy, które należy rozwiązać jako pierwsze.- Wygenerowane pliki AIDL mogą zawierać ostrzeżenia i sugestie, które wymagają podjęcia działań. Te komentarze zaczynają się od
//
. - Oczyść i ulepszaj pakiet.
- Sprawdź adnotację
@JavaDerive
, aby dowiedzieć się, jakie funkcje mogą być potrzebne, np.toString
lubequals
.
Utwórz tylko te cele, których potrzebujesz:
- Wyłącz backendy, których nie będziesz używać. Używaj backendu NDK zamiast backendu CPP. Zapoznaj się z artykułem Tworzenie aplikacji na potrzeby środowiska wykonawczego AIDL.
- Usuń biblioteki tłumaczeń lub wygenerowany kod, którego nie będziesz używać.
Najważniejsze różnice między AIDL a HIDL:
- Korzystanie z wbudowanych
Status
i wyjątków AIDL zwykle polepsza interfejs i eliminuje potrzebę korzystania z innego typu stanu związanego z interfejsem. - Argumenty interfejsu AIDL w metodach nie są domyślnie
@nullable
, tak jak w HIDL.
- Korzystanie z wbudowanych
SEPolicy dla interfejsów HAL AIDL
Typ usługi AIDL, który jest widoczny dla kodu dostawcy, musi mieć atrybut hal_service_type
. Poza tym konfiguracja sepolicy jest taka sama jak w przypadku innych usług AIDL (chociaż istnieją specjalne atrybuty dla interfejsów HAL). Oto przykładowa definicja kontekstu usługi HAL:
type hal_foo_service, service_manager_type, hal_service_type;
W przypadku większości usług zdefiniowanych przez platformę kontekst usługi z odpowiednim typem jest już dodany (na przykład android.hardware.foo.IFoo/default
jest już oznaczony jako hal_foo_service
). Jeśli jednak klient frameworku obsługuje wiele nazw instancji, dodatkowe nazwy instancji należy dodać w plikach service_contexts
dotyczących urządzenia:
android.hardware.foo.IFoo/custom_instance u:object_r:hal_foo_service:s0
Podczas tworzenia nowego typu HAL musisz dodać atrybuty HAL. Określony atrybut HAL może być powiązany z wieloma typami usług (każdy z nich może mieć wiele wystąpień, jak już wspomniano). W przypadku HAL foo
jest hal_attribute(foo)
. To makro definiuje atrybuty hal_foo_client
i hal_foo_server
. W przypadku danej domeny makro hal_client_domain
i hal_server_domain
kojarzą domenę z danym atrybutem HAL. Na przykład serwer systemowy jest klientem tego interfejsu HAL zgodnie z zasadami hal_client_domain(system_server, hal_foo)
. Serwer HAL zawiera również hal_server_domain(my_hal_domain, hal_foo)
.
Zwykle dla danego atrybutu HAL tworzysz też domenę, taką jak hal_foo_default
, na potrzeby referencyjnych lub przykładowych atrybutów HAL. Niektóre urządzenia używają jednak tych domen dla własnych serwerów. Rozróżnianie domen na wielu serwerach ma znaczenie tylko wtedy, gdy jest wiele serwerów, które obsługują ten sam interfejs i potrzebują innego zestawu uprawnień w swoich implementacjach.
We wszystkich tych makronach hal_foo
nie jest obiektem sepolicy. Zamiast tego te tokeny są używane przez te makropolecenia do odwoływania się do grupy atrybutów powiązanych z parą klient-serwer.
Jednak do tej pory atrybuty hal_foo_service
i hal_foo
(para atrybutów z hal_attribute(foo)
) nie są powiązane. Atrybut HAL jest powiązany z usługami HAL AIDL za pomocą makra hal_attribute_service
(makrom hal_attribute_hwservice
używają HAL-e HIDL), na przykład hal_attribute_service(hal_foo, hal_foo_service)
. Oznacza to, że procesy hal_foo_client
mogą uzyskać dostęp do HAL, a procesy hal_foo_server
mogą zarejestrować HAL. Egzekwowanie tych zasad rejestracji jest realizowane przez menedżera kontekstu (servicemanager
).
Nazwy usług nie zawsze odpowiadają atrybutom HAL, np. hal_attribute_service(hal_foo, hal_foo2_service)
. Ogólnie, ponieważ zakłada to, że usługi są zawsze używane razem, możesz usunąć hal_foo2_service
i użyć hal_foo_service
we wszystkich kontekstach usług. Jeśli HALs ustawia wiele wystąpień hal_attribute_service
, oznacza to, że pierwotna nazwa atrybutu HAL nie jest wystarczająco ogólna i nie można jej zmienić.
Połączenie tych wszystkich elementów w przykładzie HAL wygląda tak:
public/attributes:
// define hal_foo, hal_foo_client, hal_foo_server
hal_attribute(foo)
public/service.te
// define hal_foo_service
type hal_foo_service, hal_service_type, protected_service, service_manager_type
public/hal_foo.te:
// allow binder connection from client to server
binder_call(hal_foo_client, hal_foo_server)
// allow client to find the service, allow server to register the service
hal_attribute_service(hal_foo, hal_foo_service)
// allow binder communication from server to service_manager
binder_use(hal_foo_server)
private/service_contexts:
// bind an AIDL service name to the selinux type
android.hardware.foo.IFooXxxx/default u:object_r:hal_foo_service:s0
private/<some_domain>.te:
// let this domain use the hal service
binder_use(some_domain)
hal_client_domain(some_domain, hal_foo)
vendor/<some_hal_server_domain>.te
// let this domain serve the hal service
hal_server_domain(some_hal_server_domain, hal_foo)
Dołączone interfejsy rozszerzeń
Rozszerzenie może być dołączone do dowolnego interfejsu Binder, niezależnie od tego, czy jest to interfejs najwyższego poziomu zarejestrowany bezpośrednio w menedżerze usługi, czy interfejs podrzędny. Podczas uzyskiwania rozszerzenia musisz potwierdzić, że jego typ jest zgodny z oczekiwaniami. Rozszerzenia możesz ustawiać tylko w procesie obsługijącym pakiet.
Używaj załączonych rozszerzeń, gdy rozszerzenie zmienia działanie istniejącego interfejsu HAL. Jeśli potrzebna jest zupełnie nowa funkcja, nie musisz stosować tego mechanizmu i możesz zarejestrować interfejs rozszerzenia bezpośrednio w menedżerze usługi. Dołączone interfejsy rozszerzeń mają największy sens, gdy są dołączone do interfejsów podrzędnych, ponieważ te hierarchie mogą być głębokie lub wieloinstancyjne. Korzystanie z globalnego rozszerzenia do odzwierciedlenia hierarchii interfejsu bindera innej usługi wymaga rozbudowanej księgowości, aby zapewnić funkcje równoważne bezpośrednio dołączonym rozszerzeniom.
Aby ustawić rozszerzenie w binderze, użyj tych interfejsów API:
- Backend NDK:
AIBinder_setExtension
- Java backend:
android.os.Binder.setExtension
- Backend CPP:
android::Binder::setExtension
- Backend Rust:
binder::Binder::set_extension
Aby uzyskać rozszerzenie bindera, użyj tych interfejsów API:
- Backend NDK:
AIBinder_getExtension
- Java backend:
android.os.IBinder.getExtension
- Backend CPP:
android::IBinder::getExtension
- Backend Rust:
binder::Binder::get_extension
Więcej informacji o tych interfejsach API znajdziesz w dokumentacji funkcji getExtension
w odpowiednim backendzie. Przykład użycia rozszerzeń znajdziesz w artykule hardware/interfaces/tests/extension/vibrator
.
Najważniejsze różnice między AIDL a HIDL
Podczas korzystania z interfejsów HAL AIDL lub interfejsów AIDL HAL należy pamiętać o różnicach w porównaniu z pisaniem interfejsów HAL HIDL.
- Składnia języka AIDL jest zbliżona do składni języka Java. Składnia HIDL jest podobna do C++.
- Wszystkie interfejsy AIDL mają wbudowane stany błędów. Zamiast tworzyć niestandardowe typy stanu, utwórz w plikach interfejsu stałe typu int o stanie i użyj ich w backendzie w języku C++ i NDK oraz w backendzie w języku Java.
EX_SERVICE_SPECIFIC
ServiceSpecificException
Zobacz obsługę błędów. - AIDL nie uruchamia automatycznie puli wątków podczas wysyłania obiektów bindera. Musisz je uruchomić ręcznie (patrz Zarządzanie wątkiem).
- AIDL nie przerywa operacji w przypadku niezaznaczonych błędów transportu (HIDL
Return
przerywa operacje w przypadku niezaznaczonych błędów). - AIDL może zadeklarować tylko jeden typ na plik.
- Argumenty AIDL można określić jako
in
,out
lubinout
oprócz parametru wyjściowego (nie ma wywołań zwrotnych synchronicznych). - AIDL używa typu prymitywnego
fd
zamiasthandle
. - HIDL używa wersji głównych w przypadku zmian niezgodnych i wersji podrzędnych w przypadku zmian zgodnych. W AIDL zmiany zgodne wstecznie są wprowadzane na miejscu.
AIDL nie ma wyraźnej koncepcji wersji głównych; zamiast tego są one uwzględniane w nazwach pakietów. Na przykład AIDL może używać nazwy pakietu
bluetooth2
. - Domyślnie AIDL nie dziedziczy priorytetu w czasie rzeczywistym. Aby umożliwić dziedziczenie priorytetów w czasie rzeczywistym, należy użyć funkcji
setInheritRt
w każdym binderze.
Pakiet testów dostawcy (VTS) dla HAL
Android korzysta z pakietu testów dostawcy (VTS) do weryfikowania oczekiwanych implementacji HAL. VTS zapewnia zgodność wsteczną Androida ze starszymi implementacjami dostawców. W przypadku implementacji, które nie przechodzą testów VTS, występują znane problemy ze zgodnością, które mogą uniemożliwić im działanie w przyszłych wersjach systemu operacyjnego.
VTS dla HAL składa się z 2 głównych części.
1. Sprawdź, czy interfejsy HAL na urządzeniu są znane i oczekywane przez Androida
Ten zestaw testów znajdziesz w sekcji test/vts-testcase/hal/treble/vintf
.
Te testy służą do sprawdzania:
- Każdy interfejs
@VintfStability
zadeklarowany w pliku manifestu VINTF jest zamrożony w znanej wersji. Dzięki temu obie strony interfejsu zgadzają się co do dokładnej definicji tej wersji interfejsu. Jest to konieczne do podstawowego działania. - Na tym urządzeniu dostępne są wszystkie HAL-e zadeklarowane w manifeście VINTF. Każdy klient, który ma wystarczające uprawnienia do korzystania z deklarowanej usługi HAL, musi mieć możliwość uzyskania dostępu do tych usług i korzystania z nich w dowolnym momencie.
- Wszystkie HAL-e zadeklarowane w pliku manifestu VINTF wyświetlają wersję interfejsu, którą zadeklarowano w pliku manifestu.
- Na urządzeniu nie są wyświetlane żadne przestarzałe interfejsy HAL. Android przestaje obsługiwać starsze wersje interfejsów HAL zgodnie z opisem w cyklu życia FCM.
- Wymagane interfejsy HAL są obecne na urządzeniu. Niektóre interfejsy HAL są wymagane do prawidłowego działania Androida.
2. Sprawdź oczekiwane działanie każdego interfejsu HAL.
Każdy interfejs HAL ma własne testy VTS, które sprawdzają oczekiwane działanie klientów. Przypadki testowe są uruchamiane w przypadku każdej instancji zadeklarowanego interfejsu HAL i wymuszają określone zachowanie na podstawie wersji zaimplementowanego interfejsu.
Testy te obejmują wszystkie aspekty implementacji HAL, na których opiera się framework Androida lub które mogą być używane w przyszłości.
Testy te obejmują weryfikację obsługi funkcji, obsługi błędów i innych zachowań, których klient może oczekiwać od usługi.
Kamienie milowe VTS dotyczące rozwoju HAL
Testy VTS powinny być aktualizowane podczas tworzenia lub modyfikowania interfejsów HAL systemu Android.
Testy VTS muszą być ukończone i gotowe do weryfikacji implementacji dostawców przed zamrożeniem wersji interfejsu API dostawcy Androida. Muszą być gotowe przed zamrożeniem interfejsów, aby deweloperzy mogli tworzyć swoje implementacje, weryfikować je i przekazywać opinie deweloperom interfejsu HAL.
VTS w Cuttlefish
Gdy sprzęt jest niedostępny, Android używa Cuttlefish jako narzędzia do tworzenia interfejsów HAL. Umożliwia to testowanie VTS i testowanie integracji Androida na dużą skalę.
hal_implementation_test
sprawdza, czy Cuttlefish ma implementacje najnowszych wersji interfejsu HAL, aby upewnić się, że Android jest gotowy do obsługi nowych interfejsów, oraz czy testy VTS są gotowe do testowania implementacji nowych dostawców, gdy tylko pojawią się nowe wersje sprzętu i urządzeń.