de.vanita5.twittnuker.provider.TwidereDataProvider.java Source code

Java tutorial

Introduction

Here is the source code for de.vanita5.twittnuker.provider.TwidereDataProvider.java

Source

/*
 * Twittnuker - Twitter client for Android
 *
 * Copyright (C) 2013-2014 vanita5 <mail@vanita5.de>
 *
 * This program incorporates a modified version of Twidere.
 * Copyright (C) 2012-2014 Mariotaku Lee <mariotaku.lee@gmail.com>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package de.vanita5.twittnuker.provider;

import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.ContentProvider;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Handler;
import android.os.Looper;
import android.os.ParcelFileDescriptor;
import android.support.annotation.NonNull;
import android.support.v4.app.NotificationCompat;
import android.util.Log;

import com.squareup.otto.Bus;

import org.apache.commons.lang3.ArrayUtils;
import org.mariotaku.jsonserializer.JSONFileIO;
import org.mariotaku.querybuilder.Columns.Column;
import org.mariotaku.querybuilder.Expression;
import org.mariotaku.querybuilder.RawItemArray;
import org.mariotaku.querybuilder.query.SQLSelectQuery;
import de.vanita5.twittnuker.Constants;
import de.vanita5.twittnuker.R;
import de.vanita5.twittnuker.app.TwittnukerApplication;
import de.vanita5.twittnuker.model.AccountPreferences;
import de.vanita5.twittnuker.model.NotificationContent;
import de.vanita5.twittnuker.model.ParcelableDirectMessage;
import de.vanita5.twittnuker.model.ParcelableStatus;
import de.vanita5.twittnuker.model.SupportTabSpec;
import de.vanita5.twittnuker.model.UnreadItem;
import de.vanita5.twittnuker.provider.TwidereDataStore.CachedRelationships;
import de.vanita5.twittnuker.provider.TwidereDataStore.CachedUsers;
import de.vanita5.twittnuker.provider.TwidereDataStore.DirectMessages;
import de.vanita5.twittnuker.provider.TwidereDataStore.Drafts;
import de.vanita5.twittnuker.provider.TwidereDataStore.Preferences;
import de.vanita5.twittnuker.provider.TwidereDataStore.SearchHistory;
import de.vanita5.twittnuker.provider.TwidereDataStore.Statuses;
import de.vanita5.twittnuker.provider.TwidereDataStore.UnreadCounts;
import de.vanita5.twittnuker.util.AsyncTwitterWrapper;
import de.vanita5.twittnuker.util.TwidereArrayUtils;
import de.vanita5.twittnuker.util.CustomTabUtils;
import de.vanita5.twittnuker.util.ImagePreloader;
import de.vanita5.twittnuker.util.MediaPreviewUtils;
import de.vanita5.twittnuker.util.NotificationHelper;
import de.vanita5.twittnuker.util.SQLiteDatabaseWrapper;
import de.vanita5.twittnuker.util.SQLiteDatabaseWrapper.LazyLoadCallback;
import de.vanita5.twittnuker.util.SharedPreferencesWrapper;
import de.vanita5.twittnuker.util.TwidereQueryBuilder.CachedUsersQueryBuilder;
import de.vanita5.twittnuker.util.TwidereQueryBuilder.ConversationQueryBuilder;
import de.vanita5.twittnuker.util.collection.NoDuplicatesCopyOnWriteArrayList;
import de.vanita5.twittnuker.util.ParseUtils;
import de.vanita5.twittnuker.util.Utils;
import de.vanita5.twittnuker.util.message.UnreadCountUpdatedEvent;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;

import twitter4j.http.HostAddressResolver;

import static de.vanita5.twittnuker.util.Utils.clearAccountColor;
import static de.vanita5.twittnuker.util.Utils.clearAccountName;
import static de.vanita5.twittnuker.util.Utils.getAccountIds;
import static de.vanita5.twittnuker.util.Utils.getActivatedAccountIds;
import static de.vanita5.twittnuker.util.Utils.getNotificationUri;
import static de.vanita5.twittnuker.util.Utils.getTableId;
import static de.vanita5.twittnuker.util.Utils.getTableNameById;
import static de.vanita5.twittnuker.util.Utils.isFiltered;

public final class TwidereDataProvider extends ContentProvider implements Constants, LazyLoadCallback {

    private static final String UNREAD_STATUSES_FILE_NAME = "unread_statuses";
    private static final String UNREAD_MENTIONS_FILE_NAME = "unread_mentions";
    private static final String UNREAD_MESSAGES_FILE_NAME = "unread_messages";

    private ContentResolver mContentResolver;
    private SQLiteDatabaseWrapper mDatabaseWrapper;
    private NotificationManager mNotificationManager;
    private SharedPreferencesWrapper mPreferences;
    private ImagePreloader mImagePreloader;
    private HostAddressResolver mHostAddressResolver;
    private NotificationHelper mNotificationHelper;
    private Handler mHandler;

    private final List<ParcelableStatus> mNewStatuses = new CopyOnWriteArrayList<>();
    private final List<ParcelableStatus> mNewMentions = new CopyOnWriteArrayList<>();
    private final List<ParcelableDirectMessage> mNewMessages = new CopyOnWriteArrayList<>();

    private final List<UnreadItem> mUnreadStatuses = new NoDuplicatesCopyOnWriteArrayList<>();
    private final List<UnreadItem> mUnreadMentions = new NoDuplicatesCopyOnWriteArrayList<>();
    private final List<UnreadItem> mUnreadMessages = new NoDuplicatesCopyOnWriteArrayList<>();

    @Override
    public int bulkInsert(final Uri uri, @NonNull final ContentValues[] valuesArray) {
        try {
            final int tableId = getTableId(uri);
            final String table = getTableNameById(tableId);
            switch (tableId) {
            case TABLE_ID_DIRECT_MESSAGES_CONVERSATION:
            case TABLE_ID_DIRECT_MESSAGES:
            case TABLE_ID_DIRECT_MESSAGES_CONVERSATIONS_ENTRIES:
                return 0;
            }
            int result = 0;
            final long[] newIds = new long[valuesArray.length];
            if (table != null) {
                mDatabaseWrapper.beginTransaction();
                if (tableId == TABLE_ID_CACHED_USERS) {
                    for (final ContentValues values : valuesArray) {
                        final Expression where = Expression.equals(CachedUsers.USER_ID,
                                values.getAsLong(CachedUsers.USER_ID));
                        mDatabaseWrapper.update(table, values, where.getSQL(), null);
                        newIds[result++] = mDatabaseWrapper.insertWithOnConflict(table, null, values,
                                SQLiteDatabase.CONFLICT_IGNORE);
                    }
                } else if (tableId == TABLE_ID_SEARCH_HISTORY) {
                    for (final ContentValues values : valuesArray) {
                        values.put(SearchHistory.RECENT_QUERY, System.currentTimeMillis());
                        final Expression where = Expression.equalsArgs(SearchHistory.QUERY);
                        final String[] args = { values.getAsString(SearchHistory.QUERY) };
                        mDatabaseWrapper.update(table, values, where.getSQL(), args);
                        newIds[result++] = mDatabaseWrapper.insertWithOnConflict(table, null, values,
                                SQLiteDatabase.CONFLICT_IGNORE);
                    }
                } else if (shouldReplaceOnConflict(tableId)) {
                    for (final ContentValues values : valuesArray) {
                        newIds[result++] = mDatabaseWrapper.insertWithOnConflict(table, null, values,
                                SQLiteDatabase.CONFLICT_REPLACE);
                    }
                } else {
                    for (final ContentValues values : valuesArray) {
                        newIds[result++] = mDatabaseWrapper.insert(table, null, values);
                    }
                }
                mDatabaseWrapper.setTransactionSuccessful();
                mDatabaseWrapper.endTransaction();
            }
            if (result > 0) {
                onDatabaseUpdated(tableId, uri);
            }
            onNewItemsInserted(uri, tableId, valuesArray, newIds);
            return result;
        } catch (final SQLException e) {
            throw new IllegalStateException(e);
        }
    }

    @Override
    public int delete(final Uri uri, final String selection, final String[] selectionArgs) {
        try {
            final int tableId = getTableId(uri);
            final String table = getTableNameById(tableId);
            switch (tableId) {
            case TABLE_ID_DIRECT_MESSAGES_CONVERSATION:
            case TABLE_ID_DIRECT_MESSAGES:
            case TABLE_ID_DIRECT_MESSAGES_CONVERSATIONS_ENTRIES:
                return 0;
            case VIRTUAL_TABLE_ID_NOTIFICATIONS: {
                final List<String> segments = uri.getPathSegments();
                if (segments.size() == 1) {
                    clearNotification();
                } else if (segments.size() == 2) {
                    final int notificationType = ParseUtils.parseInt(segments.get(1));
                    clearNotification(notificationType, 0);
                } else if (segments.size() == 3) {
                    final int notificationType = ParseUtils.parseInt(segments.get(1));
                    final long accountId = ParseUtils.parseLong(segments.get(2));
                    clearNotification(notificationType, accountId);
                }
                return 1;
            }
            case VIRTUAL_TABLE_ID_UNREAD_COUNTS: {
                final List<String> segments = uri.getPathSegments();
                final int segmentsSize = segments.size();
                if (segmentsSize == 1)
                    return clearUnreadCount();
                else if (segmentsSize == 2)
                    return clearUnreadCount(ParseUtils.parseInt(segments.get(1)));
                else if (segmentsSize == 4)
                    return removeUnreadItems(ParseUtils.parseInt(segments.get(1)),
                            ParseUtils.parseLong(segments.get(2)),
                            TwidereArrayUtils.parseLongArray(segments.get(3), ','));
                return 0;
            }
            }
            if (table == null)
                return 0;
            final int result = mDatabaseWrapper.delete(table, selection, selectionArgs);
            if (result > 0) {
                onDatabaseUpdated(tableId, uri);
            }
            return result;
        } catch (final SQLException e) {
            throw new IllegalStateException(e);
        }
    }

    @Override
    public String getType(final Uri uri) {
        return null;
    }

    @Override
    public Uri insert(final Uri uri, final ContentValues values) {
        try {
            final int tableId = getTableId(uri);
            final String table = getTableNameById(tableId);
            switch (tableId) {
            case TABLE_ID_DIRECT_MESSAGES_CONVERSATION:
            case TABLE_ID_DIRECT_MESSAGES:
            case TABLE_ID_DIRECT_MESSAGES_CONVERSATIONS_ENTRIES:
                return null;
            }
            if (table == null)
                return null;
            final long rowId;
            if (tableId == TABLE_ID_CACHED_USERS) {
                final Expression where = Expression.equals(CachedUsers.USER_ID,
                        values.getAsLong(CachedUsers.USER_ID));
                mDatabaseWrapper.update(table, values, where.getSQL(), null);
                rowId = mDatabaseWrapper.insertWithOnConflict(table, null, values, SQLiteDatabase.CONFLICT_IGNORE);
            } else if (tableId == TABLE_ID_SEARCH_HISTORY) {
                values.put(SearchHistory.RECENT_QUERY, System.currentTimeMillis());
                final Expression where = Expression.equalsArgs(SearchHistory.QUERY);
                final String[] args = { values.getAsString(SearchHistory.QUERY) };
                mDatabaseWrapper.update(table, values, where.getSQL(), args);
                rowId = mDatabaseWrapper.insertWithOnConflict(table, null, values, SQLiteDatabase.CONFLICT_IGNORE);
            } else if (tableId == TABLE_ID_CACHED_RELATIONSHIPS) {
                final long accountId = values.getAsLong(CachedRelationships.ACCOUNT_ID);
                final long userId = values.getAsLong(CachedRelationships.USER_ID);
                final Expression where = Expression.and(
                        Expression.equals(CachedRelationships.ACCOUNT_ID, accountId),
                        Expression.equals(CachedRelationships.USER_ID, userId));
                if (mDatabaseWrapper.update(table, values, where.getSQL(), null) > 0) {
                    final String[] projection = { CachedRelationships._ID };
                    final Cursor c = mDatabaseWrapper.query(table, projection, where.getSQL(), null, null, null,
                            null);
                    if (c.moveToFirst()) {
                        rowId = c.getLong(0);
                    } else {
                        rowId = 0;
                    }
                    c.close();
                } else {
                    rowId = mDatabaseWrapper.insertWithOnConflict(table, null, values,
                            SQLiteDatabase.CONFLICT_IGNORE);
                }
            } else if (shouldReplaceOnConflict(tableId)) {
                rowId = mDatabaseWrapper.insertWithOnConflict(table, null, values, SQLiteDatabase.CONFLICT_REPLACE);
            } else {
                rowId = mDatabaseWrapper.insert(table, null, values);
            }
            onDatabaseUpdated(tableId, uri);
            onNewItemsInserted(uri, tableId, values, rowId);
            return Uri.withAppendedPath(uri, String.valueOf(rowId));
        } catch (final SQLException e) {
            throw new IllegalStateException(e);
        }
    }

    @Override
    public boolean onCreate() {
        final Context context = getContext();
        final TwittnukerApplication app = TwittnukerApplication.getInstance(context);
        mHandler = new Handler(Looper.getMainLooper());
        mDatabaseWrapper = new SQLiteDatabaseWrapper(this);
        mHostAddressResolver = app.getHostAddressResolver();
        mPreferences = SharedPreferencesWrapper.getInstance(context, SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE);
        mImagePreloader = new ImagePreloader(context, app.getImageLoader());
        restoreUnreadItems();
        mNotificationHelper = new NotificationHelper(context);
        // final GetWritableDatabaseTask task = new
        // GetWritableDatabaseTask(context, helper, mDatabaseWrapper);
        // task.executeTask();
        return true;
    }

    @Override
    public SQLiteDatabase onCreateSQLiteDatabase() {
        final TwittnukerApplication app = TwittnukerApplication.getInstance(getContext());
        final SQLiteOpenHelper helper = app.getSQLiteOpenHelper();
        return helper.getWritableDatabase();
    }

    @Override
    public ParcelFileDescriptor openFile(final Uri uri, final String mode) throws FileNotFoundException {
        if (uri == null || mode == null)
            throw new IllegalArgumentException();
        final int table_id = getTableId(uri);
        switch (table_id) {
        case VIRTUAL_TABLE_ID_CACHED_IMAGES: {
            return getCachedImageFd(uri.getQueryParameter(QUERY_PARAM_URL));
        }
        case VIRTUAL_TABLE_ID_CACHE_FILES: {
            return getCacheFileFd(uri.getLastPathSegment());
        }
        }
        return null;
    }

    @Override
    public Cursor query(final Uri uri, final String[] projection, final String selection,
            final String[] selectionArgs, final String sortOrder) {
        try {
            final int tableId = getTableId(uri);
            final String table = getTableNameById(tableId);
            switch (tableId) {
            case VIRTUAL_TABLE_ID_DATABASE_READY: {
                if (mDatabaseWrapper.isReady())
                    return new MatrixCursor(projection != null ? projection : new String[0]);
                return null;
            }
            case VIRTUAL_TABLE_ID_ALL_PREFERENCES: {
                return getPreferencesCursor(mPreferences, null);
            }
            case VIRTUAL_TABLE_ID_PREFERENCES: {
                return getPreferencesCursor(mPreferences, uri.getLastPathSegment());
            }
            case VIRTUAL_TABLE_ID_DNS: {
                return getDNSCursor(uri.getLastPathSegment());
            }
            case VIRTUAL_TABLE_ID_CACHED_IMAGES: {
                return getCachedImageCursor(uri.getQueryParameter(QUERY_PARAM_URL));
            }
            case VIRTUAL_TABLE_ID_NOTIFICATIONS: {
                final List<String> segments = uri.getPathSegments();
                if (segments.size() == 2)
                    return getNotificationsCursor(ParseUtils.parseInt(segments.get(1), -1));
                else
                    return getNotificationsCursor();
            }
            case VIRTUAL_TABLE_ID_UNREAD_COUNTS: {
                final List<String> segments = uri.getPathSegments();
                if (segments.size() == 2)
                    return getUnreadCountsCursor(ParseUtils.parseInt(segments.get(1), -1));
                else
                    return getUnreadCountsCursor();
            }
            case VIRTUAL_TABLE_ID_UNREAD_COUNTS_BY_TYPE: {
                final List<String> segments = uri.getPathSegments();
                if (segments.size() != 3)
                    return null;
                return getUnreadCountsCursorByType(segments.get(2));
            }
            case TABLE_ID_DIRECT_MESSAGES_CONVERSATION: {
                final List<String> segments = uri.getPathSegments();
                if (segments.size() != 4)
                    return null;
                final long accountId = ParseUtils.parseLong(segments.get(2));
                final long conversationId = ParseUtils.parseLong(segments.get(3));
                final SQLSelectQuery query = ConversationQueryBuilder.buildByConversationId(projection, accountId,
                        conversationId, selection, sortOrder);
                final Cursor c = mDatabaseWrapper.rawQuery(query.getSQL(), selectionArgs);
                setNotificationUri(c, DirectMessages.CONTENT_URI);
                return c;
            }
            case TABLE_ID_DIRECT_MESSAGES_CONVERSATION_SCREEN_NAME: {
                final List<String> segments = uri.getPathSegments();
                if (segments.size() != 4)
                    return null;
                final long accountId = ParseUtils.parseLong(segments.get(2));
                final String screenName = segments.get(3);
                final SQLSelectQuery query = ConversationQueryBuilder.buildByScreenName(projection, accountId,
                        screenName, selection, sortOrder);
                final Cursor c = mDatabaseWrapper.rawQuery(query.getSQL(), selectionArgs);
                setNotificationUri(c, DirectMessages.CONTENT_URI);
                return c;
            }
            case VIRTUAL_TABLE_ID_CACHED_USERS_WITH_RELATIONSHIP: {
                final long accountId = ParseUtils.parseLong(uri.getLastPathSegment(), -1);
                final SQLSelectQuery query = CachedUsersQueryBuilder.buildWithRelationship(projection, selection,
                        sortOrder, accountId);
                final Cursor c = mDatabaseWrapper.rawQuery(query.getSQL(), selectionArgs);
                setNotificationUri(c, CachedUsers.CONTENT_URI);
                return c;
            }
            case VIRTUAL_TABLE_ID_CACHED_USERS_WITH_SCORE: {
                final long accountId = ParseUtils.parseLong(uri.getLastPathSegment(), -1);
                final SQLSelectQuery query = CachedUsersQueryBuilder.buildWithScore(projection, selection,
                        sortOrder, accountId);
                final Cursor c = mDatabaseWrapper.rawQuery(query.getSQL(), selectionArgs);
                setNotificationUri(c, CachedUsers.CONTENT_URI);
                return c;
            }
            case VIRTUAL_TABLE_ID_DRAFTS_UNSENT: {
                final TwittnukerApplication app = TwittnukerApplication.getInstance(getContext());
                final AsyncTwitterWrapper twitter = app.getTwitterWrapper();
                final RawItemArray sendingIds = new RawItemArray(twitter.getSendingDraftIds());
                final Expression where;
                if (selection != null) {
                    where = Expression.and(new Expression(selection),
                            Expression.notIn(new Column(Drafts._ID), sendingIds));
                } else {
                    where = Expression.and(Expression.notIn(new Column(Drafts._ID), sendingIds));
                }
                final Cursor c = mDatabaseWrapper.query(Drafts.TABLE_NAME, projection, where.getSQL(),
                        selectionArgs, null, null, sortOrder);
                setNotificationUri(c, getNotificationUri(tableId, uri));
                return c;
            }
            }
            if (table == null)
                return null;
            final Cursor c = mDatabaseWrapper.query(table, projection, selection, selectionArgs, null, null,
                    sortOrder);
            setNotificationUri(c, getNotificationUri(tableId, uri));
            return c;
        } catch (final SQLException e) {
            throw new IllegalStateException(e);
        }
    }

    @Override
    public int update(final Uri uri, final ContentValues values, final String selection,
            final String[] selectionArgs) {
        try {
            final int tableId = getTableId(uri);
            final String table = getTableNameById(tableId);
            int result = 0;
            if (table != null) {
                switch (tableId) {
                case TABLE_ID_DIRECT_MESSAGES_CONVERSATION:
                case TABLE_ID_DIRECT_MESSAGES:
                case TABLE_ID_DIRECT_MESSAGES_CONVERSATIONS_ENTRIES:
                    return 0;
                }
                result = mDatabaseWrapper.update(table, values, selection, selectionArgs);
            }
            if (result > 0) {
                onDatabaseUpdated(tableId, uri);
            }
            return result;
        } catch (final SQLException e) {
            throw new IllegalStateException(e);
        }
    }

    private void clearNotification() {
        mNewStatuses.clear();
        mNewMentions.clear();
        mNewMessages.clear();
    }

    private void clearNotification(final int notificationType, final long accountId) {
        switch (notificationType) {
        case NOTIFICATION_ID_HOME_TIMELINE: {
            mNewStatuses.clear();
            break;
        }
        case NOTIFICATION_ID_MENTIONS_TIMELINE: {
            mNewMentions.clear();
            break;
        }
        case NOTIFICATION_ID_DIRECT_MESSAGES: {
            mNewMessages.clear();
            break;
        }
        default: {
        }
        }
    }

    private int clearUnreadCount() {
        int result = 0;
        result += mUnreadStatuses.size();
        result += mUnreadMentions.size();
        result += mUnreadMentions.size();
        mUnreadStatuses.clear();
        mUnreadMentions.clear();
        mUnreadMessages.clear();
        saveUnreadItemsFile(mUnreadStatuses, UNREAD_STATUSES_FILE_NAME);
        saveUnreadItemsFile(mUnreadMentions, UNREAD_MENTIONS_FILE_NAME);
        saveUnreadItemsFile(mUnreadMessages, UNREAD_MESSAGES_FILE_NAME);
        notifyContentObserver(UnreadCounts.CONTENT_URI);
        return result;
    }

    private int clearUnreadCount(final int position) {
        final Context context = getContext();
        final int result;
        final SupportTabSpec tab = CustomTabUtils.getAddedTabAt(context, position);
        final String type = tab.type;
        if (TAB_TYPE_HOME_TIMELINE.equals(type) || TAB_TYPE_STAGGERED_HOME_TIMELINE.equals(type)) {
            final long account_id = tab.args != null ? tab.args.getLong(EXTRA_ACCOUNT_ID, -1) : -1;
            final long[] account_ids = account_id > 0 ? new long[] { account_id } : getActivatedAccountIds(context);
            result = clearUnreadCount(mUnreadStatuses, account_ids);
            saveUnreadItemsFile(mUnreadStatuses, UNREAD_STATUSES_FILE_NAME);
        } else if (TAB_TYPE_MENTIONS_TIMELINE.equals(type)) {
            final long account_id = tab.args != null ? tab.args.getLong(EXTRA_ACCOUNT_ID, -1) : -1;
            final long[] account_ids = account_id > 0 ? new long[] { account_id } : getActivatedAccountIds(context);
            result = clearUnreadCount(mUnreadMentions, account_ids);
            mUnreadMentions.clear();
            saveUnreadItemsFile(mUnreadMentions, UNREAD_MENTIONS_FILE_NAME);
        } else if (TAB_TYPE_DIRECT_MESSAGES.equals(type)) {
            final long account_id = tab.args != null ? tab.args.getLong(EXTRA_ACCOUNT_ID, -1) : -1;
            final long[] account_ids = account_id > 0 ? new long[] { account_id } : getActivatedAccountIds(context);
            result = clearUnreadCount(mUnreadMessages, account_ids);
            mUnreadMessages.clear();
            saveUnreadItemsFile(mUnreadMessages, UNREAD_MESSAGES_FILE_NAME);
        } else
            return 0;
        if (result > 0) {
            notifyUnreadCountChanged(position);
        }
        return result;
    }

    /**
     * Creates notifications for mentions and DMs
     * @param pref
     * @param type
     * @param build
     */
    private void createNotifications(final AccountPreferences pref, final String type, final Object o_status,
            final boolean build) {
        if (mPreferences.getBoolean(KEY_ENABLE_PUSH, false))
            return;
        NotificationContent notification = null;

        if (o_status instanceof ParcelableStatus) {
            ParcelableStatus status = (ParcelableStatus) o_status;
            notification = new NotificationContent();
            notification.setAccountId(status.account_id);
            notification.setFromUser(status.user_screen_name);
            notification.setType(type);
            notification.setMessage(status.text_unescaped);
            notification.setTimestamp(status.timestamp);
            notification.setProfileImageUrl(status.user_profile_image_url);
            notification.setOriginalStatus(status);
        } else if (o_status instanceof ParcelableDirectMessage) {
            ParcelableDirectMessage dm = (ParcelableDirectMessage) o_status;
            notification = new NotificationContent();
            notification.setAccountId(dm.account_id);
            notification.setFromUser(dm.sender_screen_name);
            notification.setType(type);
            notification.setMessage(dm.text_unescaped);
            notification.setTimestamp(dm.timestamp);
            notification.setProfileImageUrl(dm.sender_profile_image_url);
            notification.setOriginalMessage(dm);
        }
        if (notification != null) {
            mNotificationHelper.cachePushNotification(notification);
        }
        if (build)
            mNotificationHelper.buildNotificationByType(notification, pref, false);
    }

    private Cursor getCachedImageCursor(final String url) {
        if (Utils.isDebugBuild()) {
            Log.d(LOGTAG, String.format("getCachedImageCursor(%s)", url));
        }
        final MatrixCursor c = new MatrixCursor(TwidereDataStore.CachedImages.MATRIX_COLUMNS);
        final File file = mImagePreloader.getCachedImageFile(url);
        if (url != null && file != null) {
            c.addRow(new String[] { url, file.getPath() });
        }
        return c;
    }

    private ParcelFileDescriptor getCachedImageFd(final String url) throws FileNotFoundException {
        if (Utils.isDebugBuild()) {
            Log.d(LOGTAG, String.format("getCachedImageFd(%s)", url));
        }
        final File file = mImagePreloader.getCachedImageFile(url);
        if (file == null)
            return null;
        return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
    }

    private ParcelFileDescriptor getCacheFileFd(final String name) throws FileNotFoundException {
        if (name == null)
            return null;
        final Context mContext = getContext();
        final File cacheDir = mContext.getCacheDir();
        final File file = new File(cacheDir, name);
        if (!file.exists())
            return null;
        return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
    }

    private ContentResolver getContentResolver() {
        if (mContentResolver != null)
            return mContentResolver;
        final Context context = getContext();
        return mContentResolver = context.getContentResolver();
    }

    private Cursor getDNSCursor(final String host) {
        final MatrixCursor c = new MatrixCursor(TwidereDataStore.DNS.MATRIX_COLUMNS);
        try {
            final String address = mHostAddressResolver.resolve(host);
            if (host != null && address != null) {
                c.addRow(new String[] { host, address });
            }
        } catch (final IOException e) {

        }
        return c;
    }

    private NotificationManager getNotificationManager() {
        if (mNotificationManager != null)
            return mNotificationManager;
        final Context context = getContext();
        return mNotificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
    }

    private Cursor getNotificationsCursor() {
        final MatrixCursor c = new MatrixCursor(TwidereDataStore.Notifications.MATRIX_COLUMNS);
        c.addRow(new Integer[] { NOTIFICATION_ID_HOME_TIMELINE, mUnreadStatuses.size() });
        c.addRow(new Integer[] { NOTIFICATION_ID_MENTIONS_TIMELINE, mNewMentions.size() });
        c.addRow(new Integer[] { NOTIFICATION_ID_DIRECT_MESSAGES, mNewMessages.size() });
        return c;
    }

    private Cursor getNotificationsCursor(final int id) {
        final MatrixCursor c = new MatrixCursor(TwidereDataStore.Notifications.MATRIX_COLUMNS);
        if (id == NOTIFICATION_ID_HOME_TIMELINE) {
            c.addRow(new Integer[] { id, mNewStatuses.size() });
        } else if (id == NOTIFICATION_ID_MENTIONS_TIMELINE) {
            c.addRow(new Integer[] { id, mNewMentions.size() });
        } else if (id == NOTIFICATION_ID_DIRECT_MESSAGES) {
            c.addRow(new Integer[] { id, mNewMessages.size() });
        }
        return c;
    }

    private Cursor getUnreadCountsCursor() {
        final MatrixCursor c = new MatrixCursor(TwidereDataStore.UnreadCounts.MATRIX_COLUMNS);
        return c;
    }

    private Cursor getUnreadCountsCursor(final int position) {
        final MatrixCursor c = new MatrixCursor(TwidereDataStore.UnreadCounts.MATRIX_COLUMNS);
        final Context context = getContext();
        final SupportTabSpec tab = CustomTabUtils.getAddedTabAt(context, position);
        if (tab == null)
            return c;
        final int count;
        if (TAB_TYPE_HOME_TIMELINE.equals(tab.type) || TAB_TYPE_STAGGERED_HOME_TIMELINE.equals(tab.type)) {
            final long account_id = tab.args != null ? tab.args.getLong(EXTRA_ACCOUNT_ID, -1) : -1;
            final long[] account_ids = account_id > 0 ? new long[] { account_id } : getActivatedAccountIds(context);
            count = getUnreadCount(mUnreadStatuses, account_ids);
        } else if (TAB_TYPE_MENTIONS_TIMELINE.equals(tab.type)) {
            final long account_id = tab.args != null ? tab.args.getLong(EXTRA_ACCOUNT_ID, -1) : -1;
            final long[] account_ids = account_id > 0 ? new long[] { account_id } : getActivatedAccountIds(context);
            count = getUnreadCount(mUnreadMentions, account_ids);
        } else if (TAB_TYPE_DIRECT_MESSAGES.equals(tab.type)) {
            final long account_id = tab.args != null ? tab.args.getLong(EXTRA_ACCOUNT_ID, -1) : -1;
            final long[] account_ids = account_id > 0 ? new long[] { account_id } : getActivatedAccountIds(context);
            count = getUnreadCount(mUnreadMessages, account_ids);
        } else {
            count = 0;
        }
        if (tab.type != null) {
            c.addRow(new Object[] { position, tab.type, count });
        }
        return c;
    }

    private Cursor getUnreadCountsCursorByType(final String type) {
        final MatrixCursor c = new MatrixCursor(TwidereDataStore.UnreadCounts.MATRIX_COLUMNS);
        final int count;
        if (TAB_TYPE_HOME_TIMELINE.equals(type) || TAB_TYPE_STAGGERED_HOME_TIMELINE.equals(type)) {
            count = mUnreadStatuses.size();
        } else if (TAB_TYPE_MENTIONS_TIMELINE.equals(type)) {
            count = mUnreadMentions.size();
        } else if (TAB_TYPE_DIRECT_MESSAGES.equals(type)) {
            count = mUnreadMessages.size();
        } else {
            count = 0;
        }
        if (type != null) {
            c.addRow(new Object[] { -1, type, count });
        }
        return c;
    }

    private void notifyContentObserver(final Uri uri) {
        final ContentResolver cr = getContentResolver();
        if (uri == null || cr == null)
            return;
        cr.notifyChange(uri, null);
    }

    private int notifyIncomingMessagesInserted(final ContentValues... values) {
        if (values == null || values.length == 0)
            return 0;
        // Add statuses that not filtered to list for future use.
        int result = 0;
        int i = 1;
        for (final ContentValues value : values) {
            final ParcelableDirectMessage message = new ParcelableDirectMessage(value);
            mNewMessages.add(message);
            if (mUnreadMessages.add(new UnreadItem(message.sender_id, message.account_id))) { //we got a new dm
                //DM Notification
                final AccountPreferences[] prefs = AccountPreferences
                        .getNotificationEnabledPreferences(getContext(), getAccountIds(getContext()));
                final AccountPreferences pref = AccountPreferences.getAccountPreferences(prefs, message.account_id);
                if (pref != null && pref.isDirectMessagesNotificationEnabled()) {
                    createNotifications(pref, NotificationContent.NOTIFICATION_TYPE_MENTION, message,
                            i >= values.length);
                }
                result++;
            }
        }
        if (result > 0) {
            saveUnreadItemsFile(mUnreadMessages, UNREAD_MESSAGES_FILE_NAME);
        }
        return result;
    }

    private int notifyMentionsInserted(final AccountPreferences[] prefs, final ContentValues... values) {
        if (values == null || values.length == 0)
            return 0;
        // Add statuses that not filtered to list for future use.
        int result = 0;
        final boolean enabled = mPreferences.getBoolean(KEY_FILTERS_IN_MENTIONS_TIMELINE, true);
        final boolean filtersForRts = mPreferences.getBoolean(KEY_FILTERS_FOR_RTS, true);
        int i = 1;
        for (final ContentValues value : values) {
            final ParcelableStatus status = new ParcelableStatus(value);
            if (!enabled || !isFiltered(mDatabaseWrapper.getSQLiteDatabase(), status, filtersForRts)) {
                final AccountPreferences pref = AccountPreferences.getAccountPreferences(prefs, status.account_id);
                if (pref == null || status.user_is_following || !pref.isNotificationFollowingOnly()) {
                    mNewMentions.add(status);
                }
                if (mUnreadMentions.add(new UnreadItem(status.id, status.account_id))) { //we got a new mention
                    //Mention Notification
                    if (pref != null && pref.isMentionsNotificationEnabled()) {
                        createNotifications(pref, NotificationContent.NOTIFICATION_TYPE_MENTION, status,
                                i >= values.length);
                    }
                    result++;
                }
            }
            i++;
        }
        if (result > 0) {
            saveUnreadItemsFile(mUnreadMentions, UNREAD_MENTIONS_FILE_NAME);
        }
        return result;
    }

    private int notifyStatusesInserted(final ContentValues... values) {
        if (values == null || values.length == 0)
            return 0;
        // Add statuses that not filtered to list for future use.
        int result = 0;
        final boolean enabled = mPreferences.getBoolean(KEY_FILTERS_IN_HOME_TIMELINE, true);
        final boolean filtersForRts = mPreferences.getBoolean(KEY_FILTERS_FOR_RTS, true);
        for (final ContentValues value : values) {
            final ParcelableStatus status = new ParcelableStatus(value);
            if (!enabled || !isFiltered(mDatabaseWrapper.getSQLiteDatabase(), status, filtersForRts)) {
                mNewStatuses.add(status);
                if (mUnreadStatuses.add(new UnreadItem(status.id, status.account_id))) {
                    result++;
                }
            }
        }
        if (result > 0) {
            saveUnreadItemsFile(mUnreadStatuses, UNREAD_STATUSES_FILE_NAME);
        }
        return result;
    }

    private void notifyUnreadCountChanged(final int position) {
        final Context context = getContext();
        final Bus bus = TwittnukerApplication.getInstance(context).getMessageBus();
        mHandler.post(new Runnable() {
            @Override
            public void run() {
                bus.post(new UnreadCountUpdatedEvent(position));
            }
        });
        notifyContentObserver(UnreadCounts.CONTENT_URI);
    }

    private void onDatabaseUpdated(final int tableId, final Uri uri) {
        if (uri == null)
            return;
        switch (tableId) {
        case TABLE_ID_ACCOUNTS: {
            clearAccountColor();
            clearAccountName();
            break;
        }
        }
        notifyContentObserver(getNotificationUri(tableId, uri));
    }

    private void onNewItemsInserted(final Uri uri, final int tableId, final ContentValues values,
            final long newId) {
        onNewItemsInserted(uri, tableId, new ContentValues[] { values }, new long[] { newId });

    }

    private void onNewItemsInserted(final Uri uri, final int tableId, final ContentValues[] valuesArray,
            final long[] newIds) {
        if (uri == null || valuesArray == null || valuesArray.length == 0)
            return;
        preloadImages(valuesArray);
        if (!uri.getBooleanQueryParameter(QUERY_PARAM_NOTIFY, true))
            return;
        switch (tableId) {
        case TABLE_ID_STATUSES: {
            notifyStatusesInserted(valuesArray);
            final List<ParcelableStatus> items = new ArrayList<ParcelableStatus>(mNewStatuses);
            Collections.sort(items);
            //TODO Notifications for new tweets in timeline
            notifyUnreadCountChanged(NOTIFICATION_ID_HOME_TIMELINE);
            break;
        }
        case TABLE_ID_MENTIONS: {
            final AccountPreferences[] prefs = AccountPreferences.getNotificationEnabledPreferences(getContext(),
                    getAccountIds(getContext()));
            notifyMentionsInserted(prefs, valuesArray);
            notifyUnreadCountChanged(NOTIFICATION_ID_MENTIONS_TIMELINE);
            break;
        }
        case TABLE_ID_DIRECT_MESSAGES_INBOX: {
            notifyIncomingMessagesInserted(valuesArray);
            notifyUnreadCountChanged(NOTIFICATION_ID_DIRECT_MESSAGES);
            break;
        }
        case TABLE_ID_DRAFTS: {
            break;
        }
        }
    }

    private void preloadImages(final ContentValues... values) {
        if (values == null)
            return;
        for (final ContentValues v : values) {
            if (mPreferences.getBoolean(KEY_PRELOAD_PROFILE_IMAGES, false)) {
                mImagePreloader.preloadImage(v.getAsString(Statuses.USER_PROFILE_IMAGE_URL));
                mImagePreloader.preloadImage(v.getAsString(DirectMessages.SENDER_PROFILE_IMAGE_URL));
                mImagePreloader.preloadImage(v.getAsString(DirectMessages.RECIPIENT_PROFILE_IMAGE_URL));
            }
            if (mPreferences.getBoolean(KEY_PRELOAD_PREVIEW_IMAGES, false)) {
                final String textHtml = v.getAsString(Statuses.TEXT_HTML);
                for (final String link : MediaPreviewUtils.getSupportedLinksInStatus(textHtml)) {
                    mImagePreloader.preloadImage(link);
                }
            }
        }
    }

    private int removeUnreadItems(final int tab_position, final long account_id, final long... ids) {
        if (tab_position < 0 || account_id == 0 || ids == null || ids.length == 0)
            return 0;
        final UnreadItem[] items = new UnreadItem[ids.length];
        for (int i = 0, j = ids.length; i < j; i++) {
            items[i] = new UnreadItem(ids[i], account_id);
        }
        return removeUnreadItems(tab_position, items);
    }

    private synchronized int removeUnreadItems(final int tab_position, final UnreadItem... items) {
        if (tab_position < 0 || items == null || items.length == 0)
            return 0;
        final int result;
        String notificationType = null;
        final String type = CustomTabUtils.getAddedTabTypeAt(getContext(), tab_position);
        final List<UnreadItem> arrItems = Arrays.asList(items);
        if (TAB_TYPE_HOME_TIMELINE.equals(type)) {
            final int size = mUnreadStatuses.size();
            mUnreadStatuses.removeAll(arrItems);
            result = size - mUnreadStatuses.size();
            if (result != 0) {
                saveUnreadItemsFile(mUnreadStatuses, UNREAD_STATUSES_FILE_NAME);
            }
        } else if (TAB_TYPE_MENTIONS_TIMELINE.equals(type)) {
            notificationType = NotificationContent.NOTIFICATION_TYPE_MENTION;
            final int size = mUnreadMentions.size();
            mUnreadMentions.removeAll(arrItems);
            result = size - mUnreadMentions.size();
            if (result != 0) {
                saveUnreadItemsFile(mUnreadMentions, UNREAD_MENTIONS_FILE_NAME);
            }
        } else if (TAB_TYPE_DIRECT_MESSAGES.equals(type)) {
            notificationType = NotificationContent.NOTIFICATION_TYPE_DIRECT_MESSAGE;
            final int size = mUnreadMessages.size();
            mUnreadMessages.removeAll(arrItems);
            result = size - mUnreadMessages.size();
            if (result != 0) {
                saveUnreadItemsFile(mUnreadMessages, UNREAD_MESSAGES_FILE_NAME);
            }
        } else
            return 0;
        if (result != 0) {
            notifyUnreadCountChanged(tab_position);
        }

        if (notificationType != null) {
            //get unique account ids from removed items
            List<Long> accountIds = new ArrayList<>();
            for (UnreadItem unreadItem : arrItems) {
                if (!accountIds.contains(unreadItem.account_id) && unreadItem.account_id > 0) {
                    accountIds.add(unreadItem.account_id);
                }
            }

            if (!accountIds.isEmpty()) {
                for (Long accountId : accountIds) {
                    mNotificationHelper.deleteCachedNotifications(accountId, notificationType);
                }
            } else { //remove for all accounts
                for (final long id : getAccountIds(getContext())) {
                    mNotificationHelper.deleteCachedNotifications(id, notificationType);
                }
            }
        }

        return result;
    }

    private void restoreUnreadItems() {
        restoreUnreadItemsFile(mUnreadStatuses, UNREAD_STATUSES_FILE_NAME);
        restoreUnreadItemsFile(mUnreadMentions, UNREAD_MENTIONS_FILE_NAME);
        restoreUnreadItemsFile(mUnreadMessages, UNREAD_MESSAGES_FILE_NAME);
    }

    private void restoreUnreadItemsFile(final Collection<UnreadItem> items, final String name) {
        if (items == null || name == null)
            return;
        try {
            final File file = JSONFileIO.getSerializationFile(getContext(), name);
            final List<UnreadItem> restored = JSONFileIO.readArrayList(file);
            if (restored != null) {
                items.addAll(restored);
            }
        } catch (final IOException e) {
            e.printStackTrace();
        }
    }

    private void saveUnreadItemsFile(final Collection<UnreadItem> items, final String name) {
        if (items == null || name == null)
            return;
        try {
            final File file = JSONFileIO.getSerializationFile(getContext(), name);
            JSONFileIO.writeArray(file, items.toArray(new UnreadItem[items.size()]));
        } catch (final IOException e) {
            e.printStackTrace();
        }
    }

    private void setNotificationUri(final Cursor c, final Uri uri) {
        final ContentResolver cr = getContentResolver();
        if (cr == null || c == null || uri == null)
            return;
        c.setNotificationUri(cr, uri);
    }

    private static int clearUnreadCount(final List<UnreadItem> set, final long[] accountIds) {
        if (accountIds == null)
            return 0;
        int count = 0;
        for (final UnreadItem item : set.toArray(new UnreadItem[set.size()])) {
            if (item != null && ArrayUtils.contains(accountIds, item.account_id) && set.remove(item)) {
                count++;
            }
        }
        return count;
    }

    private static List<ParcelableDirectMessage> getMessagesForAccounts(final List<ParcelableDirectMessage> items,
            final long accountId) {
        if (items == null)
            return Collections.emptyList();
        final List<ParcelableDirectMessage> result = new ArrayList<>();
        for (final ParcelableDirectMessage item : items.toArray(new ParcelableDirectMessage[items.size()])) {
            if (item.account_id == accountId) {
                result.add(item);
            }
        }
        return result;
    }

    private static Cursor getPreferencesCursor(final SharedPreferencesWrapper preferences, final String key) {
        final MatrixCursor c = new MatrixCursor(TwidereDataStore.Preferences.MATRIX_COLUMNS);
        final Map<String, Object> map = new HashMap<>();
        final Map<String, ?> all = preferences.getAll();
        if (key == null) {
            map.putAll(all);
        } else {
            map.put(key, all.get(key));
        }
        for (final Map.Entry<String, ?> item : map.entrySet()) {
            final Object value = item.getValue();
            final int type = getPreferenceType(value);
            c.addRow(new Object[] { item.getKey(), ParseUtils.parseString(value), type });
        }
        return c;
    }

    private static int getPreferenceType(final Object object) {
        if (object == null)
            return Preferences.TYPE_NULL;
        else if (object instanceof Boolean)
            return Preferences.TYPE_BOOLEAN;
        else if (object instanceof Integer)
            return Preferences.TYPE_INTEGER;
        else if (object instanceof Long)
            return Preferences.TYPE_LONG;
        else if (object instanceof Float)
            return Preferences.TYPE_FLOAT;
        else if (object instanceof String)
            return Preferences.TYPE_STRING;
        return Preferences.TYPE_INVALID;
    }

    private static List<ParcelableStatus> getStatusesForAccounts(final List<ParcelableStatus> items,
            final long accountId) {
        if (items == null)
            return Collections.emptyList();
        final List<ParcelableStatus> result = new ArrayList<>();
        for (final ParcelableStatus item : items.toArray(new ParcelableStatus[items.size()])) {
            if (item.account_id == accountId) {
                result.add(item);
            }
        }
        return result;
    }

    private static int getUnreadCount(final List<UnreadItem> set, final long... accountIds) {
        if (set == null || set.isEmpty())
            return 0;
        int count = 0;
        for (final UnreadItem item : set.toArray(new UnreadItem[set.size()])) {
            if (item != null && ArrayUtils.contains(accountIds, item.account_id)) {
                count++;
            }
        }
        return count;
    }

    private static <T> T safeGet(final List<T> list, final int index) {
        return index >= 0 && index < list.size() ? list.get(index) : null;
    }

    private static boolean shouldReplaceOnConflict(final int table_id) {
        switch (table_id) {
        case TABLE_ID_CACHED_HASHTAGS:
        case TABLE_ID_CACHED_STATUSES:
        case TABLE_ID_CACHED_USERS:
        case TABLE_ID_CACHED_RELATIONSHIPS:
        case TABLE_ID_SEARCH_HISTORY:
        case TABLE_ID_FILTERED_USERS:
        case TABLE_ID_FILTERED_KEYWORDS:
        case TABLE_ID_FILTERED_SOURCES:
        case TABLE_ID_FILTERED_LINKS:
            return true;
        }
        return false;
    }

    @SuppressWarnings("unused")
    private static class GetWritableDatabaseTask extends AsyncTask<Void, Void, SQLiteDatabase> {
        private final Context mContext;
        private final SQLiteOpenHelper mHelper;
        private final SQLiteDatabaseWrapper mWrapper;

        GetWritableDatabaseTask(final Context context, final SQLiteOpenHelper helper,
                final SQLiteDatabaseWrapper wrapper) {
            mContext = context;
            mHelper = helper;
            mWrapper = wrapper;
        }

        @Override
        protected SQLiteDatabase doInBackground(final Void... params) {
            return mHelper.getWritableDatabase();
        }

        @Override
        protected void onPostExecute(final SQLiteDatabase result) {
            mWrapper.setSQLiteDatabase(result);
            if (result != null) {
                mContext.sendBroadcast(new Intent(BROADCAST_DATABASE_READY));
            }
        }
    }

}