Sterowanie urządzeniami zewnętrznymi

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ą.

Rysunek 1. Obszar sterowania urządzeniem w interfejsie Androida.

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:

Przełącz widżet
Przełącz
Przełącz za pomocą widżetu z suwakiem
Przełącz za pomocą suwaka
Widżet zakresu
Zakres (nie można go włączyć ani wyłączyć)
Bezstanowy widżet przełączania
Przełącznik bezstanowy
Widżet panelu temperatury (zamknięty)
Panel temperatury (zamknięty)
Rysunek 2. Kolekcja widżetów opartych na szablonach.

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.

Obraz pokazujący widżet panelu temperatury (otwarty)
Rysunek 3. Otwórz widżet panelu temperatury.

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:

  1. 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.
  2. Określ nazwę widoczną dla użytkownika, lokalizację urządzenia (np. kuchnię) i inne elementy tekstowe UI powiązane z elementem sterującym.
  3. 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, czyli ControlAction. 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:

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(): tworzy Publisher ze wszystkimi ustawieniami dostępnymi w aplikacji. Użyj Control.StatelessBuilder(), aby utworzyć obiekty Control dla tego wydawcy.
  • createPublisherFor(): tworzy element Publisher dla listy określonych ustawień określonych za pomocą identyfikatorów w postaci ciągów znaków. Do utworzenia tych obiektów Control użyj Control.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):

Obraz przedstawiający interfejs systemu służący do sterowania urządzeniami
Rysunek 4. Elementy sterujące urządzeniami w menu systemowym.

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:

Obraz przedstawiający menu systemowe zawierające elementy sterujące podświetleniem i termostatem
Rysunek 5. Elementy sterujące oświetleniem i termostatami do dodania.

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.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);
    }
 
    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:

  1. Tworzy Flow.
  2. Poczekaj 1 sekundę.
  3. Tworzy i emituje stan inteligentnego oświetlenia.
  4. Poczekaj sekundę.
  5. 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 Consumer consumer) {
        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.

Obraz przedstawiający sterowanie oświetleniem i termostatem
Rysunek 6. Sterowanie oświetleniem i termostatami.