Sterowanie urządzeniami zewnętrznymi

W Androidzie 11 i nowszych funkcja Szybkie sterowanie urządzeniami umożliwia użytkownikowi szybkie wyświetlanie i sterowanie urządzeniami zewnętrznymi, takimi jak światła, termostaty i kamery, w ramach 3 interakcji z Menu uruchamiania. Producent OEM urządzenia wybiera, z jakiego menu korzystać. Agregatory urządzeń, takie jak Google Home, oraz aplikacje innych firm mogą wyświetlać urządzenia na tym ekranie. Na tej stronie dowiesz się, jak wyświetlać elementy sterujące urządzeniami w tej przestrzeni i łączyć je z aplikacją sterującą.

Rysunek 1. Przestrzeń kontrolowania urządzenia w interfejsie Androida.

Aby dodać tę pomoc, utwórz i ogłoś ControlsProviderService. Utwórz ustawienia obsługiwane przez aplikację na podstawie wstępnie zdefiniowanych typów elementów sterujących, a potem utwórz wydawców dla tych ustawień.

Interfejs użytkownika

Urządzenia są wyświetlane w sekcji Sterowanie urządzeniami jako widżety szablonowe. Dostępnych jest 5 widżetów sterowania urządzeniem, jak pokazano na rysunku poniżej:

Przełącz widżet
Przełącz
Przełączanie za pomocą suwaka
Przełącz za pomocą suwaka
Widżet zakresu
Zasięg (nie można go włączyć ani wyłączyć)
Widżet przełącznika bez stanu
Bezstanowy przełącznik
Widget panelu temperatury (zamknięty)
Panel temperatury (zamknięty)
Rysunek 2. Kolekcja widżetów utworzonych według szablonów.

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

Obraz pokazujący widżet panelu temperatury (otwarty)
Rysunek 3. Widżet panelu temperatury otwarty.

Tworzenie usługi

W tej sekcji dowiesz się, jak utworzyć ControlsProviderService. Ta usługa informuje interfejs użytkownika systemu Android, że Twoja aplikacja zawiera elementy sterujące urządzeniem, które muszą być wyświetlane w obszarze Elementy sterujące urządzeniami w interfejsie Androida.

Interfejs API ControlsProviderService zakłada znajomość strumieni reaktywnych, zgodnie z definicją w projekcie Reaktywne strumienie na GitHubie i zaimplementowanej w interfejsach języka Java 9. Interfejs API opiera się na tych koncepcjach:

  • Wydawca: wydawcą jest Twoja aplikacja.
  • 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. Okno to może zamknąć zarówno wydawca, jak i subskrybent.

Oświadczenie o usługach

Aplikacja musi zadeklarować usługę – np. MyCustomControlService – w swoim pliku manifestu aplikacji.

Usługa musi zawierać filtr intencji dla ControlsProviderService. Ten filtr umożliwia aplikacjom dodawanie elementów sterujących do interfejsu użytkownika systemu.

Potrzebujesz też elementu label wyświetlanego w elementach sterujących w interfejsie systemu.

Przykład deklarowania usługi:

<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 ustaw go jako rozszerzenie 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 zapełnić kreator, określ urządzenie, którym chcesz sterować, i sposób interakcji użytkownika z nim. Wykonaj te czynności:

  1. Wybierz typ urządzenia reprezentowanego przez element sterujący. Klasa DeviceTypes to wyliczenie wszystkich obsługiwanych urządzeń. Służy on 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 użytkownikowi stan elementu sterującego oraz dostępne metody 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żywać tego elementu do przekazywania informacji o elementach sterujących, ale użytkownik nie może z nimi wchodzić w interakcje.
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ę zgodnie z żądanym nowym stanem, 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 jest kombinacją szablonów ToggleTemplate i RangeTemplate. Obsługuje zdarzenia dotykowe, a także suwak, na przykład do sterowania oświetleniem z możliwością przyciemniania.
TemperatureControlTemplate ModeAction, BooleanAction, FloatAction Oprócz opakowania poprzednich działań ten szablon umożliwia użytkownikowi wybór trybu, takiego jak ogrzewanie, chłodzenie, ogrzewanie/chłodzenie, tryb eko lub wyłączenie.
StatelessTemplate CommandAction Służy do wskazania elementu sterującego, który umożliwia obsługę dotykiem, ale którego stanu nie można określić, np. pilota do telewizora z podczerwienią. Za pomocą tego szablonu możesz zdefiniować rutynę lub makro, które jest zbiorem zmian stanu i sterowania.

Dzięki tym informacjom 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, potrzebny będzie wydawca. Wydawca informuje interfejs systemu o istnieniu elementu sterującego. Klasa ControlsProviderService udostępnia 2 metody wydawcy, które musisz zastąpić w kodzie aplikacji:

  • createPublisherForAllAvailable(): tworzy obiekt Publisher dla wszystkich elementów sterujących dostępnych w aplikacji. Aby tworzyć obiekty Control dla tego wydawcy, użyj obiektu Control.StatelessBuilder().
  • createPublisherFor(): tworzy element Publisher dla listy określonych elementów sterujących, określonych za pomocą identyfikatorów ciągu znaków. Do utworzenia tych obiektów Control użyj Control.StatefulBuilder, ponieważ wydawca musi przypisać stan do każdego elementu sterującego.

Tworzenie wydawcy

Gdy aplikacja po raz pierwszy publikuje elementy sterujące w interfejsie systemu, nie zna stanu poszczególnych elementów. 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 używa klasy konstruktora 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.

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

Groovy

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 pliku 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<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, jak pokazano na rysunku 4.

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

Po kliknięciu Elementy sterujące urządzeniami pojawi się drugi ekran, na którym możesz wybrać swoją aplikację. Po jej wybraniu zobaczysz, jak poprzedni fragment kodu tworzy niestandardowe menu systemowe z nowymi elementami sterującymi, jak pokazano na rysunku 5:

Obraz przedstawiający menu systemu z kontrolą światła i termostatu
Rysunek 5. Elementy sterujące oświetleniem i termostatem do dodania.

Teraz zaimplementuj metodę createPublisherFor(), dodając do obiektu Service ten kod:

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. Czeka przez 1 sekundę.
  3. Tworzy i wysyła stan inteligentnego oświetlenia.
  4. Czeka jeszcze przez sekundę.
  5. Tworzy i wysyła stan termostatu.

Obsługa działań

Metoda performControlAction() sygnalizuje, że użytkownik wchodzi w interakcję z opublikowanym elementem sterującym. 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 utworzyć przykład, dodaj do pliku Service te elementy:

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 Urządzenia i sprawdź elementy sterujące światłem i termostatem.

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