michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: package org.mozilla.gecko.fxa.activities; michael@0: michael@0: import java.util.HashMap; michael@0: import java.util.Map; michael@0: import java.util.Set; michael@0: michael@0: import org.mozilla.gecko.R; michael@0: import org.mozilla.gecko.background.common.log.Logger; michael@0: import org.mozilla.gecko.background.preferences.PreferenceFragment; michael@0: import org.mozilla.gecko.fxa.FirefoxAccounts; michael@0: import org.mozilla.gecko.fxa.FxAccountConstants; michael@0: import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount; michael@0: import org.mozilla.gecko.fxa.login.Married; michael@0: import org.mozilla.gecko.fxa.login.State; michael@0: import org.mozilla.gecko.fxa.sync.FxAccountSyncStatusHelper; michael@0: import org.mozilla.gecko.sync.SyncConfiguration; michael@0: michael@0: import android.accounts.Account; michael@0: import android.content.ContentResolver; michael@0: import android.content.Context; michael@0: import android.content.Intent; michael@0: import android.content.SharedPreferences; michael@0: import android.os.Bundle; michael@0: import android.os.Handler; michael@0: import android.preference.CheckBoxPreference; michael@0: import android.preference.Preference; michael@0: import android.preference.Preference.OnPreferenceClickListener; michael@0: import android.preference.PreferenceCategory; michael@0: import android.preference.PreferenceScreen; michael@0: michael@0: /** michael@0: * A fragment that displays the status of an AndroidFxAccount. michael@0: *

michael@0: * The owning activity is responsible for providing an AndroidFxAccount at michael@0: * appropriate times. michael@0: */ michael@0: public class FxAccountStatusFragment extends PreferenceFragment implements OnPreferenceClickListener { michael@0: private static final String LOG_TAG = FxAccountStatusFragment.class.getSimpleName(); michael@0: michael@0: // When a checkbox is toggled, wait 5 seconds (for other checkbox actions) michael@0: // before trying to sync. Should we kill off the fragment before the sync michael@0: // request happens, that's okay: the runnable will run if the UI thread is michael@0: // still around to service it, and since we're not updating any UI, we'll just michael@0: // schedule the sync as usual. See also comment below about garbage michael@0: // collection. michael@0: private static final long DELAY_IN_MILLISECONDS_BEFORE_REQUESTING_SYNC = 5 * 1000; michael@0: michael@0: protected Preference emailPreference; michael@0: michael@0: protected Preference needsPasswordPreference; michael@0: protected Preference needsUpgradePreference; michael@0: protected Preference needsVerificationPreference; michael@0: protected Preference needsMasterSyncAutomaticallyEnabledPreference; michael@0: protected Preference needsAccountEnabledPreference; michael@0: michael@0: protected PreferenceCategory syncCategory; michael@0: michael@0: protected CheckBoxPreference bookmarksPreference; michael@0: protected CheckBoxPreference historyPreference; michael@0: protected CheckBoxPreference tabsPreference; michael@0: protected CheckBoxPreference passwordsPreference; michael@0: michael@0: protected volatile AndroidFxAccount fxAccount; michael@0: michael@0: // Used to post delayed sync requests. michael@0: protected Handler handler; michael@0: michael@0: // Member variable so that re-posting pushes back the already posted instance. michael@0: // This Runnable references the fxAccount above, but it is not specific to a michael@0: // single account. (That is, it does not capture a single account instance.) michael@0: protected Runnable requestSyncRunnable; michael@0: michael@0: protected final InnerSyncStatusDelegate syncStatusDelegate = new InnerSyncStatusDelegate(); michael@0: michael@0: protected Preference ensureFindPreference(String key) { michael@0: Preference preference = findPreference(key); michael@0: if (preference == null) { michael@0: throw new IllegalStateException("Could not find preference with key: " + key); michael@0: } michael@0: return preference; michael@0: } michael@0: michael@0: @Override michael@0: public void onCreate(Bundle savedInstanceState) { michael@0: super.onCreate(savedInstanceState); michael@0: addPreferencesFromResource(R.xml.fxaccount_status_prefscreen); michael@0: michael@0: emailPreference = ensureFindPreference("email"); michael@0: michael@0: needsPasswordPreference = ensureFindPreference("needs_credentials"); michael@0: needsUpgradePreference = ensureFindPreference("needs_upgrade"); michael@0: needsVerificationPreference = ensureFindPreference("needs_verification"); michael@0: needsMasterSyncAutomaticallyEnabledPreference = ensureFindPreference("needs_master_sync_automatically_enabled"); michael@0: needsAccountEnabledPreference = ensureFindPreference("needs_account_enabled"); michael@0: michael@0: syncCategory = (PreferenceCategory) ensureFindPreference("sync_category"); michael@0: michael@0: bookmarksPreference = (CheckBoxPreference) ensureFindPreference("bookmarks"); michael@0: historyPreference = (CheckBoxPreference) ensureFindPreference("history"); michael@0: tabsPreference = (CheckBoxPreference) ensureFindPreference("tabs"); michael@0: passwordsPreference = (CheckBoxPreference) ensureFindPreference("passwords"); michael@0: michael@0: if (!FxAccountConstants.LOG_PERSONAL_INFORMATION) { michael@0: removeDebugButtons(); michael@0: } else { michael@0: connectDebugButtons(); michael@0: } michael@0: michael@0: needsPasswordPreference.setOnPreferenceClickListener(this); michael@0: needsVerificationPreference.setOnPreferenceClickListener(this); michael@0: needsAccountEnabledPreference.setOnPreferenceClickListener(this); michael@0: michael@0: bookmarksPreference.setOnPreferenceClickListener(this); michael@0: historyPreference.setOnPreferenceClickListener(this); michael@0: tabsPreference.setOnPreferenceClickListener(this); michael@0: passwordsPreference.setOnPreferenceClickListener(this); michael@0: } michael@0: michael@0: /** michael@0: * We intentionally don't refresh here. Our owning activity is responsible for michael@0: * providing an AndroidFxAccount to our refresh method in its onResume method. michael@0: */ michael@0: @Override michael@0: public void onResume() { michael@0: super.onResume(); michael@0: } michael@0: michael@0: @Override michael@0: public boolean onPreferenceClick(Preference preference) { michael@0: if (preference == needsPasswordPreference) { michael@0: Intent intent = new Intent(getActivity(), FxAccountUpdateCredentialsActivity.class); michael@0: // Per http://stackoverflow.com/a/8992365, this triggers a known bug with michael@0: // the soft keyboard not being shown for the started activity. Why, Android, why? michael@0: intent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION); michael@0: startActivity(intent); michael@0: michael@0: return true; michael@0: } michael@0: michael@0: if (preference == needsVerificationPreference) { michael@0: FxAccountConfirmAccountActivity.resendCode(getActivity().getApplicationContext(), fxAccount); michael@0: michael@0: Intent intent = new Intent(getActivity(), FxAccountConfirmAccountActivity.class); michael@0: // Per http://stackoverflow.com/a/8992365, this triggers a known bug with michael@0: // the soft keyboard not being shown for the started activity. Why, Android, why? michael@0: intent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION); michael@0: startActivity(intent); michael@0: michael@0: return true; michael@0: } michael@0: michael@0: if (preference == needsAccountEnabledPreference) { michael@0: fxAccount.enableSyncing(); michael@0: refresh(); michael@0: michael@0: return true; michael@0: } michael@0: michael@0: if (preference == bookmarksPreference || michael@0: preference == historyPreference || michael@0: preference == passwordsPreference || michael@0: preference == tabsPreference) { michael@0: saveEngineSelections(); michael@0: return true; michael@0: } michael@0: michael@0: return false; michael@0: } michael@0: michael@0: protected void setCheckboxesEnabled(boolean enabled) { michael@0: bookmarksPreference.setEnabled(enabled); michael@0: historyPreference.setEnabled(enabled); michael@0: tabsPreference.setEnabled(enabled); michael@0: passwordsPreference.setEnabled(enabled); michael@0: } michael@0: michael@0: /** michael@0: * Show at most one error preference, hiding all others. michael@0: * michael@0: * @param errorPreferenceToShow michael@0: * single error preference to show; if null, hide all error preferences michael@0: */ michael@0: protected void showOnlyOneErrorPreference(Preference errorPreferenceToShow) { michael@0: final Preference[] errorPreferences = new Preference[] { michael@0: this.needsPasswordPreference, michael@0: this.needsUpgradePreference, michael@0: this.needsVerificationPreference, michael@0: this.needsMasterSyncAutomaticallyEnabledPreference, michael@0: this.needsAccountEnabledPreference, michael@0: }; michael@0: for (Preference errorPreference : errorPreferences) { michael@0: final boolean currentlyShown = null != findPreference(errorPreference.getKey()); michael@0: final boolean shouldBeShown = errorPreference == errorPreferenceToShow; michael@0: if (currentlyShown == shouldBeShown) { michael@0: continue; michael@0: } michael@0: if (shouldBeShown) { michael@0: syncCategory.addPreference(errorPreference); michael@0: } else { michael@0: syncCategory.removePreference(errorPreference); michael@0: } michael@0: } michael@0: } michael@0: michael@0: protected void showNeedsPassword() { michael@0: syncCategory.setTitle(R.string.fxaccount_status_sync); michael@0: showOnlyOneErrorPreference(needsPasswordPreference); michael@0: setCheckboxesEnabled(false); michael@0: } michael@0: michael@0: protected void showNeedsUpgrade() { michael@0: syncCategory.setTitle(R.string.fxaccount_status_sync); michael@0: showOnlyOneErrorPreference(needsUpgradePreference); michael@0: setCheckboxesEnabled(false); michael@0: } michael@0: michael@0: protected void showNeedsVerification() { michael@0: syncCategory.setTitle(R.string.fxaccount_status_sync); michael@0: showOnlyOneErrorPreference(needsVerificationPreference); michael@0: setCheckboxesEnabled(false); michael@0: } michael@0: michael@0: protected void showNeedsMasterSyncAutomaticallyEnabled() { michael@0: syncCategory.setTitle(R.string.fxaccount_status_sync); michael@0: showOnlyOneErrorPreference(needsMasterSyncAutomaticallyEnabledPreference); michael@0: setCheckboxesEnabled(false); michael@0: } michael@0: michael@0: protected void showNeedsAccountEnabled() { michael@0: syncCategory.setTitle(R.string.fxaccount_status_sync); michael@0: showOnlyOneErrorPreference(needsAccountEnabledPreference); michael@0: setCheckboxesEnabled(false); michael@0: } michael@0: michael@0: protected void showConnected() { michael@0: syncCategory.setTitle(R.string.fxaccount_status_sync_enabled); michael@0: showOnlyOneErrorPreference(null); michael@0: setCheckboxesEnabled(true); michael@0: } michael@0: michael@0: protected class InnerSyncStatusDelegate implements FirefoxAccounts.SyncStatusListener { michael@0: protected final Runnable refreshRunnable = new Runnable() { michael@0: @Override michael@0: public void run() { michael@0: refresh(); michael@0: } michael@0: }; michael@0: michael@0: @Override michael@0: public Context getContext() { michael@0: return FxAccountStatusFragment.this.getActivity(); michael@0: } michael@0: michael@0: @Override michael@0: public Account getAccount() { michael@0: return fxAccount.getAndroidAccount(); michael@0: } michael@0: michael@0: @Override michael@0: public void onSyncStarted() { michael@0: if (fxAccount == null) { michael@0: return; michael@0: } michael@0: Logger.info(LOG_TAG, "Got sync started message; refreshing."); michael@0: getActivity().runOnUiThread(refreshRunnable); michael@0: } michael@0: michael@0: @Override michael@0: public void onSyncFinished() { michael@0: if (fxAccount == null) { michael@0: return; michael@0: } michael@0: Logger.info(LOG_TAG, "Got sync finished message; refreshing."); michael@0: getActivity().runOnUiThread(refreshRunnable); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Notify the fragment that a new AndroidFxAccount instance is current. michael@0: *

michael@0: * Important: call this method on the UI thread! michael@0: *

michael@0: * In future, this might be a Loader. michael@0: * michael@0: * @param fxAccount new instance. michael@0: */ michael@0: public void refresh(AndroidFxAccount fxAccount) { michael@0: if (fxAccount == null) { michael@0: throw new IllegalArgumentException("fxAccount must not be null"); michael@0: } michael@0: this.fxAccount = fxAccount; michael@0: michael@0: handler = new Handler(); // Attached to current (assumed to be UI) thread. michael@0: michael@0: // Runnable is not specific to one Firefox Account. This runnable will keep michael@0: // a reference to this fragment alive, but we expect posted runnables to be michael@0: // serviced very quickly, so this is not an issue. michael@0: requestSyncRunnable = new RequestSyncRunnable(); michael@0: michael@0: // We would very much like register these status observers in bookended michael@0: // onResume/onPause calls, but because the Fragment gets onResume during the michael@0: // Activity's super.onResume, it hasn't yet been told its Firefox Account. michael@0: // So we register the observer here (and remove it in onPause), and open michael@0: // ourselves to the possibility that we don't have properly paired michael@0: // register/unregister calls. michael@0: FxAccountSyncStatusHelper.getInstance().startObserving(syncStatusDelegate); michael@0: michael@0: refresh(); michael@0: } michael@0: michael@0: @Override michael@0: public void onPause() { michael@0: super.onPause(); michael@0: FxAccountSyncStatusHelper.getInstance().stopObserving(syncStatusDelegate); michael@0: } michael@0: michael@0: protected void refresh() { michael@0: // refresh is called from our onResume, which can happen before the owning michael@0: // Activity tells us about an account (via our public michael@0: // refresh(AndroidFxAccount) method). michael@0: if (fxAccount == null) { michael@0: throw new IllegalArgumentException("fxAccount must not be null"); michael@0: } michael@0: michael@0: emailPreference.setTitle(fxAccount.getEmail()); michael@0: michael@0: try { michael@0: // There are error states determined by Android, not the login state michael@0: // machine, and we have a chance to present these states here. We handle michael@0: // them specially, since we can't surface these states as part of syncing, michael@0: // because they generally stop syncs from happening regularly. michael@0: michael@0: // The action to enable syncing the Firefox Account doesn't require michael@0: // leaving this activity, so let's present it first. michael@0: final boolean isSyncing = fxAccount.isSyncing(); michael@0: if (!isSyncing) { michael@0: showNeedsAccountEnabled(); michael@0: return; michael@0: } michael@0: michael@0: // Interrogate the Firefox Account's state. michael@0: State state = fxAccount.getState(); michael@0: switch (state.getNeededAction()) { michael@0: case NeedsUpgrade: michael@0: showNeedsUpgrade(); michael@0: break; michael@0: case NeedsPassword: michael@0: showNeedsPassword(); michael@0: break; michael@0: case NeedsVerification: michael@0: showNeedsVerification(); michael@0: break; michael@0: default: michael@0: showConnected(); michael@0: } michael@0: michael@0: // We check for the master setting last, since it is not strictly michael@0: // necessary for the user to address this error state: it's really a michael@0: // warning state. We surface it for the user's convenience, and to prevent michael@0: // confused folks wondering why Sync is not working at all. michael@0: final boolean masterSyncAutomatically = ContentResolver.getMasterSyncAutomatically(); michael@0: if (!masterSyncAutomatically) { michael@0: showNeedsMasterSyncAutomaticallyEnabled(); michael@0: return; michael@0: } michael@0: } finally { michael@0: // No matter our state, we should update the checkboxes. michael@0: updateSelectedEngines(); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Query shared prefs for the current engine state, and update the UI michael@0: * accordingly. michael@0: *

michael@0: * In future, we might want this to be on a background thread, or implemented michael@0: * as a Loader. michael@0: */ michael@0: protected void updateSelectedEngines() { michael@0: try { michael@0: SharedPreferences syncPrefs = fxAccount.getSyncPrefs(); michael@0: Map engines = SyncConfiguration.getUserSelectedEngines(syncPrefs); michael@0: if (engines != null) { michael@0: bookmarksPreference.setChecked(engines.containsKey("bookmarks") && engines.get("bookmarks")); michael@0: historyPreference.setChecked(engines.containsKey("history") && engines.get("history")); michael@0: passwordsPreference.setChecked(engines.containsKey("passwords") && engines.get("passwords")); michael@0: tabsPreference.setChecked(engines.containsKey("tabs") && engines.get("tabs")); michael@0: return; michael@0: } michael@0: michael@0: // We don't have user specified preferences. Perhaps we have seen a meta/global? michael@0: Set enabledNames = SyncConfiguration.getEnabledEngineNames(syncPrefs); michael@0: if (enabledNames != null) { michael@0: bookmarksPreference.setChecked(enabledNames.contains("bookmarks")); michael@0: historyPreference.setChecked(enabledNames.contains("history")); michael@0: passwordsPreference.setChecked(enabledNames.contains("passwords")); michael@0: tabsPreference.setChecked(enabledNames.contains("tabs")); michael@0: return; michael@0: } michael@0: michael@0: // Okay, we don't have userSelectedEngines or enabledEngines. That means michael@0: // the user hasn't specified to begin with, we haven't specified here, and michael@0: // we haven't already seen, Sync engines. We don't know our state, so michael@0: // let's check everything (the default) and disable everything. michael@0: bookmarksPreference.setChecked(true); michael@0: historyPreference.setChecked(true); michael@0: passwordsPreference.setChecked(true); michael@0: tabsPreference.setChecked(true); michael@0: setCheckboxesEnabled(false); michael@0: } catch (Exception e) { michael@0: Logger.warn(LOG_TAG, "Got exception getting engines to select; ignoring.", e); michael@0: return; michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Persist engine selections to local shared preferences, and request a sync michael@0: * to persist selections to remote storage. michael@0: */ michael@0: protected void saveEngineSelections() { michael@0: final Map engineSelections = new HashMap(); michael@0: engineSelections.put("bookmarks", bookmarksPreference.isChecked()); michael@0: engineSelections.put("history", historyPreference.isChecked()); michael@0: engineSelections.put("passwords", passwordsPreference.isChecked()); michael@0: engineSelections.put("tabs", tabsPreference.isChecked()); michael@0: michael@0: // No GlobalSession.config, so store directly to shared prefs. We do this on michael@0: // a background thread to avoid IO on the main thread and strict mode michael@0: // warnings. michael@0: new Thread(new PersistEngineSelectionsRunnable(engineSelections)).start(); michael@0: } michael@0: michael@0: protected void requestDelayedSync() { michael@0: Logger.info(LOG_TAG, "Posting a delayed request for a sync sometime soon."); michael@0: handler.removeCallbacks(requestSyncRunnable); michael@0: handler.postDelayed(requestSyncRunnable, DELAY_IN_MILLISECONDS_BEFORE_REQUESTING_SYNC); michael@0: } michael@0: michael@0: /** michael@0: * Remove all traces of debug buttons. By default, no debug buttons are shown. michael@0: */ michael@0: protected void removeDebugButtons() { michael@0: final PreferenceScreen statusScreen = (PreferenceScreen) ensureFindPreference("status_screen"); michael@0: final PreferenceCategory debugCategory = (PreferenceCategory) ensureFindPreference("debug_category"); michael@0: statusScreen.removePreference(debugCategory); michael@0: } michael@0: michael@0: /** michael@0: * A Runnable that persists engine selections to shared prefs, and then michael@0: * requests a delayed sync. michael@0: *

michael@0: * References the member fxAccount and is specific to the Android michael@0: * account associated to that account. michael@0: */ michael@0: protected class PersistEngineSelectionsRunnable implements Runnable { michael@0: private final Map engineSelections; michael@0: michael@0: protected PersistEngineSelectionsRunnable(Map engineSelections) { michael@0: this.engineSelections = engineSelections; michael@0: } michael@0: michael@0: @Override michael@0: public void run() { michael@0: try { michael@0: // Name shadowing -- do you like it, or do you love it? michael@0: AndroidFxAccount fxAccount = FxAccountStatusFragment.this.fxAccount; michael@0: if (fxAccount == null) { michael@0: return; michael@0: } michael@0: Logger.info(LOG_TAG, "Persisting engine selections: " + engineSelections.toString()); michael@0: SyncConfiguration.storeSelectedEnginesToPrefs(fxAccount.getSyncPrefs(), engineSelections); michael@0: requestDelayedSync(); michael@0: } catch (Exception e) { michael@0: Logger.warn(LOG_TAG, "Got exception persisting selected engines; ignoring.", e); michael@0: return; michael@0: } michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * A Runnable that requests a sync. michael@0: *

michael@0: * References the member fxAccount, but is not specific to the michael@0: * Android account associated to that account. michael@0: */ michael@0: protected class RequestSyncRunnable implements Runnable { michael@0: @Override michael@0: public void run() { michael@0: // Name shadowing -- do you like it, or do you love it? michael@0: AndroidFxAccount fxAccount = FxAccountStatusFragment.this.fxAccount; michael@0: if (fxAccount == null) { michael@0: return; michael@0: } michael@0: Logger.info(LOG_TAG, "Requesting a sync sometime soon."); michael@0: fxAccount.requestSync(); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * A separate listener to separate debug logic from main code paths. michael@0: */ michael@0: protected class DebugPreferenceClickListener implements OnPreferenceClickListener { michael@0: @Override michael@0: public boolean onPreferenceClick(Preference preference) { michael@0: final String key = preference.getKey(); michael@0: if ("debug_refresh".equals(key)) { michael@0: Logger.info(LOG_TAG, "Refreshing."); michael@0: refresh(); michael@0: } else if ("debug_dump".equals(key)) { michael@0: fxAccount.dump(); michael@0: } else if ("debug_force_sync".equals(key)) { michael@0: Logger.info(LOG_TAG, "Force syncing."); michael@0: fxAccount.requestSync(FirefoxAccounts.FORCE); michael@0: // No sense refreshing, since the sync will complete in the future. michael@0: } else if ("debug_forget_certificate".equals(key)) { michael@0: State state = fxAccount.getState(); michael@0: try { michael@0: Married married = (Married) state; michael@0: Logger.info(LOG_TAG, "Moving to Cohabiting state: Forgetting certificate."); michael@0: fxAccount.setState(married.makeCohabitingState()); michael@0: refresh(); michael@0: } catch (ClassCastException e) { michael@0: Logger.info(LOG_TAG, "Not in Married state; can't forget certificate."); michael@0: // Ignore. michael@0: } michael@0: } else if ("debug_require_password".equals(key)) { michael@0: Logger.info(LOG_TAG, "Moving to Separated state: Forgetting password."); michael@0: State state = fxAccount.getState(); michael@0: fxAccount.setState(state.makeSeparatedState()); michael@0: refresh(); michael@0: } else if ("debug_require_upgrade".equals(key)) { michael@0: Logger.info(LOG_TAG, "Moving to Doghouse state: Requiring upgrade."); michael@0: State state = fxAccount.getState(); michael@0: fxAccount.setState(state.makeDoghouseState()); michael@0: refresh(); michael@0: } else { michael@0: return false; michael@0: } michael@0: return true; michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Iterate through debug buttons, adding a special deubg preference click michael@0: * listener to each of them. michael@0: */ michael@0: protected void connectDebugButtons() { michael@0: // Separate listener to really separate debug logic from main code paths. michael@0: final OnPreferenceClickListener listener = new DebugPreferenceClickListener(); michael@0: michael@0: // We don't want to use Android resource strings for debug UI, so we just michael@0: // use the keys throughout. michael@0: final Preference debugCategory = ensureFindPreference("debug_category"); michael@0: debugCategory.setTitle(debugCategory.getKey()); michael@0: michael@0: String[] debugKeys = new String[] { michael@0: "debug_refresh", michael@0: "debug_dump", michael@0: "debug_force_sync", michael@0: "debug_forget_certificate", michael@0: "debug_require_password", michael@0: "debug_require_upgrade" }; michael@0: for (String debugKey : debugKeys) { michael@0: final Preference button = ensureFindPreference(debugKey); michael@0: button.setTitle(debugKey); // Not very friendly, but this is for debugging only! michael@0: button.setOnPreferenceClickListener(listener); michael@0: } michael@0: } michael@0: }