Kok Bisa Icon di Aplikasi Jam Berubah Sesuai Waktunya?

Pada suatu malam, saya sedang menemani istri yang begadang mengerjakan tugasnya. Sambil menemani, saya pun bermain hp, scrolling di berbagai aplikasi media sosial, mencoba untuk update tentang apa yang sedang terjadi di dunia maya.
Saat sedang berpindah-pindah dari aplikasi satu ke lainnya, terbesit di pikiran, jam berapa ya sekarang? Lalu, saat sedang ingin melihat informasi jam yang terletak di pojok kiri layar hp, secara sekilas, saya melihat icon di aplikasi jam yang panahnya menunjukkan pukul 11. Kemudian, saya cek dengan informasi jam yang terletak di pojok kiri layar hp, dan wow, ternyata sesuai.
Apakah itu sebuah kebetulan? Untuk memastikan, saya coba lagi besok paginya. Saya coba buka hp di jam 8, dan benar saja, panah di icon aplikasi jam menunjukkan pukul 8. Saya coba pastikan sekali lagi jam 12 siang, dan sekali lagi, benar, panah icon aplikasi jam menunjukkan pukul 12.
Saya pun bergumam, kok bisa? Sebagai developer, kita tahu kalau icon pada aplikasi itu berbentuk gambar statis dan seharusnya icon tersebut sudah di-compile dengan source code aplikasi menjadi satu, sehingga seharusnya tidak bisa secara programmatically diubah saat runtime. Lalu, bagaimana cara membuat fitur tersebut pada aplikasi Android?
Hmmm, okay, rasa ingin tahu sudah tidak terbendung, saya coba langsung googling tentang hal ini hari itu juga.
Terdapat banyak pendapat bagaimana cara mengimplementasikan fitur tersebut. Setelah sekian bacaan yang saya baca, saya rasa jawaban dari akun Stack Overflow CommonsWare menjawab secara umum bagaimana fitur ini dapat tercapai.
Secara sederhana, dia mengatakan kalau sebenarnya tidak ada cara official dari Android SDK untuk membuat sebuah icon pada aplikasi berubah saat runtime. Kemungkinan besar yang kita lihat hanyalah sebuah widget dari aplikasi jam tersebut atau manufakturer dari hp meng-override OS Android agar dapat mengimplementasikan fitur ini pada aplikasi jam bawaan mereka.
Pendapat ini menurut saya masuk akal. Kita umum sekali melihat hp yang sudah ter-install aplikasi-aplikasi bawaan dari manufakturer hp memiliki kemampuan “khusus”, seperti tidak dapat di-uninstall misalnya 😅. Jika bisa membuat fitur agar tidak bisa di-uninstall, maka sangat mungkin mereka juga membuat cara agar aplikasi mereka dapat mengubah icon secara runtime, bukan?
Lebih lanjut, akun Stack Overflow CommonsWare juga mengatakan bahwa cara alternatif yang ia lihat orang-orang gunakan adalah dengan menggunakan tag <activity-alias> di AndroidManifest.
“Yesss, ada solusi ternyata” adalah perasan saya ketika membacanya 😅. Saya pun langsung bergegas untuk eksplorasi apa itu tag <activity-alias>.
Setelah dibaca-baca, tag<activity-alias> adalah cara kita untuk membuat beberapa entry point ke aplikasi dengan skenario yang berbeda-beda, namun tetap merujuk ke entry point utama.
Katakanlah kita punya sebuah MainActivity yang dijadikan sebagai titik masuk utama aplikasi. Pada layar hp, shortcut dari aplikasi akan terbuat dan tampil menunjukkan nama dan iconnya.
Nah, kita dapat mengubah nama dan icon aplikasi secara langsung (tanpa perlu install ulang aplikasi) dengan cara membuat alias dari MainActivity. Semisal, ketika pengguna baru saja membeli versi pro dari aplikasi, maka kita ingin agar icon aplikasinya berubah juga.
Oleh karena itu, untuk membuatnya, kita dapat menggunakan <activity-alias> untuk membuat Activity baru, misal MainActivityPro yang merujuk ke MainActivity namun dengan modifikasi icon aplikasi.
Agar lebih memahaminya, mari kita lihat potongan kode pada AndroidManifest berikut.
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application
...
android:icon="@mipmap/app_launcher"
android:roundIcon="@mipmap/app_launcher_round">
<activity
...
android:name=".MainActivity">
...
</activity>
<activity-alias
...
android:name=".MainActivityPro"
android:icon="@mipmap/app_pro_launcher"
android:roundIcon="@mipmap/app_pro_launcher_round"
android:targetActivity=".MainActivity">
...
</activity-alias>
</application>
</manifest>
Seperti yang bisa kita lihat, terdapat dua Activity, yaitu MainActivity dan MainActivityPro. Sebenarnya keduanya sama saja, MainActivity adalah entry point utama dari aplikasi, sementara MainActivityPro merujuk ke MainActivity namun dengan meng-override icon aplikasi.
Nah, sedikit peraturan yang harus diterapkan ketika menggunakan tag <activity-alias> adalah sebagai berikut.
- Menggunakan atribut android:enabled, hanya boleh ada satu Activity yang diaktifkan ketika pertama kali aplikasi di-install.
- Semua tag <activity-alias> harus memiliki intent filter yang sama dengan Activity yang dirujuk.
Oleh karena itu, berikut adalah kode keseluruhan di AndroidManifest.
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/app_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/app_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.MyApplication"
tools:targetApi="31">
<activity
android:name=".MainActivity"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity-alias
android:name=".MainActivityPro"
android:enabled="false"
android:exported="true"
android:icon="@mipmap/app_pro_launcher"
android:roundIcon="@mipmap/app_pro_launcher_round"
android:targetActivity=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity-alias>
</application>
</manifest>
Kemudian, langkah selanjutnya adalah mengaktifkan Activity alias secara programmatically. Bagaimana caranya? Perkenalkan setComponentEnabledSetting ✨
setComponentEnabledSetting adalah metode dari PackageManager yang dapat kita gunakan sebagai trigger untuk mengaktifkan / menonaktifkan Activity alias. Dengan ini, kita dapat mengubah icon aplikasi saat runtime 🤗
fun Activity.changeIcon() {
packageManager.setComponentEnabledSetting(
ComponentName(
this,
"$packageName.MainActivity"
),
PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
PackageManager.DONT_KILL_APP
)
packageManager.setComponentEnabledSetting(
ComponentName(
this,
"$packageName.MainActivityPro"
),
PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
PackageManager.DONT_KILL_APP
)
}
Metode setComponentEnabledSetting cukup mudah digunakan. Parameter pertama adalah nama komponen Activity-nya, parameter kedua adalah state dari komponen tersebut, dan parameter ketiga adalah perilaku opsional ketika komponen di-trigger. Pada contoh kali ini, kita buat fungsi yang dapat dipanggil untuk menonaktifkan MainActivity dan setelahnya mengaktifkan komponen MainActivityPro.
Selanjutnya kita tinggal panggil saja fungsi tersebut dari UI.
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyApplicationTheme {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
){
Button(onClick = {this@MainActivity.changeIcon()}) {
Text(text = "Buy Pro version")
}
}
}
}
}
}
Sekarang, waktunya Markicob ~ Mari kita coba 😊

Yeayyy icon aplikasi berhasil berubah dan fungsionalitas aplikasi pun masih tetap berjalan.
Namun, saya paham kalau di sini icon aplikasi perlu di trigger dahulu untuk bisa berubah. Padahal di awal, kita ingin agar bisa membuat icon aplikasi berubah secara otomatis seperti pada icon aplikasi jam, bukan?
Okey, tenang, eksplorasi tidak berhenti sampai sini.
Setelah saya telusuri lagi, ternyata ada cara agar kita bisa mengubahnya secara otomatis. Caranya masih sama, dengan menggunakan metode setComponentEnabledSetting namun dengan menjadwalkan pemanggilan metode tersebut dengan AlarmManager.
Eitsss, sebentar. Biar makin seru, yuk, kita cosplay seolah kita sedang membuat aplikasi jam dengan fitur icon aplikasi yang berubah sesuai jamnya. Di sini, karena saya tidak pintar mengedit gambar, maka saya pakai teks saja ya sebagai pengganti panah di icon jam😅

Sipp! Kembalilah ke Android Studio, buat icon aplikasi dengan gambar-gambar tersebut, dan kita siap untuk mulai mengerjakan!
Pertama, mari kita buat 12 Activity alias dengan icon yang merepresentasikan setiap jamnya di AndroidManifest.
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application
...
android:icon="@mipmap/app_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/app_launcher_round"
tools:targetApi="31">
<activity
android:name=".MainActivity"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
...
<activity-alias
android:name=".MainActivityHour1"
android:enabled="false"
android:exported="true"
android:icon="@mipmap/app_hour1_pro_launcher"
android:roundIcon="@mipmap/app_hour1_pro_launcher_round"
android:targetActivity=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity-alias>
<activity-alias
android:name=".MainActivityHour2"
android:enabled="false"
android:exported="true"
android:icon="@mipmap/app_hour2_pro_launcher"
android:roundIcon="@mipmap/app_hour2_pro_launcher_round"
android:targetActivity=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity-alias>
...
<activity-alias
android:name=".MainActivityHour12"
android:enabled="false"
android:exported="true"
android:icon="@mipmap/app_hour12_pro_launcher"
android:roundIcon="@mipmap/app_hour12_pro_launcher_round"
android:targetActivity=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity-alias>
</application>
</manifest>
Selanjutnya, kita akan menggunakan AlarmManager untuk dapat menjadwalkan pengaktifan setiap Activity alias sesuai jamnya.
AlarmManager akan menjadwalkan tugas untuk mengirim sebuah pesan broadcast ke sistem Android. Pesan broadcast berisi sebuah PendingIntent ke suatu BroadcastReceiver. Setelah menerima pesan, sistem Android akan meneruskan pesan tersebut ke Broadcast Receiver yang dapat menangani pesan tersebut. Oleh karena itu, marilah kita membuat sebuah kelas BroadcastReceiver.
class AlarmReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent?) {
}
}
Kelas AlarmReceiver meng-inherit kelas BroadcastReceiver sehingga wajib meng-override metode onReceive. Metode ini akan kita gunakan untuk menangani aksi ketika pesan broadcast diterima.
Kita berpindah sejenak dari metode onReceive. Karena metode onReceive menerima pesan, maka, marilah kita buat sebuah metode untuk mengirim pesan pada kelas AlarmManager.
class AlarmReceiver : BroadcastReceiver() {
...
fun setChangeAppIconAlarm(context: Context){
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
val calendar = Calendar.getInstance()
val alarmIntent = Intent(context, AlarmReceiver::class.java)
val currentHour = calendar[Calendar.HOUR]
alarmIntent.putExtra(EXTRA_HOUR, currentHour)
val pendingIntent = PendingIntent.getBroadcast(
context,
TASK_ID,
alarmIntent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
)
alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC, calendar.timeInMillis, pendingIntent)
Log.d("AlarmReceiver", "Alarm requested!")
}
companion object {
const val EXTRA_HOUR = "hour"
private const val TASK_ID = 101
}
}
Metode setChangeAppIconAlarm adalah metode yang kita peruntukkan untuk mengirim pesan broadcast. Mula-mula, metode akan mendapatkan jam terkini, lalu menyisipkannya di PendingIntent, dan kemudian menjadwalkan pengiriman pesan broadcast sesuai dengan waktunya menggunakan metode setExactAndAllowWhileIdle dari AlarmManager.
Selanjutnya, akan muncul warning pada baris pemanggilan setExactAndAllowWhileIdle. Oleh karena itu, tambahkan permission berikut di AndroidManifest.
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<uses-permission android:name="android.permission.USE_EXACT_ALARM"/>
Ohh, iya. Mumpung masih di AndroidManifest, jangan lupa untuk menambahkan tag Receiver berikut ya. Hal ini agar sistem Android dapat mengetahui keberedaan AlarmReceiver sebagai kelas BroadcastReceiver.
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<uses-permission android:name="android.permission.USE_EXACT_ALARM"/>
<application
...
android:icon="@mipmap/app_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/app_launcher_round">
...
<receiver
android:name=".AlarmReceiver"
android:enabled="true"
android:exported="false" />
</application>
</manifest>
Sip! Kembali lagi ke metode onReceive. Di sini kita akan mengaktifkan setiap Activity alias sesuai jamnya menggunakan jam terkini yang diterima dari PendingIntent.
class AlarmReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent?) {
val hour = intent?.getIntExtra(EXTRA_HOUR, -1)
Log.d("AlarmReceiver", "Alarm triggered!")
Log.d("AlarmReceiver", "Alarm message: $hour!")
when(hour){
0 -> changeIconToHour12(context)
1 -> changeIconToHour1(context)
2 -> changeIconToHour2(context)
3 -> changeIconToHour3(context)
4 -> changeIconToHour4(context)
5 -> changeIconToHour5(context)
6 -> changeIconToHour6(context)
7 -> changeIconToHour7(context)
8 -> changeIconToHour8(context)
9 -> changeIconToHour9(context)
10 -> changeIconToHour10(context)
11 -> changeIconToHour11(context)
}
}
fun setChangeAppIconAlarm(context: Context){
...
}
private fun changeIconToHour1(context: Context){
val packageManager = context.packageManager
val packageName = context.packageName
packageManager.setComponentEnabledSetting(
ComponentName(
context,
"$packageName.MainActivityHour1"
),
PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
PackageManager.DONT_KILL_APP
)
}
private fun changeIconToHour2(context: Context){
val packageManager = context.packageManager
val packageName = context.packageName
packageManager.setComponentEnabledSetting(
ComponentName(
context,
"$packageName.MainActivityHour2"
),
PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
PackageManager.DONT_KILL_APP
)
}
...
private fun changeIconToHour12(context: Context){
val packageManager = context.packageManager
val packageName = context.packageName
packageManager.setComponentEnabledSetting(
ComponentName(
context,
"$packageName.MainActivityHour12"
),
PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
PackageManager.DONT_KILL_APP
)
}
companion object {
const val EXTRA_HOUR = "hour"
private const val TASK_ID = 101
}
}
Mantap! Selanjutnya, kita tinggal panggil metode setChangeAppIconAlarm dari MainActivity.
class MainActivity : ComponentActivity() {
private lateinit var alarmReceiver: AlarmReceiver
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
...
}
alarmReceiver = AlarmReceiver()
}
override fun onStop() {
super.onStop()
alarmReceiver.setChangeAppIconAlarm(this)
}
}
Mungkin Anda bertanya, kenapa pemanggilan metode setChangeAppIconAlarm diletakkan di onStop? Jawabannya adalah karena pengaktifan Activity alias akan menyebabkan aplikasi untuk force close jika diletakkan pada onCreate sehingga kita akan eksekusi metode pada saat aplikasi akan ditutup oleh pengguna.
Mungkin Anda akan bertanya lagi, kalau begitu, kenapa tidak memanggil metode setChangeAppIconAlarm di onDestroy? Jawabannya adalah karena setelah beberapa kali percobaan yang saya lakukan, sering kali, bahkan, sepertinya selalu, pesan broadcast gagal untuk dikirim oleh AlarmManager. Sehingga sebagai alternatif, kita letakkan metode tersebut di onStop.
Sekarang, uninstall terlebih dahulu aplikasi, lalu Markicob lagi 😊

Yeay! Aplikasi berhasil mengubah icon sesuai jamnya. Namun, seperti yang kita lihat diatas, terdapat 2 icon yang terbuat untuk aplikasi. Hal ini karena terdapat dua Activity yang aktif dalam satu waktu (MainActivity dan MainActivityHour10). Oleh karena itu, kita perlu menonaktifkan terlebih dahulu semua Activity sebelum mengaktifkan sebuah Activity alias pada metode onReceive.
class AlarmReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent?) {
val hour = intent?.getIntExtra(EXTRA_HOUR, -1)
Log.d("AlarmReceiver", "Alarm triggered!")
Log.d("AlarmReceiver", "Alarm message: $hour!")
disabledAllActivityComponent(context)
when(hour){
0 -> changeIconToHour12(context)
1 -> changeIconToHour1(context)
2 -> changeIconToHour2(context)
3 -> changeIconToHour3(context)
4 -> changeIconToHour4(context)
5 -> changeIconToHour5(context)
6 -> changeIconToHour6(context)
7 -> changeIconToHour7(context)
8 -> changeIconToHour8(context)
9 -> changeIconToHour9(context)
10 -> changeIconToHour10(context)
11 -> changeIconToHour11(context)
}
}
fun setChangeAppIconAlarm(context: Context){
...
}
private fun disabledAllActivityComponent(context: Context){
val packageManager = context.packageManager
val packageName = context.packageName
packageManager.setComponentEnabledSetting(
ComponentName(
context,
"$packageName.MainActivity"
),
PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
PackageManager.DONT_KILL_APP
)
packageManager.setComponentEnabledSetting(
ComponentName(
context,
"$packageName.MainActivityHour1"
),
PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
PackageManager.DONT_KILL_APP
)
packageManager.setComponentEnabledSetting(
ComponentName(
context,
"$packageName.MainActivityHour2"
),
PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
PackageManager.DONT_KILL_APP
)
...
packageManager.setComponentEnabledSetting(
ComponentName(
context,
"$packageName.MainActivityHour12"
),
PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
PackageManager.DONT_KILL_APP
)
}
...
}
Mantap! Uninstall kembali aplikasi, lalu coba jalankan kembali!

Asyik! Hanya ada satu icon aplikasi terbuat dan aplikasi pun berhasil mengubah icon aplikasi sesuai jamnya.
Perjalanan belum selesai! Karena kita ingin agar icon aplikasi berubah setiap jamnya, maka kita perlu menambahkan logika berikut di metode onReceive dan setChangeAppIconAlarm untuk mengirim pesan broadcast baru pada jam berikutnya.
class AlarmReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent?) {
val hour = intent?.getIntExtra(EXTRA_HOUR, -1)
Log.d("AlarmReceiver", "Alarm triggered!")
Log.d("AlarmReceiver", "Alarm message: $hour!")
disabledAllActivityComponent(context)
when(hour){
0 -> changeIconToHour12(context)
1 -> changeIconToHour1(context)
2 -> changeIconToHour2(context)
3 -> changeIconToHour3(context)
4 -> changeIconToHour4(context)
5 -> changeIconToHour5(context)
6 -> changeIconToHour6(context)
7 -> changeIconToHour7(context)
8 -> changeIconToHour8(context)
9 -> changeIconToHour9(context)
10 -> changeIconToHour10(context)
11 -> changeIconToHour11(context)
}
if (hour != null){
val nextHour = if (hour == 11){
0
} else {
hour.plus(1)
}
setChangeAppIconAlarm(context, nextHour)
}
}
fun setChangeAppIconAlarm(context: Context, nextHour: Int = -1){
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
val calendar = Calendar.getInstance()
val alarmIntent = Intent(context, AlarmReceiver::class.java)
if (nextHour == -1){
val currentHour = calendar[Calendar.HOUR]
alarmIntent.putExtra(EXTRA_HOUR, currentHour)
} else {
calendar.set(Calendar.HOUR, nextHour)
val setNextHour = calendar[Calendar.HOUR]
alarmIntent.putExtra(EXTRA_HOUR, setNextHour)
}
val pendingIntent = PendingIntent.getBroadcast(
context,
TASK_ID,
alarmIntent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
)
alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC, calendar.timeInMillis, pendingIntent)
Log.d("AlarmReceiver", "Alarm requested!")
}
...
}
Terakhir, dengan doa bapak dan ibu, uninstall kembali aplikasi, lalu coba jalankan kembali!
Horeee! Icon aplikasi berhasil berubah setiap jamnya. Lebih lanjut, kita pun dapat melihat pada logcat bahwa pesan broadcast untuk jam berikutnya berhasil terbuat walaupun aplikasi telah ditutup.
Penutup
Seru juga ya ternyata eksplorasi hal-hal unik di aplikasi mobile. Sebagai penulis, kasus kali ini cukup menyenangkan untuk dieksplorasi karena sebenarnya dari dulu sudah penasaran, namun belum kesampaian untuk dieksplorasi 😅
Sekian untuk kali ini. Source code proyek dapat dilihat di Github Repository berikut. Jika kamu punya ide terkait hal berikutnya yang menarik untuk kita eksplor, silahkan ketik di kolom komentar, ya! Adios!