So, my question restated is when you go to Settings -> Accounts & Sync and select the an account that was created that your SyncAdapter is syncing with a cloud server, and select remove account, what happens as far as your SyncAdapter is concerned? There is a dialog that displays asking you to confirm and that the data on the phone associated with that account will be removed. I cannot easily believe that the framework can automatically remove the data my SyncAdapter has stored in the local database, but it seems to imply that removing the account will (and I would agree that is should) remove that data. Is there a addition to my SyncAdapter that will serve sort of as the callback for the account removal to handle deleting all the appropriate data from the local database? Maybe it has to be done through the AccountManager instead; my AccountManager gets notified when the account gets removed and from there I can trigger the data deletion without the SyncAdapter.
EDIT:
On a related note, is the sync manager calling my SyncAdapter for each account that it synchronizes when a new account is added? I see a onPerformSync(...) being executed for previously added accounts along with the just added account when I add an account, and would like to stop that.
I discovered the solution is to make the app's ContentProvider implement OnAccountsUpdateListener. Attach the ContentProvider as a listener in its onCreate method with account_manager.addOnAccountsUpdatedListener(this, null, false) and then implement the interface method like
#Override
public void onAccountsUpdated(final Account[] accounts) {
Ln.i("Accounts updated.");
final Iterable<String> account_list = new Iterable<String>() {
#Override
public Iterator<String> iterator() {
return new Iterator<String>() {
private final Iterator<Account> account_list = Arrays.asList(accounts).iterator();
#Override
public boolean hasNext() {
return account_list.hasNext();
}
/** Extracts the next account name and wraps it in single quotes. */
#Override
public String next() {
return "'" + account_list.next().name + "'";
}
#Override
public void remove() { throw new UnsupportedOperationException("Not implemented"); }
};
}
};
final String account_set = TextUtils.join(", ", account_list);
Ln.i("Current accounts: %s", account_set);
// Removes content that is associated with accounts that are not currently connected
final SelectionBuilder builder = new SelectionBuilder();
builder.table(Tables.CALENDARS)
.where(Calendars.CALENDAR_USER + " NOT IN (?)", account_set);
new SafeAsyncTask() {
#Override
public Void call() throws Exception {
_model.openWritableDatabase();
_model.delete(builder);
return null;
}
}.execute();
getContext().getContentResolver().notifyChange(Calendars.NO_SYNC_URI, null, false);
}
I construct a String of the currently connected accounts, then build a SQL query with that String. I perform a delete on the database in a background thread on that query to remove the data associated with accounts not currently connected. And I notify that content changed, but does not need to synchronized with the server.
No, but your Authenticator does[1]. This method is called before the account is removed:
AbstractAccountAuthenticator.getAccountRemovalAllowed(AccountAuthenticatorResponse, Account)
the Account param is the account being deleted - the default behaviour is to allow removal of the account:
return super.getAccountRemovalAllowed(response, account); // returns Bundle[{booleanResult=true}]
..but I guess it's a hook that you can use to tidy things up or block the account being removed should you wish to.
[1] - this is a dirty hack; please see Dandre's comment.
Another option is to register for the android.accounts.LOGIN_ACCOUNTS_CHANGED broadcast that the AccountManager sends out. Unfortunately, this broadcast is sent out whenever any account is changed and the broadcast does not deliver further information what has changed either.
So you'd have to query the account manager and look how many of "your" accounts it has left and delete the data of the missing ones.
Related
I am storing user FCM device tokens in Firebase. When the user logs in, the token is added to the user's profile like this:
if (FirebaseAuth.getInstance().getCurrentUser()!=null) {
FirebaseInstanceId.getInstance().getInstanceId().addOnSuccessListener(new OnSuccessListener<InstanceIdResult>() {
#Override
public void onSuccess(InstanceIdResult instanceIdResult) {
DeviceToken token = new DeviceToken(instanceIdResult.getToken());
CollectionReference deviceTokens = mUserCollection.document(mSignedInUserID).collection("device_tokens");
deviceTokens.document(token.getTokenID()).set(token);
}
});
}
This works. However, I also want to delete that document when the user signs out. I am attempting to do so like this:
FirebaseInstanceId.getInstance().getInstanceId().addOnSuccessListener(new OnSuccessListener<InstanceIdResult>() {
#Override
public void onSuccess(InstanceIdResult instanceIdResult) {
Log.d(TAG,instanceIdResult.getToken());
DocumentReference deactivatedToken = mUserCollection.document(mSignedInUserID).collection("device_tokens").document(instanceIdResult.getToken());
deactivatedToken.delete();
mAuth.signOut();
recreate();
}
});
Everything works in that method except for the actual deletion of that document, and the log statement confirms that the user's current ID matches the title of the document to be deleted. A simulation for a signed in user writing to that location returns allowed. What am I doing wrong?
Note that, with your code, the user is going to get signed out before the document is deleted. That's because the delete() is asynchronous (as well as all database operations), and returns immediately, before the work is complete. If I had to guess, I'd say that your authentication token is getting wiped out before the delete operation actually gets sent, and the delete is effectively acting as an unauthenticated user. So, what you should do is wait for the delete to complete for actually signing out the user. Use the Task returned by delete() to know when that finishes. It'll work the same way as the Task returned by getInstanceId().
I am implementing AWS with an Android application for the first time.
We would like to use Cognito to authenticate our users, and selectively provide data from DynamoDB.
I have successfully set up my user pool and can see new registrations appear in the user list. Trying to login with an email that does not exist fails.
However, Cognito always logs in with a valid email address, regardless of password input.
What is wrong with my process?
public class CognitoController extends Application {
static CognitoUserPool pool;
static String userEmail;
public void onCreate(){
super.onCreate();
pool = new CognitoUserPool(this,
"us-east-xxxx",
"xxxx",
"xxxx",
new ClientConfiguration(),
Regions.US_EAST_1);
}
}
-
private void actionAdminLogin(){
UtilityInterfaceTools.hideSoftKeyboard(AdminLoginActivity.this);
String inputEmail = ((EditText) findViewById(R.id.input_admin_email)).getText().toString();
String inputPassword = ((EditText) findViewById(R.id.input_admin_password)).getText().toString();
CognitoController.userEmail = inputEmail;
details = new AuthenticationDetails(inputEmail, inputPassword, null);
AuthenticationHandler auther = new AuthenticationHandler() {
#Override
public void onSuccess(CognitoUserSession userSession, CognitoDevice newDevice) {
Toast.makeText(AdminLoginActivity.this, "Congratulations It Works...", Toast.LENGTH_LONG).show();
startActivity(new Intent(AdminLoginActivity.this, AdminPortalActivity.class));
finish();
}
#Override
public void getAuthenticationDetails(AuthenticationContinuation continuation, String email) {
continuation.setAuthenticationDetails(details);
continuation.continueTask();
}
#Override
public void getMFACode(MultiFactorAuthenticationContinuation continuation) {
continuation.continueTask();
}
#Override
public void authenticationChallenge(ChallengeContinuation continuation) {
continuation.continueTask();
}
#Override
public void onFailure(Exception exception) {
TextView errorMessage = findViewById(R.id.message_invalid_credentials);
errorMessage.setText(exception.toString());
errorMessage.setVisibility(View.VISIBLE);
}
};
CognitoController.pool.getUser(inputEmail).getSessionInBackground(auther);
}
I think your problem (which is not a problem by the way) is either:
In your pool Cognito setting, you chose your devices to be remembered.
Remembered
devices are also tracked. During user authentication, the key and secret pair assigned to a remembered device is used to authenticate the device to verify that it is the same device that the user previously used to sign in to the application. APIs to see remembered devices have been added to new releases of the Android, iOS, and JavaScript SDKs. You can also see remembered devices from the Amazon Cognito console.
The token is already cached:
Caching
The Mobile SDK for Android caches the last successfully authenticated user and the user's tokens locally on the device, in SharedPreferences. The SDK also provides methods to get the last successfully authenticated user.
Your Application Update
In fact for better user experience, you want the user to use the app, and don't need to login every time that she wants to use your app (e.g., look at mail apps, social media apps, etc.). However, you application need to handle that, you have two choices here:
Redirect to login if necessary: If the user is already logged in and wants to use the application again, your app needs to verify the user against the Cognito user pool, and only then, redirect the user to the login page if necessary.
Remove the token: If you really want the user to login every time that she uses the application, then remove the token if the user signs out; but I do not recommend this, for the sake of user experience.
According to Firebase cloud messaging documentation, for subscribing a user to a topic I need to call
FirebaseMessaging.getInstance().subscribeToTopic("news");
In my application, I need all users to be subscribed to my cloud
messaging topic. Since return value is void, the question is how
can I understand that subscription was successful?
Is it a bad practice to call subscribeToTopic each time my
application starts?
1. How can I understand that subscription was successful?
Edit:
You could now check if subscription is successful by adding addOnSuccessListener()
FirebaseMessaging.getInstance().subscribeToTopic("news").addOnSuccessListener(new OnSuccessListener<Void>() {
#Override
public void onSuccess(Void aVoid) {
Toast.makeText(getApplicationContext(),"Success",Toast.LENGTH_LONG).show();
}
});
Original:
There is nothing explicitly mentioned in the docs about a response received when the subscription is successful.
However, if you need to mandate all of your users to be subscribed to a specific topic, you should call the subscribeToTopic on your app's first install. This will most likely make sure that there is a connection to the internet (since it's probably been downloaded and installed via the Play Store) and the subscription successful.
However, if you want to make sure, you can also handle he checking via your own App Server. As mentioned in the docs:
You can take advantage of Instance ID APIs to perform basic topic management tasks from the server side. Given the registration token(s) of client app instances, you can do the following:
Find out details about a client app instance's subscriptions, including each topic name and subscribe date. See Get information about app instances.
Check through the registration tokens, if they haven't been successfully subsribed to your topic, send a notification to it where it will trigger your client app to call subscribeToTopic.
2. Is it a bad practice to call subscribeToTopic each time my application starts?
Edit: Adding it in from the comments section: Subscribing on app start should be fine.
Thank you #FrankvanPuffelen for verifying. :)
I have written this function and tested. May be helpful.
private void subscribeToMessaging(){
SharedPreferences prefs = getSharedPreferences(SETTINGS_TITLE, MODE_PRIVATE);
// Getting value from shared preferences
boolean isSubscriptionEnable = prefs.getBoolean(SETTING_NOTIFICATION, true);
// if "isSubscriptionEnable" is true then check whether its already subscribed or not
if (isSubscriptionEnable){
boolean alreadySubscribed = prefs.getBoolean(SETTING_ALREADY_SUBSCRIBED, false);
// if not already subscribed then subscribe to topic and save value to shared preferences
if (!alreadySubscribed){
FirebaseMessaging.getInstance().subscribeToTopic("global").addOnSuccessListener(new OnSuccessListener<Void>() {
#Override
public void onSuccess(Void aVoid) {
Toast.makeText(getApplicationContext(),"Success",Toast.LENGTH_LONG).show();
}
});
SharedPreferences.Editor editor = getSharedPreferences(SETTINGS_TITLE, MODE_PRIVATE).edit();
editor.putBoolean(SETTING_ALREADY_SUBSCRIBED, true);
editor.apply();
Toast.makeText(this, "Subscribed", Toast.LENGTH_LONG).show();
} else {
Toast.makeText(this, "Already subscribed", Toast.LENGTH_LONG).show();
}
}
}
Don't forget to write these lines above onCreate()
public static final String SETTINGS_TITLE = "settings";
public static final String SETTING_NOTIFICATION = "notification_state";
public static final String SETTING_ALREADY_SUBSCRIBED = "already_subscribed";
i'm developing an android application with accounts, sync and content provider. adding an account works, syncing also and some data is saved in the content provider.
now, when the user deletes the account using the settings, syncing stops, but the data stays in the content provider.
i'd like to delete it, but i don't know how to catch the event of account deletion.
There is AccountManager.addOnAccountsUpdatedListener(), i've tried to add it to the sync service, but the sync service is started only for the sync and then stopped. so whenever the account gets deleted while there is no sync, it can't get caught.
is there a best practice on how to handle private data when an account gets deleted?
First You should add OnAccountsUpdateListener to you AccountManager using this command:
AccountManager mAccountMgr = AccountManager.get(getContext());
mAccountMgr.addOnAccountsUpdatedListener(new AccountsUpdateListener(), null, false);
AccountsUpdateListener is implemented class of OnAccountsUpdateListener like this:
private class AccountsUpdateListener implements OnAccountsUpdateListener {
#Override
public void onAccountsUpdated(Account[] accounts) {
Account newAccount = null;
for (final Account account : accounts) {
if (account.type.equals(mAccountType)) {
newAccount = account;
}
}
if (newAccount == null) {
// account removed, now you can handle your private data and remove anything you want here
}
}
}
onAccountsUpdated fired when you add an account or an account removed. so you can check your account type to find specified account in accounts array. if it doesn't exist it removed !
mAccountType is your account type. e.g mAccountType = "your application name"
You should use method getAccountRemovalAllowed from your AbstactAccountAuthenticator:
class AccountAuthenticatorImpl(context: Context) : AbstractAccountAuthenticator(context) {
override fun getAccountRemovalAllowed(
response: AccountAuthenticatorResponse?,
account: Account?
): Bundle {
val result = super.getAccountRemovalAllowed(response, account)
val canDelete = result.getBoolean(android.accounts.AccountManager.KEY_BOOLEAN_RESULT, false)
if (canDelete) {
// TODO: account was deleted, so react on it
accountRepository.onAccountDeletedInSystem()
}
return result
}
}
I'm struggling to understand the Android AccountManager API. As far as I got thinks working I can use the blockingGetAuthToken method and specify whether Android should provide a notification for user to allow or deny the request. Another possibility is to use getAuthToken and check if KEY_INTENT is returned. If that's the case I could start a new Activity where the user can confirm my request.
My problem is that I would like to call one of these two methods from within a Service. Is there any chance to get a callback once the user has made a decision?
Thanks for your help
If you want a callback after the user has made a decision it's probably better to use the asynchronous version:
AccountManager mgr = AccountManager.get(getApplicationContext());
Account[] accounts = mgr.getAccountsByType("com.mydomain");
// assert that accounts is not empty
You'll want to use an AccountManagerFuture<Bundle> to hold results of the authentication token. This has to be async since the Android device may ask the user to login in the meantime:
private AccountManagerFuture<Bundle> myFuture = null;
private AccountManagerCallback<Bundle> myCallback = new AccountManagerCallback<Bundle>() {
#Override public void run(final AccountManagerFuture<Bundle> arg0) {
try {
myFuture.getResult().get(AccountManager.KEY_AUTHTOKEN); // this is your auth token
} catch (Exception e) {
// handle error
}
}
}
Now you can ask for the auth token asynchronously:
myFuture = mgr.getAuthToken(accounts[0], AUTH_TOKEN_TYPE, true, myCallback, null);