Harici cihazları kontrol etme

Android 11 ve sonraki sürümlerdeki Hızlı Erişim Cihaz Kontrolleri özelliği, kullanıcının varsayılan başlatıcıdan üç etkileşim içinde bir kullanıcı olanağı aracılığıyla ışıklar, termostatlar ve kameralar gibi harici cihazları hızlı bir şekilde görüntülemesine ve kontrol etmesine olanak tanır. Cihaz OEM'leri, hangi başlatıcıyı kullanacaklarını seçer. Cihaz toplayıcılar (ör. Google Home) ve üçüncü taraf tedarikçi uygulamaları, bu alanda gösterilecek cihazlar sağlayabilir. Bu sayfada, cihaz kontrollerini bu alanda nasıl göstereceğiniz ve kontrol uygulamanıza nasıl bağlayacağınız açıklanmaktadır.

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

Bu desteği eklemek için ControlsProviderService oluşturup bildirin. Uygulamanızın desteklediği kontrolleri önceden tanımlanmış kontrol türlerine göre oluşturun ve ardından bu kontroller 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çma/kapatma
Açma/kapatma
Kaydırma çubuğu widget'ı ile açma/kapatma
Kaydırma çubuğuyla açma/kapatma
Aralık widget'ı
Aralık (açılıp kapatılamaz)
Durumsuz açma/kapatma widget'ı
Durum bilgisiz açma/kapatma düğmesi
Sıcaklık paneli widget'ı (kapalı)
Sıcaklık paneli (kapalı)
Şekil 2. Şablonlu widget'lar koleksiyonu.

Bir widget'a dokunup basılı tuttuğunuzda daha ayrıntılı kontrol için uygulamaya yönlendirilirsiniz. Her widget'ta simgeyi ve rengi özelleştirebilirsiniz. Ancak en iyi kullanıcı deneyimi için varsayılan küme cihaza uyuyorsa varsayılan simgeyi ve rengi kullanın.

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

Hizmeti oluşturun

Bu bölümde, ControlsProviderService oluşturma işlemi 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 kontrolleri içerdiğini bildirir.

ControlsProviderService API, Reactive Streams GitHub projesinde tanımlandığı ve Java 9 Flow arayüzlerinde uygulandığı şekliyle reaktif akışlar hakkında bilgi sahibi olduğunuzu varsayar. API aşağıdaki kavramlar üzerine kurulmuş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ığı. Bu pencereyi yayıncı veya abone kapatabilir.

Hizmeti beyan etme

Uygulamanız, uygulama manifestinde MyCustomControlService gibi bir hizmet bildirmelidir.

Hizmet, ControlsProviderService için bir amaç filtresi içermelidir. Bu filtre, uygulamaların sistem kullanıcı arayüzüne kontroller eklemesine olanak tanır.

Ayrıca, sistem kullanıcı arayüzündeki kontrollerde gösterilen bir label de gerekir.

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 ControlsProviderService()'i genişletin:

Kotlin

    class MyCustomControlService : ControlsProviderService() {
        ...
    }
    

Java

    public class MyCustomJavaControlService extends ControlsProviderService {
        ...
    }
    

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

API, kontrolleri oluşturmak için oluşturucu yöntemler sağlar. Oluşturucuyu doldurmak için kontrol etmek istediğiniz cihazı ve kullanıcının bu cihazla nasıl etkileşimde bulunduğunu 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 numaralandırılmış listesidir. Tür, kullanıcı arayüzünde cihazın simgelerini ve renklerini belirlemek için kullanılır.
  2. Kullanıcıya gösterilen adı, cihaz konumunu (ör. mutfak) ve kontrolle 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 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, mevcut şablonlardan bazıları ve destekledikleri işlemler özetlenmektedir:
Şablon İşlem Açıklama
ControlTemplate.getNoTemplateObject() None Uygulama, kontrolle ilgili bilgileri aktarmak için bunu kullanabilir ancak kullanıcı bununla etkileşimde bulunamaz.
ToggleTemplate BooleanAction Etkin ve devre dışı durumları arasında geçiş yapılabilen bir kontrolü temsil eder. BooleanAction nesnesi, kullanıcı kontrolü dokunduğunda istenen yeni durumu temsil etmek için 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, güncellenmiş değerle birlikte uygulamaya yeni bir FloatAction nesnesi gönderin.
ToggleRangeTemplate BooleanAction, FloatAction Bu şablon, ToggleTemplate ve RangeTemplate kombinasyonudur. Dokunma etkinliklerinin yanı sıra, kısılabilir ışıkları kontrol etmek için kullanılan kaydırma çubuğu gibi öğeleri de destekler.
TemperatureControlTemplate ModeAction, BooleanAction, FloatAction Bu şablon, yukarıdaki 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 özelliği sağlayan ancak durumu belirlenemeyen bir kontrolü (ör. kızılötesi televizyon uzaktan kumandası) belirtmek için kullanılır. Bu şablonu, kontrol ve durum değişikliklerinin birleştirilmesiyle oluşan bir rutin veya makro tanımlamak için kullanabilirsiniz.

Bu bilgilerle şu kontrolü oluşturabilirsiniz:

Örneğin, akıllı bir ampulü ve termostatı kontrol etmek için MyCustomControlService öğenize aşağıdaki sabitleri 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şturduktan sonra bir yayıncıya ihtiyacı vardır. Yayıncı, sistem kullanıcı arayüzünü kontrolün varlığı hakkında bilgilendirir. ControlsProviderService sınıfında, uygulama kodunuzda geçersiz kılmanız gereken iki yayıncı yöntemi vardır:

  • createPublisherForAllAvailable(): Uygulamanızda bulunan tüm kontroller için Publisher oluşturur. Bu yayıncı için Control nesneleri oluşturmak üzere Control.StatelessBuilder() kullanın.
  • createPublisherFor(): Dize tanımlayıcılarıyla tanımlanan belirli kontrollerin listesi için Publisher oluşturur. Yayıncı her denetime bir durum atamak zorunda olduğundan bu Control.StatefulBuilder nesneleri oluşturmak için Control kullanın.

Yayıncıyı oluşturma

Uygulamanız, kontrolleri ilk kez sistem kullanıcı arayüzünde yayınladığında her kontrolün durumunu bilmez. Durumu almak, cihaz sağlayıcının ağında birçok atlama içeren zaman alıcı bir işlem olabilir. Sistemde kullanılabilen denetimleri duyurmak için createPublisherForAllAvailable() yöntemini kullanın. Bu yöntemde, her kontrolün durumu bilinmediğinden Control.StatelessBuilder oluşturucu sınıfı kullanılır.

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

ControlsProviderService oluşturmak için Kotlin eş yordamlarını kullanmak üzere build.gradle öğenize yeni bir bağımlılık ekleyin:

Groovy

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 Service snippet'ini createPublisherForAllAvailable() uygulamak için 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<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);
        }
    }
    

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

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

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

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

Şimdi createPublisherFor() yöntemini uygulayın ve Service öğenize aşağıdakileri ekleyin:

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

Bu örnekte, createPublisherFor() yöntemi, uygulamanızın yapması gereken işlemlerin sahte bir uygulamasını içerir: durumunu almak için cihazınızla iletişim kurmak ve bu durumu sisteme göndermek.

createPublisherFor() yöntemi, aşağıdaki işlemleri yaparak gerekli Reactive Streams API'yi 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ınlanmış bir kontrolle etkileşimde bulunduğunda sinyal gönderir. 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 dosyanıza 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 ve ışık ile termostat kontrollerinizi görün.

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