Sterowanie urządzeniami zewnętrznymi

W Androidzie 11 i nowszych funkcja sterowania urządzeniami z szybkim dostępem umożliwia użytkownikowi szybkie przeglądanie i kontrolowanie urządzeń zewnętrznych, takich jak oświetlenie, termostaty czy kamery, w ramach 3 interakcji z domyślnego programu uruchamiającego. Program uruchamiający określa producent urządzenia. Urządzenia do wyświetlania w tym miejscu mogą udostępniać agregatorzy urządzeń – np. Google Home – i aplikacje innych firm. Z tej strony dowiesz się, jak wyświetlić elementy sterujące urządzeniem w tym obszarze i połączyć je z aplikacją do sterowania.

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

Aby dodać tę obsługę, utwórz i zadeklaruj 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ądzeniami, jak pokazano na 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ć)
Widżet przełączania bezstanowego
Przełącznik bezstanowy
Widżet panelu temperatury (zamknięty)
Panel temperatury (zamknięty)
Rysunek 2. Kolekcja widżetów utworzonych według szablonów.

Kliknięcie i przytrzymanie widżetu powoduje otwarcie aplikacji, która daje Ci 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 przedstawiający widżet panelu temperatury (otwarty)
Rysunek 3. Otwórz widżet panelu temperatury.

Tworzenie usługi

Z tej sekcji dowiesz się, jak utworzyć ControlsProviderService. Ta usługa informuje interfejs systemu Android, że Twoja aplikacja zawiera elementy sterujące, które muszą znajdować się w obszarze Sterowanie 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. Działanie interfejsu API opiera się na następujących koncepcjach:

  • Wydawca: aplikacja jest wydawcą.
  • Subskrybent: interfejs systemu to aplikacja subskrybująca, która może żądać od wydawcy szeregu 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ę – np. MyCustomControlService – w swoim pliku manifestu aplikacji.

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

Potrzebujesz też elementu label wyświetlanego 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 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 zawiera listę 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. kuchnię) i inne elementy tekstowe interfejsu powiązane z elementem sterującym.
  3. Wybrać najlepszy szablon do interakcji z użytkownikami. Elementy sterujące otrzymują właściwość ControlTemplate z aplikacji. Ten szablon bezpośrednio pokazuje użytkownikowi stan kontrolny, a także dostępne metody wprowadzania, czyli ControlAction. W tabeli poniżej opisujemy niektóre dostępne szablony i obsługiwane przez nie działania:
Szablon Działanie Opis
ControlTemplate.getNoTemplateObject() None Aplikacja może użyć tego elementu do przekazania informacji o elemencie sterującym, ale użytkownik nie może wejść z tym elementem w interakcję.
ToggleTemplate BooleanAction Reprezentuje element sterujący, który można przełączać między stanami włączony i wyłączony. Obiekt BooleanAction zawiera pole, które zmienia się zgodnie z żądanym nowym stanem, gdy użytkownik kliknie element sterujący.
RangeTemplate FloatAction Reprezentuje widżet z określonymi wartościami minimalną, maksymalną i liczbą kroków. Gdy użytkownik wejdzie w interakcję z suwakiem, wyślij z powrotem do aplikacji nowy obiekt FloatAction ze zaktualizowaną wartością.
ToggleRangeTemplate BooleanAction, FloatAction Ten szablon jest kombinacją atrybutów ToggleTemplate i RangeTemplate. Obsługuje też zdarzenia dotykowe oraz suwak, np. do sterowania przyciemnianym światłem.
TemperatureControlTemplate ModeAction, BooleanAction, FloatAction Oprócz opisu poprzednich działań ten szablon pozwala użytkownikowi ustawić tryb ogrzewanie, chłodzenie, ogrzewanie/chłodzenie, eko lub wyłączenie.
StatelessTemplate CommandAction Wskazuje element sterujący, który obsługuje dotyk, ale nie można określić jego stanu, np. pilota do telewizora na podczerwień. Przy użyciu tego szablonu możesz zdefiniować rutynę lub makro, które łączą zmiany ustawień i stanu.

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

Żeby na przykład sterować inteligentną żarówką i termostatem, dodaj do elementu 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 ma 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. Użyj Control.StatelessBuilder(), by utworzyć obiekty Control dla tego wydawcy.
  • 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 Twoja aplikacja po raz pierwszy opublikuje elementy sterujące w interfejsie systemu, nie zna stanu każdego z nich. Uzyskanie stanu może być czasochłonne i wymaga wielu przeskoków w sieci dostawcy urządzeń. Użyj metody createPublisherForAllAvailable(), aby reklamować w systemie dostępne elementy sterujące. Ta metoda korzysta z klasy konstruktora Control.StatelessBuilder, ponieważ stan każdego elementu sterującego jest nieznany.

Gdy elementy sterujące pojawią się w interfejsie Androida , użytkownik będzie mógł wybrać ulubione elementy.

Aby użyć współrzędnych Kotlina do utworzenia ControlsProviderService, dodaj nową zależność do 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, 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ń w dół menu systemowe i znajdź przycisk Sterowanie urządzeniami, który widać na rysunku 4:

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

Kliknięcie Sterowanie urządzeniami powoduje przejście do drugiego ekranu, na którym możesz wybrać aplikację. Po wybraniu aplikacji zobaczysz, jak poprzedni fragment tworzy niestandardowe menu systemowe z nowymi elementami sterującymi, tak 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 obiektu Service ten kod:

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 musi robić aplikacja: komunikuje się z urządzeniem, by sprawdzić stan, i wysyła ten stan do systemu.

Metoda createPublisherFor() korzysta z współrzędnych i przepływów Kotlin w celu spełnienia wymagań interfejsu Reactive Streams API. W tym celu:

  1. Tworzy Flow.
  2. Poczekaj 1 sekundę.
  3. Tworzy i emituje stan inteligentnego oświetlenia.
  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 opublikowanym elementem sterującym. Działanie zależy od typu wysłanego pliku ControlAction. Wykonaj odpowiednie działanie dla 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ądzeniami i zobacz ustawienia oświetlenia i termostatu.

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