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