Android Development

Android AccountManager - Handling the deprecation of removeAccount() in API 22

February 27, 2018

The Situation

In Android API level 22 (Lollipop MR1), the method

AccountManager.removeAccount(Account, AccountManagerCallback<Boolean>, Handler) // old method

was deprecated. The SDK documentation says to instead use this method,

AccountManager.removeAccount(Account, Activity, AccountManagerCallback<Bundle>, Handler) // new method

added in the same API level.

AccountManager.removeAccount() is a method used to remove an account tracked by the Android OS for your app, and needs to be called when the user logs out of their account in your app.

You’ll want to be a prudent developer and migrate away from deprecated methods, but if your minSdkVersion is less than 22, you’ll need to resolve this seemingly large discrepancy between these two AccountManager methods. To accomplish this, we’ll start with a draft of a utility method that will call one or the other depending on what version of Android your app is running in, with an argument signature that matches the new version of the removeAccount() method.

public static void removeAccount(
 Account account,
 Activity activity,
 final AccountManagerCallback<Bundle> callback,
 Handler handler
) {
 AccountManager accountManager = AccountManager.get(activity);

 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
 // API 22 and above
 accountManager.removeAccount(account, activity, callback, handler);
 } else {
 // API 21 and below
 accountManager.removeAccount(account, /* AccountManagerCallback<Boolean> */, handler);
 // we need to figure out what to do for the second argument
 }
}

The Discrepancy

The big difference between these two methods is the AccountManagerCallback<boolean></boolean> and AccountManagerCallback<bundle></bundle> arguments, both callbacks that asynchronously return an AccountManagerFuture<!--?-->. In the case of the former,

new AccountManagerCallback<Boolean>() {
 @Override
 public void run(AccountManagerFuture<Boolean> future) {
 // get the success status of the account manager operation from `future`
 Boolean result = future.getResult();

 // do something with the result...
 }
}

How do we go from this Boolean to a Bundle? The answer lies in what the new removeAccount() method returns in its AccountManagerCallback<bundle></bundle> callback. If you were to execute the new removeAccount() method, you would find that the AccountManagerCallback<bundle></bundle> asynchronously returns a Bundle that contains AccountManager.KEY_BOOLEAN_RESULT and a Boolean as a key/value pair. The Boolean value returned indicates the success or failure of the account removal operation. We can take the Boolean result from the old callback and adapt it to the new callback.

public static void removeAccount(
 Account account,
 Activity activity,
 final AccountManagerCallback<Bundle> callback,
 Handler handler
) {
 AccountManager accountManager = AccountManager.get(activity);

 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
 // API 22 and above
 accountManager.removeAccount(account, activity, callback, handler);
 } else {
 // API 21 and below
 accountManager.removeAccount(
 account,
 new AccountManagerCallback<Boolean>() {
 @Override
 public void run(final AccountManagerFuture<Boolean> future) {
 // call our AccountManagerCallback<Bundle> callback and put the Boolean result from `future`
 // into a Bundle in the form that the system is expecting
 callback.run(new AccountManagerFuture<Bundle>() {
 @Override
 public Bundle getResult()
 throws OperationCanceledException, IOException, AuthenticatorException
 {
 Bundle result = new Bundle();
 result.putBoolean(AccountManager.KEY_BOOLEAN_RESULT, future.getResult());
 return result;
 }
 });
 }
 },
 handler
 );
 }
}

The AccountManagerCallback<Boolean> interface has a few other methods that also need to be implemented and adapted to an AccountManagerCallback<Bundle> callback.

new AccountManagerCallback<Boolean>() {
 @Override
 public void run(final AccountManagerFuture<Boolean> future) {
 if (callback == null) {
 return;
 }

 callback.run(new AccountManagerFuture<Bundle>() {
 @Override
 public boolean cancel(boolean mayInterruptIfRunning) {
 return future.cancel(mayInterruptIfRunning);
 }

 @Override
 public boolean isCancelled() {
 return future.isCancelled();
 }

 @Override
 public boolean isDone() {
 return future.isDone();
 }

 @Override
 public Bundle getResult()
 throws OperationCanceledException, IOException, AuthenticatorException
 {
 Bundle result = new Bundle();
 result.putBoolean(AccountManager.KEY_BOOLEAN_RESULT, future.getResult());
 return result;
 }

 @Override
 public Bundle getResult(long timeout, TimeUnit unit)
 throws OperationCanceledException, IOException, AuthenticatorException
 {
 Bundle result = new Bundle();
 result.putBoolean(AccountManager.KEY_BOOLEAN_RESULT, future.getResult(timeout, unit));
 return result;
 }
 });
 }
}

We’ve taken the results of the old-style callback and “forwarded” them to the new-style callback. Since the future.getResult() method can throw OperationCanceledException, IOException, and AuthenticatorException, we’ll “forward” them by passing them up to be handled by the new-style callback.

Putting it all together

Putting all of this together results in a utility method that resolves the discrepancy between the old and new way of removing an account from the system.

/*
 * In API 22 (LOLLIPOP_MR1),
 * AccountManager.removeAccount(Account, Activity, AccountManagerCallback<Bundle>, Handler) was added, and
 * AccountManager.removeAccount(Account, AccountManagerCallback<Boolean>, Handler) was deprecated.
 *
 * To resolve this when running on a pre-22 device, call the old .removeAccount(), get the Boolean result,
 * put it in a Bundle using key AccountManager.KEY_BOOLEAN_RESULT,
 * then call AccountManagerCallback<Bundle> callback.run() with a new AccountManagerFuture<Bundle>
 * that returns this Bundle.
 */
public static void removeAccount(
 @NonNull Account account,
 @NonNull Activity activity,
 @Nullable final AccountManagerCallback<Bundle> callback,
 @Nullable Handler handler
) {
 AccountManager accountManager = AccountManager.get(activity);

 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
 accountManager.removeAccount(account, activity, callback, handler);
 } else {
 //noinspection deprecation - we don't need our IDE to warn us that this is deprecated
 accountManager.removeAccount(
 account,
 new AccountManagerCallback<Boolean>() {
 @Override
 public void run(final AccountManagerFuture<Boolean> future) {
 if (callback == null) {
 return;
 }

 callback.run(new AccountManagerFuture<Bundle>() {
 @Override
 public boolean cancel(boolean mayInterruptIfRunning) {
 return future.cancel(mayInterruptIfRunning);
 }

 @Override
 public boolean isCancelled() {
 return future.isCancelled();
 }

 @Override
 public boolean isDone() {
 return future.isDone();
 }

 @Override
 public Bundle getResult()
 throws OperationCanceledException, IOException, AuthenticatorException
 {
 Bundle result = new Bundle();
 result.putBoolean(AccountManager.KEY_BOOLEAN_RESULT, future.getResult());
 return result;
 }

 @Override
 public Bundle getResult(long timeout, TimeUnit unit)
 throws OperationCanceledException, IOException, AuthenticatorException
 {
 Bundle result = new Bundle();
 result.putBoolean(AccountManager.KEY_BOOLEAN_RESULT, future.getResult(timeout, unit));
 return result;
 }
 });
 }
 },
 handler
 );
 }
}

You can find this code in this Github gist.

Now when you need to remove an account, you can just call

removeAccount(
 account,
 activity,
 new AccountManagerCallback<Bundle>() {
 @Override
 public void run(AccountManagerFuture<Bundle> future) {
 try {
 Bundle result = future.getResult();
 boolean success = result.getBoolean(AccountManager.KEY_BOOLEAN_RESULT);

 if (success) {
 // the account was successfully removed
 } else {
 // failed to remove the account
 }
 } catch (OperationCanceledException | IOException | AuthenticatorException e) {
 // something went wrong
 }
 }
 },
 null
);

Let’s do it with Java 8 Lambdas

If you’re using the latest version of the Android Gradle plugin (as of this writing) in your build.gradle file

buildscript {
 ...

 dependencies {
 classpath 'com.android.tools.build:gradle:3.0.1'
 ...
 }
}

and using Java 8 source compatibility

compileOptions {
 sourceCompatibility JavaVersion.VERSION_1_8
 targetCompatibility JavaVersion.VERSION_1_8
}

then you can write the same method using lambdas.

public static void removeAccount(
 @NonNull Account account,
 @NonNull Activity activity,
 @Nullable final AccountManagerCallback<Bundle> callback,
 @Nullable Handler handler
) {
 AccountManager accountManager = AccountManager.get(activity);

 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
 accountManager.removeAccount(account, activity, callback, handler);
 } else {
 //noinspection deprecation - we don't need our IDE to warn us that this is deprecated
 accountManager.removeAccount(
 account,
 future -> {
 if (callback == null) {
 return;
 }

 callback.run(new AccountManagerFuture<Bundle>() {
 @Override
 public boolean cancel(boolean mayInterruptIfRunning) {
 return future.cancel(mayInterruptIfRunning);
 }

 @Override
 public boolean isCancelled() {
 return future.isCancelled();
 }

 @Override
 public boolean isDone() {
 return future.isDone();
 }

 @Override
 public Bundle getResult()
 throws OperationCanceledException, IOException, AuthenticatorException {
 Bundle result = new Bundle();
 result.putBoolean(AccountManager.KEY_BOOLEAN_RESULT, future.getResult());
 return result;
 }

 @Override
 public Bundle getResult(long timeout, TimeUnit unit)
 throws OperationCanceledException, IOException, AuthenticatorException {
 Bundle result = new Bundle();
 result.putBoolean(AccountManager.KEY_BOOLEAN_RESULT, future.getResult(timeout, unit));
 return result;
 }
 });
 },
 handler
 );
 }
}

Let’s do it in Kotlin

This utility method is a perfect candidate for a Kotlin extension method on AccountManager.

/*
 * In API 22 (LOLLIPOP_MR1),
 * AccountManager.removeAccount(Account, Activity, AccountManagerCallback<Bundle>, Handler) was added, and
 * AccountManager.removeAccount(Account, AccountManagerCallback<Boolean>, Handler) was deprecated.
 *
 * To resolve this when running on a pre-22 device, call the old .removeAccount(), get the Boolean result,
 * put it in a Bundle using key AccountManager.KEY_BOOLEAN_RESULT,
 * then call AccountManagerCallback<Bundle> callback.run() with a new AccountManagerFuture<Bundle>
 * that returns this Bundle.
 */
fun AccountManager.removeAccount(
 account: Account,
 activity: Activity? = null,
 handler: Handler? = null,
 callback: (AccountManagerFuture<Bundle>) -> Unit
) {
 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
 removeAccount(account, activity, callback, handler)
 } else {
 @Suppress("DEPRECATION")
 removeAccount(
 account,
 { future ->
 callback(object : AccountManagerFuture<Bundle> {
 override fun cancel(mayInterruptIfRunning: Boolean): Boolean {
 return future.cancel(mayInterruptIfRunning)
 }

 override fun isCancelled(): Boolean {
 return future.isCancelled
 }

 override fun isDone(): Boolean {
 return future.isDone
 }

 override fun getResult(): Bundle {
 val bundle = Bundle()
 bundle.putBoolean(AccountManager.KEY_BOOLEAN_RESULT, future.getResult())
 return bundle
 }

 override fun getResult(timeout: Long, unit: TimeUnit): Bundle {
 val bundle = Bundle()
 bundle.putBoolean(AccountManager.KEY_BOOLEAN_RESULT, future.getResult(timeout, unit))
 return bundle
 }
 })
 },
 handler
 )
 }
}

You can find this code in this Github gist. The Kotlin version is courtesy of my co-workers Josh Friend and Scott Schmitz.

Joseph Kreiser
Joseph Kreiser
Software Developer

Looking for more like this?

Sign up for our monthly newsletter to receive helpful articles, case studies, and stories from our team.

The 5 Minute Accessibility Strategy
Android Development iOS

The 5 Minute Accessibility Strategy

May 18, 2023

We discuss how you can make a plan in just 5 minutes to provide accessibility in your mobile app.

Read more
Quickly Prototyping a Ktor HTTP API
Development Web

Quickly Prototyping a Ktor HTTP API

August 18, 2022

Whether it’s needing a quick REST API for a personal project, or quickly prototyping a mockup for a client, I like to look for web server frameworks that help me get up and running with minimal configuration and are easy to use. I recently converted a personal project’s API from an Express web server to a Ktor web server and it felt like a breeze. I’ll share below an example of what I found and how easy it is to get a Ktor server up and running.

Read more
Lessons Learned from our Associate Developer
Team

Lessons Learned from our Associate Developer

September 13, 2023

One of our Associate Software Developers, Rohit, reflects on his time at MichiganLabs working on a short-term project, what he learned about real-world development, and the software consultancy business model.

Read more
View more articles