Harici cihazları kontrol etme

Android 11 ve sonraki sürümlerde Hızlı Erişim Cihaz Kontrolleri özelliği, kullanıcının varsayılan bir başlatıcıdan üç etkileşim içinde ışıklar, termostatlar ve kameralar gibi harici cihazları, varsayılan bir başlatıcıdan üç etkileşim içinde hızlıca görüntüleyip kontrol etmesini sağlar. Kullanacağı başlatıcıyı cihazın OEM'si seçer. Google Home gibi cihaz toplayıcılar ve üçüncü taraf tedarikçi uygulamaları, bu alanda görüntülenecek cihazlar sağlayabilir. Bu sayfada, bu alanda cihaz kontrollerini nasıl göstereceğiniz ve kontrol uygulamanıza nasıl bağlayacağınız 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, ardından bu denetimler için yayıncılar oluşturun.

Kullanıcı arayüzü

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

Widget'ı aç/kapat
Aç/Kapat
Kaydırma çubuğu widget'ıyla aç/kapat
Kaydırma çubuğuyla değiştir
Aralık widget'ı
Aralık (etkinleştirilemez 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 ayrıntılı kontrol için ilgili 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şiyorsa varsayılan simgeyi 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 oluşturma işleminin nasıl yapılacağı gösterilmektedir. Bu hizmet, Android sistem kullanıcı arayüzüne, uygulamanızın, Android kullanıcı arayüzünün Cihaz kontrolleri alanında gösterilmesi gereken cihaz denetimleri içerdiğini bildirir.

ControlsProviderService API, Reaktif Akışlar GitHub projesinde tanımlanan ve Java 9 Akış arayüzlerinde uygulanan reaktif akışlarla ilgili bilgi sahibi olduğunu varsayar. API aşağıdaki kavramlar temel alınarak oluşturulmuştur:

  • 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ın uygulama manifest dosyasında MyCustomControlService gibi bir hizmet beyan etmesi gerekir.

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 denetimlerde görüntülenen bir label öğesine de ihtiyacınız vardır.

Aşağıdaki örnekte bir hizmetin nasıl tanımlanacağı 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>

Sonra, MyCustomControlService.kt adında yeni bir Kotlin dosyası oluşturun ve ControlsProviderService() dosyasını genişletin.

Kotlin

    class MyCustomControlService : ControlsProviderService() {
        ...
    }
    

Java

    public class MyCustomJavaControlService extends ControlsProviderService {
        ...
    }
    

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

API, kontrolleri oluşturmak için oluşturucu yöntemleri sunar. Geliştiriciyi 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 bir listesidir. Tür, kullanıcı arayüzündeki cihazın simge ve renklerini belirlemek için kullanılır.
  2. Kullanıcının göreceği adı, cihaz konumunu (ör. mutfak) ve denetimle ilişkili diğer kullanıcı arayüzü metin öğelerini belirleyin.
  3. Kullanıcı etkileşimini desteklemek için en iyi şablonu seçin. Kontrollere uygulamadan bir ControlTemplate atanır. Bu şablon, kontrol durumunu ve kullanılabilir giriş yöntemlerini (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 Açıklama
ControlTemplate.getNoTemplateObject() None Uygulama, denetim hakkında bilgi aktarmak için bunu kullanabilir ancak kullanıcı denetimle etkileşimde bulunamaz.
ToggleTemplate BooleanAction Etkin ve devre dışı durumlar arasında geçiş yapabilen 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 min, maks. ve adım değerlerine sahip bir kaydırma çubuğu widget'ını gösterir. Kullanıcı, kaydırma çubuğuyla etkileşimde bulunduğunda uygulamaya güncellenmiş değerle yeni bir FloatAction nesnesi gönderin.
ToggleRangeTemplate BooleanAction, FloatAction Bu şablon, ToggleTemplate ve RangeTemplate öğelerinin bir kombinasyonudur. Dokunma etkinliklerini ve kaydırma çubuğunu (ör. kısılabilir ışıkları kontrol etme) 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 Dokunma yeteneği sağlayan ancak durumu belirlenemeyen bir kontrolü (ör. IR televizyon uzaktan kumandası) belirtmek için kullanılır. Bu şablonu kullanarak kontrol ve durum değişikliklerinin toplamı olan bir rutin veya makro tanımlayabilirsiniz.

Bu bilgileri kullanarak denetimi oluşturabilirsiniz:

Ö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ılar oluşturma

Kontrolü oluşturduktan sonra bir yayıncıya ihtiyacı vardır. Yayıncı, kontrolün mevcut olduğunu sistem kullanıcı arayüzüne bildirir. ControlsProviderService sınıfında, uygulama kodunuzda geçersiz kılmanız gereken iki yayıncı yöntemi bulunur:

  • createPublisherForAllAvailable(): Uygulamanızda kullanılabilen tüm denetimler için bir Publisher oluşturur. Bu yayıncı için Control nesne oluşturmak amacıyla Control.StatelessBuilder() öğesini kullanın.
  • createPublisherFor(): Belirli bir kontrol listesi için dize tanımlayıcılarıyla tanımlandığı şekilde bir Publisher oluşturur. Yayıncının her denetime bir durum ataması gerektiğinden bu Control nesnelerini oluşturmak için Control.StatefulBuilder kullanın.

Yayıncıyı oluşturma

Uygulamanız, kontrolleri sistem kullanıcı arayüzüne ilk kez yayınladığında, uygulama her kontrolün durumunu bilmez. Durumun belirlenmesi, cihaz sağlayıcısının ağında birçok atlama işlemini gerektiren zaman alıcı 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 öğenize yeni bir bağımlılık ekleyin:

Eski

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() öğesini 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ğı doğru hızlıca kaydırın ve şekil 4'te gösterilen Cihaz kontrolleri 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 gidersiniz. Uygulamanızı seçtikten sonra, önceki snippet'in yeni kontrollerinizi gösteren özel bir sistem menüsünü (Şekil 5'te gösterildiği gibi) nasıl oluşturduğunu görürsünüz:

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

Şimdi, aşağıdakini Service sayfanıza 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ı gerekenin sahte bir uygulamasını içeriyor: durumunu almak için cihazınızla iletişim kurma ve bu durumu sisteme gönderme.

createPublisherFor() yöntemi, aşağıdaki işlemleri yaparak gerekli Reactive Streams API'sini karşılamak için Kotlin eş yordamlarını ve akışlarını kullanır:

  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.

İşlemleri işleme

performControlAction() yöntemi, kullanıcı yayınlanan bir kontrolle etkileşimde bulunduğunda sinyal verir. Gönderilen ControlAction türü, işlemi belirler. Belirtilen 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 Service öğenize aşağıdakileri 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.