1/*
2 * Copyright (C) 2011 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.email.activity.setup;
18
19import android.app.ActionBar;
20import android.app.LoaderManager;
21import android.content.ContentUris;
22import android.content.ContentValues;
23import android.content.Context;
24import android.content.CursorLoader;
25import android.content.Intent;
26import android.content.Loader;
27import android.content.res.Resources;
28import android.database.Cursor;
29import android.net.Uri;
30import android.os.Bundle;
31import android.preference.CheckBoxPreference;
32import android.preference.ListPreference;
33import android.preference.Preference;
34import android.preference.Preference.OnPreferenceChangeListener;
35import android.preference.PreferenceActivity;
36import android.preference.PreferenceFragment;
37import android.support.annotation.NonNull;
38import android.text.TextUtils;
39import android.view.MenuItem;
40
41import com.android.email.R;
42import com.android.emailcommon.Logging;
43import com.android.emailcommon.provider.Account;
44import com.android.emailcommon.provider.EmailContent.AccountColumns;
45import com.android.emailcommon.provider.EmailContent.MailboxColumns;
46import com.android.emailcommon.provider.Mailbox;
47import com.android.emailcommon.provider.Policy;
48import com.android.emailcommon.utility.EmailAsyncTask;
49import com.android.emailcommon.utility.Utility;
50import com.android.mail.providers.Folder;
51import com.android.mail.providers.UIProvider;
52import com.android.mail.ui.MailAsyncTaskLoader;
53import com.android.mail.utils.LogUtils;
54import com.google.common.base.Preconditions;
55
56import java.util.ArrayList;
57import java.util.Arrays;
58import java.util.HashMap;
59import java.util.List;
60import java.util.Map;
61
62/**
63 * "Mailbox settings" activity.
64 *
65 * It's used to update per-mailbox sync settings.  It normally updates Mailbox settings, unless
66 * the target mailbox is Inbox, in which case it updates Account settings instead.
67 *
68 * All changes made by the user will not be immediately saved to the database, as changing the
69 * sync window may result in removal of messages.  Instead, we only save to the database in {@link
70 * #onDestroy()}, unless it's called for configuration changes.
71 */
72public class MailboxSettings extends PreferenceActivity {
73    private static final String EXTRA_FOLDERS_URI = "FOLDERS_URI";
74    private static final String EXTRA_INBOX_ID = "INBOX_ID";
75
76    private static final int FOLDERS_LOADER_ID = 0;
77    private Uri mFoldersUri;
78    private int mInboxId;
79    private final List<Folder> mFolders = new ArrayList<>();
80
81    /**
82     * Starts the activity
83     */
84    public static Intent getIntent(Context context, Uri foldersUri, Folder inbox) {
85        final Intent i = new Intent(context, MailboxSettings.class);
86        i.putExtra(EXTRA_FOLDERS_URI, foldersUri);
87        i.putExtra(EXTRA_INBOX_ID, inbox.id);
88        return i;
89    }
90
91    @Override
92    protected void onCreate(Bundle savedInstanceState) {
93        // This needs to happen before super.onCreate() since that calls onBuildHeaders()
94        mInboxId = getIntent().getIntExtra(EXTRA_INBOX_ID, -1);
95        mFoldersUri = getIntent().getParcelableExtra(EXTRA_FOLDERS_URI);
96
97        if (mFoldersUri != null) {
98            getLoaderManager().initLoader(FOLDERS_LOADER_ID, null,
99                    new MailboxSettingsFolderLoaderCallbacks());
100        }
101
102        super.onCreate(savedInstanceState);
103
104        // Always show "app up" as we expect our parent to be an Email activity.
105        ActionBar actionBar = getActionBar();
106        if (actionBar != null) {
107            actionBar.setDisplayOptions(ActionBar.DISPLAY_HOME_AS_UP, ActionBar.DISPLAY_HOME_AS_UP);
108            // Hide the app icon.
109            actionBar.setIcon(android.R.color.transparent);
110            actionBar.setDisplayUseLogoEnabled(false);
111        }
112    }
113
114    @Override
115    public void onBuildHeaders(List<Header> target) {
116        if (mFolders.isEmpty()) {
117            final Header dummy = new Header();
118            dummy.titleRes = R.string.mailbox_name_display_inbox;
119            dummy.fragment = MailboxSettingsFragment.class.getName();
120            dummy.fragmentArguments = MailboxSettingsFragment.getArguments(mInboxId);
121            target.add(dummy);
122        } else {
123            for (final Folder f : mFolders) {
124                final Header h = new Header();
125                if (!TextUtils.isEmpty(f.hierarchicalDesc)) {
126                    h.title = f.hierarchicalDesc;
127                } else {
128                    h.title = f.name;
129                }
130                h.fragment = MailboxSettingsFragment.class.getName();
131                h.fragmentArguments = MailboxSettingsFragment.getArguments(f.id);
132                if (f.id == mInboxId) {
133                    target.add(0, h);
134                } else {
135                    target.add(h);
136                }
137            }
138        }
139    }
140
141    @Override
142    protected boolean isValidFragment(String fragmentName) {
143        // Activity is not exported
144        return true;
145    }
146
147    @Override
148    public boolean onOptionsItemSelected(MenuItem item) {
149        if (item.getItemId() == android.R.id.home) {
150            onBackPressed();
151            return true;
152        }
153        return super.onOptionsItemSelected(item);
154    }
155
156    /**
157     * Setup the entries and entry values for the sync lookback preference
158     * @param context the caller's context
159     * @param pref a ListPreference to be set up
160     * @param maxLookback The maximum lookback allowed, or 0 if no max.
161     * @param showWithDefault Whether to show the version with default, or without.
162     */
163    public static void setupLookbackPreferenceOptions(final Context context,
164            final ListPreference pref, final int maxLookback, final boolean showWithDefault) {
165        final Resources resources = context.getResources();
166        // Load the complete list of entries/values
167        CharSequence[] entries;
168        CharSequence[] values;
169        final int offset;
170        if (showWithDefault) {
171            entries = resources.getTextArray(
172                    R.array.account_settings_mail_window_entries_with_default);
173            values = resources.getTextArray(
174                    R.array.account_settings_mail_window_values_with_default);
175            offset = 1;
176        } else {
177            entries = resources.getTextArray(R.array.account_settings_mail_window_entries);
178            values = resources.getTextArray(R.array.account_settings_mail_window_values);
179            offset = 0;
180        }
181        // If we have a maximum lookback policy, enforce it
182        if (maxLookback > 0) {
183            final int size = maxLookback + offset;
184            entries = Arrays.copyOf(entries, size);
185            values = Arrays.copyOf(values, size);
186        }
187        // Set up the preference
188        pref.setEntries(entries);
189        pref.setEntryValues(values);
190        pref.setSummary(pref.getEntry());
191    }
192
193    private class MailboxSettingsFolderLoaderCallbacks
194            implements LoaderManager.LoaderCallbacks<Cursor> {
195
196        @Override
197        public Loader<Cursor> onCreateLoader(int i, Bundle bundle) {
198            return new CursorLoader(MailboxSettings.this, mFoldersUri,
199                    UIProvider.FOLDERS_PROJECTION, null, null, null);
200        }
201
202        @Override
203        public void onLoadFinished(Loader<Cursor> cursorLoader, Cursor cursor) {
204            if (cursor == null) {
205                return;
206            }
207            mFolders.clear();
208
209            while(cursor.moveToNext()) {
210                final Folder folder = new Folder(cursor);
211                if (!folder.supportsCapability(UIProvider.FolderCapabilities.IS_VIRTUAL) &&
212                        !folder.isTrash() && !folder.isDraft() && !folder.isOutbox()) {
213                    mFolders.add(folder);
214                }
215            }
216
217            invalidateHeaders();
218        }
219
220        @Override
221        public void onLoaderReset(Loader<Cursor> cursorLoader) {
222            mFolders.clear();
223        }
224    }
225
226    public static class MailboxSettingsFragment extends PreferenceFragment {
227        private static final String EXTRA_MAILBOX_ID = "MailboxId";
228
229        private static final String BUNDLE_MAILBOX = "MailboxSettings.mailbox";
230        private static final String BUNDLE_MAX_LOOKBACK = "MailboxSettings.maxLookback";
231        private static final String BUNDLE_SYNC_ENABLED_VALUE = "MailboxSettings.syncEnabled";
232        private static final String BUNDLE_SYNC_WINDOW_VALUE = "MailboxSettings.syncWindow";
233
234        private static final String PREF_SYNC_ENABLED_KEY = "sync_enabled";
235        private static final String PREF_SYNC_WINDOW_KEY = "sync_window";
236
237        private Mailbox mMailbox;
238        /** The maximum lookback allowed for this mailbox, or 0 if no max. */
239        private int mMaxLookback;
240
241        private CheckBoxPreference mSyncEnabledPref;
242        private ListPreference mSyncLookbackPref;
243
244        private static Bundle getArguments(long mailboxId) {
245            final Bundle b = new Bundle(1);
246            b.putLong(EXTRA_MAILBOX_ID, mailboxId);
247            return b;
248        }
249
250        public MailboxSettingsFragment() {}
251
252        @Override
253        public void onActivityCreated(Bundle savedInstanceState) {
254            super.onActivityCreated(savedInstanceState);
255            final long mailboxId = getArguments().getLong(EXTRA_MAILBOX_ID, Mailbox.NO_MAILBOX);
256            if (mailboxId == Mailbox.NO_MAILBOX) {
257                getActivity().finish();
258            }
259
260            addPreferencesFromResource(R.xml.mailbox_preferences);
261
262            mSyncEnabledPref = (CheckBoxPreference) findPreference(PREF_SYNC_ENABLED_KEY);
263            mSyncLookbackPref = (ListPreference) findPreference(PREF_SYNC_WINDOW_KEY);
264
265            mSyncLookbackPref.setOnPreferenceChangeListener(mPreferenceChanged);
266
267            if (savedInstanceState != null) {
268                mMailbox = savedInstanceState.getParcelable(BUNDLE_MAILBOX);
269                mMaxLookback = savedInstanceState.getInt(BUNDLE_MAX_LOOKBACK);
270                mSyncEnabledPref
271                        .setChecked(savedInstanceState.getBoolean(BUNDLE_SYNC_ENABLED_VALUE));
272                mSyncLookbackPref.setValue(savedInstanceState.getString(BUNDLE_SYNC_WINDOW_VALUE));
273                onDataLoaded();
274            } else {
275                // Make them disabled until we load data
276                enablePreferences(false);
277                getLoaderManager().initLoader(0, getArguments(), new MailboxLoaderCallbacks());
278            }
279        }
280
281        private void enablePreferences(boolean enabled) {
282            mSyncEnabledPref.setEnabled(enabled);
283            mSyncLookbackPref.setEnabled(enabled);
284        }
285
286        @Override
287        public void onSaveInstanceState(@NonNull Bundle outState) {
288            super.onSaveInstanceState(outState);
289            outState.putParcelable(BUNDLE_MAILBOX, mMailbox);
290            outState.putInt(BUNDLE_MAX_LOOKBACK, mMaxLookback);
291            outState.putBoolean(BUNDLE_SYNC_ENABLED_VALUE, mSyncEnabledPref.isChecked());
292            outState.putString(BUNDLE_SYNC_WINDOW_VALUE, mSyncLookbackPref.getValue());
293        }
294
295        /**
296         * We save all the settings in onDestroy, *unless it's for configuration changes*.
297         */
298        @Override
299        public void onDestroy() {
300            super.onDestroy();
301            if (!getActivity().isChangingConfigurations()) {
302                saveToDatabase();
303            }
304        }
305
306        private static class MailboxLoader extends MailAsyncTaskLoader<Map<String, Object>> {
307            /** Projection for loading an account's policy key. */
308            private static final String[] POLICY_KEY_PROJECTION =
309                    { AccountColumns.POLICY_KEY };
310            private static final int POLICY_KEY_COLUMN = 0;
311
312            /** Projection for loading the max email lookback. */
313            private static final String[] MAX_EMAIL_LOOKBACK_PROJECTION =
314                    { Policy.MAX_EMAIL_LOOKBACK };
315            private static final int MAX_EMAIL_LOOKBACK_COLUMN = 0;
316
317            public static final String RESULT_KEY_MAILBOX = "mailbox";
318            public static final String RESULT_KEY_MAX_LOOKBACK = "maxLookback";
319
320            private final long mMailboxId;
321
322            private MailboxLoader(Context context, long mailboxId) {
323                super(context);
324                mMailboxId = mailboxId;
325            }
326
327            @Override
328            public Map<String, Object> loadInBackground() {
329                final Map<String, Object> result = new HashMap<>();
330
331                final Mailbox mailbox = Mailbox.restoreMailboxWithId(getContext(), mMailboxId);
332                result.put(RESULT_KEY_MAILBOX, mailbox);
333                result.put(RESULT_KEY_MAX_LOOKBACK, 0);
334
335                if (mailbox == null) {
336                    return result;
337                }
338
339                // Get the max lookback from our policy, if we have one.
340                final Long policyKey = Utility.getFirstRowLong(getContext(),
341                        ContentUris.withAppendedId(Account.CONTENT_URI, mailbox.mAccountKey),
342                        POLICY_KEY_PROJECTION, null, null, null, POLICY_KEY_COLUMN);
343                if (policyKey == null) {
344                    // No policy, nothing to look up.
345                    return result;
346                }
347
348                final int maxLookback = Utility.getFirstRowInt(getContext(),
349                        ContentUris.withAppendedId(Policy.CONTENT_URI, policyKey),
350                        MAX_EMAIL_LOOKBACK_PROJECTION, null, null, null,
351                        MAX_EMAIL_LOOKBACK_COLUMN, 0);
352                result.put(RESULT_KEY_MAX_LOOKBACK, maxLookback);
353
354                return result;
355            }
356
357            @Override
358            protected void onDiscardResult(Map<String, Object> result) {}
359        }
360
361        private class MailboxLoaderCallbacks
362                implements LoaderManager.LoaderCallbacks<Map<String, Object>> {
363            @Override
364            public Loader<Map<String, Object>> onCreateLoader(int id, Bundle args) {
365                final long mailboxId = args.getLong(EXTRA_MAILBOX_ID);
366                return new MailboxLoader(getActivity(), mailboxId);
367            }
368
369            @Override
370            public void onLoadFinished(Loader<Map<String, Object>> loader,
371                    Map<String, Object> data) {
372                final Mailbox mailbox = (Mailbox)
373                        (data == null ? null : data.get(MailboxLoader.RESULT_KEY_MAILBOX));
374                if (mailbox == null) {
375                    getActivity().finish();
376                    return;
377                }
378
379                mMailbox = mailbox;
380                mMaxLookback = (Integer) data.get(MailboxLoader.RESULT_KEY_MAX_LOOKBACK);
381
382                mSyncEnabledPref.setChecked(mMailbox.mSyncInterval != 0);
383                mSyncLookbackPref.setValue(String.valueOf(mMailbox.mSyncLookback));
384                onDataLoaded();
385                if (mMailbox.mType != Mailbox.TYPE_DRAFTS) {
386                    enablePreferences(true);
387                }
388            }
389
390            @Override
391            public void onLoaderReset(Loader<Map<String, Object>> loader) {}
392        }
393
394        /**
395         * Called when {@link #mMailbox} is loaded (either by the loader or from the saved state).
396         */
397        private void onDataLoaded() {
398            Preconditions.checkNotNull(mMailbox);
399
400            // Update the title with the mailbox name.
401            final ActionBar actionBar = getActivity().getActionBar();
402            final String mailboxName = mMailbox.mDisplayName;
403            if (actionBar != null) {
404                actionBar.setTitle(mailboxName);
405                actionBar.setSubtitle(getString(R.string.mailbox_settings_activity_title));
406            } else {
407                getActivity().setTitle(
408                        getString(R.string.mailbox_settings_activity_title_with_mailbox,
409                                mailboxName));
410            }
411
412            MailboxSettings.setupLookbackPreferenceOptions(getActivity(), mSyncLookbackPref,
413                    mMaxLookback, true);
414        }
415
416
417        private final OnPreferenceChangeListener mPreferenceChanged =
418                new OnPreferenceChangeListener() {
419            @Override
420            public boolean onPreferenceChange(Preference preference, Object newValue) {
421                mSyncLookbackPref.setValue((String) newValue);
422                mSyncLookbackPref.setSummary(mSyncLookbackPref.getEntry());
423                return false;
424            }
425        };
426
427        /**
428         * Save changes to the database.
429         *
430         * Note it's called from {@link #onDestroy()}, which is called on the UI thread where we're
431         * not allowed to touch the database, so it uses {@link EmailAsyncTask} to do the save on a
432         * bg thread. This unfortunately means there's a chance that the app gets killed before the
433         * save is finished.
434         */
435        private void saveToDatabase() {
436            if (mMailbox == null) {
437                // We haven't loaded yet, nothing to save.
438                return;
439            }
440            final int syncInterval = mSyncEnabledPref.isChecked() ? 1 : 0;
441            final int syncLookback = Integer.valueOf(mSyncLookbackPref.getValue());
442
443            final boolean syncIntervalChanged = syncInterval != mMailbox.mSyncInterval;
444            final boolean syncLookbackChanged = syncLookback != mMailbox.mSyncLookback;
445
446            // Only save if a preference has changed value.
447            if (!syncIntervalChanged && !syncLookbackChanged) {
448                return;
449            }
450
451            LogUtils.i(Logging.LOG_TAG, "Saving mailbox settings...");
452            enablePreferences(false);
453
454            final long id = mMailbox.mId;
455            final Context context = getActivity().getApplicationContext();
456
457            new EmailAsyncTask<Void, Void, Void> (null /* no cancel */) {
458                @Override
459                protected Void doInBackground(Void... params) {
460                    final ContentValues cv = new ContentValues(2);
461                    final Uri uri;
462                    if (syncIntervalChanged) {
463                        cv.put(MailboxColumns.SYNC_INTERVAL, syncInterval);
464                    }
465                    if (syncLookbackChanged) {
466                        cv.put(MailboxColumns.SYNC_LOOKBACK, syncLookback);
467                    }
468                    uri = ContentUris.withAppendedId(Mailbox.CONTENT_URI, id);
469                    context.getContentResolver().update(uri, cv, null, null);
470
471                    LogUtils.i(Logging.LOG_TAG, "Saved: " + uri);
472                    return null;
473                }
474            }.executeSerial((Void [])null);
475        }
476    }
477}
478