I just spend few hours analyzing and fixing a memory leak in Android application. With every orientation change the full Context, including the whole Activity was leaked. Long story short, the problem was caused by misuse of CursorAdapter: in subclass constructor we called CursorAdapter(context, null, false) instead of CursorAdapter(context, null, 0).
The difference is quite subtle. If you use the second constructor, you have to take care of handling content updates yourself. If you use the first constructor, the CursorAdapter will register an additional ContentObserver for you, but you need to manually reset the Cursor.
The funny thing is, this behavior is described in javadocs, but the documentation is spread between the constructor and FLAG_REGISTER_CONTENT_OBSERVER flag documentation. The second part contains most crucial information: you don't need to use this flag when you intend to use your adapter with CursorLoader.
If for some reason you want to use the adapter without CursorLoader, you should use the CursorAdapter(context, null, false) constructor, and call swapCursor(null) when leaving the Activity or Fragment.
Porcupine Programmer
Programming rants, random stuff and some more programming.
Wednesday, April 24, 2013
Saturday, March 23, 2013
libphonenumber crash on Android 3.2
Few days ago I saw the most peculiar crash on a tablet with Android 3.2:
Long story short, it turns out that some old version of libphonenumber, which doesn't support this particular phone number format, is included in the Android build on my 3.2 device. You can verify such thing by calling:
I started to wonder if any other libraries are affected, so I picked up some class names from the most popular libraries (as reported by AppBrain) and it seems there might be a similar issue with Apache Commons Codec jar. Fortunately there are no issues with stuff like Guava, GSON or support lib.
What's the workaround for this issue? Fork the library and change the package name.
java.lang.NoSuchFieldError: com.google.i18n.phonenumbers.PhoneNumberUtil$PhoneNumberFormat.RFC3966Of course there is such field in this class, it works on every other Android hardware I have access to. This was a debug build, so it couldn't have been the Proguard issue. The other functionality from the com.google.i18n.phonenumbers package worked fine, the issue only appeared if I wanted to format a phone number using this specific format.
Long story short, it turns out that some old version of libphonenumber, which doesn't support this particular phone number format, is included in the Android build on my 3.2 device. You can verify such thing by calling:
Class.forName("com.google.i18n.phonenumbers.PhoneNumberUtil");
inside a project without any libs - on this single 3.2 device it will return the valid Class object, on every other device I tried it throws ClassNotFoundException.I started to wonder if any other libraries are affected, so I picked up some class names from the most popular libraries (as reported by AppBrain) and it seems there might be a similar issue with Apache Commons Codec jar. Fortunately there are no issues with stuff like Guava, GSON or support lib.
What's the workaround for this issue? Fork the library and change the package name.
Android UI struggles: making a button with centered text and icon
Every time I work on the UI of Android app I get the feeling that there is either something terribly wrong with the Android UI framework or with my understanding of how it works. I can reason about how the app works on the higher level, but I cannot apply the same methodology to Android UI, except for the simplest designs. I have read a lot of Android source code, I have written few dozens of sample-like apps, but I still cannot just think of the views structure, type it in and be done - for complicated layouts with some optional elements (i.e. which are sometimes visible and sometimes gone) I need at least few attempts and, I confess, sometimes I'm desperate enough to do the "let's change this and see what happens" coding. Extremely frustrating.
I'm going to describe my struggles with Android UI on this blog, so if I'm doing something terribly wrong, hopefully someone will enlighten me by posting a comment; and in case something is terribly wrong with Android UI framework, I might be able to help other programmers in distress.
Today I have a simple task for you: create a button with some text and icon to the left of the text. The contents (both icon and text) should be centered inside the button.
That's simple right? Here's the XML layout which comes to mind first:
Unfortunately, no cookie for you:
Someone decided that compound drawables should be always draw next to the View's padding, so we have to try something else. For example TextView centered inside the FrameLayout.
Almost there, but the text has a wrong size and color. There is something called "textAppearanceButton", but apparently it's not what the Buttons use:
OK, so let's use the buttonStyle again, this time on TextView:
Now we need to get rid of the extra background, reset minimum height and width and make it not focusable and not clickable (otherwise tapping the caption won't have any effect):
Lo and behold, it works!
We'd really like to use is something like textAppearance="?android:attr/buttonStyle.textAppearance", but there is no such syntax. How about extracting all the attributes from TextView into some "buttonCaption" style with "?android:attr/buttonStyle" parent? No can do either: you can only inherit your style from the concrete @style, not from the styleable attribute.
But what we can do is to use Button and create a style with no parent: Android will use the default button style and apply our captionOnly style:
I'm going to describe my struggles with Android UI on this blog, so if I'm doing something terribly wrong, hopefully someone will enlighten me by posting a comment; and in case something is terribly wrong with Android UI framework, I might be able to help other programmers in distress.
Today I have a simple task for you: create a button with some text and icon to the left of the text. The contents (both icon and text) should be centered inside the button.
That's simple right? Here's the XML layout which comes to mind first:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical" >
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:drawableLeft="@android:drawable/ic_delete"
android:gravity="center"
android:text="Button Challenge" />
</LinearLayout>
Unfortunately, no cookie for you:
Someone decided that compound drawables should be always draw next to the View's padding, so we have to try something else. For example TextView centered inside the FrameLayout.
<FrameLayout
style="?android:attr/buttonStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content" >
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:drawableLeft="@android:drawable/ic_delete"
android:gravity="center"
android:text="Button Challenge" />
</FrameLayout>
Almost there, but the text has a wrong size and color. There is something called "textAppearanceButton", but apparently it's not what the Buttons use:
OK, so let's use the buttonStyle again, this time on TextView:
Now we need to get rid of the extra background, reset minimum height and width and make it not focusable and not clickable (otherwise tapping the caption won't have any effect):
<FrameLayout
style="?android:attr/buttonStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content" >
<TextView
style="?android:attr/buttonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:background="@null"
android:clickable="false"
android:drawableLeft="@android:drawable/ic_delete"
android:focusable="false"
android:gravity="center"
android:minHeight="0dp"
android:minWidth="0dp"
android:text="Button Challenge" />
</FrameLayout>
Lo and behold, it works!
We'd really like to use is something like textAppearance="?android:attr/buttonStyle.textAppearance", but there is no such syntax. How about extracting all the attributes from TextView into some "buttonCaption" style with "?android:attr/buttonStyle" parent? No can do either: you can only inherit your style from the concrete @style, not from the styleable attribute.
But what we can do is to use Button and create a style with no parent: Android will use the default button style and apply our captionOnly style:
<style name="captionOnly">
<item name="android:background">@null</item>
<item name="android:clickable">false</item>
<item name="android:focusable">false</item>
<item name="android:minHeight">0dp</item>
<item name="android:minWidth">0dp</item>
</style>
<FrameLayout
style="?android:attr/buttonStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content" >
<Button
style="@style/captionOnly"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:drawableLeft="@android:drawable/ic_delete"
android:gravity="center"
android:text="Button Challenge" />
</FrameLayout>
Saturday, March 16, 2013
Android nested Fragments in practice
Last November I wrote about the new feature in rev11 of Android support package - Fragments nesting. Recently I had an opportunity to use this feature in practice and I'd like to share my experience with it.
The basics are simple: each FragmentActivity and each Fragment has it's own FragmentManager. Inside the Fragment you may call getFragmentManager() to get the FragmentManager this Fragment was added to, or getChildFragmentManager() to get the FragmentManager which can be used to nest Fragments inside this Fragment. This basic flow works fine, but I have found two issues.
If you have a Fragment with nested Fragments and you save its state with saveFragmentInstanceState() and try to use it in setInitialSavedState() on another instance of this Fragment, you'll get the BadParcelableException from onCreate. Fortunately it's an obvious bug which is easy to fix: you just need to set the correct ClassLoader for a Bundle containing this Fragment's state. There is a patch for it in support library project Gerrit, and if you need this fix ASAP you may use this fork of support lib on Github.
The second issue is related with the Fragments backstack. Inside each FragmentManager you may build stack of Fragments with FragmentTransaction.addToBackStack() and later on use popBackStack() to go back to the previous state. Pressing hardware back key is also supposed to pop the Fragments from the back stack, but it doesn't take into account any nested Fragments, only Fragments added to the Activity's FragmentManager. This is not so easy to fix, but you may use the following workaround:
Note that there are two issues with this workaround: it allows adding only one backstack entry and this setup won't be automatically recreated from state saved by saveFragmentInstanceState() (fortunately it does work with orientation change). Both issues probably can be solved by some additional hacks, but writing workarounds for workarounds is not something I do unless I really have to, and in this case I neither issue affected me.
Besides those bumps the nested Fragments are a real blessing which allows much more cleaner and reusable code.
The basics are simple: each FragmentActivity and each Fragment has it's own FragmentManager. Inside the Fragment you may call getFragmentManager() to get the FragmentManager this Fragment was added to, or getChildFragmentManager() to get the FragmentManager which can be used to nest Fragments inside this Fragment. This basic flow works fine, but I have found two issues.
If you have a Fragment with nested Fragments and you save its state with saveFragmentInstanceState() and try to use it in setInitialSavedState() on another instance of this Fragment, you'll get the BadParcelableException from onCreate. Fortunately it's an obvious bug which is easy to fix: you just need to set the correct ClassLoader for a Bundle containing this Fragment's state. There is a patch for it in support library project Gerrit, and if you need this fix ASAP you may use this fork of support lib on Github.
The second issue is related with the Fragments backstack. Inside each FragmentManager you may build stack of Fragments with FragmentTransaction.addToBackStack() and later on use popBackStack() to go back to the previous state. Pressing hardware back key is also supposed to pop the Fragments from the back stack, but it doesn't take into account any nested Fragments, only Fragments added to the Activity's FragmentManager. This is not so easy to fix, but you may use the following workaround:
String FAKE_BACKSTACK_ENTRY = "fakeBackstackEntry";
getFragmentManager()
.beginTransaction()
.addToBackStack(null)
// call replace/add
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN)
.commit();
final FragmentManager rootFragmentManager = getActivity().getSupportFragmentManager();
rootFragmentManager
.beginTransaction()
.addToBackStack(null)
.add(new Fragment(), FAKE_BACKSTACK_ENTRY)
.commit();
rootFragmentManager.addOnBackStackChangedListener(new OnBackStackChangedListener() {
@Override
public void onBackStackChanged() {
if (rootFragmentManager.findFragmentByTag(FAKE_BACKSTACK_ENTRY) == null) {
getFragmentManager().popBackStack();
rootFragmentManager.removeOnBackStackChangedListener(this);
}
}
});
Quick explanation: together with the actual backstack entry we want to add, we also add the fake backstack entry with empty Fragment to top level FragmentManager and set up OnBackStackChangedListener. When user presses hardware back button, the fake backstack entry is popped, the backstack listener is triggered and our implementation pops the backstack inside our Fragment. The backstack listeners are not persisted throughout the orientation change, so we need to setup it again inside onCreate().Note that there are two issues with this workaround: it allows adding only one backstack entry and this setup won't be automatically recreated from state saved by saveFragmentInstanceState() (fortunately it does work with orientation change). Both issues probably can be solved by some additional hacks, but writing workarounds for workarounds is not something I do unless I really have to, and in this case I neither issue affected me.
Besides those bumps the nested Fragments are a real blessing which allows much more cleaner and reusable code.
Tuesday, March 5, 2013
Weekend hack: viewing markdown attachments in GMail on Android
Recently I wanted to open a markdown email attachment on my Nexus 4, but after clicking "readme.md" instead of seeing the file contents I saw this message:
I downloaded few apps from Google Play, but the message was still appearing. The same applications could open a local markdown file, so I went back to GMail app to download the attachment, but another unpleasant surprise awaited me:
There is no "overflow" menu on the attachment (see the screenshot below), which means I couldn't access the "Save" option, so I could open it as a local file.
At this point I was:
My first idea was to create an "GMail Attachment Forwarder" app which registers for any content from GMail, gets the attachment mail by querying the DISPLAY_NAME column on the Uri supplied by GMail, save this information along with original GMail Uri in public ContentProvider, and start the activity using Uri exposed by my ContentProvider which does contain attachment name. This ContentProvider should also forward any action to original GMail Uri.
Unfortunatly I was foiled by the ContentProvider's permissions systems: the Activity in my app was temporarily granted with the read permissions for GMail's ContentProvider, but this permissions did not extend to my ContentProvider and the app I was forwarding the attachment to failed because of the insufficient permissions.
This approach didn't work, but having a catch-all handler for GMail attachments unlocked the attachment actions. I also noticed that when the attachment is downloaded, the GMail uses a slightly different intent:
I downloaded few apps from Google Play, but the message was still appearing. The same applications could open a local markdown file, so I went back to GMail app to download the attachment, but another unpleasant surprise awaited me:
There is no "overflow" menu on the attachment (see the screenshot below), which means I couldn't access the "Save" option, so I could open it as a local file.
At this point I was:
- Pissed off, because, cmon, GMail is probably the most used app working on the mature operating system and I can't download a fucking file with it.
- Curious, because it looked liked an interesting issue with GMail app.
03-04 21:12:50.477: W/Gmail(13823): Unable to find supporting activity. mime-type: application/octet-stream, uri: content://gmail-ls/jerzy.chalupski@gmail.com/messages/121/attachments/0.1/BEST/false, normalized mime-type: application/octet-stream normalized uri: content://gmail-ls/jerzy.chalupski@gmail.com/messages/121/attachments/0.1/BEST/falseNote the Uri: there is no file name and no file extension, and the mime-type is a generic application/octet-stream (most likely because the "md" extension is not present in libcore.net.MimeUtils). The markdown viewers/editors I downloaded probably register intent filters for specific file extensions, so they don't know they could handle this file. It sucks big time, because it means that the applications for viewing files with non-standard extensions would have to register for application/octet-stream mime-type, and even though they handle very specific file types they all appear in the app chooser dialog for many different file types, which defeats the whole purpose of Android Intent system and reduces the UX.
My first idea was to create an "GMail Attachment Forwarder" app which registers for any content from GMail, gets the attachment mail by querying the DISPLAY_NAME column on the Uri supplied by GMail, save this information along with original GMail Uri in public ContentProvider, and start the activity using Uri exposed by my ContentProvider which does contain attachment name. This ContentProvider should also forward any action to original GMail Uri.
Unfortunatly I was foiled by the ContentProvider's permissions systems: the Activity in my app was temporarily granted with the read permissions for GMail's ContentProvider, but this permissions did not extend to my ContentProvider and the app I was forwarding the attachment to failed because of the insufficient permissions.
This approach didn't work, but having a catch-all handler for GMail attachments unlocked the attachment actions. I also noticed that when the attachment is downloaded, the GMail uses a slightly different intent:
03-04 23:05:34.005: I/ActivityManager(526): START u0 {act=android.intent.action.VIEW dat=file:///storage/emulated/0/Download/readme-1.md typ=application/octet-stream flg=0x80001 cmp=com.chalup.markdownviewer/.MainActivity} from pid 3063
This led me to plan B: have an app which enables the attachment download and use other apps to open downloaded attachments. I renamed my app to GMail Attachment Unlocker, cleared the manifest and source folder leaving only a single, automatically closing activity:
The full source code is available on my Github (althought there really isn't much more than what is posted here). In the end I also ended up writing my own markdown viewer (source code in another repo on my Github), because none of the apps I have downloaded properly rendered <pre> tags (hint: you have to use WebView.loadDataWithBaseUrl instead of WebView.loadData).
<application
android:allowBackup="true"
android:label="@string/app_name"
android:theme="@android:style/Theme.NoDisplay" >
<activity
android:name="com.chalup.gmailattachmentunlocker.MainActivity"
android:label="@string/app_name" >
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="gmail-ls"
android:mimeType="*/*"
android:scheme="content" />
</intent-filter>
</activity>
</application>
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
finish();
}
}
The full source code is available on my Github (althought there really isn't much more than what is posted here). In the end I also ended up writing my own markdown viewer (source code in another repo on my Github), because none of the apps I have downloaded properly rendered <pre> tags (hint: you have to use WebView.loadDataWithBaseUrl instead of WebView.loadData).
Wednesday, February 20, 2013
Guava on Android
In November 2012, the revision 21 of Android SDK Tools was released and one of the items in the release notes made me a very happy panda:
IntelliJ users could work around this issue by enabling Proguard for debug builds, which could reduce the size of dex input by removing unused code. Eclipse users might try generalizing the Treeshaker plugin, which does pretty much the same inside a custom compilation step added before dexing. But there was no straight way to use Guava and keep the build times on the sane level.
Now the first build still takes ages, and the Eclipse still crashes if you don't bump its heap space, but for all consecutive builds everything works blazing fast. Goodbye hand rolled stuff, welcome immutable collections, fluent comparators, hashCode helper and tons of other goodness. I keep finding in our code base whole chunks of code which can be replaced with one or two lines utilizing Guava features. I plan to post a summary of those changes.
Final note: if you are using Proguard, remember to add Guava specific entries mentioned in the documentation.
Improved the build time by pre-dexing libraries (both JAR files and library projects).This change solved the most problematic issue with Guava and other large libraries - build time. Before this change Android tools executed dex for your code and every referenced library every time you wanted to launch the application, which in case of Guava took ages and required increasing heap space for Java VM, because the Eclipse closed with "Unable to execute dex: Java heap space" error.
IntelliJ users could work around this issue by enabling Proguard for debug builds, which could reduce the size of dex input by removing unused code. Eclipse users might try generalizing the Treeshaker plugin, which does pretty much the same inside a custom compilation step added before dexing. But there was no straight way to use Guava and keep the build times on the sane level.
Now the first build still takes ages, and the Eclipse still crashes if you don't bump its heap space, but for all consecutive builds everything works blazing fast. Goodbye hand rolled stuff, welcome immutable collections, fluent comparators, hashCode helper and tons of other goodness. I keep finding in our code base whole chunks of code which can be replaced with one or two lines utilizing Guava features. I plan to post a summary of those changes.
Final note: if you are using Proguard, remember to add Guava specific entries mentioned in the documentation.
Wednesday, January 30, 2013
Android: sync adapter lifecycle
In Android sticky broadcast perils I hinted that the ContentResolver.isSyncActive might not yield the results you'd expect. I described this issue in the talk I gave during the KrakDroid 2012 conference, but the chances are you weren't there, so I decided to write a blog post about it.
ContentResolver contains bunch of static methods with "sync" in their name: there is requestSync to start sync process, isSyncPending and isSyncActive for polling the sync state, addStatusChangeListener for listening for sync status and finally cancelSync for stopping the ongoing synchronization process. The list looks fine, in a sense that theoretically it's enough to implement the most sync-related functionality on the UI side. Let's see what is the relation between sync status reported by ContentResolver's sync methods and onPerformSync method in your SyncAdapter.
After calling requestSync, the sync for a given account and authority is added to the pending list, meaning that the sync will be executed as soon as possible (for example when syncs for other authorities are finished). In this state the isSyncPending returns true, the SyncStatusObservers registered with SYNC_OBSERVER_TYPE_PENDING mask will be triggered, and so on. This happens before your onPerformSync code is executed. Nothing especially surprising yet. The key point here is, you should take into consideration that your sync request might spend a lot of time in this state, especially if many other SyncAdapters are registered in the system. For example, it's a good idea to indicate this state somehow in the UI, otherwise your app might seem unresponsive.
When there are no other pending or active sync requests, your sync operation will move to active state. The onPerformSync will start executing in the background thread, SyncStatusObservers will trigger for both SYNC_OBSERVER_TYPE_ACTIVE (because the sync request enters this state) and SYNC_OBSERVER_TYPE_PENDING (because the sync request leaves this state) masks, isSyncPending will return false, and isSyncActive will return true. In the happy case, when the onPerformSync method will finish normally, the SyncStatusObservers for SYNC_OBSERVER_TYPE_ACTIVE state will trigger again, and isSyncActive will return false again. Booring.
The things get funny when the cancelSync is called during onPerformSync execution. The sync thread will be interrupted and the onSyncCancelled method in SyncAdapter will be called. The SyncStatusObservers will trigger, isSyncActive will return false and so on, and... at some point the onPerformSync method will finish execution.
Say what? Wasn't the sync thread interrupted? It was, but not in a "Bang, you're dead" way, but in a "polite" way as described by Herb Sutter. All the stuff described in the Thread.interrupt happened, but in 99% of cases it means that the thread continues to execute as usual, except the interrupted flag is now set. To really support cancelling the sync thread you'd have to define an interruption points at which you'll check this flag and return early from onPerformSync.
Things get even funnier here: when I used the isInterrupted method for polling the state of the sync thread, I got the bad case of heisenbug. In 9 cases out of 10 everything worked as expected, but every now and then the thread continued to execute even though earlier the onSyncCancelled was called. I guess somewhere else the InterruptedException was caught and never rethrown or someone else was polling the sync thread with interrupted and cleared the flag. To pinpoint the root cause of this behavior I'd have to read through a lot of code, so instead I implemented my own flag and set it in onSyncCancelled callback. Works like a charm.
Why is this an issue though? Can't we just let onPerformSync to finish in some undefined future? In most cases that's exactly the right way to think about this issue, but if the onPerformSync holds a lock on some resource like database handle, you might need to ensure that this lock is released as soon as possible after user cancels the sync.
Recap: show the sync pending state in the UI and if you really have to know when the sync has ended, do not trust the ContentResolver sync methods.
ContentResolver contains bunch of static methods with "sync" in their name: there is requestSync to start sync process, isSyncPending and isSyncActive for polling the sync state, addStatusChangeListener for listening for sync status and finally cancelSync for stopping the ongoing synchronization process. The list looks fine, in a sense that theoretically it's enough to implement the most sync-related functionality on the UI side. Let's see what is the relation between sync status reported by ContentResolver's sync methods and onPerformSync method in your SyncAdapter.
After calling requestSync, the sync for a given account and authority is added to the pending list, meaning that the sync will be executed as soon as possible (for example when syncs for other authorities are finished). In this state the isSyncPending returns true, the SyncStatusObservers registered with SYNC_OBSERVER_TYPE_PENDING mask will be triggered, and so on. This happens before your onPerformSync code is executed. Nothing especially surprising yet. The key point here is, you should take into consideration that your sync request might spend a lot of time in this state, especially if many other SyncAdapters are registered in the system. For example, it's a good idea to indicate this state somehow in the UI, otherwise your app might seem unresponsive.
When there are no other pending or active sync requests, your sync operation will move to active state. The onPerformSync will start executing in the background thread, SyncStatusObservers will trigger for both SYNC_OBSERVER_TYPE_ACTIVE (because the sync request enters this state) and SYNC_OBSERVER_TYPE_PENDING (because the sync request leaves this state) masks, isSyncPending will return false, and isSyncActive will return true. In the happy case, when the onPerformSync method will finish normally, the SyncStatusObservers for SYNC_OBSERVER_TYPE_ACTIVE state will trigger again, and isSyncActive will return false again. Booring.
The things get funny when the cancelSync is called during onPerformSync execution. The sync thread will be interrupted and the onSyncCancelled method in SyncAdapter will be called. The SyncStatusObservers will trigger, isSyncActive will return false and so on, and... at some point the onPerformSync method will finish execution.
Say what? Wasn't the sync thread interrupted? It was, but not in a "Bang, you're dead" way, but in a "polite" way as described by Herb Sutter. All the stuff described in the Thread.interrupt happened, but in 99% of cases it means that the thread continues to execute as usual, except the interrupted flag is now set. To really support cancelling the sync thread you'd have to define an interruption points at which you'll check this flag and return early from onPerformSync.
Things get even funnier here: when I used the isInterrupted method for polling the state of the sync thread, I got the bad case of heisenbug. In 9 cases out of 10 everything worked as expected, but every now and then the thread continued to execute even though earlier the onSyncCancelled was called. I guess somewhere else the InterruptedException was caught and never rethrown or someone else was polling the sync thread with interrupted and cleared the flag. To pinpoint the root cause of this behavior I'd have to read through a lot of code, so instead I implemented my own flag and set it in onSyncCancelled callback. Works like a charm.
Why is this an issue though? Can't we just let onPerformSync to finish in some undefined future? In most cases that's exactly the right way to think about this issue, but if the onPerformSync holds a lock on some resource like database handle, you might need to ensure that this lock is released as soon as possible after user cancels the sync.
Recap: show the sync pending state in the UI and if you really have to know when the sync has ended, do not trust the ContentResolver sync methods.
Subscribe to:
Posts (Atom)





