Bug 790931 - Broadcast when Sync Android Account is being deleted to many Firefox Apps. r=rnewman

This commit is contained in:
Nick Alexander 2012-09-28 09:40:51 -07:00
Родитель dcad379d15
Коммит 86b4e88932
11 изменённых файлов: 302 добавлений и 184 удалений

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Просмотреть файл

@ -18,4 +18,33 @@ public class GlobalConstants {
public static final String BROWSER_INTENT_CLASS = BROWSER_INTENT_PACKAGE + ".App";
public static final String ACCOUNTTYPE_SYNC = "@MOZ_ANDROID_SHARED_ACCOUNT_TYPE@";
/**
* Bug 790931: this signing-level permission protects broadcast intents that
* should be received only by Firefox versions sharing the same Android
* Account type.
*/
public static final String PER_ACCOUNT_TYPE_PERMISSION = "@MOZ_ANDROID_SHARED_ACCOUNT_TYPE@.permission.PER_ACCOUNT_TYPE";
/**
* Bug 790931: this action is broadcast when an Android Account is deleted.
* This allows each installed Firefox to delete any pickle file and to (try
* to) wipe its client record from the server.
* <p>
* It is protected by signing-level permission PER_ACCOUNT_TYPE_PERMISSION and
* can be received only by Firefox versions sharing the same Android Account
* type.
* <p>
* See {@link SyncAccounts#makeSyncAccountDeletedIntent(android.content.Context, android.accounts.AccountManager, android.accounts.Account)}
* for contents of the intent.
*/
public static final String SYNC_ACCOUNT_DELETED_ACTION = "@MOZ_ANDROID_SHARED_ACCOUNT_TYPE@.accounts.SYNC_ACCOUNT_DELETED_ACTION";
/**
* Bug 790931: version number of contents of SYNC_ACCOUNT_DELETED_ACTION intent.
* <p>
* See {@link SyncAccounts#makeSyncAccountDeletedIntent(android.content.Context, android.accounts.AccountManager, android.accounts.Account)}
* for contents of the intent.
*/
public static final long SYNC_ACCOUNT_DELETED_INTENT_VERSION = 1;
}

Просмотреть файл

@ -0,0 +1,33 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.sync.receivers;
import org.mozilla.gecko.sync.Logger;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
public class SyncAccountDeletedReceiver extends BroadcastReceiver {
public static final String LOG_TAG = "SyncAccountDeletedReceiver";
/**
* This receiver can be killed as soon as it returns, but we have things to do
* that can't be done on the main thread (network activity). Therefore we
* start a service to do our clean up work for us, with Android doing the
* heavy lifting for the service's lifecycle.
* <p>
* See <a href="http://developer.android.com/reference/android/content/BroadcastReceiver.html#ReceiverLifecycle">the Android documentation</a>
* for details.
*/
@Override
public void onReceive(final Context context, Intent broadcastIntent) {
Logger.debug(LOG_TAG, "Sync Account Deleted broadcast received.");
Intent serviceIntent = new Intent(context, SyncAccountDeletedService.class);
serviceIntent.putExtras(broadcastIntent);
context.startService(serviceIntent);
}
}

Просмотреть файл

@ -0,0 +1,137 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.sync.receivers;
import org.mozilla.gecko.sync.ExtendedJSONObject;
import org.mozilla.gecko.sync.GlobalConstants;
import org.mozilla.gecko.sync.Logger;
import org.mozilla.gecko.sync.SyncConfiguration;
import org.mozilla.gecko.sync.Utils;
import org.mozilla.gecko.sync.config.AccountPickler;
import org.mozilla.gecko.sync.config.ClientRecordTerminator;
import org.mozilla.gecko.sync.setup.Constants;
import org.mozilla.gecko.sync.setup.SyncAccounts.SyncAccountParameters;
import android.accounts.AccountManager;
import android.app.IntentService;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
public class SyncAccountDeletedService extends IntentService {
public static final String LOG_TAG = "SyncAccountDeletedService";
public SyncAccountDeletedService() {
super(LOG_TAG);
}
@Override
protected void onHandleIntent(Intent intent) {
final Context context = this;
long intentVersion = intent.getLongExtra(Constants.JSON_KEY_VERSION, 0);
long expectedVersion = GlobalConstants.SYNC_ACCOUNT_DELETED_INTENT_VERSION;
if (intentVersion != expectedVersion) {
Logger.warn(LOG_TAG, "Intent malformed: version " + intentVersion + " given but version " + expectedVersion + "expected. " +
"Not cleaning up after deleted Account.");
return;
}
String accountName = intent.getStringExtra(Constants.JSON_KEY_ACCOUNT); // Android Account name, not Sync encoded account name.
if (accountName == null) {
Logger.warn(LOG_TAG, "Intent malformed: no account name given. Not cleaning up after deleted Account.");
return;
}
// Delete the Account pickle.
Logger.info(LOG_TAG, "Sync account named " + accountName + " being removed; " +
"deleting saved pickle file '" + Constants.ACCOUNT_PICKLE_FILENAME + "'.");
deletePickle(context);
SyncAccountParameters params;
try {
String payload = intent.getStringExtra(Constants.JSON_KEY_PAYLOAD);
if (payload == null) {
Logger.warn(LOG_TAG, "Intent malformed: no payload given. Not deleting client record.");
return;
}
ExtendedJSONObject o = ExtendedJSONObject.parseJSONObject(payload);
params = new SyncAccountParameters(context, AccountManager.get(context), o);
} catch (Exception e) {
Logger.warn(LOG_TAG, "Got exception fetching account parameters from intent data; not deleting client record.");
return;
}
// Bug 770785: delete the Account's client record.
Logger.info(LOG_TAG, "Account named " + accountName + " being removed; " +
"deleting client record from server.");
deleteClientRecord(context, accountName, params.password, params.serverURL);
}
public static void deletePickle(final Context context) {
try {
AccountPickler.deletePickle(context, Constants.ACCOUNT_PICKLE_FILENAME);
} catch (Exception e) {
// This should never happen, but we really don't want to die in a background thread.
Logger.warn(LOG_TAG, "Got exception deleting saved pickle file; ignoring.", e);
}
}
public static void deleteClientRecord(final Context context, final String accountName,
final String password, final String serverURL) {
String encodedUsername;
try {
encodedUsername = Utils.usernameFromAccount(accountName);
} catch (Exception e) {
Logger.warn(LOG_TAG, "Got exception deleting client record from server; ignoring.", e);
return;
}
if (accountName == null || encodedUsername == null || password == null || serverURL == null) {
Logger.warn(LOG_TAG, "Account parameters were null; not deleting client record from server.");
return;
}
// This is not exactly modular. We need to get some information about
// the account, namely the current clusterURL and client GUID, and we
// extract it by hand. We're not worried about the Account being
// deleted out from under us since the prefs remain even after Account
// deletion.
final String product = GlobalConstants.BROWSER_INTENT_PACKAGE;
final String profile = Constants.DEFAULT_PROFILE;
final long version = SyncConfiguration.CURRENT_PREFS_VERSION;
SharedPreferences prefs;
try {
prefs = Utils.getSharedPreferences(context, product, encodedUsername, serverURL, profile, version);
} catch (Exception e) {
Logger.warn(LOG_TAG, "Caught exception fetching preferences; not deleting client record from server.", e);
return;
}
final String clientGuid = prefs.getString(SyncConfiguration.PREF_ACCOUNT_GUID, null);
final String clusterURL = prefs.getString(SyncConfiguration.PREF_CLUSTER_URL, null);
// Finally, a good place to do this.
prefs.edit().clear().commit();
if (clientGuid == null) {
Logger.warn(LOG_TAG, "Client GUID was null; not deleting client record from server.");
return;
}
if (clusterURL == null) {
Logger.warn(LOG_TAG, "Cluster URL was null; not deleting client record from server.");
return;
}
try {
ClientRecordTerminator.deleteClientRecord(encodedUsername, password, clusterURL, clientGuid);
} catch (Exception e) {
// This should never happen, but we really don't want to die in a background thread.
Logger.warn(LOG_TAG, "Got exception deleting client record from server; ignoring.", e);
}
}
}

Просмотреть файл

@ -7,8 +7,6 @@ package org.mozilla.gecko.sync.setup;
import java.io.File;
import java.io.UnsupportedEncodingException;
import java.security.NoSuchAlgorithmException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import org.mozilla.gecko.db.BrowserContract;
import org.mozilla.gecko.sync.CredentialException;
@ -23,8 +21,6 @@ import org.mozilla.gecko.sync.repositories.android.RepoUtils;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.accounts.AccountManagerCallback;
import android.accounts.AccountManagerFuture;
import android.content.ActivityNotFoundException;
import android.content.ContentResolver;
import android.content.Context;
@ -60,34 +56,6 @@ public class SyncAccounts {
return AccountManager.get(c).getAccountsByType(GlobalConstants.ACCOUNTTYPE_SYNC);
}
/**
* Asynchronously invalidate the auth token for a Sync account.
*
* @param accountManager Android account manager.
* @param account Android account.
*/
public static void invalidateAuthToken(final AccountManager accountManager, final Account account) {
if (account == null) {
return;
}
// blockingGetAuthToken must not be called from the main thread.
ThreadPool.run(new Runnable() {
@Override
public void run() {
String authToken;
try {
authToken = accountManager.blockingGetAuthToken(account, Constants.AUTHTOKEN_TYPE_PLAIN, true);
} catch (Exception e) {
Logger.warn(LOG_TAG, "Got exception while invalidating auth token.", e);
return;
}
accountManager.invalidateAuthToken(GlobalConstants.ACCOUNTTYPE_SYNC, authToken);
}
});
}
/**
* Returns true if a Sync account is set up, or we have a pickled Sync account
* on disk that should be un-pickled (Bug 769745). If we have a pickled Sync
@ -488,39 +456,6 @@ public class SyncAccounts {
return intent;
}
protected static class SyncAccountVersion0Callback implements AccountManagerCallback<Bundle> {
protected final Context context;
protected final CountDownLatch latch;
public String authToken = null;
public SyncAccountVersion0Callback(final Context context, final CountDownLatch latch) {
this.context = context;
this.latch = latch;
}
@Override
public void run(AccountManagerFuture<Bundle> future) {
try {
Bundle bundle = future.getResult(60L, TimeUnit.SECONDS);
if (bundle.containsKey(AccountManager.KEY_INTENT)) {
throw new IllegalStateException("KEY_INTENT included in AccountManagerFuture bundle.");
}
if (bundle.containsKey(AccountManager.KEY_ERROR_MESSAGE)) {
throw new IllegalStateException("KEY_ERROR_MESSAGE (= " + bundle.getString(AccountManager.KEY_ERROR_MESSAGE) + ") "
+ " included in AccountManagerFuture bundle.");
}
authToken = bundle.getString(AccountManager.KEY_AUTHTOKEN);
} catch (Exception e) {
// Do nothing -- caller will find null authToken.
Logger.warn(LOG_TAG, "Got exception fetching auth token; ignoring and returning null auth token instead.", e);
} finally {
latch.countDown();
}
}
}
/**
* Synchronously extract Sync account parameters from Android account version
* 0, using plain auth token type.
@ -528,6 +463,7 @@ public class SyncAccounts {
* Safe to call from main thread.
*
* @param context
* Android context.
* @param accountManager
* Android account manager.
* @param account
@ -537,24 +473,6 @@ public class SyncAccounts {
*/
public static SyncAccountParameters blockingFromAndroidAccountV0(final Context context, final AccountManager accountManager, final Account account)
throws CredentialException {
final CountDownLatch latch = new CountDownLatch(1);
final SyncAccountVersion0Callback callback = new SyncAccountVersion0Callback(context, latch);
new Thread(new Runnable() {
@Override
public void run() {
// Get an auth token.
accountManager.getAuthToken(account, Constants.AUTHTOKEN_TYPE_PLAIN, true, callback, null);
}
}).start();
try {
latch.await();
} catch (InterruptedException e) {
Logger.warn(LOG_TAG, "Got exception waiting for Sync account parameters; throwing.");
throw new CredentialException.MissingAllCredentialsException(e);
}
String username;
try {
username = Utils.usernameFromAccount(account.name);
@ -564,17 +482,17 @@ public class SyncAccounts {
throw new CredentialException.MissingCredentialException("username");
}
final String password = callback.authToken;
/*
* If we are accessing an Account that we don't own, Android will throw an
* unchecked <code>SecurityException</code> saying
* "W FxSync(XXXX) java.lang.SecurityException: caller uid XXXXX is different than the authenticator's uid".
* We catch that error and throw accordingly.
*/
String password;
String syncKey;
String serverURL;
try {
password = accountManager.getPassword(account);
syncKey = accountManager.getUserData(account, Constants.OPTION_SYNCKEY);
serverURL = accountManager.getUserData(account, Constants.OPTION_SERVER);
} catch (SecurityException e) {
@ -610,4 +528,56 @@ public class SyncAccounts {
throw new CredentialException.MissingAllCredentialsException(e);
}
}
/**
* Bug 790931: create an intent announcing that a Sync account will be
* deleted.
* <p>
* This intent <b>must</b> be broadcast with secure permissions, because it
* contains sensitive user information including the Sync account password and
* Sync key.
* <p>
* Version 1 of the created intent includes extras with keys
* <code>Constants.JSON_KEY_VERSION</code>,
* <code>Constants.JSON_KEY_TIMESTAMP</code>, and
* <code>Constants.JSON_KEY_ACCOUNT</code> (which is the Android Account name,
* not the encoded Sync Account name).
* <p>
* If possible, it contains the key <code>Constants.JSON_KEY_PAYLOAD</code>
* with value the Sync account parameters as JSON, <b>except the Sync key has
* been replaced with the empty string</b>. (We replace, rather than remove,
* the Sync key because SyncAccountParameters expects a non-null Sync key.)
*
* @see SyncAccountParameters#asJSON
*
* @param context
* Android context.
* @param accountManager
* Android account manager.
* @param account
* Android account being removed.
* @return <code>Intent</code> to broadcast.
*/
public static Intent makeSyncAccountDeletedIntent(final Context context, final AccountManager accountManager, final Account account) {
final Intent intent = new Intent(GlobalConstants.SYNC_ACCOUNT_DELETED_ACTION);
intent.putExtra(Constants.JSON_KEY_VERSION, Long.valueOf(GlobalConstants.SYNC_ACCOUNT_DELETED_INTENT_VERSION));
intent.putExtra(Constants.JSON_KEY_TIMESTAMP, Long.valueOf(System.currentTimeMillis()));
intent.putExtra(Constants.JSON_KEY_ACCOUNT, account.name);
SyncAccountParameters accountParameters = null;
try {
accountParameters = SyncAccounts.blockingFromAndroidAccountV0(context, accountManager, account);
} catch (Exception e) {
Logger.warn(LOG_TAG, "Caught exception fetching account parameters.", e);
}
if (accountParameters != null) {
ExtendedJSONObject json = accountParameters.asJSON();
json.put(Constants.JSON_KEY_SYNCKEY, ""); // Reduce attack surface area by removing Sync key.
intent.putExtra(Constants.JSON_KEY_PAYLOAD, json.toJSONString());
}
return intent;
}
}

Просмотреть файл

@ -9,11 +9,7 @@ import java.security.NoSuchAlgorithmException;
import org.mozilla.gecko.sync.GlobalConstants;
import org.mozilla.gecko.sync.Logger;
import org.mozilla.gecko.sync.SyncConfiguration;
import org.mozilla.gecko.sync.ThreadPool;
import org.mozilla.gecko.sync.Utils;
import org.mozilla.gecko.sync.config.AccountPickler;
import org.mozilla.gecko.sync.config.ClientRecordTerminator;
import org.mozilla.gecko.sync.setup.activities.SetupSyncActivity;
import android.accounts.AbstractAccountAuthenticator;
@ -24,12 +20,12 @@ import android.accounts.NetworkErrorException;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.os.IBinder;
public class SyncAuthenticatorService extends Service {
private static final String LOG_TAG = "SyncAuthService";
private SyncAccountAuthenticator sAccountAuthenticator = null;
@Override
@ -213,10 +209,11 @@ public class SyncAuthenticatorService extends Service {
* Bug 769745: persist pickled Sync account settings so that we can unpickle
* after Fennec is moved to the SD card.
* <p>
* This is <b>not</b> called when an Android Account is blown away due to the
* SD card being unmounted.
* This is <b>not</b> called when an Android Account is blown away due to
* the SD card being unmounted.
* <p>
* This is a terrible hack, but it's better than the catching the generic
* Broadcasting a Firefox intent to version sharing this Android Account is
* a terrible hack, but it's better than the catching the generic
* "accounts changed" broadcast intent and trying to figure out whether our
* Account disappeared.
*/
@ -236,92 +233,21 @@ public class SyncAuthenticatorService extends Service {
return result;
}
final String accountName = account.name;
// Delete the Account pickle in the background.
ThreadPool.run(new Runnable() {
@Override
public void run() {
Logger.info(LOG_TAG, "Account named " + accountName + " being removed; " +
"deleting saved pickle file '" + Constants.ACCOUNT_PICKLE_FILENAME + "'.");
try {
AccountPickler.deletePickle(mContext, Constants.ACCOUNT_PICKLE_FILENAME);
} catch (Exception e) {
// This should never happen, but we really don't want to die in a background thread.
Logger.warn(LOG_TAG, "Got exception deleting saved pickle file; ignoring.", e);
}
}
});
// Bug 770785: delete the Account's client record in the background. We
// want to get the Account's data synchronously, though, since it is
// possible the Account object will be invalid by the time the Runnable
// executes. We don't need to worry about accessing prefs too early since
// deleting the Account doesn't remove them -- at least, not yet. We would
// prefer to use SyncAccounts.blockingFromAndroidAccountV0, but that
// hangs, possibly because the Account Manager doesn't appreciate giving
// out an auth token while deleting the account.
final AccountManager accountManager = AccountManager.get(mContext);
final String password = accountManager.getPassword(account);
final String serverURL = accountManager.getUserData(account, Constants.OPTION_SERVER);
ThreadPool.run(new Runnable() {
@Override
public void run() {
Logger.info(LOG_TAG, "Account named " + accountName + " being removed; " +
"deleting client record from server.");
String encodedUsername;
try {
encodedUsername = Utils.usernameFromAccount(accountName);
} catch (Exception e) {
Logger.warn(LOG_TAG, "Got exception deleting client record from server; ignoring.", e);
return;
}
if (accountName == null || encodedUsername == null || password == null || serverURL == null) {
Logger.warn(LOG_TAG, "Account parameters were null; not deleting client record from server.");
return;
}
// This is not exactly modular. We need to get some information about
// the account, namely the current clusterURL and client GUID, and we
// extract it by hand. We're not worried about the Account being
// deleted out from under us since the prefs remain even after Account
// deletion.
final String product = GlobalConstants.BROWSER_INTENT_PACKAGE;
final String profile = Constants.DEFAULT_PROFILE;
final long version = SyncConfiguration.CURRENT_PREFS_VERSION;
SharedPreferences prefs;
try {
prefs = Utils.getSharedPreferences(mContext, product, encodedUsername, serverURL, profile, version);
} catch (Exception e) {
Logger.warn(LOG_TAG, "Caught exception fetching preferences; not deleting client record from server.", e);
return;
}
final String clientGuid = prefs.getString(SyncConfiguration.PREF_ACCOUNT_GUID, null);
final String clusterURL = prefs.getString(SyncConfiguration.PREF_CLUSTER_URL, null);
if (clientGuid == null) {
Logger.warn(LOG_TAG, "Client GUID was null; not deleting client record from server.");
return;
}
if (clusterURL == null) {
Logger.warn(LOG_TAG, "Cluster URL was null; not deleting client record from server.");
return;
}
try {
ClientRecordTerminator.deleteClientRecord(encodedUsername, password, clusterURL, clientGuid);
} catch (Exception e) {
Logger.warn(LOG_TAG, "Got exception deleting client record from server; ignoring.", e);
}
}
});
// Bug 790931: Broadcast a message to all Firefox versions sharing this
// Android Account type telling that this Sync Account has been deleted.
//
// We would really prefer to receive Android's
// LOGIN_ACCOUNTS_CHANGED_ACTION broadcast, but that
// doesn't include enough information about which Accounts changed to
// correctly identify whether a Sync account has been removed (when some
// Firefox versions are installed on the SD card).
//
// Broadcast intents protected with permissions are secure, so it's okay
// to include password and sync key, etc.
final Intent intent = SyncAccounts.makeSyncAccountDeletedIntent(mContext, AccountManager.get(mContext), account);
Logger.info(LOG_TAG, "Account named " + account.name + " being removed; " +
"broadcasting secure intent " + intent.getAction() + ".");
mContext.sendBroadcast(intent, GlobalConstants.PER_ACCOUNT_TYPE_PERMISSION);
return result;
}

Просмотреть файл

@ -87,7 +87,7 @@ public class SyncAdapter extends AbstractThreadedSyncAdapter implements GlobalSe
}
/**
* Handle an exception: update stats, invalidate auth token, log errors, etc.
* Handle an exception: update stats, log errors, etc.
* Wakes up sleeping threads by calling notifyMonitor().
*
* @param globalSession
@ -97,9 +97,6 @@ public class SyncAdapter extends AbstractThreadedSyncAdapter implements GlobalSe
*/
protected void processException(final GlobalSession globalSession, final Exception e) {
try {
// Just in case, invalidate auth token.
SyncAccounts.invalidateAuthToken(AccountManager.get(mContext), localAccount);
if (e instanceof SQLiteConstraintException) {
Logger.error(LOG_TAG, "Constraint exception. Aborting sync.", e);
syncResult.stats.numParseExceptions++; // This is as good as we can do.

Просмотреть файл

@ -97,6 +97,8 @@ sync/NonObjectJSONException.java
sync/NullClusterURLException.java
sync/PersistedMetaGlobal.java
sync/PrefsSource.java
sync/receivers/SyncAccountDeletedReceiver.java
sync/receivers/SyncAccountDeletedService.java
sync/receivers/UpgradeReceiver.java
sync/repositories/android/AndroidBrowserBookmarksDataAccessor.java
sync/repositories/android/AndroidBrowserBookmarksRepository.java

Просмотреть файл

@ -50,6 +50,16 @@
</intent-filter>
</receiver>
<receiver
android:name="org.mozilla.gecko.sync.receivers.SyncAccountDeletedReceiver"
android:permission="@MOZ_ANDROID_SHARED_ACCOUNT_TYPE@.permission.PER_ACCOUNT_TYPE">
<intent-filter>
<!-- This needs to be kept the same as
GlobalConstants.SYNC_ACCOUNT_DELETED_ACTION. -->
<action android:name="@MOZ_ANDROID_SHARED_ACCOUNT_TYPE@.accounts.SYNC_ACCOUNT_DELETED_ACTION"/>
</intent-filter>
</receiver>
<activity
android:theme="@style/SyncTheme"
android:icon="@drawable/icon"

Просмотреть файл

@ -7,3 +7,13 @@
<uses-permission android:name="android.permission.WRITE_SETTINGS" />
<uses-permission android:name="android.permission.READ_SYNC_STATS" />
<uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
<!-- A signature level permission granted only to the Firefox
versions sharing an Android Account type. This needs to
agree with GlobalConstants.PER_ACCOUNT_TYPE_PERMISSION. -->
<permission
android:name="@MOZ_ANDROID_SHARED_ACCOUNT_TYPE@.permission.PER_ACCOUNT_TYPE"
android:protectionLevel="signature">
</permission>
<uses-permission android:name="@MOZ_ANDROID_SHARED_ACCOUNT_TYPE@.permission.PER_ACCOUNT_TYPE" />

Просмотреть файл

@ -20,3 +20,7 @@
android:name="android.content.SyncAdapter"
android:resource="@xml/sync_syncadapter" />
</service>
<service
android:exported="false"
android:name="org.mozilla.gecko.sync.receivers.SyncAccountDeletedService" >
</service>