In this article, we dig into Google Calendar APK, take a closer look at .smali files, and a class called DynamicIconProvider.
What
If you’re an Android user, you’ve probably noticed how the Google Calendar app icon behaves:
There’s that little blue (#A7C5F8 for the skeptical) circle just sitting there, maybe signaling that there’s been something going on in the calendar app since the last time you have checked. This is not the story of that little blueish dot.
It is, however, the story of the pair of digits that you can see in the center of the above image, which happen to mysteriously coincide with today’s date (July 29th).
Why
Google calendar is probably the best calendar app (or service) that you will find out there, because one, it syncs painlessly on all your devices, and two, its icon has those friendly little digits on that nice blue (a number of colors are involved including #3764D0) background which correctly shows the day of the month, whenever you take a look at it, and this fancy, mysterious little feature, is what I’m going to try to figure out. I got sucked into this while trying to implement the exact same feature for Persian Calendar.
How
Alright, so let’s get to work. I found a Google Calendar APK on the internet (I’m not putting it here because of copyright and stuff) and decompiled it. Next, I tried something as simple as:
Please note: Code excerpts are incomplete. I have only copied the relevant parts.
grep -Ri dynamic . | grep icon
Here’s an important part of the output:
./res/values-v26/arrays.xml: <array name="calendar_icons_dynamic_nexus_round">
Now here’s an important portion of that file:
<array name="calendar_icons_dynamic">
<item>@drawable/logo_calendar_01_adaptive</item>
<item>@drawable/logo_calendar_02_adaptive</item>
<item>@drawable/logo_calendar_03_adaptive</item>
<item>@drawable/logo_calendar_04_adaptive</item>
<item>@drawable/logo_calendar_05_adaptive</item>
Now let’s have a look at res/drawable-v26/logo_calendar_03_adaptive.xml:
="1.0"="utf-8"
<adaptive-icon
xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/adaptive_background" />
<foreground>
<layer-list>
<item android:drawable="@drawable/adaptive_base" />
<item android:drawable="@drawable/calendar_date_03_adaptive" />
</layer-list>
</foreground>
</adaptive-icon>
Quite self explanatory, right? Each of those resources are png
s containing exactly what you’re thinking: The adaptive_base
:
And the calendar_date_03_adaptive
:
After that, I took a dive into .smali files and also browsed the remains of the actual code after converting the apk
to jar
using dex2jar
(I used jd-gui
).
After hours of navigating the code, I hadn’t found a single reference to any of these drawables that I mentioned above. This is why I set the calendar app aside and went diving into pixel launcher’s source code. Here’s what you can find there in a class called DyanmicIconProvider
:
private boolean fe(String paramString)
{
return "com.google.android.calendar".equals(paramString);
}
public Drawable getIcon(LauncherActivityInfo paramLauncherActivityInfo,
int paramInt, boolean paramBoolean)
{
localObject3 = null;
Object localObject4 = paramLauncherActivityInfo.getApplicationInfo().packageName;
if (fe((String)localObject4)) {}
And all of a sudden, everything makes sense if you take a glimpse on other parts of this file:
public DynamicIconProvider(Context paramContext)
{
IntentFilter localIntentFilter = new IntentFilter("android.intent.action.DATE_CHANGED");
localIntentFilter.addAction("android.intent.action.TIME_SET");
localIntentFilter.addAction("android.intent.action.TIMEZONE_CHANGED");
paramContext.registerReceiver(this.gv, localIntentFilter, null,
new Handler(LauncherModel.getWorkerLooper()));
this.mContext = paramContext;
this.mPackageManager = paramContext.getPackageManager();
}
Note the action class paths. I also found this c.class
with barely even readable decompiled function/class names which is probably responsible for updating the face of the clock app (deskclock
). Here are some interesting parts:
localPackageManager.getApplicationInfo("com.google.android.deskclock", 8320);
localb.fW = localBundle.getInt
("com.google.android.apps.nexuslauncher.HOUR_LAYER_INDEX", -1);
localb.fY = localBundle.getInt
("com.google.android.apps.nexuslauncher.MINUTE_LAYER_INDEX", -1);
localb.fZ = localBundle.getInt
("com.google.android.apps.nexuslauncher.SECOND_LAYER_INDEX", -1);
localb.fS = localBundle.getInt("com.google.android.apps.nexuslauncher.DEFAULT_HOUR", 0);
localb.fT = localBundle.getInt("com.google.android.apps.nexuslauncher.DEFAULT_MINUTE", 0);
localb.fU = localBundle.getInt("com.google.android.apps.nexuslauncher.DEFAULT_SECOND", 0);
It is also interesting to note the difference between the design decisions in case of the calendar app and the deskclock
app adaptive icons. To further understand this, let’s have a look at a part of launcher_clock.xml from deskclock
:
<foreground>
<layer-list>
<item>
<rotate android:drawable="@mipmap/launcher_clock_hour"
android:fromDegrees="300.0" android:toDegrees="5300.0"
android:pivotX="50.0%" android:pivotY="50.0%" />
</item>
<item>
<rotate android:drawable="@mipmap/launcher_clock_minute"
android:fromDegrees="60.0" android:toDegrees="60060.0"
android:pivotX="50.0%" android:pivotY="50.0%" />
</item>
<item>
<rotate android:drawable="@mipmap/launcher_clock_second"
android:fromDegrees="180.0" android:toDegrees="6180.0"
android:pivotX="50.0%" android:pivotY="50.0%" android:level="300" />
</item>
<item android:drawable="@mipmap/launcher_clock_top" />
<item android:drawable="@mipmap/launcher_clock_banner" />
</layer-list>
</foreground>
And apart from this, there is no other reference to launcher_clock_second
in the codebase. Interestingly enough, we can see two very different solutions for a fairly similar problem: dynamic launcher icons. Due to the complications of online text rendering, the authors have decided to pre-cook all the required icons in case of the calendar app. In the case of deskclock
however, there would just be too many icons if they were to create the drawables beforehand, which has resulted in bringing this seemingly unrelated (as far as a launcher is concerned) logic into the code of the pixel launcher:
bool1 = bool2;
if (this.fV.getDrawable(this.fW).setLevel((i + (12 - j)) % 12 * 60 + this.fX.get(12))) {
bool1 = true;
}
}
So
This is where my search ends. This is bad news for you if you wanted this fancy little feature in your app too. With the launcher handling all the workload here and not even providing an internal API, we are probably not likely to see this feature be available to the public anytime soon. Let’s go back to widgets.
Update 1: Thank you Ebrahim for finding this piece of code. This is most likely the actual source code of the part that I was investigating, from LIFE-OS, under com.google.android.apps.nexuslauncher
;