Sterowanie urządzeniami zewnętrznymi

W Androidzie 11 i nowszych wersjach funkcja Szybki dostęp do elementów sterujących urządzeniami umożliwia użytkownikowi szybkie wyświetlanie i sterowanie urządzeniami zewnętrznymi, takimi jak oświetlenie, termostaty i kamery, za pomocą afordancji użytkownika w trzech interakcjach z poziomu domyślnego programu uruchamiającego. Producent urządzenia wybiera, jakiego programu uruchamiającego używa. Agregatory urządzeń, np. Google Home, i aplikacje dostawców zewnętrznych mogą udostępniać urządzenia do wyświetlania w tym miejscu. Na tej stronie dowiesz się, jak wyświetlać elementy sterujące urządzeniami w tym miejscu i jak połączyć je z aplikacją sterującą.

Rysunek 1. Miejsce na elementy sterujące urządzeniami w interfejsie Androida.

Aby dodać tę obsługę, utwórz i zadeklaruj ControlsProviderService. Utwórz elementy sterujące obsługiwane przez aplikację na podstawie predefiniowanych typów elementów sterujących, a następnie utwórz wydawców tych elementów.

Interfejs użytkownika

Urządzenia są wyświetlane w sekcji Elementy sterujące urządzeniami jako widżety oparte na szablonach. Dostępnych jest 5 widżetów elementów sterujących urządzeniami, jak pokazano na ilustracji poniżej:

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

Naciśnięcie i przytrzymanie widżetu powoduje przejście do aplikacji, w której można dokładniej sterować urządzeniem. Możesz dostosować ikonę i kolor każdego widżetu, ale aby zapewnić jak najlepsze wrażenia użytkownika, 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. Otwarty 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ądzeniami, 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 Reactive Streams na GitHubie i implementacją w interfejsach Java 9 Flow. Interfejs API jest oparty na tych koncepcjach:

  • Wydawca: Twoja aplikacja jest wydawcą.
  • Subskrybent: interfejs systemu jest subskrybentem i może poprosić wydawcę o określoną liczbę elementów sterujących.
  • Subskrypcja: okres, w którym wydawca może wysyłać aktualizacje do interfejsu systemu. To okno może zamknąć wydawca lub subskrybent.

Deklarowanie usługi

Twoja aplikacja musi zadeklarować usługę, np. MyCustomControlService, w swoim manifeście aplikacji.

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

Potrzebujesz też label, który będzie wyświetlany w elementach sterujących w interfejsie systemu.

Ten przykład 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 rozszerz go o ControlsProviderService():

Kotlin

    class MyCustomControlService : ControlsProviderService() {
        ...
    }
    

Java

    public class MyCustomJavaControlService extends ControlsProviderService {
        ...
    }
    

Wybieranie prawidłowego typu elementu sterującego

Interfejs API udostępnia metody tworzenia elementów sterujących. Aby wypełnić narzędzie do tworzenia, 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, które reprezentuje element sterujący. Klasa DeviceTypes to wyliczenie wszystkich obsługiwanych urządzeń. Typ jest używany do określania ikon i kolorów urządzenia w interfejsie.
  2. Określ nazwę widoczną dla użytkownika, lokalizację urządzenia (np. kuchnia) i inne elementy tekstowe interfejsu powiązane z elementem sterującym.
  3. Wybierz najlepszy szablon do obsługi interakcji z użytkownikiem. Do elementów sterujących przypisywany jest ControlTemplate z aplikacji. Ten szablon bezpośrednio pokazuje użytkownikowi stan elementu sterującego oraz dostępne metody wprowadzania danych, czyli ControlAction. W tabeli poniżej znajdziesz niektóre dostępne szablony i obsługiwane przez nie działania:
Szablon Działanie Opis
ControlTemplate.getNoTemplateObject() None Aplikacja może używać tego szablonu do przekazywania informacji o elemencie sterującym, ale użytkownik nie może z nim wchodzić w interakcję.
ToggleTemplate BooleanAction Reprezentuje element sterujący, który można przełączać między stanami włączonym i wyłączonym. Obiekt BooleanAction zawiera pole, które zmienia się aby reprezentować żądany nowy stan, gdy użytkownik kliknie element sterujący.
RangeTemplate FloatAction Reprezentuje widżet suwaka z określonymi wartościami minimalną, maksymalną i krokową. Gdy użytkownik wejdzie w interakcję z suwakiem, wyślij do aplikacji nowy FloatAction obiekt ze zaktualizowaną wartością.
ToggleRangeTemplate BooleanAction, FloatAction Ten szablon jest połączeniem ToggleTemplate i RangeTemplate. Obsługuje zdarzenia dotykowe oraz suwak, np. do sterowania ściemnianym oświetleniem.
TemperatureControlTemplate ModeAction, BooleanAction, FloatAction Oprócz hermetyzacji poprzednich działań ten szablon umożliwia użytkownikowi ustawienie trybu, np. ogrzewania, chłodzenia, ogrzewania/chłodzenia, eko lub wyłączenia.
StatelessTemplate CommandAction Służy do wskazywania elementu sterującego, który zapewnia obsługę dotyku, ale którego stanu nie można określić, np. pilota do telewizora na podczerwień. Za pomocą tego szablonu możesz zdefiniować procedurę lub makro, czyli agregację zmian stanu i elementów sterujących.

Na podstawie tych informacji możesz utworzyć element sterujący:

Aby na przykład sterować inteligentnym oświetleniem i termostatem, dodaj te stałe do 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 elementów sterujących

Po utworzeniu elementu sterującego potrzebny jest wydawca. Wydawca informuje interfejs systemu o istnieniu elementu sterującego. Klasa ControlsProviderService ma 2 metody publikowania, które musisz zastąpić w kodzie aplikacji:

  • createPublisherForAllAvailable(): tworzy Publisher dla wszystkich elementów sterujących dostępnych w aplikacji. Aby utworzyć obiekty Control dla tego wydawcy, użyj Control.StatelessBuilder().
  • createPublisherFor(): tworzy Publisher dla listy podanych elementów sterujących, zidentyfikowanych za pomocą ich identyfikatorów w postaci ciągów znaków. Aby utworzyć te obiekty 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 każdego elementu. Pobieranie stanu może być czasochłonną operacją obejmującą wiele przeskoków w sieci dostawcy urządzenia. Aby poinformować system o dostępnych elementach sterujących, użyj metody createPublisherForAllAvailable(). Ta metoda używa klasy narzędzia do tworzenia 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 sterujące.

Aby utworzyć ControlsProviderService za pomocą współprogramów Kotlin, dodaj nową zależność do build.gradle:

Dynamiczny

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 ten fragment kodu do Service, 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ń palcem w dół menu systemu i znajdź przycisk Elementy sterujące urządzeniami widoczny na rysunku 4:

Obraz przedstawiający interfejs systemu do sterowania urządzeniami
Rysunek 4. Elementy sterujące urządzeniami w menu systemu.

Kliknięcie Elementy sterujące urządzeniami powoduje przejście do drugiego ekranu, na którym możesz wybrać swoją aplikację. Po wybraniu aplikacji zobaczysz, jak poprzedni fragment kodu tworzy niestandardowe menu systemu z nowymi elementami sterującymi, jak pokazano na rysunku 5:

Obraz przedstawiający menu systemowe z elementami sterującymi oświetleniem i termostatem
Rysunek 5. Elementy sterujące oświetleniem i termostatem do dodania.

Teraz zaimplementuj metodę createPublisherFor(), dodając do Service ten fragment kodu:

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ć Twoja aplikacja: komunikować się z urządzeniem, aby pobrać jego stan, i przekazywać ten stan do systemu.

Metoda createPublisherFor() używa współprogramów i przepływów Kotlin, aby spełnić wymagania interfejsu Reactive Streams API, wykonując te czynności:

  1. Tworzy Flow.
  2. Czeka sekundę.
  3. Tworzy i emituje stan inteligentnego oświetlenia.
  4. Czeka kolejną sekundę.
  5. Tworzy i emituje stan termostatu.

Obsługa działań

Metoda performControlAction() sygnalizuje, kiedy użytkownik wchodzi w interakcję z opublikowanym elementem sterującym. Typ wysłanego ControlAction określa działanie. 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 ten fragment kodu:

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 elementy sterujące oświetleniem i termostatem.

Obraz przedstawiający sterowanie oświetleniem i termostatem
Rysunek 6. Elementy sterujące oświetleniem i termostatem.