Sterowanie urządzeniami zewnętrznymi

W Androidzie 11 i nowszych funkcja Szybki dostęp Pozwala szybko przeglądać i sterować urządzeniami zewnętrznymi, takimi jak oświetlenie, termostatów i kamer z oferty użytkownika w ciągu trzech interakcji domyślny program uruchamiający. Program uruchamiający określa producent urządzenia. Agregatory urządzeń, takie jak Google Home, oraz aplikacje innych firm mogą wyświetlać urządzenia na tym ekranie. Z tej strony dowiesz się, jak sterowanie urządzeniami w tym pokoju i połącz je ze swoją aplikacją sterującą.

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

Aby dodać tę pomoc, utwórz i ogłoś ControlsProviderService. Utwórz elementy sterujące, które obsługuje Twoja aplikacja, na podstawie wstępnie zdefiniowanych typów elementów sterujących, a następnie utwórz wydawców dla tych elementów.

Interfejs użytkownika

Urządzenia są wyświetlane w sekcji Sterowanie urządzeniami jako widżety szablonowe. Pięć są dostępne widżety sterowania urządzeniami, które przedstawiono na tej ilustracji:

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

Naciśnięcie i przytrzymanie widżetu powoduje przejście do aplikacji, aby uzyskać większą kontrolę. Dostępne opcje dostosować ikonę i kolor każdego widżetu, ale dla wygody użytkowników użyć domyślnej ikony i koloru, jeśli zestaw domyślny pasuje do urządzenia.

Obraz przedstawiają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 Twoja aplikacja zawiera elementy sterujące urządzenia które trzeba umieścić w obszarze Sterowanie urządzeniami w interfejsie Androida.

Interfejs API ControlsProviderService zakłada znajomość strumieni reaktywnych zdefiniowanych w projekcie Reaktywne strumienie na GitHubie i wdrożonych w interfejsach Java 9 Flow. Działanie interfejsu API opiera się na następujących koncepcjach:

  • Wydawca: aplikacja jest wydawcą.
  • Subskrybent: interfejs użytkownika jest subskrybentem i może prosić wydawcę o różne opcje.
  • Subskrypcja: okres, w którym wydawca może wysyłać aktualizacje do interfejsu systemu. Wydawca lub subskrybent może zamknąć to okno okno.

Deklarowanie usługi

Aplikacja musi deklarować usługę, np. MyCustomControlService, w pliku manifestu aplikacji.

Usługa musi zawierać filtr intencji dla ControlsProviderService. Ten umożliwia aplikacjom współtworzenie elementów sterujących w interfejsie systemu.

Musisz też mieć label wyświetlany w elementach sterujących w interfejsie systemu.

Przykład poniżej pokazuje, 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 przedłuż ControlsProviderService():

Kotlin

    class MyCustomControlService : ControlsProviderService() {
        ...
    }
    

Java

    public class MyCustomJavaControlService extends ControlsProviderService {
        ...
    }
    

Wybierz odpowiedni typ elementu sterującego

Interfejs API udostępnia metody kreatora do tworzenia elementów sterujących. Aby wypełnić pole jak określić urządzenie, którym chcesz sterować, i sposób interakcji użytkownika z nim. Wykonaj te czynności:

  1. Wybierz typ urządzenia, które reprezentuje element sterujący. Klasa DeviceTypes to wyliczenie 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. kuchnia) oraz inne elementy tekstowe interfejsu powiązane z elementem sterującym.
  3. Wybierz najlepszy szablon, który umożliwi interakcję z użytkownikiem. Elementy sterujące są przypisywane do aplikacji ControlTemplate. Ten szablon bezpośrednio pokazuje stan sterowania oraz dostępnych metod wprowadzania, czyli ControlAction W tabeli poniżej znajdziesz kilka dostępnych szablonów i działania, które obsługują:
Szablon Działanie Opis
ControlTemplate.getNoTemplateObject() None Aplikacja może użyć tego parametru, aby przekazać informacje o elemencie sterującym, ale nie może wchodzić z nią w interakcję.
ToggleTemplate BooleanAction Reprezentuje element sterujący, który może być przełączany między stanem włączonym i wyłączonym. Obiekt BooleanAction zawiera pole, które zmienia się będzie reprezentować żądany nowy stan, gdy użytkownik kliknie element sterujący.
RangeTemplate FloatAction Reprezentuje suwak z określonymi wartościami minimalną, maksymalną i kroku. Gdy użytkownik wejdzie w interakcję z suwak, prześlij do aplikacji nowy obiekt FloatAction z zaktualizowaną wartością.
ToggleRangeTemplate BooleanAction, FloatAction Ten szablon stanowi połączenie szablonów ToggleTemplate i RangeTemplate Obsługuje zdarzenia dotyku oraz suwak, np. do sterowania przyciemnianym światłem.
TemperatureControlTemplate ModeAction, BooleanAction, FloatAction Oprócz hermetyzacji poprzednich działań ten szablon umożliwia użytkownik ustawił tryb na ogrzewanie, chłodzenie, ogrzewanie/chłodzenie, eko lub wyłączenie.
StatelessTemplate CommandAction Określa element sterujący, który obsługuje dotyk, ale którego stan których nie da się określić, np. za pomocą pilota do telewizora na podczerwień. Możesz użyć tej szablon do zdefiniowania rutyny lub makra, które stanowią agregację wartości kontrolnych i stanu.

Dzięki tym informacjom możesz utworzyć element sterujący:

Aby na przykład sterować inteligentną żarówką i termostatem, dodaj te elementy: stałe do urządzenia MyCustomControlService:

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ń

Po utworzeniu grupy kontrolnej musisz dodać do niej wydawcę. Wydawca informuje z interfejsem użytkownika z istnieniem elementu sterującego. Zajęcia: ControlsProviderService udostępnia dwie metody wydawcy, które musisz zastąpić w kodzie aplikacji:

  • createPublisherForAllAvailable(): tworzy Publisher dla wszystkich elementów sterujących dostępnych w aplikacji. Użyj Control.StatelessBuilder() aby utworzyć obiekty Control dla tego wydawcy.
  • createPublisherFor(): tworzy Publisher dla listy podanych elementów sterujących, co jest określane przez identyfikatory ciągu znaków. Control.StatefulBuilder – do tych obiektów Control, bo wydawca musi przypisać stan każdego elementu sterującego.

Tworzenie wydawcy

Kiedy aplikacja po raz pierwszy opublikuje elementy sterujące w interfejsie systemu, aplikacja nie wie stan każdego elementu sterującego. Pobieranie stanu może być czasochłonną operacją, która wymaga wielu skoków w sieci dostawcy urządzenia. Użyj metody createPublisherForAllAvailable(), aby poinformować system o dostępnych kontrolkach. Ta metoda korzysta z metody Control.StatelessBuilder klasę konstruktora, ponieważ stan każdego elementu sterującego to nieznane.

Gdy w interfejsie Androida pojawią się elementy sterujące , użytkownik może wybrać ulubione elementów sterujących.

Aby użyć współrzędnych Kotlina do utworzenia elementu ControlsProviderService, dodaj nowy zależność od build.gradle:

Odlotowe

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 Service ten fragment kodu zastosuj 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<String, ReplayProcessor> 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 w rysunek 4.

Obraz przedstawiający interfejs systemu sterowania urządzeniami
Rysunek 4. Sterowanie urządzeniem w menu systemowym.

Kliknij Sterowanie urządzeniami, aby przejść do drugiego ekranu, na którym możesz wybrać do aplikacji. Po wybraniu aplikacji zobaczysz, jak poprzedni fragment tworzy niestandardowe menu systemowe z nowymi elementami sterującymi, jak na ilustracji 5:

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

Teraz zaimplementuj metodę createPublisherFor(), dodając do klasy Service te elementy:

Kotlin

    private val job = SupervisorJob()
    private val scope = CoroutineScope(Dispatchers.IO + job)
    private val controlFlows = mutableMapOf<String, MutableSharedFlow>()
 
    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 musi zrobić aplikacja: komunikować się z urządzeniem, aby pobrać jego stan, i przekazywać ten stan do systemu.

Metoda createPublisherFor() używa coroutines i flows w Kotlinie, aby spełnić wymagania interfejsu Reactive Streams API. Aby to zrobić:

  1. Tworzy Flow.
  2. Poczekaj 1 sekundę.
  3. Tworzy i wysyła inteligentne oświetlenie.
  4. Czekam jeszcze sekundę.
  5. Tworzy i wysyła stan termostatu.

Obsługa działań

Metoda performControlAction() sygnalizuje, kiedy użytkownik wchodzi w interakcję z atrybutem opublikowany element sterujący. Działanie zależy od typu wysłanego pliku ControlAction. Wykonaj odpowiednie działanie w przypadku danego elementu sterującego, a następnie zaktualizuj stan urządzenia w interfejsie Androida.

Aby zakoń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 zobacz, jak świecą się diody sterowanie termostatem.

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