116080bf86eb0c42308b5fe767a69453358abb556Nancy Chen/*
216080bf86eb0c42308b5fe767a69453358abb556Nancy Chen * Copyright (C) 2015 The Android Open Source Project
316080bf86eb0c42308b5fe767a69453358abb556Nancy Chen *
416080bf86eb0c42308b5fe767a69453358abb556Nancy Chen * Licensed under the Apache License, Version 2.0 (the "License");
516080bf86eb0c42308b5fe767a69453358abb556Nancy Chen * you may not use this file except in compliance with the License.
616080bf86eb0c42308b5fe767a69453358abb556Nancy Chen * You may obtain a copy of the License at
716080bf86eb0c42308b5fe767a69453358abb556Nancy Chen *
816080bf86eb0c42308b5fe767a69453358abb556Nancy Chen *      http://www.apache.org/licenses/LICENSE-2.0
916080bf86eb0c42308b5fe767a69453358abb556Nancy Chen *
1016080bf86eb0c42308b5fe767a69453358abb556Nancy Chen * Unless required by applicable law or agreed to in writing, software
1116080bf86eb0c42308b5fe767a69453358abb556Nancy Chen * distributed under the License is distributed on an "AS IS" BASIS,
1216080bf86eb0c42308b5fe767a69453358abb556Nancy Chen * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1316080bf86eb0c42308b5fe767a69453358abb556Nancy Chen * See the License for the specific language governing permissions and
1416080bf86eb0c42308b5fe767a69453358abb556Nancy Chen * limitations under the License
1516080bf86eb0c42308b5fe767a69453358abb556Nancy Chen */
1616080bf86eb0c42308b5fe767a69453358abb556Nancy Chenpackage com.android.phone.vvm.omtp.sync;
1716080bf86eb0c42308b5fe767a69453358abb556Nancy Chen
1816080bf86eb0c42308b5fe767a69453358abb556Nancy Chenimport android.content.ContentResolver;
1916080bf86eb0c42308b5fe767a69453358abb556Nancy Chenimport android.content.ContentUris;
2016080bf86eb0c42308b5fe767a69453358abb556Nancy Chenimport android.content.ContentValues;
2116080bf86eb0c42308b5fe767a69453358abb556Nancy Chenimport android.content.Context;
2216080bf86eb0c42308b5fe767a69453358abb556Nancy Chenimport android.database.Cursor;
2316080bf86eb0c42308b5fe767a69453358abb556Nancy Chenimport android.net.Uri;
2416080bf86eb0c42308b5fe767a69453358abb556Nancy Chenimport android.provider.VoicemailContract;
2516080bf86eb0c42308b5fe767a69453358abb556Nancy Chenimport android.provider.VoicemailContract.Voicemails;
26651358c4c54e01f3241ce277f61ea15bce646ebfNancy Chenimport android.telecom.PhoneAccountHandle;
2716080bf86eb0c42308b5fe767a69453358abb556Nancy Chenimport android.telecom.Voicemail;
28651358c4c54e01f3241ce277f61ea15bce646ebfNancy Chenimport android.util.Log;
2916080bf86eb0c42308b5fe767a69453358abb556Nancy Chen
3016080bf86eb0c42308b5fe767a69453358abb556Nancy Chenimport java.util.ArrayList;
3116080bf86eb0c42308b5fe767a69453358abb556Nancy Chenimport java.util.List;
3216080bf86eb0c42308b5fe767a69453358abb556Nancy Chen
3316080bf86eb0c42308b5fe767a69453358abb556Nancy Chen/**
342cf7f2935c71b0ddbdda86fa6bc18b33db2dbf99Nancy Chen * Construct queries to interact with the voicemails table.
3516080bf86eb0c42308b5fe767a69453358abb556Nancy Chen */
3616080bf86eb0c42308b5fe767a69453358abb556Nancy Chenpublic class VoicemailsQueryHelper {
37651358c4c54e01f3241ce277f61ea15bce646ebfNancy Chen    private static final String TAG = "VoicemailsQueryHelper";
38651358c4c54e01f3241ce277f61ea15bce646ebfNancy Chen
3916080bf86eb0c42308b5fe767a69453358abb556Nancy Chen    final static String[] PROJECTION = new String[] {
4016080bf86eb0c42308b5fe767a69453358abb556Nancy Chen            Voicemails._ID,              // 0
4116080bf86eb0c42308b5fe767a69453358abb556Nancy Chen            Voicemails.SOURCE_DATA,      // 1
4216080bf86eb0c42308b5fe767a69453358abb556Nancy Chen            Voicemails.IS_READ,          // 2
4316080bf86eb0c42308b5fe767a69453358abb556Nancy Chen            Voicemails.DELETED,          // 3
4416080bf86eb0c42308b5fe767a69453358abb556Nancy Chen    };
4516080bf86eb0c42308b5fe767a69453358abb556Nancy Chen
4616080bf86eb0c42308b5fe767a69453358abb556Nancy Chen    public static final int _ID = 0;
4716080bf86eb0c42308b5fe767a69453358abb556Nancy Chen    public static final int SOURCE_DATA = 1;
4816080bf86eb0c42308b5fe767a69453358abb556Nancy Chen    public static final int IS_READ = 2;
4916080bf86eb0c42308b5fe767a69453358abb556Nancy Chen    public static final int DELETED = 3;
5016080bf86eb0c42308b5fe767a69453358abb556Nancy Chen
5142f5cb37b049a2cb1b047ae997538ac3afb90a21Nancy Chen    final static String READ_SELECTION = Voicemails.DIRTY + "=1 AND "
5242f5cb37b049a2cb1b047ae997538ac3afb90a21Nancy Chen                + Voicemails.DELETED + "!=1 AND " + Voicemails.IS_READ + "=1";
5316080bf86eb0c42308b5fe767a69453358abb556Nancy Chen    final static String DELETED_SELECTION = Voicemails.DELETED + "=1";
5416080bf86eb0c42308b5fe767a69453358abb556Nancy Chen
5516080bf86eb0c42308b5fe767a69453358abb556Nancy Chen    private Context mContext;
5616080bf86eb0c42308b5fe767a69453358abb556Nancy Chen    private ContentResolver mContentResolver;
5716080bf86eb0c42308b5fe767a69453358abb556Nancy Chen    private Uri mSourceUri;
5816080bf86eb0c42308b5fe767a69453358abb556Nancy Chen
5916080bf86eb0c42308b5fe767a69453358abb556Nancy Chen    public VoicemailsQueryHelper(Context context) {
6016080bf86eb0c42308b5fe767a69453358abb556Nancy Chen        mContext = context;
6116080bf86eb0c42308b5fe767a69453358abb556Nancy Chen        mContentResolver = context.getContentResolver();
6216080bf86eb0c42308b5fe767a69453358abb556Nancy Chen        mSourceUri = VoicemailContract.Voicemails.buildSourceUri(mContext.getPackageName());
6316080bf86eb0c42308b5fe767a69453358abb556Nancy Chen    }
6416080bf86eb0c42308b5fe767a69453358abb556Nancy Chen
6516080bf86eb0c42308b5fe767a69453358abb556Nancy Chen    /**
6616080bf86eb0c42308b5fe767a69453358abb556Nancy Chen     * Get all the local read voicemails that have not been synced to the server.
6716080bf86eb0c42308b5fe767a69453358abb556Nancy Chen     *
6816080bf86eb0c42308b5fe767a69453358abb556Nancy Chen     * @return A list of read voicemails.
6916080bf86eb0c42308b5fe767a69453358abb556Nancy Chen     */
7016080bf86eb0c42308b5fe767a69453358abb556Nancy Chen    public List<Voicemail> getReadVoicemails() {
7116080bf86eb0c42308b5fe767a69453358abb556Nancy Chen        return getLocalVoicemails(READ_SELECTION);
7216080bf86eb0c42308b5fe767a69453358abb556Nancy Chen    }
7316080bf86eb0c42308b5fe767a69453358abb556Nancy Chen
7416080bf86eb0c42308b5fe767a69453358abb556Nancy Chen    /**
7516080bf86eb0c42308b5fe767a69453358abb556Nancy Chen     * Get all the locally deleted voicemails that have not been synced to the server.
7616080bf86eb0c42308b5fe767a69453358abb556Nancy Chen     *
7716080bf86eb0c42308b5fe767a69453358abb556Nancy Chen     * @return A list of deleted voicemails.
7816080bf86eb0c42308b5fe767a69453358abb556Nancy Chen     */
7916080bf86eb0c42308b5fe767a69453358abb556Nancy Chen    public List<Voicemail> getDeletedVoicemails() {
8016080bf86eb0c42308b5fe767a69453358abb556Nancy Chen        return getLocalVoicemails(DELETED_SELECTION);
8116080bf86eb0c42308b5fe767a69453358abb556Nancy Chen    }
8216080bf86eb0c42308b5fe767a69453358abb556Nancy Chen
8316080bf86eb0c42308b5fe767a69453358abb556Nancy Chen    /**
8416080bf86eb0c42308b5fe767a69453358abb556Nancy Chen     * Get all voicemails locally stored.
8516080bf86eb0c42308b5fe767a69453358abb556Nancy Chen     *
8616080bf86eb0c42308b5fe767a69453358abb556Nancy Chen     * @return A list of all locally stored voicemails.
8716080bf86eb0c42308b5fe767a69453358abb556Nancy Chen     */
8816080bf86eb0c42308b5fe767a69453358abb556Nancy Chen    public List<Voicemail> getAllVoicemails() {
8916080bf86eb0c42308b5fe767a69453358abb556Nancy Chen        return getLocalVoicemails(null);
9016080bf86eb0c42308b5fe767a69453358abb556Nancy Chen    }
9116080bf86eb0c42308b5fe767a69453358abb556Nancy Chen
9216080bf86eb0c42308b5fe767a69453358abb556Nancy Chen    /**
9316080bf86eb0c42308b5fe767a69453358abb556Nancy Chen     * Utility method to make queries to the voicemail database.
9416080bf86eb0c42308b5fe767a69453358abb556Nancy Chen     *
9516080bf86eb0c42308b5fe767a69453358abb556Nancy Chen     * @param selection A filter declaring which rows to return. {@code null} returns all rows.
9616080bf86eb0c42308b5fe767a69453358abb556Nancy Chen     * @return A list of voicemails according to the selection statement.
9716080bf86eb0c42308b5fe767a69453358abb556Nancy Chen     */
9816080bf86eb0c42308b5fe767a69453358abb556Nancy Chen    private List<Voicemail> getLocalVoicemails(String selection) {
9916080bf86eb0c42308b5fe767a69453358abb556Nancy Chen        Cursor cursor = mContentResolver.query(mSourceUri, PROJECTION, selection, null, null);
10016080bf86eb0c42308b5fe767a69453358abb556Nancy Chen        if (cursor == null) {
10116080bf86eb0c42308b5fe767a69453358abb556Nancy Chen            return null;
10216080bf86eb0c42308b5fe767a69453358abb556Nancy Chen        }
10316080bf86eb0c42308b5fe767a69453358abb556Nancy Chen        try {
10416080bf86eb0c42308b5fe767a69453358abb556Nancy Chen            List<Voicemail> voicemails = new ArrayList<Voicemail>();
10516080bf86eb0c42308b5fe767a69453358abb556Nancy Chen            while (cursor.moveToNext()) {
1062cf7f2935c71b0ddbdda86fa6bc18b33db2dbf99Nancy Chen                final long id = cursor.getLong(_ID);
1072cf7f2935c71b0ddbdda86fa6bc18b33db2dbf99Nancy Chen                final String sourceData = cursor.getString(SOURCE_DATA);
1086337d1be5db67c7b81e7998988ca53da50ab44c2Nancy Chen                final boolean isRead = cursor.getInt(IS_READ) == 1;
1096337d1be5db67c7b81e7998988ca53da50ab44c2Nancy Chen                Voicemail voicemail = Voicemail
1106337d1be5db67c7b81e7998988ca53da50ab44c2Nancy Chen                        .createForUpdate(id, sourceData)
1116337d1be5db67c7b81e7998988ca53da50ab44c2Nancy Chen                        .setIsRead(isRead).build();
11216080bf86eb0c42308b5fe767a69453358abb556Nancy Chen                voicemails.add(voicemail);
11316080bf86eb0c42308b5fe767a69453358abb556Nancy Chen            }
11416080bf86eb0c42308b5fe767a69453358abb556Nancy Chen            return voicemails;
11516080bf86eb0c42308b5fe767a69453358abb556Nancy Chen        } finally {
11616080bf86eb0c42308b5fe767a69453358abb556Nancy Chen            cursor.close();
11716080bf86eb0c42308b5fe767a69453358abb556Nancy Chen        }
11816080bf86eb0c42308b5fe767a69453358abb556Nancy Chen    }
11916080bf86eb0c42308b5fe767a69453358abb556Nancy Chen
12016080bf86eb0c42308b5fe767a69453358abb556Nancy Chen    /**
12116080bf86eb0c42308b5fe767a69453358abb556Nancy Chen     * Deletes a list of voicemails from the voicemail content provider.
12216080bf86eb0c42308b5fe767a69453358abb556Nancy Chen     *
12316080bf86eb0c42308b5fe767a69453358abb556Nancy Chen     * @param voicemails The list of voicemails to delete
12416080bf86eb0c42308b5fe767a69453358abb556Nancy Chen     * @return The number of voicemails deleted
12516080bf86eb0c42308b5fe767a69453358abb556Nancy Chen     */
12616080bf86eb0c42308b5fe767a69453358abb556Nancy Chen    public int deleteFromDatabase(List<Voicemail> voicemails) {
12716080bf86eb0c42308b5fe767a69453358abb556Nancy Chen        int count = voicemails.size();
12816080bf86eb0c42308b5fe767a69453358abb556Nancy Chen        if (count == 0) {
12916080bf86eb0c42308b5fe767a69453358abb556Nancy Chen            return 0;
13016080bf86eb0c42308b5fe767a69453358abb556Nancy Chen        }
13116080bf86eb0c42308b5fe767a69453358abb556Nancy Chen
13216080bf86eb0c42308b5fe767a69453358abb556Nancy Chen        StringBuilder sb = new StringBuilder();
13316080bf86eb0c42308b5fe767a69453358abb556Nancy Chen        for (int i = 0; i < count; i++) {
13416080bf86eb0c42308b5fe767a69453358abb556Nancy Chen            if (i > 0) {
13516080bf86eb0c42308b5fe767a69453358abb556Nancy Chen                sb.append(",");
13616080bf86eb0c42308b5fe767a69453358abb556Nancy Chen            }
13716080bf86eb0c42308b5fe767a69453358abb556Nancy Chen            sb.append(voicemails.get(i).getId());
13816080bf86eb0c42308b5fe767a69453358abb556Nancy Chen        }
13916080bf86eb0c42308b5fe767a69453358abb556Nancy Chen
14016080bf86eb0c42308b5fe767a69453358abb556Nancy Chen        String selectionStatement = String.format(Voicemails._ID + " IN (%s)", sb.toString());
14116080bf86eb0c42308b5fe767a69453358abb556Nancy Chen        return mContentResolver.delete(Voicemails.CONTENT_URI, selectionStatement, null);
14216080bf86eb0c42308b5fe767a69453358abb556Nancy Chen    }
14316080bf86eb0c42308b5fe767a69453358abb556Nancy Chen
14416080bf86eb0c42308b5fe767a69453358abb556Nancy Chen    /**
14516080bf86eb0c42308b5fe767a69453358abb556Nancy Chen     * Utility method to delete a single voicemail.
14616080bf86eb0c42308b5fe767a69453358abb556Nancy Chen     */
14716080bf86eb0c42308b5fe767a69453358abb556Nancy Chen    public void deleteFromDatabase(Voicemail voicemail) {
14816080bf86eb0c42308b5fe767a69453358abb556Nancy Chen        mContentResolver.delete(Voicemails.CONTENT_URI, Voicemails._ID + "=?",
14916080bf86eb0c42308b5fe767a69453358abb556Nancy Chen                new String[] { Long.toString(voicemail.getId()) });
15016080bf86eb0c42308b5fe767a69453358abb556Nancy Chen    }
15116080bf86eb0c42308b5fe767a69453358abb556Nancy Chen
15216080bf86eb0c42308b5fe767a69453358abb556Nancy Chen    /**
15316080bf86eb0c42308b5fe767a69453358abb556Nancy Chen     * Sends an update command to the voicemail content provider for a list of voicemails.
15416080bf86eb0c42308b5fe767a69453358abb556Nancy Chen     * From the view of the provider, since the updater is the owner of the entry, a blank
15516080bf86eb0c42308b5fe767a69453358abb556Nancy Chen     * "update" means that the voicemail source is indicating that the server has up-to-date
15616080bf86eb0c42308b5fe767a69453358abb556Nancy Chen     * information on the voicemail. This flips the "dirty" bit to "0".
15716080bf86eb0c42308b5fe767a69453358abb556Nancy Chen     *
15816080bf86eb0c42308b5fe767a69453358abb556Nancy Chen     * @param voicemails The list of voicemails to update
15916080bf86eb0c42308b5fe767a69453358abb556Nancy Chen     * @return The number of voicemails updated
16016080bf86eb0c42308b5fe767a69453358abb556Nancy Chen     */
16116080bf86eb0c42308b5fe767a69453358abb556Nancy Chen    public int markReadInDatabase(List<Voicemail> voicemails) {
16216080bf86eb0c42308b5fe767a69453358abb556Nancy Chen        int count = voicemails.size();
16316080bf86eb0c42308b5fe767a69453358abb556Nancy Chen        for (int i = 0; i < count; i++) {
16416080bf86eb0c42308b5fe767a69453358abb556Nancy Chen            markReadInDatabase(voicemails.get(i));
16516080bf86eb0c42308b5fe767a69453358abb556Nancy Chen        }
16616080bf86eb0c42308b5fe767a69453358abb556Nancy Chen        return count;
16716080bf86eb0c42308b5fe767a69453358abb556Nancy Chen    }
16816080bf86eb0c42308b5fe767a69453358abb556Nancy Chen
16916080bf86eb0c42308b5fe767a69453358abb556Nancy Chen    /**
17016080bf86eb0c42308b5fe767a69453358abb556Nancy Chen     * Utility method to mark single message as read.
17116080bf86eb0c42308b5fe767a69453358abb556Nancy Chen     */
17216080bf86eb0c42308b5fe767a69453358abb556Nancy Chen    public void markReadInDatabase(Voicemail voicemail) {
17316080bf86eb0c42308b5fe767a69453358abb556Nancy Chen        Uri uri = ContentUris.withAppendedId(mSourceUri, voicemail.getId());
17416080bf86eb0c42308b5fe767a69453358abb556Nancy Chen        ContentValues contentValues = new ContentValues();
1756337d1be5db67c7b81e7998988ca53da50ab44c2Nancy Chen        contentValues.put(Voicemails.IS_READ, "1");
17616080bf86eb0c42308b5fe767a69453358abb556Nancy Chen        mContentResolver.update(uri, contentValues, null, null);
17716080bf86eb0c42308b5fe767a69453358abb556Nancy Chen    }
178651358c4c54e01f3241ce277f61ea15bce646ebfNancy Chen
179651358c4c54e01f3241ce277f61ea15bce646ebfNancy Chen    /**
180651358c4c54e01f3241ce277f61ea15bce646ebfNancy Chen     * Check if a particular voicemail has already been inserted. If not, insert the new voicemail.
181651358c4c54e01f3241ce277f61ea15bce646ebfNancy Chen     * @param voicemail The voicemail to insert.
182651358c4c54e01f3241ce277f61ea15bce646ebfNancy Chen     */
183651358c4c54e01f3241ce277f61ea15bce646ebfNancy Chen    public void insertIfUnique(Voicemail voicemail) {
184651358c4c54e01f3241ce277f61ea15bce646ebfNancy Chen        if (isVoicemailUnique(voicemail)) {
185651358c4c54e01f3241ce277f61ea15bce646ebfNancy Chen            VoicemailContract.Voicemails.insert(mContext, voicemail);
186651358c4c54e01f3241ce277f61ea15bce646ebfNancy Chen        } else {
187651358c4c54e01f3241ce277f61ea15bce646ebfNancy Chen            Log.w(TAG, "Voicemail already exists.");
188651358c4c54e01f3241ce277f61ea15bce646ebfNancy Chen        }
189651358c4c54e01f3241ce277f61ea15bce646ebfNancy Chen    }
190651358c4c54e01f3241ce277f61ea15bce646ebfNancy Chen
191651358c4c54e01f3241ce277f61ea15bce646ebfNancy Chen    /**
192651358c4c54e01f3241ce277f61ea15bce646ebfNancy Chen     * Voicemail is unique if the tuple of (phone account component name, phone account id, source
193651358c4c54e01f3241ce277f61ea15bce646ebfNancy Chen     * data) is unique. If the phone account is missing, we also consider this unique since it's
194651358c4c54e01f3241ce277f61ea15bce646ebfNancy Chen     * simply an "unknown" account.
195651358c4c54e01f3241ce277f61ea15bce646ebfNancy Chen     * @param voicemail The voicemail to check if it is unique.
196651358c4c54e01f3241ce277f61ea15bce646ebfNancy Chen     * @return {@code true} if the voicemail is unique, {@code false} otherwise.
197651358c4c54e01f3241ce277f61ea15bce646ebfNancy Chen     */
198651358c4c54e01f3241ce277f61ea15bce646ebfNancy Chen    private boolean isVoicemailUnique(Voicemail voicemail) {
199651358c4c54e01f3241ce277f61ea15bce646ebfNancy Chen        Cursor cursor = null;
200651358c4c54e01f3241ce277f61ea15bce646ebfNancy Chen        PhoneAccountHandle phoneAccount = voicemail.getPhoneAccount();
201651358c4c54e01f3241ce277f61ea15bce646ebfNancy Chen        if (phoneAccount != null) {
202651358c4c54e01f3241ce277f61ea15bce646ebfNancy Chen            String phoneAccountComponentName = phoneAccount.getComponentName().flattenToString();
203651358c4c54e01f3241ce277f61ea15bce646ebfNancy Chen            String phoneAccountId = phoneAccount.getId();
204651358c4c54e01f3241ce277f61ea15bce646ebfNancy Chen            String sourceData = voicemail.getSourceData();
205651358c4c54e01f3241ce277f61ea15bce646ebfNancy Chen            if (phoneAccountComponentName == null || phoneAccountId == null || sourceData == null) {
206651358c4c54e01f3241ce277f61ea15bce646ebfNancy Chen                return true;
207651358c4c54e01f3241ce277f61ea15bce646ebfNancy Chen            }
208651358c4c54e01f3241ce277f61ea15bce646ebfNancy Chen            try {
209651358c4c54e01f3241ce277f61ea15bce646ebfNancy Chen                String whereClause =
210651358c4c54e01f3241ce277f61ea15bce646ebfNancy Chen                        Voicemails.PHONE_ACCOUNT_COMPONENT_NAME + "=? AND " +
211651358c4c54e01f3241ce277f61ea15bce646ebfNancy Chen                        Voicemails.PHONE_ACCOUNT_ID + "=? AND " + Voicemails.SOURCE_DATA + "=?";
212651358c4c54e01f3241ce277f61ea15bce646ebfNancy Chen                String[] whereArgs = { phoneAccountComponentName, phoneAccountId, sourceData };
213651358c4c54e01f3241ce277f61ea15bce646ebfNancy Chen                cursor = mContentResolver.query(
214651358c4c54e01f3241ce277f61ea15bce646ebfNancy Chen                        mSourceUri, PROJECTION, whereClause, whereArgs, null);
215651358c4c54e01f3241ce277f61ea15bce646ebfNancy Chen                if (cursor.getCount() == 0) {
216651358c4c54e01f3241ce277f61ea15bce646ebfNancy Chen                    return true;
217651358c4c54e01f3241ce277f61ea15bce646ebfNancy Chen                } else {
218651358c4c54e01f3241ce277f61ea15bce646ebfNancy Chen                    return false;
219651358c4c54e01f3241ce277f61ea15bce646ebfNancy Chen                }
220651358c4c54e01f3241ce277f61ea15bce646ebfNancy Chen            }
221651358c4c54e01f3241ce277f61ea15bce646ebfNancy Chen            finally {
222651358c4c54e01f3241ce277f61ea15bce646ebfNancy Chen                if (cursor != null) {
223651358c4c54e01f3241ce277f61ea15bce646ebfNancy Chen                    cursor.close();
224651358c4c54e01f3241ce277f61ea15bce646ebfNancy Chen                }
225651358c4c54e01f3241ce277f61ea15bce646ebfNancy Chen            }
226651358c4c54e01f3241ce277f61ea15bce646ebfNancy Chen        }
227651358c4c54e01f3241ce277f61ea15bce646ebfNancy Chen        return true;
228651358c4c54e01f3241ce277f61ea15bce646ebfNancy Chen    }
22916080bf86eb0c42308b5fe767a69453358abb556Nancy Chen}
230