Harici cihazları kontrol etme

Android 11 ve sonraki sürümlerde Hızlı Erişim Cihaz Kontrolleri özelliği, kullanıcının ışık, termostat ve kamera gibi harici cihazları, varsayılan bir başlatıcıdan üç etkileşimde bulunarak hızla görüntülemesini ve kontrol etmesini sağlar. Cihazın OEM hangi başlatıcıyı kullanacağını seçer. Cihaz toplayıcılar (ör. Google Home) ve üçüncü taraf tedarikçi uygulamaları, bu alanda görüntülenecek cihazlar sağlayabilir. Bu sayfada, cihaz denetimlerinin bu alanda nasıl görüneceği ve kontrol uygulamanıza nasıl bağlanacağı gösterilmektedir.

Şekil 1. Android kullanıcı arayüzündeki cihaz kontrol alanı.

Bu desteği eklemek için bir ControlsProviderService oluşturup beyan edin. Önceden tanımlanmış kontrol türlerine göre uygulamanızın desteklediği denetimleri ve ardından bu denetimler için yayıncılar oluşturun.

Kullanıcı arayüzü

Cihazlar, şablonlu widget'lar olarak Cihaz denetimleri altında gösterilir. Aşağıdaki şekilde gösterildiği gibi beş adet cihaz kontrol widget'ı mevcuttur:

Widget'ı aç/kapat
Aç/Kapat
Kaydırma çubuğu widget'ıyla aç/kapat
Kaydırma çubuğuyla aç/kapat
Aralık widget'ı
Aralık (açılamaz veya kapatılamaz)
Durum bilgisiz açma/kapatma widget'ı
Durum bilgisiz açma/kapatma
Sıcaklık paneli widget'ı (kapalı)
Sıcaklık paneli (kapalı)
Şekil 2. Şablonlu widget koleksiyonu.

Bir widget'a dokunup basılı tuttuğunuzda daha fazla kontrol için uygulamaya yönlendirilirsiniz. Her widget'ın simgesini ve rengini özelleştirebilirsiniz, ancak en iyi kullanıcı deneyimi için varsayılan ayar cihazla eşleşirse varsayılan simge ve rengi kullanın.

Sıcaklık paneli widget'ını gösteren resim (açık)
Şekil 3. Sıcaklık paneli widget'ını aç.

Hizmeti oluşturma

Bu bölümde, ControlsProviderService'in nasıl oluşturulacağı gösterilmektedir. Bu hizmet, Android sistem kullanıcı arayüzüne, uygulamanızın Android kullanıcı arayüzünün Cihaz denetimleri alanında gösterilmesi gereken cihaz denetimleri içerdiğini bildirir.

ControlsProviderService API, Reaktif Akışlar GitHub projesinde tanımlandığı ve Java 9 Akış arayüzlerinde uygulanan reaktif akışlar hakkında bilgi sahibi olduğunu varsayar. API, aşağıdaki kavramlar çerçevesinde derlenmiştir:

  • Yayıncı: Uygulamanız yayıncıdır.
  • Abone: Sistem kullanıcı arayüzü, abonedir ve yayıncıdan çeşitli kontroller isteyebilir.
  • Abonelik: Yayıncının Sistem kullanıcı arayüzüne güncelleme gönderebileceği zaman aralığı. Yayıncı veya abone bu pencereyi kapatabilir.

Hizmeti beyan etme

Uygulamanız, uygulama manifest dosyasında MyCustomControlService gibi bir hizmeti beyan etmelidir.

Hizmet, ControlsProviderService için bir intent filtresi içermelidir. Bu filtre, uygulamaların sistem kullanıcı arayüzüne kontrol katkısında bulunmasını sağlar.

Ayrıca, sistem kullanıcı arayüzündeki kontrollerde görüntülenen bir label öğesine ihtiyacınız vardır.

Aşağıdaki örnekte bir hizmetin nasıl beyan edileceği gösterilmektedir:

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

Ardından MyCustomControlService.kt adlı yeni bir Kotlin dosyası oluşturun ve bu dosyayı ControlsProviderService() uzantısına getirin:

Kotlin

    class MyCustomControlService : ControlsProviderService() {
        ...
    }
    

Java

    public class MyCustomJavaControlService extends ControlsProviderService {
        ...
    }
    

Doğru kontrol türünü seçme

API, denetimleri oluşturmak için oluşturucu yöntemleri sağlar. Oluşturucuyu doldurmak için, kontrol etmek istediğiniz cihazı ve kullanıcının bu cihazla nasıl etkileşimde bulunacağını belirleyin. Aşağıdaki adımları uygulayın:

  1. Kontrolün temsil ettiği cihaz türünü seçin. DeviceTypes sınıfı, desteklenen tüm cihazların sıralamasıdır. Bu tür, kullanıcı arayüzünde cihazın simgelerinin ve renklerinin belirlenmesi için kullanılır.
  2. Kullanıcıya görünen adı, cihazın konumunu (ör. mutfak) ve kontrolle ilişkili diğer kullanıcı arayüzü metin öğelerini belirleyin.
  3. Kullanıcı etkileşimini destekleyecek en iyi şablonu seçin. Kontrollere uygulamadan ControlTemplate atanır. Bu şablon, kontrol durumunu ve kullanılabilir giriş yöntemlerini (yani ControlAction) doğrudan kullanıcıya gösterir. Aşağıdaki tabloda, kullanılabilir şablonlardan bazıları ve destekledikleri işlemler özetlenmiştir:
Şablon İşlem Description
ControlTemplate.getNoTemplateObject() None Uygulama, kontrol hakkında bilgi aktarmak için bunu kullanabilir, ancak kullanıcı etkileşimde bulunamaz.
ToggleTemplate BooleanAction Etkin ve devre dışı durumları arasında geçiş yapılabilen bir kontrolü temsil eder. BooleanAction nesnesi, kullanıcı kontrole dokunduğunda istenen yeni durumu temsil edecek şekilde değişen bir alan içerir.
RangeTemplate FloatAction Belirtilen minimum, maksimum ve adım değerlerine sahip bir kaydırma çubuğu widget'ını temsil eder. Kullanıcı kaydırma çubuğuyla etkileşimde bulunduğunda uygulamaya güncellenen değerle yeni bir FloatAction nesnesi gönderin.
ToggleRangeTemplate BooleanAction, FloatAction Bu şablon, ToggleTemplate ve RangeTemplate öğelerinin kombinasyonudur. Dokunma etkinliklerini ve kısılabilir ışıkları kontrol etme gibi bir kaydırma çubuğunu destekler.
TemperatureControlTemplate ModeAction, BooleanAction, FloatAction Bu şablon, önceki işlemleri kapsamanın yanı sıra kullanıcının ısıtma, soğutma, ısıtma/soğutma, eko veya kapalı gibi bir mod ayarlamasına da olanak tanır.
StatelessTemplate CommandAction Kızılötesi televizyon uzaktan kumandası gibi, dokunma özelliği sağlayan ancak durumu belirlenemeyen bir kontrolü belirtmek için kullanılır. Kontrol ve durum değişikliklerinin toplamı olan bir rutin veya makro tanımlamak için bu şablonu kullanabilirsiniz.

Bu bilgileri kullanarak aşağıdakileri kontrol edebilirsiniz:

Örneğin, akıllı ampulü ve termostatı kontrol etmek için MyCustomControlService cihazınıza aşağıdaki sabit değerleri ekleyin:

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

Kontroller için yayıncı oluşturma

Kontrolü oluşturmanızın ardından kontrolün bir yayıncısı olması gerekir. Yayıncı, sistem kullanıcı arayüzüne denetimin varlığı hakkında bilgi verir. ControlsProviderService sınıfı, uygulama kodunuzda geçersiz kılmanız gereken iki yayıncı yöntemi içerir:

  • createPublisherForAllAvailable(): uygulamanızda kullanılabilen tüm denetimler için bir Publisher oluşturur. Bu yayıncı için Control nesneleri oluşturmak için Control.StatelessBuilder() kullanın.
  • createPublisherFor(): Dize tanımlayıcılarıyla tanımlanan kontrollerin listesi için bir Publisher oluşturur. Yayıncının her kontrole bir durum ataması gerektiğinden bu Control nesnelerini oluşturmak için Control.StatefulBuilder öğesini kullanın.

Yayıncıyı oluşturma

Uygulamanız sistem kullanıcı arayüzünde kontrolleri ilk kez yayınladığında her kontrolün durumunu bilmez. Durum bilgisini almak, cihaz sağlayıcısının ağında çok sayıda atlama içeren zaman alan bir işlem olabilir. Mevcut denetimleri sisteme tanıtmak için createPublisherForAllAvailable() yöntemini kullanın. Her bir kontrolün durumu bilinmediğinden bu yöntem Control.StatelessBuilder oluşturucu sınıfını kullanır.

Kontroller Android kullanıcı arayüzünde göründükten sonra , kullanıcı favori kontrolleri seçebilir.

Kotlin eş yordamlarını kullanarak ControlsProviderService oluşturmak için build.gradle cihazınıza yeni bir bağımlılık ekleyin:

Modern

dependencies {
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-jdk9:1.6.4"
}

Kotlin

dependencies {
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-jdk9:1.6.4")
}

Gradle dosyalarınızı senkronize ettikten sonra createPublisherForAllAvailable() uygulamasını uygulamak için Service öğenize aşağıdaki snippet'i ekleyin:

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

Sistem menüsünü aşağı kaydırın ve şekil 4'te gösterilen Cihaz denetimleri düğmesini bulun:

Cihaz denetimleri için sistem kullanıcı arayüzünü gösteren resim
Şekil 4. Sistem menüsündeki cihaz denetimleri.

Cihaz kontrolleri'ne dokunduğunuzda, uygulamanızı seçebileceğiniz ikinci bir ekrana yönlendirilirsiniz. Uygulamanızı seçtikten sonra, önceki snippet'in yeni kontrollerinizi gösteren özel bir sistem menüsünü nasıl oluşturduğunu Şekil 5'te gösterilmiştir:

Işık ve termostat kontrolünü içeren sistem menüsünü gösteren resim
Şekil 5. Eklenecek ışık ve termostat kontrolleri.

Şimdi aşağıdaki kodu Service öğenize ekleyerek createPublisherFor() yöntemini uygulayın:

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();
    }
    

Bu örnekte createPublisherFor() yöntemi, uygulamanızın yapması gereken şeyi şu şekilde sahte bir uygulama içerir: durumunu almak için cihazınızla iletişim kurma ve bu durumu sisteme bildirme.

createPublisherFor() yöntemi, Kotlin eş yordamlarını ve akışlarını kullanarak gerekli Reaktif Streams API'yi karşılamak için aşağıdakileri yapar:

  1. Flow oluşturur.
  2. Bir saniye bekler.
  3. Akıllı ışığın durumunu oluşturur ve yayar.
  4. Bir saniye daha bekler.
  5. Termostatın durumunu oluşturur ve yayınlar.

İşlem yapma

performControlAction() yöntemi, kullanıcı yayınlanan bir kontrolle etkileşimde bulunduğunda sinyal gönderir. Gönderilen ControlAction türü işlemi belirler. Verilen kontrol için uygun işlemi gerçekleştirin ve ardından Android kullanıcı arayüzünde cihazın durumunu güncelleyin.

Örneği tamamlamak için aşağıdaki kodu Service öğenize ekleyin:

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());
        }
    }
    

Uygulamayı çalıştırın, Cihaz kontrolleri menüsüne erişin, ışık ve termostat kontrollerinizi görün.

Işık ve termostat kontrolünü gösteren resim
Şekil 6. Işık ve termostat kontrolleri.