W Androidzie 11 i nowszych funkcja szybkiego dostępu do urządzeń umożliwia szybkie przeglądanie i kontrolowanie urządzeń zewnętrznych, takich jak światła, termostaty i kamery, za pomocą 3 interakcji użytkownika z domyślnego programu uruchamiającego. Program uruchamiający wybiera producent urządzenia OEM. Agregatory urządzeń, np. Google Home, i aplikacje innych firm mogą udostępniać urządzenia do wyświetlania w tym pokoju. Na tej stronie dowiesz się, jak umieścić elementy sterujące urządzeniem w tym obszarze i połączyć je z aplikacją sterującą.
Aby dodać tę obsługę, utwórz i zadeklaruj ControlsProviderService
. Utwórz elementy sterujące obsługiwane przez aplikację na podstawie wstępnie zdefiniowanych typów, a następnie utwórz wydawców dla tych ustawień.
Interfejs użytkownika
Urządzenia są wyświetlane w sekcji Elementy sterujące urządzeniami jako widżety szablonowe. Dostępnych jest 5 widżet do sterowania urządzeniami, pokazanych na tej ilustracji:
|
|
|
|
|
Gdy naciśniesz i przytrzymasz widżet, przejdziesz do aplikacji, gdzie możesz ją dokładniej kontrolować. Możesz dostosować ikonę i kolor każdego widżetu, ale dla wygody użytkowników użyj domyślnej ikony i koloru, jeśli to ustawienie jest zgodne z ustawieniami urządzenia.
Tworzenie usługi
W tej sekcji dowiesz się, jak utworzyć ControlsProviderService
.
Ta usługa informuje interfejs systemu Android, że aplikacja zawiera elementy sterujące urządzenia, które muszą się znajdować w obszarze Sterowanie urządzeniem w interfejsie Androida.
Interfejs ControlsProviderService
API zakłada znajomość strumieni reaktywnych, zgodnie z definicją w projekcie dotyczącym strumieni reaktywnych na GitHubie i zaimplementowanym w interfejsach Java 9 Flow.
Podstawą interfejsu API są te koncepcje:
- Wydawca: aplikacja jest wydawcą.
- Subskrybent: interfejs systemu to subskrybent, który może poprosić wydawcę o określenie ustawień.
- Subskrypcja:okres, w którym wydawca może wysyłać aktualizacje do interfejsu systemu. Wydawca lub subskrybent może zamknąć to okno.
Deklarowanie usługi
Aplikacja musi zadeklarować usługę – na przykład MyCustomControlService
– w swoim manifeście.
Usługa musi zawierać filtr intencji dla: ControlsProviderService
. Ten filtr umożliwia aplikacjom kontrolę nad interfejsem systemu.
Potrzebujesz też elementu label
wyświetlanego w elementach sterujących w interfejsie systemu.
Z przykładu poniżej dowiesz się, jak zadeklarować usługę:
<service
android:name="MyCustomControlService"
android:label="My Custom Controls"
android:permission="android.permission.BIND_CONTROLS"
android:exported="true"
>
<intent-filter>
<action android:name="android.service.controls.ControlsProviderService" />
</intent-filter>
</service>
Następnie utwórz nowy plik Kotlin o nazwie MyCustomControlService.kt
i dodaj do niego rozszerzenie ControlsProviderService()
:
Kotlin
class MyCustomControlService : ControlsProviderService() { ... }
Java
public class MyCustomJavaControlService extends ControlsProviderService { ... }
Wybór właściwego typu elementu sterującego
Interfejs API udostępnia metody tworzenia elementów sterujących. Aby wypełnić kreator, określ urządzenie, którym chcesz sterować, oraz sposób interakcji użytkownika z tym narzędziem. Wykonaj te czynności:
- Wybierz typ urządzenia reprezentowanego przez element sterujący. Klasa
DeviceTypes
to liczba wszystkich obsługiwanych urządzeń. Typ służy do określania ikon i kolorów urządzenia w interfejsie. - Określ nazwę widoczną dla użytkownika, lokalizację urządzenia (np. kuchnię) i inne elementy tekstowe UI powiązane z elementem sterującym.
- Wybierz szablon, który ułatwia interakcję użytkownika. Elementy sterujące są przypisywane z aplikacji
ControlTemplate
. Ten szablon bezpośrednio pokazuje użytkownikowi stan sterowania oraz dostępne metody wprowadzania, czyliControlAction
. W tabeli poniżej opisujemy niektóre dostępne szablony i działania, które obsługują:
Szablon | Działanie | Description |
ControlTemplate.getNoTemplateObject()
|
None
|
Aplikacja może go wykorzystać do przekazania informacji o elemencie sterującym, ale użytkownik nie może z niego korzystać. |
ToggleTemplate
|
BooleanAction
|
Reprezentuje element sterujący, który można przełączać ze stanu włączenia lub wyłączenia. Obiekt BooleanAction zawiera pole, które zmienia się w sposób reprezentowania żądanego nowego stanu, gdy użytkownik kliknie element sterujący.
|
RangeTemplate
|
FloatAction
|
Reprezentuje widżet z suwakiem z określonymi wartościami minimalnej, maksymalnej i liczby kroków. Gdy użytkownik wejdzie w interakcję z suwakiem, wyślij do aplikacji nowy obiekt FloatAction ze zaktualizowaną wartością.
|
ToggleRangeTemplate
|
BooleanAction, FloatAction
|
Ten szablon stanowi połączenie właściwości ToggleTemplate i RangeTemplate . Obsługuje zdarzenia dotknięcia oraz suwak, np. do sterowania oświetleniem, które można przyciemnić.
|
TemperatureControlTemplate
|
ModeAction, BooleanAction, FloatAction
|
Oprócz poprzednich działań szablon umożliwia użytkownikowi ustawienie trybu, takiego jak ogrzewanie, chłodzenie, ogrzewanie/chłodzenie, eko lub wyłączenie. |
StatelessTemplate
|
CommandAction
|
Służy do wskazywania elementu sterującego obsługującego dotyk, ale którego stanu nie można określić – takiego jak pilot do telewizora podczerwień. Możesz użyć tego szablonu do zdefiniowania procedury lub makra, które są agregacją zmian kontroli i stanu. |
Na podstawie tych informacji możesz utworzyć element sterujący:
- Jeśli stan elementu sterującego jest nieznany, użyj klasy kompilacji
Control.StatelessBuilder
. - Jeśli stan elementu sterującego jest znany, użyj klasy kompilacji
Control.StatefulBuilder
.
Aby na przykład sterować inteligentną żarówką i termostatem, dodaj do MyCustomControlService
te stałe:
Kotlin
private const val LIGHT_ID = 1234 private const val LIGHT_TITLE = "My fancy light" private const val LIGHT_TYPE = DeviceTypes.TYPE_LIGHT private const val THERMOSTAT_ID = 5678 private const val THERMOSTAT_TITLE = "My fancy thermostat" private const val THERMOSTAT_TYPE = DeviceTypes.TYPE_THERMOSTAT class MyCustomControlService : ControlsProviderService() { ... }
Java
public class MyCustomJavaControlService extends ControlsProviderService { private final int LIGHT_ID = 1337; private final String LIGHT_TITLE = "My fancy light"; private final int LIGHT_TYPE = DeviceTypes.TYPE_LIGHT; private final int THERMOSTAT_ID = 1338; private final String THERMOSTAT_TITLE = "My fancy thermostat"; private final int THERMOSTAT_TYPE = DeviceTypes.TYPE_THERMOSTAT; ... }
Tworzenie wydawców na potrzeby ustawień
Gdy utworzysz element sterujący, musi on zostać utworzony przez wydawcę. Wydawca informuje interfejs systemu
o istnieniu elementu sterującego. Klasa ControlsProviderService
ma 2 metody wydawcy, które musisz zastąpić w kodzie aplikacji:
createPublisherForAllAvailable()
: tworzyPublisher
ze wszystkimi ustawieniami dostępnymi w aplikacji. UżyjControl.StatelessBuilder()
, aby utworzyć obiektyControl
dla tego wydawcy.createPublisherFor()
: tworzy elementPublisher
dla listy określonych ustawień określonych za pomocą identyfikatorów w postaci ciągów znaków. Do utworzenia tych obiektówControl
użyjControl.StatefulBuilder
, ponieważ wydawca musi przypisać stan każdej z nich.
Tworzenie wydawcy
Gdy aplikacja po raz pierwszy publikuje elementy sterujące w interfejsie systemu, nie wie, jaki jest stan każdego z nich. Uzyskanie stanu może być czasochłonne i wymagać wielu przeskoków w sieci dostawcy urządzenia. Aby poinformować system o dostępnych opcjach kontroli, użyj metody createPublisherForAllAvailable()
. Ta metoda korzysta z klasy montera Control.StatelessBuilder
, ponieważ stan każdego elementu sterującego jest nieznany.
Gdy elementy sterujące pojawią się w interfejsie Androida , użytkownik może wybrać ulubione elementy.
Aby użyć współprogramów Kotlin do utworzenia elementu ControlsProviderService
, dodaj nową zależność do build.gradle
:
Odlotowy
dependencies { implementation "org.jetbrains.kotlinx:kotlinx-coroutines-jdk9:1.6.4" }
Kotlin
dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-jdk9:1.6.4") }
Po zsynchronizowaniu plików Gradle dodaj do interfejsu Service
ten fragment kodu, aby zaimplementować createPublisherForAllAvailable()
:
Kotlin
class MyCustomControlService : ControlsProviderService() { override fun createPublisherForAllAvailable(): Flow.Publisher= flowPublish { send(createStatelessControl(LIGHT_ID, LIGHT_TITLE, LIGHT_TYPE)) send(createStatelessControl(THERMOSTAT_ID, THERMOSTAT_TITLE, THERMOSTAT_TYPE)) } private fun createStatelessControl(id: Int, title: String, type: Int): Control { val intent = Intent(this, MainActivity::class.java) .putExtra(EXTRA_MESSAGE, title) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) val action = PendingIntent.getActivity( this, id, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE ) return Control.StatelessBuilder(id.toString(), action) .setTitle(title) .setDeviceType(type) .build() } override fun createPublisherFor(controlIds: List ): Flow.Publisher { TODO() } override fun performControlAction( controlId: String, action: ControlAction, consumer: Consumer ) { TODO() } }
Java
public class MyCustomJavaControlService extends ControlsProviderService { private final int LIGHT_ID = 1337; private final String LIGHT_TITLE = "My fancy light"; private final int LIGHT_TYPE = DeviceTypes.TYPE_LIGHT; private final int THERMOSTAT_ID = 1338; private final String THERMOSTAT_TITLE = "My fancy thermostat"; private final int THERMOSTAT_TYPE = DeviceTypes.TYPE_THERMOSTAT; private boolean toggleState = false; private float rangeState = 18f; private final Map> controlFlows = new HashMap<>(); @NonNull @Override public Flow.Publisher createPublisherForAllAvailable() { List controls = new ArrayList<>(); controls.add(createStatelessControl(LIGHT_ID, LIGHT_TITLE, LIGHT_TYPE)); controls.add(createStatelessControl(THERMOSTAT_ID, THERMOSTAT_TITLE, THERMOSTAT_TYPE)); return FlowAdapters.toFlowPublisher(Flowable.fromIterable(controls)); } @NonNull @Override public Flow.Publisher createPublisherFor(@NonNull List controlIds) { ReplayProcessor updatePublisher = ReplayProcessor.create(); controlIds.forEach(control -> { controlFlows.put(control, updatePublisher); updatePublisher.onNext(createLight()); updatePublisher.onNext(createThermostat()); }); return FlowAdapters.toFlowPublisher(updatePublisher); } }
Przesuń menu systemowe w dół i znajdź przycisk Sterowanie urządzeniem (widoczny na ilustracji 4):
Kliknięcie Zarządzanie urządzeniami powoduje przejście do drugiego ekranu, na którym można wybrać aplikację. Po wybraniu aplikacji zobaczysz, jak w poprzednim fragmencie kodu tworzy się niestandardowe menu systemowe z nowymi elementami sterującymi, jak widać na ilustracji 5:
Teraz zaimplementuj metodę createPublisherFor()
, dodając do elementu Service
te dane:
Kotlin
private val job = SupervisorJob() private val scope = CoroutineScope(Dispatchers.IO + job) private val controlFlows = mutableMapOf>() private var toggleState = false private var rangeState = 18f override fun createPublisherFor(controlIds: List ): Flow.Publisher { val flow = MutableSharedFlow (replay = 2, extraBufferCapacity = 2) controlIds.forEach { controlFlows[it] = flow } scope.launch { delay(1000) // Retrieving the toggle state. flow.tryEmit(createLight()) delay(1000) // Retrieving the range state. flow.tryEmit(createThermostat()) } return flow.asPublisher() } private fun createLight() = createStatefulControl( LIGHT_ID, LIGHT_TITLE, LIGHT_TYPE, toggleState, ToggleTemplate( LIGHT_ID.toString(), ControlButton( toggleState, toggleState.toString().uppercase(Locale.getDefault()) ) ) ) private fun createThermostat() = createStatefulControl( THERMOSTAT_ID, THERMOSTAT_TITLE, THERMOSTAT_TYPE, rangeState, RangeTemplate( THERMOSTAT_ID.toString(), 15f, 25f, rangeState, 0.1f, "%1.1f" ) ) private fun createStatefulControl(id: Int, title: String, type: Int, state: T, template: ControlTemplate): Control { val intent = Intent(this, MainActivity::class.java) .putExtra(EXTRA_MESSAGE, "$title $state") .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) val action = PendingIntent.getActivity( this, id, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE ) return Control.StatefulBuilder(id.toString(), action) .setTitle(title) .setDeviceType(type) .setStatus(Control.STATUS_OK) .setControlTemplate(template) .build() } override fun onDestroy() { super.onDestroy() job.cancel() }
Java
@NonNull @Override public Flow.PublishercreatePublisherFor(@NonNull List controlIds) { ReplayProcessor updatePublisher = ReplayProcessor.create(); controlIds.forEach(control -> { controlFlows.put(control, updatePublisher); updatePublisher.onNext(createLight()); updatePublisher.onNext(createThermostat()); }); return FlowAdapters.toFlowPublisher(updatePublisher); } private Control createStatelessControl(int id, String title, int type) { Intent intent = new Intent(this, MainActivity.class) .putExtra(EXTRA_MESSAGE, title) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); PendingIntent action = PendingIntent.getActivity( this, id, intent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE ); return new Control.StatelessBuilder(id + "", action) .setTitle(title) .setDeviceType(type) .build(); } private Control createLight() { return createStatefulControl( LIGHT_ID, LIGHT_TITLE, LIGHT_TYPE, toggleState, new ToggleTemplate( LIGHT_ID + "", new ControlButton( toggleState, String.valueOf(toggleState).toUpperCase(Locale.getDefault()) ) ) ); } private Control createThermostat() { return createStatefulControl( THERMOSTAT_ID, THERMOSTAT_TITLE, THERMOSTAT_TYPE, rangeState, new RangeTemplate( THERMOSTAT_ID + "", 15f, 25f, rangeState, 0.1f, "%1.1f" ) ); } private Control createStatefulControl(int id, String title, int type, T state, ControlTemplate template) { Intent intent = new Intent(this, MainActivity.class) .putExtra(EXTRA_MESSAGE, "$title $state") .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); PendingIntent action = PendingIntent.getActivity( this, id, intent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE ); return new Control.StatefulBuilder(id + "", action) .setTitle(title) .setDeviceType(type) .setStatus(Control.STATUS_OK) .setControlTemplate(template) .build(); }
W tym przykładzie metoda createPublisherFor()
zawiera fałszywą implementację tego, co aplikacja musi robić: komunikuje się z urządzeniem, aby sprawdzić stan urządzenia, i wysyła ten stan do systemu.
Metoda createPublisherFor()
korzysta z współprogramów i przepływów Kotlin, aby spełnić wymagany interfejs Reactive Streams API. W tym celu:
- Tworzy
Flow
. - Poczekaj 1 sekundę.
- Tworzy i emituje stan inteligentnego oświetlenia.
- Poczekaj sekundę.
- Tworzy i emituje stan termostatu.
Obsługa działań
Metoda performControlAction()
sygnalizuje, gdy użytkownik wchodzi w interakcję z opublikowanym elementem sterującym. O działaniu decyduje typ wysłania pliku ControlAction
.
Wykonaj odpowiednie działanie dla danego elementu sterującego, a następnie zaktualizuj stan urządzenia w interfejsie Androida.
Aby dokończyć przykład, dodaj do Service
:
Kotlin
override fun performControlAction( controlId: String, action: ControlAction, consumer: Consumer) { controlFlows[controlId]?.let { flow -> when (controlId) { LIGHT_ID.toString() -> { consumer.accept(ControlAction.RESPONSE_OK) if (action is BooleanAction) toggleState = action.newState flow.tryEmit(createLight()) } THERMOSTAT_ID.toString() -> { consumer.accept(ControlAction.RESPONSE_OK) if (action is FloatAction) rangeState = action.newValue flow.tryEmit(createThermostat()) } else -> consumer.accept(ControlAction.RESPONSE_FAIL) } } ?: consumer.accept(ControlAction.RESPONSE_FAIL) }
Java
@Override public void performControlAction(@NonNull String controlId, @NonNull ControlAction action, @NonNull Consumerconsumer) { ReplayProcessor processor = controlFlows.get(controlId); if (processor == null) return; if (controlId.equals(LIGHT_ID + "")) { consumer.accept(ControlAction.RESPONSE_OK); if (action instanceof BooleanAction) toggleState = ((BooleanAction) action).getNewState(); processor.onNext(createLight()); } if (controlId.equals(THERMOSTAT_ID + "")) { consumer.accept(ControlAction.RESPONSE_OK); if (action instanceof FloatAction) rangeState = ((FloatAction) action).getNewValue() processor.onNext(createThermostat()); } }
Uruchom aplikację, otwórz menu Sterowanie urządzeniem i sprawdź ustawienia oświetlenia i termostatu.