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.emailcommon.provider;
18
19import android.content.ContentResolver;
20import android.content.ContentUris;
21import android.content.ContentValues;
22import android.content.Context;
23import android.database.Cursor;
24import android.text.TextUtils;
25
26import com.android.emailcommon.Logging;
27import com.android.emailcommon.provider.EmailContent.AccountColumns;
28import com.android.emailcommon.provider.EmailContent.MailboxColumns;
29import com.android.mail.utils.LogUtils;
30
31import java.util.HashMap;
32
33public class MailboxUtilities {
34
35    public static final String FIX_PARENT_KEYS_METHOD = "fix_parent_keys";
36
37    public static final String WHERE_PARENT_KEY_UNINITIALIZED =
38        "(" + MailboxColumns.PARENT_KEY + " isnull OR " + MailboxColumns.PARENT_KEY + "=" +
39        Mailbox.PARENT_KEY_UNINITIALIZED + ")";
40    // The flag we use in Account to indicate a mailbox change in progress
41    private static final int ACCOUNT_MAILBOX_CHANGE_FLAG = Account.FLAGS_SYNC_ADAPTER;
42
43    /**
44     * Recalculate a mailbox's flags and the parent key of any children
45     * @param context the caller's context
46     * @param parentCursor a cursor to a mailbox that requires fixup
47     */
48    @Deprecated
49    public static void setFlagsAndChildrensParentKey(Context context, Cursor parentCursor,
50            String accountSelector) {
51        ContentResolver resolver = context.getContentResolver();
52        String[] selectionArgs = new String[1];
53        ContentValues parentValues = new ContentValues();
54        // Get the data we need first
55        long parentId = parentCursor.getLong(Mailbox.CONTENT_ID_COLUMN);
56        int parentFlags = 0;
57        int parentType = parentCursor.getInt(Mailbox.CONTENT_TYPE_COLUMN);
58        String parentServerId = parentCursor.getString(Mailbox.CONTENT_SERVER_ID_COLUMN);
59        // All email-type boxes hold mail
60        if (parentType <= Mailbox.TYPE_NOT_EMAIL) {
61            parentFlags |= Mailbox.FLAG_HOLDS_MAIL + Mailbox.FLAG_SUPPORTS_SETTINGS;
62        }
63        // Outbox, Drafts, and Sent don't allow mail to be moved to them
64        if (parentType == Mailbox.TYPE_MAIL || parentType == Mailbox.TYPE_TRASH ||
65                parentType == Mailbox.TYPE_JUNK || parentType == Mailbox.TYPE_INBOX) {
66            parentFlags |= Mailbox.FLAG_ACCEPTS_MOVED_MAIL;
67        }
68        // There's no concept of "append" in EAS so FLAG_ACCEPTS_APPENDED_MAIL is never used
69        // Mark parent mailboxes as parents & add parent key to children
70        // An example of a mailbox with a null serverId would be an Outbox that we create locally
71        // for hotmail accounts (which don't have a server-based Outbox)
72        if (parentServerId != null) {
73            selectionArgs[0] = parentServerId;
74            Cursor childCursor = resolver.query(Mailbox.CONTENT_URI,
75                    Mailbox.ID_PROJECTION, MailboxColumns.PARENT_SERVER_ID + "=? AND " +
76                    accountSelector, selectionArgs, null);
77            if (childCursor == null) return;
78            try {
79                while (childCursor.moveToNext()) {
80                    parentFlags |= Mailbox.FLAG_HAS_CHILDREN | Mailbox.FLAG_CHILDREN_VISIBLE;
81                    ContentValues childValues = new ContentValues();
82                    childValues.put(Mailbox.PARENT_KEY, parentId);
83                    long childId = childCursor.getLong(Mailbox.ID_PROJECTION_COLUMN);
84                    resolver.update(ContentUris.withAppendedId(Mailbox.CONTENT_URI, childId),
85                            childValues, null, null);
86                }
87            } finally {
88                childCursor.close();
89            }
90        } else {
91            // Mark this is having no parent, so that we don't examine this mailbox again
92            parentValues.put(Mailbox.PARENT_KEY, Mailbox.NO_MAILBOX);
93            LogUtils.w(Logging.LOG_TAG, "Mailbox with null serverId: " +
94                    parentCursor.getString(Mailbox.CONTENT_DISPLAY_NAME_COLUMN) + ", type: " +
95                    parentType);
96        }
97        // Save away updated flags and parent key (if any)
98        parentValues.put(Mailbox.FLAGS, parentFlags);
99        resolver.update(ContentUris.withAppendedId(Mailbox.CONTENT_URI, parentId),
100                parentValues, null, null);
101    }
102
103    /**
104     * Recalculate a mailbox's flags and the parent key of any children
105     * @param context the caller's context
106     * @param accountSelector (see description below in fixupUninitializedParentKeys)
107     * @param serverId the server id of an individual mailbox
108     */
109    @Deprecated
110    public static void setFlagsAndChildrensParentKey(Context context, String accountSelector,
111            String serverId) {
112        Cursor cursor = context.getContentResolver().query(Mailbox.CONTENT_URI,
113                Mailbox.CONTENT_PROJECTION, MailboxColumns.SERVER_ID + "=? AND " + accountSelector,
114                new String[] {serverId}, null);
115        if (cursor == null) return;
116        try {
117            if (cursor.moveToFirst()) {
118                setFlagsAndChildrensParentKey(context, cursor, accountSelector);
119            }
120        } finally {
121            cursor.close();
122        }
123    }
124
125    /**
126     * Given an account selector, specifying the account(s) on which to work, create the parentKey
127     * and flags for each mailbox in the account(s) that is uninitialized (parentKey = 0 or null)
128     *
129     * @param accountSelector a sqlite WHERE clause expression to be used in determining the
130     * mailboxes to be acted upon, e.g. accountKey IN (1, 2), accountKey = 12, etc.
131     */
132    @Deprecated
133    public static void fixupUninitializedParentKeys(Context context, String accountSelector) {
134        // Sanity check first on our arguments
135        if (accountSelector == null) throw new IllegalArgumentException();
136        // The selection we'll use to find uninitialized parent key mailboxes
137        String noParentKeySelection = WHERE_PARENT_KEY_UNINITIALIZED + " AND " + accountSelector;
138
139        // We'll loop through mailboxes with an uninitialized parent key
140        ContentResolver resolver = context.getContentResolver();
141        Cursor noParentKeyMailboxCursor =
142                resolver.query(Mailbox.CONTENT_URI, Mailbox.CONTENT_PROJECTION,
143                        noParentKeySelection, null, null);
144        if (noParentKeyMailboxCursor == null) return;
145        try {
146            while (noParentKeyMailboxCursor.moveToNext()) {
147                setFlagsAndChildrensParentKey(context, noParentKeyMailboxCursor, accountSelector);
148                String parentServerId =
149                        noParentKeyMailboxCursor.getString(Mailbox.CONTENT_PARENT_SERVER_ID_COLUMN);
150                // Fixup the parent so that the children's parentKey is updated
151                if (parentServerId != null) {
152                    setFlagsAndChildrensParentKey(context, accountSelector, parentServerId);
153                }
154            }
155        } finally {
156            noParentKeyMailboxCursor.close();
157        }
158
159        // Any mailboxes without a parent key should have parentKey set to -1 (no parent)
160        ContentValues values = new ContentValues();
161        values.put(Mailbox.PARENT_KEY, Mailbox.NO_MAILBOX);
162        resolver.update(Mailbox.CONTENT_URI, values, noParentKeySelection, null);
163     }
164
165    private static void setAccountSyncAdapterFlag(Context context, long accountId, boolean start) {
166        Account account = Account.restoreAccountWithId(context, accountId);
167        if (account == null) return;
168        // Set temporary flag indicating state of update of mailbox list
169        ContentValues cv = new ContentValues();
170        cv.put(AccountColumns.FLAGS, start ? (account.mFlags | ACCOUNT_MAILBOX_CHANGE_FLAG) :
171            account.mFlags & ~ACCOUNT_MAILBOX_CHANGE_FLAG);
172        context.getContentResolver().update(
173                ContentUris.withAppendedId(Account.CONTENT_URI, account.mId), cv, null, null);
174    }
175
176    /**
177     * Indicate that the specified account is starting the process of changing its mailbox list
178     * @param context the caller's context
179     * @param accountId the account that is starting to change its mailbox list
180     */
181    public static void startMailboxChanges(Context context, long accountId) {
182        setAccountSyncAdapterFlag(context, accountId, true);
183    }
184
185    /**
186     * Indicate that the specified account is ending the process of changing its mailbox list
187     * @param context the caller's context
188     * @param accountId the account that is finished with changes to its mailbox list
189     */
190    public static void endMailboxChanges(Context context, long accountId) {
191        setAccountSyncAdapterFlag(context, accountId, false);
192    }
193
194    /**
195     * Check that we didn't leave the account's mailboxes in a (possibly) inconsistent state
196     * If we did, make them consistent again
197     * @param context the caller's context
198     * @param accountId the account whose mailboxes are to be checked
199     */
200    @Deprecated
201    public static void checkMailboxConsistency(Context context, long accountId) {
202        // If our temporary flag is set, we were interrupted during an update
203        // First, make sure we're current (really fast w/ caching)
204        Account account = Account.restoreAccountWithId(context, accountId);
205        if (account == null) return;
206        if ((account.mFlags & ACCOUNT_MAILBOX_CHANGE_FLAG) != 0) {
207            LogUtils.w(Logging.LOG_TAG, "Account " + account.mDisplayName +
208                    " has inconsistent mailbox data; fixing up...");
209            // Set all account mailboxes to uninitialized parent key
210            ContentValues values = new ContentValues();
211            values.put(Mailbox.PARENT_KEY, Mailbox.PARENT_KEY_UNINITIALIZED);
212            String accountSelector = Mailbox.ACCOUNT_KEY + "=" + account.mId;
213            ContentResolver resolver = context.getContentResolver();
214            resolver.update(Mailbox.CONTENT_URI, values, accountSelector, null);
215            // Fix up keys and flags
216            MailboxUtilities.fixupUninitializedParentKeys(context, accountSelector);
217            // Clear the temporary flag
218            endMailboxChanges(context, accountId);
219        }
220    }
221
222    private static final String[] HIERARCHY_PROJECTION = new String[] {
223        MailboxColumns._ID, MailboxColumns.DISPLAY_NAME, MailboxColumns.PARENT_KEY,
224        MailboxColumns.HIERARCHICAL_NAME
225    };
226    private static final int HIERARCHY_ID = 0;
227    private static final int HIERARCHY_NAME = 1;
228    private static final int HIERARCHY_PARENT_KEY = 2;
229    private static final int HIERARCHY_HIERARCHICAL_NAME = 3;
230
231    private static String getHierarchicalName(Context context, long id, HashMap<Long, String> map,
232            String name, long parentId) {
233        String hierarchicalName;
234        if (map.containsKey(id)) {
235            return map.get(id);
236        } else if (parentId == Mailbox.NO_MAILBOX) {
237            hierarchicalName = name;
238        } else {
239            Mailbox parent = Mailbox.restoreMailboxWithId(context, parentId);
240            if (parent == null) return name + "/" + "??";
241            hierarchicalName = getHierarchicalName(context, parentId, map, parent.mDisplayName,
242                    parent.mParentKey) + "/" + name;
243        }
244        map.put(id, hierarchicalName);
245        return hierarchicalName;
246    }
247
248    public static void setupHierarchicalNames(Context context, long accountId) {
249        Account account = Account.restoreAccountWithId(context, accountId);
250        if (account == null) return;
251        // Start by clearing all names
252        ContentValues values = new ContentValues();
253        String accountSelector = Mailbox.ACCOUNT_KEY + "=" + account.mId;
254        ContentResolver resolver = context.getContentResolver();
255        HashMap<Long, String> nameMap = new HashMap<Long, String>();
256        Cursor c = resolver.query(Mailbox.CONTENT_URI, HIERARCHY_PROJECTION, accountSelector,
257                null, null);
258        try {
259            while(c.moveToNext()) {
260                long id = c.getLong(HIERARCHY_ID);
261                String displayName = c.getString(HIERARCHY_NAME);
262                String name = getHierarchicalName(context, id, nameMap, displayName,
263                        c.getLong(HIERARCHY_PARENT_KEY));
264                String oldHierarchicalName = c.getString(HIERARCHY_HIERARCHICAL_NAME);
265                // Don't write the name unless it has changed or we don't need one (it's top-level)
266                if (name.equals(oldHierarchicalName) ||
267                        ((name.equals(displayName)) && TextUtils.isEmpty(oldHierarchicalName))) {
268                    continue;
269                }
270                // If the name has changed, update it
271                values.put(MailboxColumns.HIERARCHICAL_NAME, name);
272                resolver.update(ContentUris.withAppendedId(Mailbox.CONTENT_URI, id), values, null,
273                        null);
274            }
275        } finally {
276            c.close();
277        }
278    }
279}
280