A strange bug on the Samsung Touchwiz launcher
This is a post in the The mysterious case of… series, where we tackle obscure issues in Android that we run into while developing apps, and we explain how we fixed (or worked around) them. If you have something interesting to contribute to this series, get in touch!
While working on a project with my team, we have faced a really strange bug. We were adding a homescreen widget to an Android app. Main coding had been completed, QA was not reporting any scary bug with it, the client loved it. Happy times!
We thought that everything was working just fine — and that was actually the case, mostly. We left the office, happy to have gotten the widget pretty quickly together. As Murphy’s Law likes to remind us (quite more often that we would like it to), things were going to get painfully wrong.
The bearer of bad news
Next morning I got to the office, fired up the email, looked at the usual bulk of QA-related emails from the overseas team, when my heart suddenly dropped a beat. I was staring at this email from JIRA: Widget disappears after status changes (login/logout, …).
Oh, crap.
According to the QA tester, the widget had the bad habit of disappearing from Samsung devices’ home screens under certain circumstances. The widget was there, you could long press and move it, but it wouldn’t display anything. A big empty transparent box. To add insult to injury, my colleague that actually coded the widget was home sick, and I’d never looked at the widget code yet.
After verifying that the issue was not reproducible on my Nexus devices, nor on my trusty Android VM, I went hunting for a Galaxy S4. At that point I was strongly suspecting a bug in the Touchwiz launcher. Samsung is quite well known for having the weirdest bugs in their software, after all.
Maybe, I thought, I won’t be able to reproduce it. Maybe Samsung fixed that bug at some point, and QA’s just using a reeeeally old firmware version. I was really hoping for a win-win miracle to happen.
Of course, it didn’t. The bug was pretty easily reproducible. So… onto code review and let’s see if there is some bug in there
Just when you think things are getting weird…
The widget (can’t show it yet, sorry — still unreleased) was a simple one. There wasn’t much code behind it, and thus there wasn’t much that could go that wrong with it. Furthermore, the bug is clearly a drawing issue, I thought, and widgets are drawn by the launcher, as they are RemoteViews. A first round of reviewing didn’t bring anything to pop to my eyes. Stepping through the code didn’t help either. What I noticed there, though, was that the widget actually showed up from time to time in the launcher, flashing for a few milliseconds, and then disappearing.
Now, that’s strange! What have you done this time, Samsung?
Reiterating the same debugging steps and paying closer attention, I noticed the widget was not only flashing its contents. Before that, it would show a random icon with a label, for another (system?) app installed on the S4. The mystery was getting thicker, but the code was working as intended, and I ruled it off for the day. Who knows, maybe when the guy who wrote the widget comes back to the office tomorrow, he’ll be able to help me find a solution.
…they just get Samsung-weird!
Fast-forward to the following morning, pair coding session to try and find a solution for this issue. We ran through the whole code again, test with different accounts, different scenarios. Tried disabling some fancy stuff we have to do to use custom fonts on the widget (more on that in another article, maybe), then disabling everything but the static background; still no luck.
SSDD, until we happened to shortcut to the home screen from a step in the test scenario where we usually waited for something to happen. The widget was displaying random stuff for a second, flashing through 3-4 other app’s widgets/icons (always the same ones) and then disappearing. We were just too slow in reaching the home screen in our previous tests to notice this awesomeness.
What the f@&$%g s#!t, Samsung!
If this were native code, I’d blame some pointer going AWOL and the launcher not noticing it. But this isn’t native code, right? So it had to be that the TouchWiz launcher somehow messed up the widget IDs and showed random content instead of what we passed it.
The needle in the haystack
We now had a clue of what could be going wrong, so we could better focus our searches. There was no help to be found online, not even on StackOverflow, for this specific issue. But. Given that the weird behaviour happens when updating the widget, the issue was most likely somewhere in there.
Searching for some reference to see if we were doing the update thing properly, we noticed a couple of little, harmless phrases in this answer on StackOverflow:
It is important that we use our own keys with the widget update action, if we use AppWidgetManager.EXTRA_WIDGET_IDS
we will not only break our own widget, but others as well.
AppWidgetManager man = AppWidgetManager.getInstance(context);
int[] ids =
man.getAppWidgetIds(new ComponentName(context,
MyWidgetProvider.class));
Intent updateIntent = new Intent();
updateIntent.setAction(
AppWidgetManager.ACTION_APPWIDGET_UPDATE);
updateIntent.putExtra(MyWidgetProvider.WIDGET_ID_KEY, ids);
updateIntent.putExtra(MyWidgetProvider.WIDGET_DATA_KEY, data);
context.sendBroadcast(updateIntent);
If using this method with multiple providers MAKE SURE they use different keys. Otherwise you may find widget a’s code updating widget b and that can have some bizarre consequences.
Epiphany: that was what our code was doing! We were putting this extra in the update intent:
updateIntent.putExtra(AppWidgetManager.EXTRA_WIDGET_IDS, ids);
There was the problem! Funnily enough, the documentation does not warn you against doing this. Oh, yeah, that is always the case, right.
Back to the issue: as opposed as what happened back in the day (2011) when Chris wrote his answer, using AppWidgetManager.EXTRA_WIDGET_IDS
doesn’t mess up your widget or other apps’ anymore, on all other launchers we’ve come across. Our QA couldn’t reproduce the issue on any other phones but Samsung’s.
(That is an amazingly good answer, by the way. Thanks, Chris!)
Getting s#!t fixed
Fixing the issue was actually kinda easy, once we spotted the issue. Following Chris’ advice, I begun tweaking our code to avoid using the dreaded AppWidgetManager.EXTRA_WIDGET_IDS
extra, instead relying on our own extra key. Luckily, I already had a pretty good implementation to use as a reference: FWeather.
Implementing the same mechanism took just a few hours, included testing on the S4 (it woooorks!), and doing some regression to ensure that it didn’t have any undesired side effects.
Mission accomplished!
But, we still had no explanation for the random flashing going on. What the hell is going on there, you ask?
The bigger picture
After posting a gist with some sample code for updating widgets the right way, the always awesome Mark Murphy popped up with a really insightful comment:
YourupdateIntent
should add in theWidgetProvider
as the component. As it stands, your broadcast will go to allAppWidgetProvider
s on the device, which is unnecessary at best and possibly a source of crashes at worst.
He was, of course, right. The actual code we ended up with in our app is different from what is on the gist (we call the updater service directly, that then publishes the widget updates, so we had the component in the fixed code), but in that case, and considering how the code was before fixing it, he was right.
Do you see what the side effect of asking all AppWidgetProvider
s on the device to update themselves on a single set of widgetIds
and their RemoteView
s would be, if no limitation was put in place on which widget belongs to what app and everybody could update all the widgets?
Exactly. Randomly flashing stuff on the widget, with all the apps’ widget providers racing to update what they assumed was their own widget (nobody ever checks they actually own those widgetId
s, because you usually don’t need to). Depending on the order the apps are installed and the time it takes them to update, this results on a certain sequence of apps dutifully completing their widget updates and sending them to the launcher, that then assigns the RemoteView
s to the widget and draws it.
We forgot (and apparently so did Chris) to put a target component to the intent in the original widget code, to avoid spamming other apps our widgetIds
. We didn’t notice that shortcoming, because nothing happens on other devices. The AOSP seems to filter out malformed widget intents in the AppWidgetProvider
, and looks like it’s being doing it since widgets’ inception (at least for update intents).
But that filtering doesn’t check for cross-app widget updates. As it is right now, an app could try and show its own RemoteView
s on any other app’s widget, thus enabling a host of phishing attacks. Just imagine a rogue app cloning a widget layout, replacing PendingIntent
s on a banking app’s widget, and showing a seemingly legitimate login screen to users. That would be really hard to spot, in some cases even for experienced users.
Plus, retrieving other apps’ widget IDs is dead simple, making these phishing attacks pretty trivial to implement:
public static int[] getWidgetIds(Context context,
String target) {
AppWidgetManager mgr = AppWidgetManager.getInstance(context);
if (mgr != null) {
ComponentName targetComponent =
new ComponentName(context, target);
return mgr.getAppWidgetIds(targetComponent);
}
return new int[0];
}
I couldn’t determine where exactly the cross-app checks are performed, if any. I suspect there is some kind of check going on, because if there wasn’t we would see the same symptoms on non-Samsung devices as well, and that’s not the case. Maybe it’s some low-level user permission check, but I’m merely speculating here. Hopefully someone that reads this piece has the answer — if you do, please let me know!
So thanks, Mark! Your comment helped me finally close the circle on what was really happening there. Patching the issue without fully understanding what it was really left me frustrated. Now I’m feeling better!
tl;dr
If you want to force a widget update for your app, this is the right way to do it:
https://gist.github.com/rock3r/9809139