এক্সটার্নাল ডিভাইস কন্ট্রোল করা

Android 11 ও তার পরের যেকোনও ভার্সনে, 'দ্রুত অ্যাক্সেস ডিভাইস কন্ট্রোল' ফিচার ব্যবহারকারীদের ডিফল্ট লঞ্চার থেকে তিনটি ইন্টার‍্যাকশনের মধ্যে ব্যবহারকারীর সুবিধা থেকে লাইট, থার্মোস্ট্যাট ও ক্যামেরার মতো এক্সটার্নাল ডিভাইস দ্রুত দেখতে ও কন্ট্রোল করতে দেয়। কোন লঞ্চার ব্যবহার করা হবে তা ডিভাইস OEM বেছে নেয়। ডিভাইস অ্যাগ্রিগেটর—যেমন, Google Home—এবং থার্ড-পার্টি ভেন্ডর অ্যাপ এই স্পেসে দেখানোর জন্য ডিভাইস প্রদান করতে পারে। এই স্পেসে কীভাবে ডিভাইস কন্ট্রোল দেখাতে হয় এবং আপনার কন্ট্রোল অ্যাপের সাথে লিঙ্ক করতে হয়, তা এই পৃষ্ঠা থেকে জানতে পারবেন।

ছবি ১. Android UI-তে ডিভাইস কন্ট্রোল স্পেস।

এই সহায়তা যোগ করতে, ControlsProviderService তৈরি ও ঘোষণা করুন। আগে থেকে নির্ধারিত কন্ট্রোল ধরনের উপর ভিত্তি করে আপনার অ্যাপে কাজ করে এমন কন্ট্রোল তৈরি করুন এবং তারপরে এইসব কন্ট্রোলের জন্য প্রকাশক তৈরি করুন।

ইউজার ইন্টারফেস

টেমপ্লেট উইজেট হিসেবে ডিভাইস কন্ট্রোল বিকল্পের অধীনে ডিভাইস দেখানো হয়। নিচের ছবিতে দেখানো হয়েছে সেই অনুযায়ী, পাঁচটি ডিভাইস কন্ট্রোল উইজেট উপলভ্য:

ডিভাইস কন্ট্রোলের জন্য উইজেট টগল করা
টগল করুন
স্লাইডার উইজেটের সাহায্যে টগল করা
স্লাইডারের সাহায্যে টগল করুন
ডিভাইস কন্ট্রোলের জন্য রেঞ্জ স্লাইডার উইজেট
রেঞ্জ (টগল করে চালু বা বন্ধ করা যাবে না)
স্টেটলেস টগল কন্ট্রোল উইজেট
স্টেটলেস টগল
তাপমাত্রা প্যানেল উইজেট বন্ধ অবস্থায় আছে
তাপমাত্রা প্যানেল (বন্ধ)
ছবি ২. টেমপ্লেটযুক্ত উইজেটের সংগ্রহ।

উইজেট স্পর্শ করে ধরে রাখলে, আরও বেশি কন্ট্রোল করার জন্য আপনাকে অ্যাপে নিয়ে যাওয়া হবে। আপনি প্রতিটি উইজেটের আইকন ও রঙ কাস্টমাইজ করতে পারবেন, তবে সেরা ব্যবহারকারীর অভিজ্ঞতা পেতে, ডিফল্ট সেট ডিভাইসের সাথে ম্যাচ করলে ডিফল্ট আইকন ও রঙ ব্যবহার করুন।

খোলা অবস্থায় তাপমাত্রা প্যানেল উইজেট
ছবি ৩. তাপমাত্রা প্যানেল উইজেট খুলুন।

পরিষেবা তৈরি করা

এই বিভাগে কীভাবে ControlsProviderService তৈরি করতে হয় তা দেখানো হয়েছে। এই পরিষেবা Android সিস্টেম UI-কে জানায় যে আপনার অ্যাপে ডিভাইস কন্ট্রোল আছে যা Android UI-এর ডিভাইস কন্ট্রোল এরিয়ায় অবশ্যই দেখাতে হবে।

ControlsProviderService API-এর ক্ষেত্রে ধরে নেওয়া হয় যে ব্যবহারকারী রিঅ্যাক্টিভ স্ট্রিম সম্পর্কে জানেন, কারণ এটি রিঅ্যাক্টিভ স্ট্রিম GitHub প্রোজেক্টে সংজ্ঞায়িত করা হয়েছে এবং Java 9 ফ্লো ইন্টারফেসে প্রয়োগ করা হয়েছে। API নিম্নলিখিত ধারণাগুলির উপর ভিত্তি করে তৈরি করা হয়েছে:

  • প্রকাশক: আপনার আবেদন হল প্রকাশক।
  • সাবস্ক্রাইবার: সিস্টেম UI হল সাবস্ক্রাইবার এবং এটি প্রকাশকের থেকে একাধিক কন্ট্রোল অনুরোধ করতে পারে।
  • সাবস্ক্রিপশন: যে সময়সীমার মধ্যে প্রকাশক সিস্টেম UI-তে আপডেট পাঠাতে পারবেন। প্রকাশক বা সাবস্ক্রাইবার, যেকোনও ব্যক্তি এই উইন্ডো বন্ধ করতে পারবেন।

পরিষেবা ঘোষণা করা

আপনার অ্যাপকে অবশ্যই—যেমন MyCustomControlService— এর অ্যাপ ম্যানিফেস্টে একটি পরিষেবা ঘোষণা করতে হবে।

পরিষেবায় ControlsProviderService-এর জন্য অবশ্যই একটি ইনটেন্ট ফিল্টার থাকতে হবে। এই ফিল্টার, সিস্টেম UI-তে কন্ট্রোল যোগ করার জন্য অ্যাপ্লিকেশনকে সাহায্য করে।

এছাড়াও, সিস্টেম UI-এর কন্ট্রোলে দেখানো হয় এমন একটি label আপনার প্রয়োজন।

নিচের উদাহরণ থেকে কীভাবে পরিষেবা ঘোষণা করতে হয় তা জানুন:

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

এরপরে, MyCustomControlService.kt নামের একটি নতুন Kotlin ফাইল তৈরি করুন এবং এটিকে ControlsProviderService এক্সটেন্ড করুন:

class MyCustomControlService : ControlsProviderService() {
    // ...
}

কন্ট্রোলের সঠিক ধরন বেছে নিন

কন্ট্রোল তৈরি করার জন্য API-তে বিল্ডার পদ্ধতি প্রদান করা হয়। বিল্ডারকে পপুলেট করতে, আপনি যে ডিভাইস কন্ট্রোল করতে চান এবং ব্যবহারকারী কীভাবে সেটির সাথে ইন্টার‍্যাক্ট করেন তা নির্ধারণ করুন। নিচে উল্লেখ করা ধাপগুলি অনুসরণ করুন:

  1. কন্ট্রোলটি যে ধরনের ডিভাইসকে প্রতিনিধিত্ব করে সেটি বেছে নিন। DeviceTypes ক্লাস হল কাজ করে এমন সব ডিভাইসের একটি এনুমেরেশন। UI-তে ডিভাইসের আইকন ও রঙ নির্ধারণ করতে ধরন ব্যবহার করা হয়।
  2. ব্যবহারকারীর কাছে দৃশ্যমান নাম, ডিভাইসের লোকেশন—যেমন, রান্নাঘর—এবং কন্ট্রোলের সাথে যুক্ত অন্যান্য UI টেক্সচুয়াল এলিমেন্ট নির্ধারণ করুন।
  3. ব্যবহারকারীর ইন্টার‍্যাকশনকে সাপোর্ট করার জন্য সেরা টেমপ্লেট বেছে নিন। অ্যাপ্লিকেশন থেকে কন্ট্রোলগুলিকে ControlTemplate অ্যাসাইন করা হয়। এই টেমপ্লেট ব্যবহারকারীকে সরাসরি কন্ট্রোল স্টেট এবং উপলভ্য ইনপুট পদ্ধতি দেখায়—অর্থাৎ, ControlAction। নিচের টেবিলে উপলভ্য কিছু টেমপ্লেট এবং সেগুলিতে কাজ করে এমন অ্যাকশন সম্পর্কে বলা হয়েছে:
টেমপ্লেট অ্যাকশন বিবরণ
ControlTemplate.getNoTemplateObject() None কন্ট্রোল সম্পর্কে তথ্য জানানোর জন্য অ্যাপ্লিকেশন এটি ব্যবহার করতে পারে, কিন্তু ব্যবহারকারী এটির সাথে ইন্টার‍্যাক্ট করতে পারবেন না।
ToggleTemplate BooleanAction এমন একটি কন্ট্রোলকে বোঝায় যা চালু ও বন্ধ করা যায় স্টেটের মধ্যে পরিবর্তন করা যায়। ব্যবহারকারী কন্ট্রোলে ট্যাপ করলে, BooleanAction অবজেক্টে এমন একটি ফিল্ড থাকে যা পরিবর্তিত হয়ে অনুরোধ করা নতুন স্টেটকে উপস্থাপন করে।
RangeTemplate FloatAction নির্দিষ্ট ন্যূনতম, সর্বাধিক ও স্টেপ ভ্যালু সহ স্লাইডার উইজেটকে বোঝায়। ব্যবহারকারী স্লাইডারের সাথে ইন্টার‍্যাক্ট করলে, আপডেট করা ভ্যালু সহ একটি নতুন FloatAction অবজেক্ট অ্যাপ্লিকেশনে ফেরত পাঠান।
ToggleRangeTemplate BooleanAction, FloatAction এই টেমপ্লেটটি ToggleTemplate ও RangeTemplate-এর কম্বিনেশন। এটি টাচ ইভেন্ট এবং স্লাইডার, যেমন, ডিম করা যায় এমন লাইট কন্ট্রোল করার সুবিধা দেয়।
TemperatureControlTemplate ModeAction, BooleanAction, FloatAction আগের অ্যাকশনগুলিকে এনক্যাপসুলেট করা ছাড়াও, এই টেমপ্লেট ব্যবহারকারীকে হিট, কুল, হিট/কুল, ইকো বা বন্ধ করার মতো মোড সেট করতে দেয়।
StatelessTemplate CommandAction এমন কন্ট্রোল বোঝাতে ব্যবহার করা হয় যা স্পর্শ করার সুবিধা দেয়, কিন্তু যার স্টেট নির্ধারণ করা যায় না, যেমন IR টেলিভিশন রিমোট। আপনি এই টেমপ্লেট ব্যবহার করে রুটিন বা ম্যাক্রো নির্ধারণ করতে পারবেন, যা কন্ট্রোল ও স্টেট পরিবর্তনের সমষ্টি।

এই তথ্যের সাহায্যে আপনি কন্ট্রোল তৈরি করতে পারবেন:

  • কন্ট্রোলের স্টেট অজানা হলে, Control.StatelessBuilder বিল্ডার ক্লাস ব্যবহার করুন।
  • কন্ট্রোলের স্টেট জানা থাকলে, Control.StatefulBuilder বিল্ডার ক্লাস ব্যবহার করুন।

যেমন, স্মার্ট লাইট বাল্ব ও থার্মোস্ট্যাট কন্ট্রোল করতে, আপনার MyCustomControlService-এ নিম্নলিখিত ধ্রুবক যোগ করুন:

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() {
    // ...
}

কন্ট্রোলের জন্য প্রকাশক তৈরি করা

কন্ট্রোল তৈরি করার পরে, এটির জন্য একজন প্রকাশক প্রয়োজন। কন্ট্রোল আছে বলে প্রকাশক সিস্টেম UI-কে জানায়। ControlsProviderService class -এ দুটি প্রকাশক পদ্ধতি আছে যা আপনার অ্যাপ্লিকেশন কোডে অবশ্যই ওভাররাইড করতে হবে:

  • createPublisherForAllAvailable: আপনার অ্যাপে উপলভ্য সব কন্ট্রোলের জন্য Publisher তৈরি করে। এই প্রকাশকের জন্য Control অবজেক্ট তৈরি করতে Control.StatelessBuilder ব্যবহার করুন।
  • createPublisherFor: প্রদত্ত কন্ট্রোলের তালিকার জন্য Publisher তৈরি করে, যেগুলি তাদের স্ট্রিং শনাক্তকারী দ্বারা শনাক্ত করা হয়। এইসব Control অবজেক্ট তৈরি করতে Control.StatefulBuilder ব্যবহার করুন, কারণ প্রকাশককে প্রতিটি কন্ট্রোলে একটি করে স্টেট অ্যাসাইন করতে হবে।

প্রকাশক তৈরি করা

আপনার অ্যাপ যখন প্রথমবার সিস্টেম UI-তে কন্ট্রোল প্রকাশ করে, তখন অ্যাপটি প্রতিটি কন্ট্রোলের স্ট্যাটাস সম্পর্কে জানে না। ডিভাইস-প্রোভাইডারের নেটওয়ার্কে অনেক হপ ইনভলভ থাকার কারণে, স্টেট পাওয়া একটি সময়সাপেক্ষ অপারেশন হতে পারে। সিস্টেমের কাছে উপলভ্য কন্ট্রোল বিজ্ঞাপন দিতে createPublisherForAllAvailable পদ্ধতি ব্যবহার করুন। এই পদ্ধতিতে Control.StatelessBuilder বিল্ডার ক্লাস ব্যবহার করা হয়, কারণ প্রতিটি কন্ট্রোলের স্টেট অজানা।

Android UI-তে কন্ট্রোল দেখা গেলে, ব্যবহারকারীরা পছন্দের কন্ট্রোল বেছে নিতে পারবেন।

ControlsProviderService তৈরি করতে Kotlin coroutines ব্যবহার করতে, আপনার build.gradle-এ একটি নতুন ডিপেন্ডেন্সি যোগ করুন:

গ্ৰুভি

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

Kotlin

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

আপনার Gradle ফাইল সিঙ্ক করার পরে, Service to implement createPublisherForAllAvailable করতে, নিম্নলিখিত স্নিপেট যোগ করুন:

class MyCustomControlService : ControlsProviderService() {

    override fun createPublisherForAllAvailable(): Flow.Publisher<Control> =
        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<String>): Flow.Publisher<Control> {
        TODO()
    }

    override fun performControlAction(
        controlId: String,
        action: ControlAction,
        consumer: Consumer<Int>,
    ) {
        TODO()
    }
}

সিস্টেম মেনু নিচের দিকে সোয়াইপ করুন এবং ডিভাইস কন্ট্রোল বোতামটি খুঁজে নিন, যা ছবি ৪-এ দেখানো হয়েছে:

ডিভাইস কন্ট্রোলের জন্য সিস্টেম ইউজার ইন্টারফেস
ছবি ৪. সিস্টেম মেনুতে ডিভাইস কন্ট্রোল।

ডিভাইস কন্ট্রোল বিকল্পে ট্যাপ করলে, দ্বিতীয় স্ক্রিনে নেভিগেট করা হয়, যেখানে আপনি নিজের অ্যাপ বেছে নিতে পারবেন। আপনার অ্যাপ বেছে নেওয়ার পরে, আপনি দেখতে পাবেন যে কীভাবে আগের স্নিপেট একটি কাস্টম সিস্টেম মেনু তৈরি করে আপনার নতুন কন্ট্রোল দেখাচ্ছে, যেমনটি ৫ নম্বর ছবিতে দেখানো হয়েছে:

লাইট ও থার্মোস্ট্যাট কন্ট্রোল সহ সিস্টেম মেনু
ছবি ৫. যোগ করার জন্য লাইট ও থার্মোস্ট্যাট কন্ট্রোল।

এখন, createPublisherFor পদ্ধতি প্রয়োগ করুন, আপনার Service-এ নিম্নলিখিতগুলি যোগ করুন:

private val job = SupervisorJob()
private val scope = CoroutineScope(Dispatchers.IO + job)
private val controlFlows = mutableMapOf<String, MutableSharedFlow<Control>>()

private var toggleState = false
private var rangeState = 18f

override fun createPublisherFor(controlIds: List<String>): Flow.Publisher<Control> {
    val flow = MutableSharedFlow<Control>(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 <T> 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()
}

এই উদাহরণে, createPublisherFor পদ্ধতিতে আপনার অ্যাপকে কী করতে হবে তার একটি নকল ইমপ্লিমেন্টেশন আছে: ডিভাইসের স্ট্যাটাস পেতে সেটির সাথে যোগাযোগ করতে হবে এবং সেই স্ট্যাটাস সিস্টেমে পাঠাতে হবে।

createPublisherFor পদ্ধতিতে Kotlin coroutines ও flows ব্যবহার করে নিম্নলিখিত কাজগুলি করে প্রয়োজনীয় Reactive Streams API পূরণ করা হয়:

  1. Flow তৈরি করে।
  2. এক সেকেন্ড অপেক্ষা করে।
  3. স্মার্ট লাইটের অবস্থা তৈরি করে ও তা এমিট করে।
  4. আরও এক সেকেন্ড অপেক্ষা করে।
  5. থার্মোস্ট্যাটের স্টেট তৈরি করে ও তা এমিট করে।

অ্যাকশন ম্যানেজ করা

ব্যবহারকারী কোনও প্রকাশিত কন্ট্রোলের সাথে ইন্টার‍্যাক্ট করলে performControlAction পদ্ধতি সিগন্যাল দেয়। ControlAction-এর ধরন অ্যাকশন নির্ধারণ করে। প্রদত্ত কন্ট্রোলের জন্য উপযুক্ত অ্যাকশন নিন এবং তারপরে Android UI-তে ডিভাইসের স্ট্যাটাস আপডেট করুন।

উদাহরণটি সম্পূর্ণ করতে, আপনার Service-এ নিম্নলিখিতগুলি যোগ করুন:

override fun performControlAction(
    controlId: String,
    action: ControlAction,
    consumer: Consumer<Int>,
) {
    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)
}

অ্যাপ চালান, ডিভাইস কন্ট্রোল মেনু অ্যাক্সেস করুন এবং আপনার লাইট ও থার্মোস্ট্যাট কন্ট্রোল দেখুন।

লাইট ও থার্মোস্ট্যাট ডিসপ্লে করা নিয়ন্ত্রণ করে
ছবি ৬. লাইট ও থার্মোস্ট্যাট কন্ট্রোল।