diff --git a/twidere.component.common/src/main/java/org/mariotaku/twidere/TwidereConstants.java b/twidere.component.common/src/main/java/org/mariotaku/twidere/TwidereConstants.java index 00952c1e0..df90a29cb 100644 --- a/twidere.component.common/src/main/java/org/mariotaku/twidere/TwidereConstants.java +++ b/twidere.component.common/src/main/java/org/mariotaku/twidere/TwidereConstants.java @@ -64,8 +64,6 @@ public interface TwidereConstants extends SharedPreferenceConstants, IntentConst String KEYBOARD_SHORTCUTS_PREFERENCES_NAME = "keyboard_shortcuts_preferences"; String ETAG_CACHE_PREFERENCES_NAME = "etag_cache"; - String TWITTER_CONSUMER_KEY_LEGACY = "uAFVpMhBntJutfVj6abfA"; - String TWITTER_CONSUMER_SECRET_LEGACY = "JARXkJTfxo0F8MyctYy9bUmrLISjo8vXAHsZHYuk2E"; String TWITTER_CONSUMER_KEY = "0WEJk1x6AlgtjGRhyABXw"; String TWITTER_CONSUMER_SECRET = "gWXNqEFhO3fMkAqoIKpTdjK0MOJs68xnOky0FRdDTP8"; @@ -192,7 +190,6 @@ public interface TwidereConstants extends SharedPreferenceConstants, IntentConst int REQUEST_REQUEST_PERMISSIONS = 30; int REQUEST_PURCHASE_EXTRA_FEATURES = 41; - int TABLE_ID_ACCOUNTS = 1; int TABLE_ID_STATUSES = 12; int TABLE_ID_MENTIONS = 13; int TABLE_ID_ACTIVITIES_ABOUT_ME = 14; @@ -213,15 +210,7 @@ public interface TwidereConstants extends SharedPreferenceConstants, IntentConst int TABLE_ID_CACHED_STATUSES = 62; int TABLE_ID_CACHED_HASHTAGS = 63; int TABLE_ID_CACHED_RELATIONSHIPS = 64; - int VIRTUAL_TABLE_ID_NOTIFICATIONS = 101; - int VIRTUAL_TABLE_ID_PREFERENCES = 102; - int VIRTUAL_TABLE_ID_ALL_PREFERENCES = 103; int VIRTUAL_TABLE_ID_PERMISSIONS = 104; - int VIRTUAL_TABLE_ID_DNS = 105; - int VIRTUAL_TABLE_ID_CACHED_IMAGES = 106; - int VIRTUAL_TABLE_ID_CACHE_FILES = 107; - int VIRTUAL_TABLE_ID_UNREAD_COUNTS = 108; - int VIRTUAL_TABLE_ID_UNREAD_COUNTS_BY_TYPE = 109; int VIRTUAL_TABLE_ID_CACHED_USERS_WITH_RELATIONSHIP = 121; int VIRTUAL_TABLE_ID_CACHED_USERS_WITH_SCORE = 122; int VIRTUAL_TABLE_ID_DRAFTS_UNSENT = 131; @@ -243,8 +232,6 @@ public interface TwidereConstants extends SharedPreferenceConstants, IntentConst int NOTIFICATION_ID_UPDATE_STATUS = 101; int NOTIFICATION_ID_SEND_DIRECT_MESSAGE = 102; - String ICON_SPECIAL_TYPE_CUSTOMIZE = "_customize"; - String METADATA_KEY_EXTENSION = "org.mariotaku.twidere.extension"; String METADATA_KEY_EXTENSION_PERMISSIONS = "org.mariotaku.twidere.extension.permissions"; @@ -262,18 +249,12 @@ public interface TwidereConstants extends SharedPreferenceConstants, IntentConst String PERMISSION_READ = "read"; String PERMISSION_WRITE = "write"; String PERMISSION_DIRECT_MESSAGES = "direct_messages"; - @Deprecated - String PERMISSION_ACCOUNTS = "accounts"; String PERMISSION_PREFERENCES = "preferences"; int TAB_CODE_HOME_TIMELINE = 1; int TAB_CODE_NOTIFICATIONS_TIMELINE = 2; int TAB_CODE_DIRECT_MESSAGES = 4; - int TWITTER_MAX_IMAGE_SIZE = 3145728; - int TWITTER_MAX_IMAGE_WIDTH = 1024; - int TWITTER_MAX_IMAGE_HEIGHT = 2048; - String USER_TYPE_TWITTER_COM = "twitter.com"; String USER_TYPE_FANFOU_COM = "fanfou.com"; diff --git a/twidere.component.common/src/main/java/org/mariotaku/twidere/model/ParcelableDirectMessage.java b/twidere.component.common/src/main/java/org/mariotaku/twidere/model/ParcelableDirectMessage.java deleted file mode 100644 index 8815e87df..000000000 --- a/twidere.component.common/src/main/java/org/mariotaku/twidere/model/ParcelableDirectMessage.java +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Twidere - Twitter client for Android - * - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -package org.mariotaku.twidere.model; - -import org.mariotaku.commons.objectcursor.LoganSquareCursorFieldConverter; -import org.mariotaku.library.objectcursor.annotation.CursorField; -import org.mariotaku.library.objectcursor.annotation.CursorObject; -import org.mariotaku.twidere.model.util.UserKeyCursorFieldConverter; -import org.mariotaku.twidere.provider.TwidereDataStore; -import org.mariotaku.twidere.provider.TwidereDataStore.DirectMessages; - -@CursorObject -@Deprecated -public class ParcelableDirectMessage { - - @CursorField(value = DirectMessages._ID, type = TwidereDataStore.TYPE_PRIMARY_KEY, excludeWrite = true) - public long _id; - - @CursorField(value = DirectMessages.ACCOUNT_KEY, converter = UserKeyCursorFieldConverter.class) - public UserKey account_key; - - @CursorField(DirectMessages.MESSAGE_ID) - public String id; - - @CursorField(DirectMessages.MESSAGE_TIMESTAMP) - public long timestamp; - - @CursorField(DirectMessages.SENDER_ID) - public String sender_id; - - @CursorField(DirectMessages.RECIPIENT_ID) - public String recipient_id; - - @CursorField(DirectMessages.IS_OUTGOING) - public boolean is_outgoing; - - @CursorField(DirectMessages.TEXT_UNESCAPED) - public String text_unescaped; - - @CursorField(DirectMessages.TEXT_PLAIN) - public String text_plain; - - @CursorField(value = DirectMessages.SPANS, converter = LoganSquareCursorFieldConverter.class) - public SpanItem[] spans; - - @CursorField(DirectMessages.SENDER_NAME) - public String sender_name; - - @CursorField(DirectMessages.RECIPIENT_NAME) - public String recipient_name; - - @CursorField(DirectMessages.SENDER_SCREEN_NAME) - public String sender_screen_name; - - @CursorField(DirectMessages.RECIPIENT_SCREEN_NAME) - public String recipient_screen_name; - - @CursorField(DirectMessages.SENDER_PROFILE_IMAGE_URL) - public String sender_profile_image_url; - - @CursorField(DirectMessages.RECIPIENT_PROFILE_IMAGE_URL) - public String recipient_profile_image_url; - - @CursorField(DirectMessages.CONVERSATION_ID) - public String conversation_id; - - @CursorField(value = DirectMessages.MEDIA_JSON, converter = LoganSquareCursorFieldConverter.class) - public ParcelableMedia[] media; - -} diff --git a/twidere.component.common/src/main/java/org/mariotaku/twidere/model/ParcelableMessage.java b/twidere.component.common/src/main/java/org/mariotaku/twidere/model/ParcelableMessage.java index d56cf010f..35b10e3ff 100644 --- a/twidere.component.common/src/main/java/org/mariotaku/twidere/model/ParcelableMessage.java +++ b/twidere.component.common/src/main/java/org/mariotaku/twidere/model/ParcelableMessage.java @@ -44,6 +44,8 @@ import org.mariotaku.twidere.model.util.UserKeyCursorFieldConverter; import org.mariotaku.twidere.provider.TwidereDataStore; import org.mariotaku.twidere.provider.TwidereDataStore.Messages; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.Arrays; /** @@ -159,6 +161,7 @@ public class ParcelableMessage implements Parcelable { @StringDef({MessageType.TEXT, MessageType.STICKER, MessageType.CONVERSATION_CREATE, MessageType.JOIN_CONVERSATION, MessageType.PARTICIPANTS_LEAVE}) + @Retention(RetentionPolicy.SOURCE) public @interface MessageType { String CONVERSATION_CREATE = "conversation_create"; String JOIN_CONVERSATION = "join_conversation"; diff --git a/twidere.component.common/src/main/java/org/mariotaku/twidere/model/ParcelableMessageConversation.java b/twidere.component.common/src/main/java/org/mariotaku/twidere/model/ParcelableMessageConversation.java index 93478a8be..994e81201 100644 --- a/twidere.component.common/src/main/java/org/mariotaku/twidere/model/ParcelableMessageConversation.java +++ b/twidere.component.common/src/main/java/org/mariotaku/twidere/model/ParcelableMessageConversation.java @@ -44,6 +44,8 @@ import org.mariotaku.twidere.model.util.UserKeyCursorFieldConverter; import org.mariotaku.twidere.provider.TwidereDataStore; import org.mariotaku.twidere.provider.TwidereDataStore.Messages.Conversations; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.Arrays; /** @@ -212,6 +214,7 @@ public class ParcelableMessageConversation implements Parcelable { } @StringDef({ExtrasType.FANFOU, ExtrasType.TWITTER_OFFICIAL}) + @Retention(RetentionPolicy.SOURCE) public @interface ExtrasType { String FANFOU = "fanfou"; String TWITTER_OFFICIAL = "twitter_official"; diff --git a/twidere.component.common/src/main/java/org/mariotaku/twidere/model/SpanItem.java b/twidere.component.common/src/main/java/org/mariotaku/twidere/model/SpanItem.java index a6c25fb69..8744117ae 100644 --- a/twidere.component.common/src/main/java/org/mariotaku/twidere/model/SpanItem.java +++ b/twidere.component.common/src/main/java/org/mariotaku/twidere/model/SpanItem.java @@ -33,6 +33,9 @@ import com.hannesdorfmann.parcelableplease.annotation.ParcelableNoThanks; import com.hannesdorfmann.parcelableplease.annotation.ParcelablePlease; import com.hannesdorfmann.parcelableplease.annotation.ParcelableThisPlease; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + /** * Created by mariotaku on 16/3/21. */ @@ -102,6 +105,7 @@ public class SpanItem implements Parcelable { } @IntDef({SpanType.HIDE, SpanType.LINK}) + @Retention(RetentionPolicy.SOURCE) public @interface SpanType { int HIDE = -1; int LINK = 0; diff --git a/twidere.component.common/src/main/java/org/mariotaku/twidere/provider/TwidereDataStore.java b/twidere.component.common/src/main/java/org/mariotaku/twidere/provider/TwidereDataStore.java index da69e579c..030bbc218 100644 --- a/twidere.component.common/src/main/java/org/mariotaku/twidere/provider/TwidereDataStore.java +++ b/twidere.component.common/src/main/java/org/mariotaku/twidere/provider/TwidereDataStore.java @@ -168,21 +168,6 @@ public interface TwidereDataStore { Uri CONTENT_URI = Uri.withAppendedPath(BASE_CONTENT_URI, CONTENT_PATH); } - interface CachedImages extends BaseColumns { - String TABLE_NAME = "cached_images"; - String CONTENT_PATH = TABLE_NAME; - - Uri CONTENT_URI = Uri.withAppendedPath(BASE_CONTENT_URI, CONTENT_PATH); - - String URL = "url"; - - String PATH = "path"; - - String[] MATRIX_COLUMNS = {URL, PATH}; - - String[] COLUMNS = {_ID, URL, PATH}; - } - interface CachedStatuses extends Statuses { String TABLE_NAME = "cached_statuses"; String CONTENT_PATH = TABLE_NAME; @@ -412,94 +397,6 @@ public interface TwidereDataStore { } } - interface DirectMessages extends BaseColumns, InsertedDateColumns, AccountSupportColumns { - - String TABLE_NAME = "messages"; - String CONTENT_PATH = TABLE_NAME; - - Uri CONTENT_URI = Uri.withAppendedPath(BASE_CONTENT_URI, CONTENT_PATH); - - String MESSAGE_ID = "message_id"; - String MESSAGE_TIMESTAMP = "message_timestamp"; - String SENDER_ID = "sender_id"; - String RECIPIENT_ID = "recipient_id"; - String CONVERSATION_ID = "conversation_id"; - - String IS_OUTGOING = "is_outgoing"; - - String TEXT_PLAIN = "text_plain"; - String TEXT_UNESCAPED = "text_unescaped"; - String SPANS = "spans"; - String SENDER_NAME = "sender_name"; - String RECIPIENT_NAME = "recipient_name"; - String SENDER_SCREEN_NAME = "sender_screen_name"; - String RECIPIENT_SCREEN_NAME = "recipient_screen_name"; - String SENDER_PROFILE_IMAGE_URL = "sender_profile_image_url"; - String RECIPIENT_PROFILE_IMAGE_URL = "recipient_profile_image_url"; - - String MEDIA_JSON = "media_json"; - - String DEFAULT_SORT_ORDER = MESSAGE_ID + " DESC"; - - interface Conversation extends DirectMessages { - - } - - interface ConversationEntries extends BaseColumns { - - String TABLE_NAME = "messages_conversation_entries"; - - String CONTENT_PATH_SEGMENT = "conversation_entries"; - String CONTENT_PATH = DirectMessages.CONTENT_PATH + "/" + CONTENT_PATH_SEGMENT; - - Uri CONTENT_URI = Uri.withAppendedPath(DirectMessages.CONTENT_URI, CONTENT_PATH_SEGMENT); - - String MESSAGE_ID = DirectMessages.MESSAGE_ID; - String ACCOUNT_KEY = DirectMessages.ACCOUNT_KEY; - String IS_OUTGOING = DirectMessages.IS_OUTGOING; - String MESSAGE_TIMESTAMP = DirectMessages.MESSAGE_TIMESTAMP; - String NAME = "name"; - String SCREEN_NAME = "screen_name"; - String PROFILE_IMAGE_URL = "profile_image_url"; - String TEXT_UNESCAPED = "text_unescaped"; - String CONVERSATION_ID = "conversation_id"; - - int IDX__ID = 0; - int IDX_MESSAGE_TIMESTAMP = 1; - int IDX_MESSAGE_ID = 2; - int IDX_ACCOUNT_KEY = 3; - int IDX_IS_OUTGOING = 4; - int IDX_NAME = 5; - int IDX_SCREEN_NAME = 6; - int IDX_PROFILE_IMAGE_URL = 7; - int IDX_TEXT_UNESCAPED = 8; - int IDX_CONVERSATION_ID = 9; - } - - interface Inbox extends DirectMessages { - - String TABLE_NAME = "messages_inbox"; - - String CONTENT_PATH_SEGMENT = "inbox"; - String CONTENT_PATH = DirectMessages.CONTENT_PATH + "/" + CONTENT_PATH_SEGMENT; - - Uri CONTENT_URI = Uri.withAppendedPath(DirectMessages.CONTENT_URI, CONTENT_PATH_SEGMENT); - - } - - interface Outbox extends DirectMessages { - - String TABLE_NAME = "messages_outbox"; - - String CONTENT_PATH_SEGMENT = "outbox"; - String CONTENT_PATH = DirectMessages.CONTENT_PATH + "/" + CONTENT_PATH_SEGMENT; - - Uri CONTENT_URI = Uri.withAppendedPath(DirectMessages.CONTENT_URI, CONTENT_PATH_SEGMENT); - - } - - } - interface SearchHistory extends BaseColumns { String TABLE_NAME = "search_history"; @@ -516,21 +413,6 @@ public interface TwidereDataStore { String DEFAULT_SORT_ORDER = RECENT_QUERY + " DESC"; } - interface DNS extends BaseColumns { - String TABLE_NAME = "dns"; - String CONTENT_PATH = TABLE_NAME; - - Uri CONTENT_URI = Uri.withAppendedPath(BASE_CONTENT_URI, CONTENT_PATH); - - String HOST = "host"; - - String ADDRESS = "address"; - - String[] MATRIX_COLUMNS = {HOST, ADDRESS}; - - String[] COLUMNS = {_ID, HOST, ADDRESS}; - } - interface SavedSearches extends BaseColumns, AccountSupportColumns { String TABLE_NAME = "saved_searches"; @@ -674,32 +556,6 @@ public interface TwidereDataStore { } } - interface Mentions extends Statuses { - - String TABLE_NAME = "mentions"; - String CONTENT_PATH = TABLE_NAME; - - Uri CONTENT_URI = Uri.withAppendedPath(BASE_CONTENT_URI, CONTENT_PATH); - - } - - interface Notifications extends BaseColumns { - - String TABLE_NAME = "notifications"; - - String CONTENT_PATH = TABLE_NAME; - - Uri CONTENT_URI = Uri.withAppendedPath(BASE_CONTENT_URI, CONTENT_PATH); - - String ID = "id"; - - String COUNT = "count"; - - String[] MATRIX_COLUMNS = {ID, COUNT}; - - String[] COLUMNS = {_ID, ID, COUNT}; - } - interface Permissions extends BaseColumns { String TABLE_NAME = "permissions"; String CONTENT_PATH = TABLE_NAME; @@ -715,37 +571,6 @@ public interface TwidereDataStore { String[] COLUMNS = {_ID, PACKAGE_NAME, PERMISSION}; } - interface Preferences extends BaseColumns { - String TABLE_NAME = "preferences"; - String CONTENT_PATH = TABLE_NAME; - - Uri CONTENT_URI = Uri.withAppendedPath(BASE_CONTENT_URI, CONTENT_PATH); - - int TYPE_INVALID = -1; - - int TYPE_NULL = 0; - - int TYPE_BOOLEAN = 1; - - int TYPE_INTEGER = 2; - - int TYPE_LONG = 3; - - int TYPE_FLOAT = 4; - - int TYPE_STRING = 5; - - String KEY = "key"; - - String VALUE = "value"; - - String TYPE = "type"; - - String[] MATRIX_COLUMNS = {KEY, VALUE, TYPE}; - - String[] COLUMNS = {_ID, KEY, VALUE, TYPE}; - } - interface Statuses extends BaseColumns, InsertedDateColumns, AccountSupportColumns { String TABLE_NAME = "statuses"; @@ -1035,30 +860,4 @@ public interface TwidereDataStore { } - interface UnreadCounts extends BaseColumns { - - String CONTENT_PATH = "unread_counts"; - - Uri CONTENT_URI = Uri.withAppendedPath(BASE_CONTENT_URI, CONTENT_PATH); - - String TAB_POSITION = "tab_position"; - - String TAB_TYPE = "tab_type"; - - String COUNT = "count"; - - String[] MATRIX_COLUMNS = {TAB_POSITION, TAB_TYPE, COUNT}; - - String[] COLUMNS = {_ID, TAB_POSITION, TAB_TYPE, COUNT}; - - interface ByType extends UnreadCounts { - - String CONTENT_PATH_SEGMENT = "by_type"; - - String CONTENT_PATH = UnreadCounts.CONTENT_PATH + "/" + CONTENT_PATH_SEGMENT; - - Uri CONTENT_URI = Uri.withAppendedPath(UnreadCounts.CONTENT_URI, CONTENT_PATH_SEGMENT); - } - } - } diff --git a/twidere.library.extension/src/main/java/org/mariotaku/twidere/Twidere.java b/twidere.library.extension/src/main/java/org/mariotaku/twidere/Twidere.java index 217c119ad..f4e8fb7d0 100644 --- a/twidere.library.extension/src/main/java/org/mariotaku/twidere/Twidere.java +++ b/twidere.library.extension/src/main/java/org/mariotaku/twidere/Twidere.java @@ -32,9 +32,7 @@ import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.database.Cursor; import android.graphics.Color; -import android.net.Uri; import android.os.Bundle; -import android.os.ParcelFileDescriptor; import android.support.annotation.IntDef; import android.support.annotation.NonNull; import android.support.annotation.Nullable; @@ -48,9 +46,6 @@ import org.mariotaku.twidere.model.ParcelableStatus; import org.mariotaku.twidere.model.ParcelableUser; import org.mariotaku.twidere.model.ParcelableUserList; import org.mariotaku.twidere.model.UserKey; -import org.mariotaku.twidere.provider.TwidereDataStore.CacheFiles; -import org.mariotaku.twidere.provider.TwidereDataStore.CachedImages; -import org.mariotaku.twidere.provider.TwidereDataStore.DNS; import org.mariotaku.twidere.provider.TwidereDataStore.Permissions; import org.mariotaku.twidere.util.model.AccountDetailsUtils; @@ -61,7 +56,6 @@ import java.net.Inet4Address; import java.net.Inet6Address; import java.net.InetAddress; import java.net.UnknownHostException; -import java.util.ArrayList; import static android.text.TextUtils.isEmpty; @@ -78,40 +72,11 @@ public final class Twidere implements TwidereConstants { activity.finish(); } - public static ParcelFileDescriptor getCachedImageFd(final Context context, final String url) { - if (context == null || url == null) return null; - final ContentResolver resolver = context.getContentResolver(); - final Uri.Builder builder = CachedImages.CONTENT_URI.buildUpon(); - builder.appendQueryParameter(QUERY_PARAM_URL, url); - try { - return resolver.openFileDescriptor(builder.build(), "r"); - } catch (final Exception e) { - return null; - } - } - - public static ParcelFileDescriptor getCacheFileFd(final Context context, final String name) { - if (context == null || name == null) return null; - final ContentResolver resolver = context.getContentResolver(); - final Uri.Builder builder = CacheFiles.CONTENT_URI.buildUpon(); - builder.appendQueryParameter(QUERY_PARAM_NAME, name); - try { - return resolver.openFileDescriptor(builder.build(), "r"); - } catch (final Exception e) { - return null; - } - } - public static ComposingStatus getComposingStatusFromIntent(final Intent intent) { if (intent == null) return null; return new ComposingStatus(intent); } - public static TwidereSharedPreferences getSharedPreferences(final Context context) { - if (context == null) return null; - return new TwidereSharedPreferences(context); - } - public static ParcelableStatus getStatusFromIntent(final Intent intent) { if (intent == null) return null; return intent.getParcelableExtra(EXTRA_STATUS); @@ -201,31 +166,6 @@ public final class Twidere implements TwidereConstants { throw new UnknownHostException("Bad address " + host + " = " + address); } - @NonNull - public static InetAddress[] resolveHost(final Context context, final String host) throws UnknownHostException { - if (context == null || host == null) return InetAddress.getAllByName(host); - final ContentResolver resolver = context.getContentResolver(); - final Uri uri = Uri.withAppendedPath(DNS.CONTENT_URI, host); - final Cursor cur = resolver.query(uri, DNS.MATRIX_COLUMNS, null, null, null); - if (cur == null) return InetAddress.getAllByName(host); - try { - cur.moveToFirst(); - final ArrayList addresses = new ArrayList<>(); - final int idxHost = cur.getColumnIndex(DNS.HOST), idxAddr = cur.getColumnIndex(DNS.ADDRESS); - while (!cur.isAfterLast()) { - addresses.add(fromAddressString(cur.getString(idxHost), cur.getString(idxAddr))); - cur.moveToNext(); - } - if (addresses.isEmpty()) { - throw new UnknownHostException("Unknown host " + host); - } - return addresses.toArray(new InetAddress[addresses.size()]); - } finally { - cur.close(); - } - } - - @Nullable @RequiresPermission(allOf = {Manifest.permission.GET_ACCOUNTS}, conditional = true) public static Account findByAccountKey(@NonNull Context context, @NonNull UserKey userKey) { diff --git a/twidere.library.extension/src/main/java/org/mariotaku/twidere/TwidereSharedPreferences.java b/twidere.library.extension/src/main/java/org/mariotaku/twidere/TwidereSharedPreferences.java deleted file mode 100644 index 971ec18ea..000000000 --- a/twidere.library.extension/src/main/java/org/mariotaku/twidere/TwidereSharedPreferences.java +++ /dev/null @@ -1,213 +0,0 @@ -/* - * Twidere - Twitter client for Android - * - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -package org.mariotaku.twidere; - -import android.content.ContentResolver; -import android.content.Context; -import android.content.SharedPreferences; -import android.database.Cursor; -import android.net.Uri; - -import org.mariotaku.twidere.provider.TwidereDataStore; - -import java.util.HashMap; -import java.util.Map; -import java.util.Set; - -public final class TwidereSharedPreferences implements SharedPreferences { - - private final ContentResolver mResolver; - - TwidereSharedPreferences(final Context context) { - mResolver = context.getContentResolver(); - } - - @Override - public boolean contains(final String key) { - if (key == null) throw new NullPointerException(); - final Uri uri = Uri.withAppendedPath(TwidereDataStore.Preferences.CONTENT_URI, key); - final Cursor cur = mResolver.query(uri, TwidereDataStore.Preferences.MATRIX_COLUMNS, null, null, null); - if (cur == null) return false; - try { - return cur.getCount() > 0; - } finally { - cur.close(); - } - } - - @Override - public Editor edit() { - throw new UnsupportedOperationException(); - } - - @Override - public Map getAll() { - final Cursor cur = mResolver.query(TwidereDataStore.Preferences.CONTENT_URI, TwidereDataStore.Preferences.MATRIX_COLUMNS, - null, null, null); - if (cur == null) return null; - final HashMap map = new HashMap<>(); - final int key_idx = cur.getColumnIndex(TwidereDataStore.Preferences.KEY); - final int value_idx = cur.getColumnIndex(TwidereDataStore.Preferences.VALUE); - final int type_idx = cur.getColumnIndex(TwidereDataStore.Preferences.TYPE); - cur.moveToFirst(); - while (!cur.isAfterLast()) { - final Object value; - switch (cur.getInt(type_idx)) { - case TwidereDataStore.Preferences.TYPE_BOOLEAN: - value = Boolean.valueOf(cur.getString(value_idx)); - break; - case TwidereDataStore.Preferences.TYPE_INTEGER: - value = cur.getInt(value_idx); - break; - case TwidereDataStore.Preferences.TYPE_LONG: - value = cur.getLong(value_idx); - break; - case TwidereDataStore.Preferences.TYPE_FLOAT: - value = cur.getFloat(value_idx); - break; - case TwidereDataStore.Preferences.TYPE_STRING: - value = cur.getString(value_idx); - break; - case TwidereDataStore.Preferences.TYPE_NULL: - value = null; - break; - default: - continue; - } - map.put(cur.getString(key_idx), value); - cur.moveToNext(); - } - cur.close(); - return map; - } - - @Override - public boolean getBoolean(final String key, final boolean defValue) { - if (key == null) throw new NullPointerException(); - try { - return (Boolean) getPreferenceObject(key); - } catch (final NullPointerException e) { - return defValue; - } - } - - @Override - public float getFloat(final String key, final float defValue) { - if (key == null) throw new NullPointerException(); - try { - return (Float) getPreferenceObject(key); - } catch (final NullPointerException e) { - return defValue; - } - } - - @Override - public int getInt(final String key, final int defValue) { - if (key == null) throw new NullPointerException(); - try { - return (Integer) getPreferenceObject(key); - } catch (final NullPointerException e) { - return defValue; - } - } - - @Override - public long getLong(final String key, final long defValue) { - if (key == null) throw new NullPointerException(); - try { - return (Long) getPreferenceObject(key); - } catch (final NullPointerException e) { - return defValue; - } - } - - public Object getPreferenceObject(final String key) { - final Uri uri = Uri.withAppendedPath(TwidereDataStore.Preferences.CONTENT_URI, key); - final Cursor cur = mResolver.query(uri, TwidereDataStore.Preferences.MATRIX_COLUMNS, null, null, null); - if (cur == null) throw new NullPointerException(); - try { - final int key_idx = cur.getColumnIndex(TwidereDataStore.Preferences.KEY); - final int value_idx = cur.getColumnIndex(TwidereDataStore.Preferences.VALUE); - final int type_idx = cur.getColumnIndex(TwidereDataStore.Preferences.TYPE); - cur.moveToFirst(); - while (!cur.isAfterLast()) { - if (!key.equals(cur.getString(key_idx))) { - cur.moveToNext(); - continue; - } - final Object value; - switch (cur.getInt(type_idx)) { - case TwidereDataStore.Preferences.TYPE_BOOLEAN: - value = Boolean.valueOf(cur.getString(value_idx)); - break; - case TwidereDataStore.Preferences.TYPE_INTEGER: - value = cur.getInt(value_idx); - break; - case TwidereDataStore.Preferences.TYPE_LONG: - value = cur.getLong(value_idx); - break; - case TwidereDataStore.Preferences.TYPE_FLOAT: - value = cur.getFloat(value_idx); - break; - case TwidereDataStore.Preferences.TYPE_STRING: - value = cur.getString(value_idx); - break; - case TwidereDataStore.Preferences.TYPE_NULL: - value = null; - break; - default: - continue; - } - return value; - } - throw new NullPointerException(); - } finally { - cur.close(); - } - } - - @Override - public String getString(final String key, final String defValue) { - if (key == null) throw new NullPointerException(); - try { - return (String) getPreferenceObject(key); - } catch (final NullPointerException e) { - return defValue; - } - } - - @Override - public Set getStringSet(final String key, final Set defValue) { - if (key == null) throw new NullPointerException(); - throw new UnsupportedOperationException(); - } - - @Override - public void registerOnSharedPreferenceChangeListener(final OnSharedPreferenceChangeListener listener) { - throw new UnsupportedOperationException(); - } - - @Override - public void unregisterOnSharedPreferenceChangeListener(final OnSharedPreferenceChangeListener listener) { - throw new UnsupportedOperationException(); - } -} diff --git a/twidere/src/main/AndroidManifest.xml b/twidere/src/main/AndroidManifest.xml index c62a5abf2..80bef0973 100644 --- a/twidere/src/main/AndroidManifest.xml +++ b/twidere/src/main/AndroidManifest.xml @@ -79,6 +79,7 @@ + @@ -87,6 +88,10 @@ android:name="org.mariotaku.twidere.permission.PERMISSION_GROUP" android:label="@string/app_name"/> + + android:permission="org.mariotaku.twidere.permission.ACCESS_DATABASE"/> - * - * 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 . - */ - -package org.mariotaku.twidere.provider; - -import android.annotation.SuppressLint; -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.content.SharedPreferences; -import android.content.SharedPreferences.OnSharedPreferenceChangeListener; -import android.content.pm.PackageManager; -import android.content.res.Resources; -import android.database.Cursor; -import android.database.MatrixCursor; -import android.database.MergeCursor; -import android.database.SQLException; -import android.database.sqlite.SQLiteDatabase; -import android.database.sqlite.SQLiteFullException; -import android.database.sqlite.SQLiteOpenHelper; -import android.media.AudioManager; -import android.net.Uri; -import android.os.Binder; -import android.os.Handler; -import android.os.Looper; -import android.os.ParcelFileDescriptor; -import android.os.Process; -import android.provider.BaseColumns; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.v4.app.NotificationCompat; -import android.support.v4.app.NotificationCompat.InboxStyle; -import android.support.v4.text.BidiFormatter; -import android.text.TextUtils; -import android.util.Log; -import android.util.Pair; - -import com.nostra13.universalimageloader.core.ImageLoader; -import com.squareup.otto.Bus; - -import org.apache.commons.lang3.ArrayUtils; -import org.apache.commons.lang3.math.NumberUtils; -import org.mariotaku.microblog.library.twitter.model.Activity; -import org.mariotaku.sqliteqb.library.Columns.Column; -import org.mariotaku.sqliteqb.library.Expression; -import org.mariotaku.sqliteqb.library.OrderBy; -import org.mariotaku.sqliteqb.library.RawItemArray; -import org.mariotaku.sqliteqb.library.SQLConstants; -import org.mariotaku.sqliteqb.library.SQLFunctions; -import org.mariotaku.sqliteqb.library.query.SQLSelectQuery; -import org.mariotaku.twidere.BuildConfig; -import org.mariotaku.twidere.R; -import org.mariotaku.twidere.activity.HomeActivity; -import org.mariotaku.twidere.annotation.CustomTabType; -import org.mariotaku.twidere.annotation.NotificationType; -import org.mariotaku.twidere.annotation.ReadPositionTag; -import org.mariotaku.twidere.app.TwidereApplication; -import org.mariotaku.twidere.model.AccountPreferences; -import org.mariotaku.twidere.model.ActivityTitleSummaryMessage; -import org.mariotaku.twidere.model.Draft; -import org.mariotaku.twidere.model.DraftCursorIndices; -import org.mariotaku.twidere.model.ParcelableActivity; -import org.mariotaku.twidere.model.ParcelableActivityCursorIndices; -import org.mariotaku.twidere.model.ParcelableStatusCursorIndices; -import org.mariotaku.twidere.model.ParcelableUser; -import org.mariotaku.twidere.model.StringLongPair; -import org.mariotaku.twidere.model.UnreadItem; -import org.mariotaku.twidere.model.UserKey; -import org.mariotaku.twidere.model.event.UnreadCountUpdatedEvent; -import org.mariotaku.twidere.model.util.ParcelableActivityUtils; -import org.mariotaku.twidere.provider.TwidereDataStore.Activities; -import org.mariotaku.twidere.provider.TwidereDataStore.CachedHashtags; -import org.mariotaku.twidere.provider.TwidereDataStore.CachedRelationships; -import org.mariotaku.twidere.provider.TwidereDataStore.CachedStatuses; -import org.mariotaku.twidere.provider.TwidereDataStore.CachedUsers; -import org.mariotaku.twidere.provider.TwidereDataStore.DNS; -import org.mariotaku.twidere.provider.TwidereDataStore.Drafts; -import org.mariotaku.twidere.provider.TwidereDataStore.Notifications; -import org.mariotaku.twidere.provider.TwidereDataStore.Permissions; -import org.mariotaku.twidere.provider.TwidereDataStore.Preferences; -import org.mariotaku.twidere.provider.TwidereDataStore.SavedSearches; -import org.mariotaku.twidere.provider.TwidereDataStore.SearchHistory; -import org.mariotaku.twidere.provider.TwidereDataStore.Statuses; -import org.mariotaku.twidere.provider.TwidereDataStore.Suggestions; -import org.mariotaku.twidere.provider.TwidereDataStore.UnreadCounts; -import org.mariotaku.twidere.receiver.NotificationReceiver; -import org.mariotaku.twidere.service.LengthyOperationsService; -import org.mariotaku.twidere.util.ActivityTracker; -import org.mariotaku.twidere.util.AsyncTwitterWrapper; -import org.mariotaku.twidere.util.DataStoreFunctionsKt; -import org.mariotaku.twidere.util.DataStoreUtils; -import org.mariotaku.twidere.util.DebugLog; -import org.mariotaku.twidere.util.InternalTwitterContentUtils; -import org.mariotaku.twidere.util.NotificationManagerWrapper; -import org.mariotaku.twidere.util.ParseUtils; -import org.mariotaku.twidere.util.PermissionsManager; -import org.mariotaku.twidere.util.ReadStateManager; -import org.mariotaku.twidere.util.SQLiteDatabaseWrapper; -import org.mariotaku.twidere.util.SQLiteDatabaseWrapper.LazyLoadCallback; -import org.mariotaku.twidere.util.SharedPreferencesWrapper; -import org.mariotaku.twidere.util.TwidereQueryBuilder.CachedUsersQueryBuilder; -import org.mariotaku.twidere.util.UriExtraUtils; -import org.mariotaku.twidere.util.UserColorNameManager; -import org.mariotaku.twidere.util.Utils; -import org.mariotaku.twidere.util.dagger.GeneralComponentHelper; -import org.mariotaku.twidere.util.net.TwidereDns; -import org.oshkimaadziig.george.androidutils.SpanFormatter; - -import java.io.File; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.net.InetAddress; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.concurrent.Executor; -import java.util.concurrent.Executors; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import javax.inject.Inject; - -import static org.mariotaku.twidere.TwidereConstants.AUTHORITY_DRAFTS; -import static org.mariotaku.twidere.TwidereConstants.AUTHORITY_INTERACTIONS; -import static org.mariotaku.twidere.TwidereConstants.BROADCAST_NOTIFICATION_DELETED; -import static org.mariotaku.twidere.TwidereConstants.INTENT_ACTION_DISCARD_DRAFT; -import static org.mariotaku.twidere.TwidereConstants.INTENT_ACTION_SEND_DRAFT; -import static org.mariotaku.twidere.TwidereConstants.KEY_COMBINED_NOTIFICATIONS; -import static org.mariotaku.twidere.TwidereConstants.KEY_I_WANT_MY_STARS_BACK; -import static org.mariotaku.twidere.TwidereConstants.KEY_NAME_FIRST; -import static org.mariotaku.twidere.TwidereConstants.LOGTAG; -import static org.mariotaku.twidere.TwidereConstants.NOTIFICATION_ID_DIRECT_MESSAGES; -import static org.mariotaku.twidere.TwidereConstants.NOTIFICATION_ID_DRAFTS; -import static org.mariotaku.twidere.TwidereConstants.NOTIFICATION_ID_HOME_TIMELINE; -import static org.mariotaku.twidere.TwidereConstants.NOTIFICATION_ID_INTERACTIONS_TIMELINE; -import static org.mariotaku.twidere.TwidereConstants.PERMISSION_DIRECT_MESSAGES; -import static org.mariotaku.twidere.TwidereConstants.PERMISSION_PREFERENCES; -import static org.mariotaku.twidere.TwidereConstants.PERMISSION_READ; -import static org.mariotaku.twidere.TwidereConstants.PERMISSION_WRITE; -import static org.mariotaku.twidere.TwidereConstants.QUERY_PARAM_ACCOUNT_KEY; -import static org.mariotaku.twidere.TwidereConstants.QUERY_PARAM_FROM_NOTIFICATION; -import static org.mariotaku.twidere.TwidereConstants.QUERY_PARAM_NOTIFICATION_TYPE; -import static org.mariotaku.twidere.TwidereConstants.QUERY_PARAM_NOTIFY; -import static org.mariotaku.twidere.TwidereConstants.QUERY_PARAM_QUERY; -import static org.mariotaku.twidere.TwidereConstants.QUERY_PARAM_READ_POSITION; -import static org.mariotaku.twidere.TwidereConstants.QUERY_PARAM_READ_POSITIONS; -import static org.mariotaku.twidere.TwidereConstants.QUERY_PARAM_TIMESTAMP; -import static org.mariotaku.twidere.TwidereConstants.QUERY_PARAM_TYPE; -import static org.mariotaku.twidere.TwidereConstants.QUERY_PARAM_URL; -import static org.mariotaku.twidere.TwidereConstants.SCHEME_TWIDERE; -import static org.mariotaku.twidere.TwidereConstants.TABLE_ID_ACTIVITIES_ABOUT_ME; -import static org.mariotaku.twidere.TwidereConstants.TABLE_ID_CACHED_HASHTAGS; -import static org.mariotaku.twidere.TwidereConstants.TABLE_ID_CACHED_RELATIONSHIPS; -import static org.mariotaku.twidere.TwidereConstants.TABLE_ID_CACHED_STATUSES; -import static org.mariotaku.twidere.TwidereConstants.TABLE_ID_CACHED_USERS; -import static org.mariotaku.twidere.TwidereConstants.TABLE_ID_DRAFTS; -import static org.mariotaku.twidere.TwidereConstants.TABLE_ID_FILTERED_KEYWORDS; -import static org.mariotaku.twidere.TwidereConstants.TABLE_ID_FILTERED_LINKS; -import static org.mariotaku.twidere.TwidereConstants.TABLE_ID_FILTERED_SOURCES; -import static org.mariotaku.twidere.TwidereConstants.TABLE_ID_FILTERED_USERS; -import static org.mariotaku.twidere.TwidereConstants.TABLE_ID_MENTIONS; -import static org.mariotaku.twidere.TwidereConstants.TABLE_ID_MESSAGES; -import static org.mariotaku.twidere.TwidereConstants.TABLE_ID_MESSAGES_CONVERSATIONS; -import static org.mariotaku.twidere.TwidereConstants.TABLE_ID_SEARCH_HISTORY; -import static org.mariotaku.twidere.TwidereConstants.TABLE_ID_STATUSES; -import static org.mariotaku.twidere.TwidereConstants.TABLE_ID_TABS; -import static org.mariotaku.twidere.TwidereConstants.TABLE_ID_TRENDS_LOCAL; -import static org.mariotaku.twidere.TwidereConstants.VIRTUAL_TABLE_ID_ALL_PREFERENCES; -import static org.mariotaku.twidere.TwidereConstants.VIRTUAL_TABLE_ID_CACHED_IMAGES; -import static org.mariotaku.twidere.TwidereConstants.VIRTUAL_TABLE_ID_CACHED_USERS_WITH_RELATIONSHIP; -import static org.mariotaku.twidere.TwidereConstants.VIRTUAL_TABLE_ID_CACHED_USERS_WITH_SCORE; -import static org.mariotaku.twidere.TwidereConstants.VIRTUAL_TABLE_ID_CACHE_FILES; -import static org.mariotaku.twidere.TwidereConstants.VIRTUAL_TABLE_ID_DATABASE_PREPARE; -import static org.mariotaku.twidere.TwidereConstants.VIRTUAL_TABLE_ID_DNS; -import static org.mariotaku.twidere.TwidereConstants.VIRTUAL_TABLE_ID_DRAFTS_NOTIFICATIONS; -import static org.mariotaku.twidere.TwidereConstants.VIRTUAL_TABLE_ID_DRAFTS_UNSENT; -import static org.mariotaku.twidere.TwidereConstants.VIRTUAL_TABLE_ID_EMPTY; -import static org.mariotaku.twidere.TwidereConstants.VIRTUAL_TABLE_ID_NOTIFICATIONS; -import static org.mariotaku.twidere.TwidereConstants.VIRTUAL_TABLE_ID_NULL; -import static org.mariotaku.twidere.TwidereConstants.VIRTUAL_TABLE_ID_PERMISSIONS; -import static org.mariotaku.twidere.TwidereConstants.VIRTUAL_TABLE_ID_PREFERENCES; -import static org.mariotaku.twidere.TwidereConstants.VIRTUAL_TABLE_ID_RAW_QUERY; -import static org.mariotaku.twidere.TwidereConstants.VIRTUAL_TABLE_ID_SUGGESTIONS_AUTO_COMPLETE; -import static org.mariotaku.twidere.TwidereConstants.VIRTUAL_TABLE_ID_SUGGESTIONS_SEARCH; -import static org.mariotaku.twidere.TwidereConstants.VIRTUAL_TABLE_ID_UNREAD_COUNTS; -import static org.mariotaku.twidere.TwidereConstants.VIRTUAL_TABLE_ID_UNREAD_COUNTS_BY_TYPE; - -public final class TwidereDataProvider extends ContentProvider implements - OnSharedPreferenceChangeListener, LazyLoadCallback { - - public static final String TAG_OLDEST_MESSAGES = "oldest_messages"; - private static final Pattern PATTERN_SCREEN_NAME = Pattern.compile("(?i)[@\uFF20]?([a-z0-9_]{1,20})"); - @Inject - @NonNull - ReadStateManager readStateManager; - @Inject - @NonNull - AsyncTwitterWrapper twitterWrapper; - @Inject - @NonNull - ImageLoader mediaLoader; - @Inject - @NonNull - NotificationManagerWrapper notificationManager; - @Inject - @NonNull - SharedPreferencesWrapper preferences; - @Inject - @NonNull - TwidereDns dns; - @Inject - @NonNull - Bus bus; - @Inject - @NonNull - UserColorNameManager userColorNameManager; - @Inject - @NonNull - BidiFormatter bidiFormatter; - @Inject - @NonNull - ActivityTracker activityTracker; - @Inject - @NonNull - PermissionsManager permissionsManager; - - private Handler mHandler; - private ContentResolver mContentResolver; - private SQLiteDatabaseWrapper databaseWrapper; - private Executor mBackgroundExecutor; - private boolean nameFirst; - private boolean useStarForLikes; - - private static PendingIntent getMarkReadDeleteIntent(Context context, @NotificationType String type, - @Nullable UserKey accountKey, long position, - boolean extraUserFollowing) { - return getMarkReadDeleteIntent(context, type, accountKey, position, -1, -1, extraUserFollowing); - } - - private static PendingIntent getMarkReadDeleteIntent(Context context, @NotificationType String type, - @Nullable UserKey accountKey, long position, - long extraId, long extraUserId, - boolean extraUserFollowing) { - // Setup delete intent - final Intent intent = new Intent(context, NotificationReceiver.class); - intent.setAction(BROADCAST_NOTIFICATION_DELETED); - final Uri.Builder linkBuilder = new Uri.Builder(); - linkBuilder.scheme(SCHEME_TWIDERE); - linkBuilder.authority(AUTHORITY_INTERACTIONS); - linkBuilder.appendPath(type); - if (accountKey != null) { - linkBuilder.appendQueryParameter(QUERY_PARAM_ACCOUNT_KEY, accountKey.toString()); - } - linkBuilder.appendQueryParameter(QUERY_PARAM_READ_POSITION, String.valueOf(position)); - linkBuilder.appendQueryParameter(QUERY_PARAM_TIMESTAMP, String.valueOf(System.currentTimeMillis())); - linkBuilder.appendQueryParameter(QUERY_PARAM_NOTIFICATION_TYPE, type); - - UriExtraUtils.addExtra(linkBuilder, "item_id", extraId); - UriExtraUtils.addExtra(linkBuilder, "item_user_id", extraUserId); - UriExtraUtils.addExtra(linkBuilder, "item_user_following", extraUserFollowing); - intent.setData(linkBuilder.build()); - return PendingIntent.getBroadcast(context, 0, intent, 0); - } - - private static PendingIntent getMarkReadDeleteIntent(Context context, @NotificationType String notificationType, - @Nullable UserKey accountKey, StringLongPair[] positions) { - // Setup delete intent - final Intent intent = new Intent(context, NotificationReceiver.class); - final Uri.Builder linkBuilder = new Uri.Builder(); - linkBuilder.scheme(SCHEME_TWIDERE); - linkBuilder.authority(AUTHORITY_INTERACTIONS); - linkBuilder.appendPath(notificationType); - if (accountKey != null) { - linkBuilder.appendQueryParameter(QUERY_PARAM_ACCOUNT_KEY, accountKey.toString()); - } - linkBuilder.appendQueryParameter(QUERY_PARAM_READ_POSITIONS, StringLongPair.toString(positions)); - linkBuilder.appendQueryParameter(QUERY_PARAM_TIMESTAMP, String.valueOf(System.currentTimeMillis())); - linkBuilder.appendQueryParameter(QUERY_PARAM_NOTIFICATION_TYPE, notificationType); - intent.setData(linkBuilder.build()); - return PendingIntent.getBroadcast(context, 0, intent, 0); - } - - private static Cursor getPreferencesCursor(final SharedPreferencesWrapper preferences, final String key) { - final MatrixCursor c = new MatrixCursor(Preferences.MATRIX_COLUMNS); - final Map map = new HashMap<>(); - final Map all = preferences.getAll(); - if (key == null) { - map.putAll(all); - } else { - map.put(key, all.get(key)); - } - for (final Map.Entry 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 int getUnreadCount(final List 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 int getConflictAlgorithm(final int tableId) { - switch (tableId) { - 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_MESSAGES: - case TABLE_ID_MESSAGES_CONVERSATIONS: - return SQLiteDatabase.CONFLICT_REPLACE; - case TABLE_ID_FILTERED_USERS: - case TABLE_ID_FILTERED_KEYWORDS: - case TABLE_ID_FILTERED_SOURCES: - case TABLE_ID_FILTERED_LINKS: - return SQLiteDatabase.CONFLICT_IGNORE; - } - return SQLiteDatabase.CONFLICT_NONE; - } - - @Override - public int bulkInsert(@NonNull final Uri uri, @NonNull final ContentValues[] valuesArray) { - try { - return bulkInsertInternal(uri, valuesArray); - } catch (final SQLException e) { - if (handleSQLException(e)) { - try { - return bulkInsertInternal(uri, valuesArray); - } catch (SQLException e1) { - throw new IllegalStateException(e1); - } - } - throw new IllegalStateException(e); - } - } - - private boolean handleSQLException(SQLException e) { - try { - if (e instanceof SQLiteFullException) { - // Drop cached databases - databaseWrapper.delete(CachedUsers.TABLE_NAME, null, null); - databaseWrapper.delete(CachedStatuses.TABLE_NAME, null, null); - databaseWrapper.delete(CachedHashtags.TABLE_NAME, null, null); - databaseWrapper.execSQL("VACUUM"); - return true; - } - } catch (SQLException ee) { - throw new IllegalStateException(ee); - } - throw new IllegalStateException(e); - } - - private int bulkInsertInternal(@NonNull Uri uri, @NonNull ContentValues[] valuesArray) { - final int tableId = DataStoreUtils.INSTANCE.getTableId(uri); - final String table = DataStoreUtils.INSTANCE.getTableNameById(tableId); - checkWritePermission(tableId, table); - int result = 0; - final long[] newIds = new long[valuesArray.length]; - if (table != null && valuesArray.length > 0) { - databaseWrapper.beginTransaction(); - if (tableId == TABLE_ID_CACHED_USERS) { - for (final ContentValues values : valuesArray) { - final Expression where = Expression.equalsArgs(CachedUsers.USER_KEY); - databaseWrapper.update(table, values, where.getSQL(), new String[]{ - values.getAsString(CachedUsers.USER_KEY)}); - newIds[result++] = databaseWrapper.insertWithOnConflict(table, null, - values, SQLiteDatabase.CONFLICT_REPLACE); - } - } 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)}; - databaseWrapper.update(table, values, where.getSQL(), args); - newIds[result++] = databaseWrapper.insertWithOnConflict(table, null, - values, SQLiteDatabase.CONFLICT_IGNORE); - } - } else { - final int conflictAlgorithm = getConflictAlgorithm(tableId); - if (conflictAlgorithm != SQLiteDatabase.CONFLICT_NONE) { - for (final ContentValues values : valuesArray) { - newIds[result++] = databaseWrapper.insertWithOnConflict(table, null, - values, conflictAlgorithm); - } - } else { - for (final ContentValues values : valuesArray) { - newIds[result++] = databaseWrapper.insert(table, null, values); - } - } - } - databaseWrapper.setTransactionSuccessful(); - databaseWrapper.endTransaction(); - } - if (result > 0) { - onDatabaseUpdated(tableId, uri); - } - onNewItemsInserted(uri, tableId, valuesArray); - return result; - } - - @Override - public int delete(@NonNull final Uri uri, final String selection, final String[] selectionArgs) { - try { - return deleteInternal(uri, selection, selectionArgs); - } catch (final SQLException e) { - if (handleSQLException(e)) { - try { - return deleteInternal(uri, selection, selectionArgs); - } catch (SQLException e1) { - throw new IllegalStateException(e1); - } - } - throw new IllegalStateException(e); - } - } - - private int deleteInternal(@NonNull Uri uri, String selection, String[] selectionArgs) { - final int tableId = DataStoreUtils.INSTANCE.getTableId(uri); - final String table = DataStoreUtils.INSTANCE.getTableNameById(tableId); - checkWritePermission(tableId, table); - switch (tableId) { - case VIRTUAL_TABLE_ID_NOTIFICATIONS: { - final List segments = uri.getPathSegments(); - if (segments.size() == 1) { - clearNotification(); - } else if (segments.size() == 2) { - final int notificationType = NumberUtils.toInt(segments.get(1), -1); - clearNotification(notificationType, null); - } else if (segments.size() == 3) { - final int notificationType = NumberUtils.toInt(segments.get(1), -1); - final UserKey accountKey = UserKey.valueOf(segments.get(2)); - clearNotification(notificationType, accountKey); - } - return 1; - } - case VIRTUAL_TABLE_ID_UNREAD_COUNTS: { - return 0; - } - } - if (table == null) return 0; - final int result = databaseWrapper.delete(table, selection, selectionArgs); - if (result > 0) { - onDatabaseUpdated(tableId, uri); - } - return result; - } - - @Override - public String getType(@NonNull final Uri uri) { - return null; - } - - @Override - public Uri insert(@NonNull final Uri uri, final ContentValues values) { - try { - return insertInternal(uri, values); - } catch (final SQLException e) { - if (handleSQLException(e)) { - try { - return insertInternal(uri, values); - } catch (SQLException e1) { - throw new IllegalStateException(e1); - } - } - throw new IllegalStateException(e); - } - } - - private Uri insertInternal(@NonNull Uri uri, ContentValues values) { - final int tableId = DataStoreUtils.INSTANCE.getTableId(uri); - final String table = DataStoreUtils.INSTANCE.getTableNameById(tableId); - checkWritePermission(tableId, table); - final long rowId; - switch (tableId) { - case TABLE_ID_CACHED_USERS: { - final Expression where = Expression.equalsArgs(CachedUsers.USER_KEY); - final String[] whereArgs = {values.getAsString(CachedUsers.USER_KEY)}; - databaseWrapper.update(table, values, where.getSQL(), whereArgs); - rowId = databaseWrapper.insertWithOnConflict(table, null, values, - SQLiteDatabase.CONFLICT_IGNORE); - break; - } - case 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)}; - databaseWrapper.update(table, values, where.getSQL(), args); - rowId = databaseWrapper.insertWithOnConflict(table, null, values, - SQLiteDatabase.CONFLICT_IGNORE); - break; - } - case TABLE_ID_CACHED_RELATIONSHIPS: { - final String accountKey = values.getAsString(CachedRelationships.ACCOUNT_KEY); - final String userId = values.getAsString(CachedRelationships.USER_KEY); - final Expression where = Expression.and( - Expression.equalsArgs(CachedRelationships.ACCOUNT_KEY), - Expression.equalsArgs(CachedRelationships.USER_KEY) - ); - final String[] whereArgs = {accountKey, userId}; - if (databaseWrapper.update(table, values, where.getSQL(), whereArgs) > 0) { - final String[] projection = {CachedRelationships._ID}; - final Cursor c = databaseWrapper.query(table, projection, where.getSQL(), null, - null, null, null); - if (c.moveToFirst()) { - rowId = c.getLong(0); - } else { - rowId = 0; - } - c.close(); - } else { - rowId = databaseWrapper.insertWithOnConflict(table, null, values, - SQLiteDatabase.CONFLICT_IGNORE); - } - break; - } - case VIRTUAL_TABLE_ID_DRAFTS_NOTIFICATIONS: { - rowId = showDraftNotification(values); - break; - } - default: { - final int conflictAlgorithm = getConflictAlgorithm(tableId); - if (conflictAlgorithm != SQLiteDatabase.CONFLICT_NONE) { - rowId = databaseWrapper.insertWithOnConflict(table, null, values, - conflictAlgorithm); - } else if (table != null) { - rowId = databaseWrapper.insert(table, null, values); - } else { - return null; - } - break; - } - } - onDatabaseUpdated(tableId, uri); - onNewItemsInserted(uri, tableId, values); - return Uri.withAppendedPath(uri, String.valueOf(rowId)); - } - - private long showDraftNotification(ContentValues values) { - final Context context = getContext(); - if (values == null || context == null) return -1; - final Long draftId = values.getAsLong(BaseColumns._ID); - if (draftId == null) return -1; - final Expression where = Expression.equals(Drafts._ID, draftId); - final Cursor c = getContentResolver().query(Drafts.CONTENT_URI, Drafts.COLUMNS, where.getSQL(), null, null); - if (c == null) return -1; - final DraftCursorIndices i = new DraftCursorIndices(c); - final Draft item; - try { - if (!c.moveToFirst()) return -1; - item = i.newObject(c); - } catch (IOException e) { - return -1; - } finally { - c.close(); - } - final String title = context.getString(R.string.status_not_updated); - final String message = context.getString(R.string.status_not_updated_summary); - final Intent intent = new Intent(); - intent.setPackage(BuildConfig.APPLICATION_ID); - final Uri.Builder uriBuilder = new Uri.Builder(); - uriBuilder.scheme(SCHEME_TWIDERE); - uriBuilder.authority(AUTHORITY_DRAFTS); - intent.setData(uriBuilder.build()); - final NotificationCompat.Builder nb = new NotificationCompat.Builder(context); - nb.setTicker(message); - nb.setContentTitle(title); - nb.setContentText(item.text); - nb.setAutoCancel(true); - nb.setWhen(System.currentTimeMillis()); - nb.setSmallIcon(R.drawable.ic_stat_draft); - final Intent discardIntent = new Intent(context, LengthyOperationsService.class); - discardIntent.setAction(INTENT_ACTION_DISCARD_DRAFT); - final Uri draftUri = Uri.withAppendedPath(Drafts.CONTENT_URI, String.valueOf(draftId)); - discardIntent.setData(draftUri); - nb.addAction(R.drawable.ic_action_delete, context.getString(R.string.discard), PendingIntent.getService(context, 0, - discardIntent, PendingIntent.FLAG_ONE_SHOT)); - - final Intent sendIntent = new Intent(context, LengthyOperationsService.class); - sendIntent.setAction(INTENT_ACTION_SEND_DRAFT); - sendIntent.setData(draftUri); - nb.addAction(R.drawable.ic_action_send, context.getString(R.string.action_send), - PendingIntent.getService(context, 0, sendIntent, PendingIntent.FLAG_ONE_SHOT)); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); - nb.setContentIntent(PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_ONE_SHOT)); - notificationManager.notify(draftUri.toString(), NOTIFICATION_ID_DRAFTS, - nb.build()); - return draftId; - } - - @Override - public boolean onCreate() { - final Context context = getContext(); - assert context != null; - GeneralComponentHelper.build(context).inject(this); - mHandler = new Handler(Looper.getMainLooper()); - databaseWrapper = new SQLiteDatabaseWrapper(this); - preferences.registerOnSharedPreferenceChangeListener(this); - mBackgroundExecutor = Executors.newSingleThreadExecutor(); - updatePreferences(); - // final GetWritableDatabaseTask task = new - // GetWritableDatabaseTask(context, helper, mDatabaseWrapper); - // task.executeTask(); - return true; - } - - @Override - public SQLiteDatabase onCreateSQLiteDatabase() { - final TwidereApplication app = TwidereApplication.Companion.getInstance(getContext()); - final SQLiteOpenHelper helper = app.getSqLiteOpenHelper(); - return helper.getWritableDatabase(); - } - - @Override - public void onSharedPreferenceChanged(final SharedPreferences preferences, final String key) { - updatePreferences(); - } - - @Override - public ParcelFileDescriptor openFile(@NonNull final Uri uri, @NonNull final String mode) throws FileNotFoundException { - final int table_id = DataStoreUtils.INSTANCE.getTableId(uri); - final String table = DataStoreUtils.INSTANCE.getTableNameById(table_id); - final int modeCode; - switch (mode) { - case "r": - modeCode = ParcelFileDescriptor.MODE_READ_ONLY; - break; - case "rw": - modeCode = ParcelFileDescriptor.MODE_READ_WRITE; - break; - case "rwt": - modeCode = ParcelFileDescriptor.MODE_READ_WRITE | ParcelFileDescriptor.MODE_TRUNCATE; - break; - default: - throw new IllegalArgumentException(); - } - if (modeCode == ParcelFileDescriptor.MODE_READ_ONLY) { - checkReadPermission(table_id, table, null); - } else if ((modeCode & ParcelFileDescriptor.MODE_READ_WRITE) != 0) { - checkReadPermission(table_id, table, null); - checkWritePermission(table_id, table); - } - 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(@NonNull final Uri uri, final String[] projection, final String selection, final String[] selectionArgs, - final String sortOrder) { - try { - final int tableId = DataStoreUtils.INSTANCE.getTableId(uri); - final String table = DataStoreUtils.INSTANCE.getTableNameById(tableId); - checkReadPermission(tableId, table, projection); - switch (tableId) { - case VIRTUAL_TABLE_ID_DATABASE_PREPARE: { - databaseWrapper.prepare(); - return new MatrixCursor(projection != null ? projection : new String[0]); - } - case VIRTUAL_TABLE_ID_PERMISSIONS: { - final Context context = getContext(); - if (context == null) return null; - final MatrixCursor c = new MatrixCursor(Permissions.MATRIX_COLUMNS); - final PackageManager pm = context.getPackageManager(); - if (Binder.getCallingUid() == Process.myUid()) { - final Map map = permissionsManager.getAll(); - for (final Map.Entry item : map.entrySet()) { - c.addRow(new Object[]{item.getKey(), item.getValue()}); - } - } else { - final Map map = permissionsManager.getAll(); - final String[] callingPackages = pm.getPackagesForUid(Binder.getCallingUid()); - for (final Map.Entry item : map.entrySet()) { - final String key = item.getKey(); - if (ArrayUtils.contains(callingPackages, key)) { - c.addRow(new Object[]{key, item.getValue()}); - } - } - } - return c; - } - case VIRTUAL_TABLE_ID_ALL_PREFERENCES: { - return getPreferencesCursor(preferences, null); - } - case VIRTUAL_TABLE_ID_PREFERENCES: { - return getPreferencesCursor(preferences, uri.getLastPathSegment()); - } - case VIRTUAL_TABLE_ID_DNS: { - return getDNSCursor(uri.getLastPathSegment()); - } - case VIRTUAL_TABLE_ID_NOTIFICATIONS: { - final List segments = uri.getPathSegments(); - if (segments.size() == 2) { - final int def = -1; - return getNotificationsCursor(NumberUtils.toInt(segments.get(1), def)); - } else - return getNotificationsCursor(); - } - case VIRTUAL_TABLE_ID_UNREAD_COUNTS: { - final List segments = uri.getPathSegments(); - if (segments.size() == 2) { - final int def = -1; - return getUnreadCountsCursor(NumberUtils.toInt(segments.get(1), def)); - } else - return getUnreadCountsCursor(); - } - case VIRTUAL_TABLE_ID_UNREAD_COUNTS_BY_TYPE: { - final List segments = uri.getPathSegments(); - if (segments.size() != 3) return null; - return getUnreadCountsCursorByType(segments.get(2)); - } - case VIRTUAL_TABLE_ID_CACHED_USERS_WITH_RELATIONSHIP: { - final UserKey accountKey = UserKey.valueOf(uri.getLastPathSegment()); - final Pair query = CachedUsersQueryBuilder.withRelationship(projection, - selection, selectionArgs, sortOrder, accountKey); - final Cursor c = databaseWrapper.rawQuery(query.first.getSQL(), query.second); - setNotificationUri(c, CachedUsers.CONTENT_URI); - return c; - } - case VIRTUAL_TABLE_ID_CACHED_USERS_WITH_SCORE: { - final UserKey accountKey = UserKey.valueOf(uri.getLastPathSegment()); - final Pair query = CachedUsersQueryBuilder.withScore(projection, - selection, selectionArgs, sortOrder, accountKey, 0); - final Cursor c = databaseWrapper.rawQuery(query.first.getSQL(), query.second); - setNotificationUri(c, CachedUsers.CONTENT_URI); - return c; - } - case VIRTUAL_TABLE_ID_DRAFTS_UNSENT: { - final AsyncTwitterWrapper twitter = twitterWrapper; - 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 = databaseWrapper.query(Drafts.TABLE_NAME, projection, - where.getSQL(), selectionArgs, null, null, sortOrder); - setNotificationUri(c, Utils.getNotificationUri(tableId, uri)); - return c; - } - case VIRTUAL_TABLE_ID_SUGGESTIONS_AUTO_COMPLETE: { - return getAutoCompleteSuggestionsCursor(uri); - } - case VIRTUAL_TABLE_ID_SUGGESTIONS_SEARCH: { - return getSearchSuggestionCursor(uri); - } - case VIRTUAL_TABLE_ID_NULL: { - return null; - } - case VIRTUAL_TABLE_ID_EMPTY: { - return new MatrixCursor(projection != null ? projection : new String[0]); - } - case VIRTUAL_TABLE_ID_RAW_QUERY: { - if (projection != null || selection != null || sortOrder != null) { - throw new IllegalArgumentException(); - } - return databaseWrapper.rawQuery(uri.getLastPathSegment(), selectionArgs); - } - } - if (table == null) return null; - final Cursor c = databaseWrapper.query(table, projection, selection, selectionArgs, - null, null, sortOrder); - setNotificationUri(c, Utils.getNotificationUri(tableId, uri)); - return c; - } catch (final SQLException e) { - throw new IllegalStateException(e); - } - } - - private Cursor getSearchSuggestionCursor(Uri uri) { - final String query = uri.getQueryParameter(QUERY_PARAM_QUERY); - final String paramAccountKey = uri.getQueryParameter(QUERY_PARAM_ACCOUNT_KEY); - final UserKey accountKey = paramAccountKey != null ? UserKey.valueOf(paramAccountKey) : null; - if (query == null || accountKey == null) return null; - final boolean emptyQuery = TextUtils.isEmpty(query); - final String queryEscaped = query.replace("_", "^_"); - final Cursor[] cursors; - final String[] historyProjection = { - new Column(SearchHistory._ID, Suggestions.Search._ID).getSQL(), - new Column("'" + Suggestions.Search.TYPE_SEARCH_HISTORY + "'", Suggestions.Search.TYPE).getSQL(), - new Column(SearchHistory.QUERY, Suggestions.Search.TITLE).getSQL(), - new Column(SQLConstants.NULL, Suggestions.Search.SUMMARY).getSQL(), - new Column(SQLConstants.NULL, Suggestions.Search.ICON).getSQL(), - new Column("0", Suggestions.Search.EXTRA_ID).getSQL(), - new Column(SQLConstants.NULL, Suggestions.Search.EXTRA).getSQL(), - new Column(SearchHistory.QUERY, Suggestions.Search.VALUE).getSQL(), - }; - final Expression historySelection = Expression.likeRaw(new Column(SearchHistory.QUERY), "?||'%'", "^"); - @SuppressLint("Recycle") final Cursor historyCursor = databaseWrapper.query(true, - SearchHistory.TABLE_NAME, historyProjection, historySelection.getSQL(), - new String[]{queryEscaped}, null, null, SearchHistory.DEFAULT_SORT_ORDER, - TextUtils.isEmpty(query) ? "3" : "2"); - if (emptyQuery) { - final String[] savedSearchesProjection = { - new Column(SavedSearches._ID, Suggestions.Search._ID).getSQL(), - new Column("'" + Suggestions.Search.TYPE_SAVED_SEARCH + "'", Suggestions.Search.TYPE).getSQL(), - new Column(SavedSearches.QUERY, Suggestions.Search.TITLE).getSQL(), - new Column(SQLConstants.NULL, Suggestions.Search.SUMMARY).getSQL(), - new Column(SQLConstants.NULL, Suggestions.Search.ICON).getSQL(), - new Column("0", Suggestions.Search.EXTRA_ID).getSQL(), - new Column(SQLConstants.NULL, Suggestions.Search.EXTRA).getSQL(), - new Column(SavedSearches.QUERY, Suggestions.Search.VALUE).getSQL() - }; - final Expression savedSearchesWhere = Expression.equalsArgs(SavedSearches.ACCOUNT_KEY); - final String[] whereArgs = {accountKey.toString()}; - @SuppressLint("Recycle") final Cursor savedSearchesCursor = databaseWrapper.query(true, - SavedSearches.TABLE_NAME, savedSearchesProjection, savedSearchesWhere.getSQL(), - whereArgs, null, null, SavedSearches.DEFAULT_SORT_ORDER, null); - cursors = new Cursor[2]; - cursors[1] = savedSearchesCursor; - } else { - final String[] usersProjection = { - new Column(CachedUsers._ID, Suggestions.Search._ID).getSQL(), - new Column("'" + Suggestions.Search.TYPE_USER + "'", Suggestions.Search.TYPE).getSQL(), - new Column(CachedUsers.NAME, Suggestions.Search.TITLE).getSQL(), - new Column(CachedUsers.SCREEN_NAME, Suggestions.Search.SUMMARY).getSQL(), - new Column(CachedUsers.PROFILE_IMAGE_URL, Suggestions.Search.ICON).getSQL(), - new Column(CachedUsers.USER_KEY, Suggestions.Search.EXTRA_ID).getSQL(), - new Column(SQLConstants.NULL, Suggestions.Search.EXTRA).getSQL(), - new Column(CachedUsers.SCREEN_NAME, Suggestions.Search.VALUE).getSQL(), - }; - String queryTrimmed = queryEscaped.startsWith("@") ? queryEscaped.substring(1) : queryEscaped; - final String[] nicknameKeys = Utils.getMatchedNicknameKeys(query, userColorNameManager); - final Expression usersSelection = Expression.or( - Expression.likeRaw(new Column(CachedUsers.SCREEN_NAME), "?||'%'", "^"), - Expression.likeRaw(new Column(CachedUsers.NAME), "?||'%'", "^"), - Expression.inArgs(new Column(CachedUsers.USER_KEY), nicknameKeys.length)); - final String[] selectionArgs = new String[nicknameKeys.length + 2]; - selectionArgs[0] = selectionArgs[1] = queryTrimmed; - System.arraycopy(nicknameKeys, 0, selectionArgs, 2, nicknameKeys.length); - final String[] order = {CachedUsers.LAST_SEEN, CachedUsers.SCORE, CachedUsers.SCREEN_NAME, - CachedUsers.NAME}; - final boolean[] ascending = {false, false, true, true}; - final OrderBy orderBy = new OrderBy(order, ascending); - - final Pair usersQuery = CachedUsersQueryBuilder.withScore(usersProjection, - usersSelection.getSQL(), selectionArgs, orderBy.getSQL(), accountKey, 0); - @SuppressLint("Recycle") final Cursor usersCursor = databaseWrapper.rawQuery(usersQuery.first.getSQL(), usersQuery.second); - final Expression exactUserSelection = Expression.or(Expression.likeRaw(new Column(CachedUsers.SCREEN_NAME), "?", "^")); - final Cursor exactUserCursor = databaseWrapper.query(CachedUsers.TABLE_NAME, - new String[]{SQLFunctions.COUNT()}, exactUserSelection.getSQL(), - new String[]{queryTrimmed}, null, null, null, "1"); - final boolean hasName = exactUserCursor.moveToPosition(0) && exactUserCursor.getInt(0) > 0; - exactUserCursor.close(); - final MatrixCursor screenNameCursor = new MatrixCursor(Suggestions.Search.COLUMNS); - if (!hasName) { - final Matcher m = PATTERN_SCREEN_NAME.matcher(query); - if (m.matches()) { - final String screenName = m.group(1); - screenNameCursor.addRow(new Object[]{0, Suggestions.Search.TYPE_SCREEN_NAME, - screenName, null, null, 0, null, screenName}); - } - } - cursors = new Cursor[3]; - cursors[1] = screenNameCursor; - cursors[2] = usersCursor; - } - cursors[0] = historyCursor; - return new MergeCursor(cursors); - } - - private Cursor getAutoCompleteSuggestionsCursor(@NonNull Uri uri) { - final String query = uri.getQueryParameter(QUERY_PARAM_QUERY); - final String type = uri.getQueryParameter(QUERY_PARAM_TYPE); - final String accountKey = uri.getQueryParameter(QUERY_PARAM_ACCOUNT_KEY); - if (query == null || type == null) return null; - final String queryEscaped = query.replace("_", "^_"); - if (Suggestions.AutoComplete.TYPE_USERS.equals(type)) { - final String[] nicknameKeys = Utils.getMatchedNicknameKeys(query, userColorNameManager); - final Expression where = Expression.or(Expression.likeRaw(new Column(CachedUsers.SCREEN_NAME), "?||'%'", "^"), - Expression.likeRaw(new Column(CachedUsers.NAME), "?||'%'", "^"), - Expression.inArgs(new Column(CachedUsers.USER_KEY), nicknameKeys.length)); - final String[] whereArgs = new String[nicknameKeys.length + 2]; - whereArgs[0] = whereArgs[1] = queryEscaped; - System.arraycopy(nicknameKeys, 0, whereArgs, 2, nicknameKeys.length); - final String[] mappedProjection = { - new Column(CachedUsers._ID, Suggestions._ID).getSQL(), - new Column("'" + Suggestions.AutoComplete.TYPE_USERS + "'", Suggestions.TYPE).getSQL(), - new Column(CachedUsers.NAME, Suggestions.TITLE).getSQL(), - new Column(CachedUsers.SCREEN_NAME, Suggestions.SUMMARY).getSQL(), - new Column(CachedUsers.USER_KEY, Suggestions.EXTRA_ID).getSQL(), - new Column(CachedUsers.PROFILE_IMAGE_URL, Suggestions.ICON).getSQL(), - new Column(CachedUsers.SCREEN_NAME, Suggestions.VALUE).getSQL(), - }; - final String[] orderBy = {CachedUsers.SCORE, CachedUsers.LAST_SEEN, CachedUsers.SCREEN_NAME, - CachedUsers.NAME}; - final boolean[] ascending = {false, false, true, true}; - return query(Uri.withAppendedPath(CachedUsers.CONTENT_URI_WITH_SCORE, accountKey), - mappedProjection, where.getSQL(), whereArgs, new OrderBy(orderBy, ascending).getSQL()); - } else if (Suggestions.AutoComplete.TYPE_HASHTAGS.equals(type)) { - final Expression where = Expression.likeRaw(new Column(CachedHashtags.NAME), "?||'%'", "^"); - final String[] whereArgs = new String[]{queryEscaped}; - final String[] mappedProjection = { - new Column(CachedHashtags._ID, Suggestions._ID).getSQL(), - new Column("'" + Suggestions.AutoComplete.TYPE_HASHTAGS + "'", Suggestions.TYPE).getSQL(), - new Column(CachedHashtags.NAME, Suggestions.TITLE).getSQL(), - new Column("NULL", Suggestions.SUMMARY).getSQL(), - new Column("0", Suggestions.EXTRA_ID).getSQL(), - new Column("NULL", Suggestions.ICON).getSQL(), - new Column(CachedHashtags.NAME, Suggestions.VALUE).getSQL(), - }; - return query(CachedHashtags.CONTENT_URI, mappedProjection, where.getSQL(), - whereArgs, null); - } - return null; - } - - @Override - public int update(@NonNull final Uri uri, final ContentValues values, final String selection, final String[] selectionArgs) { - try { - return updateInternal(uri, values, selection, selectionArgs); - } catch (final SQLException e) { - if (handleSQLException(e)) { - try { - return updateInternal(uri, values, selection, selectionArgs); - } catch (SQLException e1) { - throw new IllegalStateException(e1); - } - } - throw new IllegalStateException(e); - } - } - - private int updateInternal(@NonNull Uri uri, ContentValues values, String selection, String[] selectionArgs) { - final int tableId = DataStoreUtils.INSTANCE.getTableId(uri); - final String table = DataStoreUtils.INSTANCE.getTableNameById(tableId); - checkWritePermission(tableId, table); - int result = 0; - if (table != null) { - result = databaseWrapper.update(table, values, selection, selectionArgs); - } - if (result > 0) { - onDatabaseUpdated(tableId, uri); - } - return result; - } - - private boolean checkPermission(final String... permissions) { - return permissionsManager.checkCallingPermission(permissions); - } - - private void checkReadPermission(final int id, final String table, final String[] projection) { - if (Binder.getCallingPid() == Process.myPid()) return; - switch (id) { - case VIRTUAL_TABLE_ID_PERMISSIONS: { - return; - } - case VIRTUAL_TABLE_ID_PREFERENCES: - case VIRTUAL_TABLE_ID_DNS: { - if (!checkPermission(PERMISSION_PREFERENCES)) - throw new SecurityException("Access preferences requires level PERMISSION_LEVEL_PREFERENCES"); - break; - } - case TABLE_ID_MESSAGES: - case TABLE_ID_MESSAGES_CONVERSATIONS: { - if (!checkPermission(PERMISSION_DIRECT_MESSAGES)) - throw new SecurityException("Access database " + table - + " requires level PERMISSION_LEVEL_DIRECT_MESSAGES"); - break; - } - case TABLE_ID_STATUSES: - case TABLE_ID_MENTIONS: - case TABLE_ID_TABS: - case TABLE_ID_DRAFTS: - case TABLE_ID_CACHED_USERS: - case TABLE_ID_FILTERED_USERS: - case TABLE_ID_FILTERED_KEYWORDS: - case TABLE_ID_FILTERED_SOURCES: - case TABLE_ID_FILTERED_LINKS: - case TABLE_ID_TRENDS_LOCAL: - case TABLE_ID_CACHED_STATUSES: - case TABLE_ID_CACHED_HASHTAGS: { - if (!checkPermission(PERMISSION_READ)) - throw new SecurityException("Access database " + table + " requires level PERMISSION_LEVEL_READ"); - break; - } - default: { - if (!permissionsManager.checkSignature(Binder.getCallingUid())) { - throw new SecurityException("Internal database " + id + " is not allowed for third-party applications"); - } - } - } - } - - private void checkWritePermission(final int id, final String table) { - if (Binder.getCallingPid() == Process.myPid()) return; - switch (id) { - case TABLE_ID_MESSAGES: - case TABLE_ID_MESSAGES_CONVERSATIONS: { - if (!checkPermission(PERMISSION_DIRECT_MESSAGES)) - throw new SecurityException("Access database " + table - + " requires level PERMISSION_LEVEL_DIRECT_MESSAGES"); - break; - } - case TABLE_ID_STATUSES: - case TABLE_ID_MENTIONS: - case TABLE_ID_TABS: - case TABLE_ID_DRAFTS: - case TABLE_ID_CACHED_USERS: - case TABLE_ID_FILTERED_USERS: - case TABLE_ID_FILTERED_KEYWORDS: - case TABLE_ID_FILTERED_SOURCES: - case TABLE_ID_FILTERED_LINKS: - case TABLE_ID_TRENDS_LOCAL: - case TABLE_ID_CACHED_STATUSES: - case TABLE_ID_CACHED_HASHTAGS: { - if (!checkPermission(PERMISSION_WRITE)) - throw new SecurityException("Access database " + table + " requires level PERMISSION_LEVEL_WRITE"); - break; - } - default: { - if (!permissionsManager.checkSignature(Binder.getCallingUid())) { - throw new SecurityException("Internal database is not allowed for third-party applications"); - } - } - } - } - - private void clearNotification() { - notificationManager.cancelAll(); - } - - private void clearNotification(final int notificationType, final UserKey accountId) { - notificationManager.cancelById(Utils.getNotificationId(notificationType, accountId)); - } - - private ParcelFileDescriptor getCachedImageFd(final String url) throws FileNotFoundException { - if (BuildConfig.DEBUG) { - Log.d(LOGTAG, String.format("getCachedImageFd(%s)", url)); - } - final File file = mediaLoader.getDiskCache().get(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 context = getContext(); - assert context != null; - final File cacheDir = context.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(); - assert context != null; - return mContentResolver = context.getContentResolver(); - } - - private Cursor getDNSCursor(final String host) { - final MatrixCursor c = new MatrixCursor(DNS.MATRIX_COLUMNS); - try { - final List addresses = dns.lookup(host); - for (InetAddress address : addresses) { - c.addRow(new String[]{host, address.getHostAddress()}); - } - } catch (final IOException ignore) { - DebugLog.w(LOGTAG, null, ignore); - } - return c; - } - - private Cursor getNotificationsCursor() { - final MatrixCursor c = new MatrixCursor(Notifications.MATRIX_COLUMNS); - return c; - } - - private Cursor getNotificationsCursor(final int id) { - final MatrixCursor c = new MatrixCursor(Notifications.MATRIX_COLUMNS); - return c; - } - - private Cursor getUnreadCountsCursor() { - final MatrixCursor c = new MatrixCursor(UnreadCounts.MATRIX_COLUMNS); - return c; - } - - private Cursor getUnreadCountsCursor(final int position) { - final MatrixCursor c = new MatrixCursor(UnreadCounts.MATRIX_COLUMNS); - - return c; - } - - private Cursor getUnreadCountsCursorByType(final String type) { - final MatrixCursor c = new MatrixCursor(UnreadCounts.MATRIX_COLUMNS); - return c; - } - - private boolean isNotificationAudible() { - return !activityTracker.isHomeActivityStarted(); - } - - private void notifyContentObserver(@NonNull final Uri uri) { - if (!uri.getBooleanQueryParameter(QUERY_PARAM_NOTIFY, true)) return; - mHandler.post(new Runnable() { - @Override - public void run() { - final ContentResolver cr = getContentResolver(); - if (cr == null) return; - cr.notifyChange(uri, null); - } - }); - } - - private void notifyUnreadCountChanged(final int position) { - 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; - notifyContentObserver(Utils.getNotificationUri(tableId, uri)); - } - - private void onNewItemsInserted(final Uri uri, final int tableId, final ContentValues values) { - onNewItemsInserted(uri, tableId, new ContentValues[]{values}); - } - - private void onNewItemsInserted(final Uri uri, final int tableId, final ContentValues[] valuesArray) { - final Context context = getContext(); - if (uri == null || valuesArray == null || valuesArray.length == 0 || context == null) - return; - switch (tableId) { - case TABLE_ID_STATUSES: { - mBackgroundExecutor.execute(new Runnable() { - @Override - public void run() { - final AccountPreferences[] prefs = AccountPreferences.getNotificationEnabledPreferences(context, - DataStoreUtils.INSTANCE.getAccountKeys(context)); - for (final AccountPreferences pref : prefs) { - if (!pref.isHomeTimelineNotificationEnabled()) continue; - final long positionTag = getPositionTag(CustomTabType.HOME_TIMELINE, pref.getAccountKey()); - showTimelineNotification(preferences, pref, positionTag); - } - notifyUnreadCountChanged(NOTIFICATION_ID_HOME_TIMELINE); - } - }); - break; - } - case TABLE_ID_ACTIVITIES_ABOUT_ME: { - mBackgroundExecutor.execute(new Runnable() { - @Override - public void run() { - final AccountPreferences[] prefs = AccountPreferences.getNotificationEnabledPreferences(context, - DataStoreUtils.INSTANCE.getAccountKeys(context)); - final boolean combined = preferences.getBoolean(KEY_COMBINED_NOTIFICATIONS); - for (final AccountPreferences pref : prefs) { - if (!pref.isInteractionsNotificationEnabled()) continue; - showInteractionsNotification(pref, getPositionTag(ReadPositionTag.ACTIVITIES_ABOUT_ME, - pref.getAccountKey()), combined); - } - notifyUnreadCountChanged(NOTIFICATION_ID_INTERACTIONS_TIMELINE); - } - }); - break; - } - case TABLE_ID_MESSAGES: { - final AccountPreferences[] prefs = AccountPreferences.getNotificationEnabledPreferences(context, - DataStoreUtils.INSTANCE.getAccountKeys(context)); - for (final AccountPreferences pref : prefs) { - if (!pref.isDirectMessagesNotificationEnabled()) continue; - final StringLongPair[] pairs = readStateManager.getPositionPairs(CustomTabType.DIRECT_MESSAGES); - // TODO show messages notifications - } - notifyUnreadCountChanged(NOTIFICATION_ID_DIRECT_MESSAGES); - break; - } - case TABLE_ID_DRAFTS: { - break; - } - } - } - - private long getPositionTag(String tag, UserKey accountKey) { - final long position = readStateManager.getPosition(Utils.getReadPositionTagWithAccount(tag, - accountKey)); - if (position != -1) return position; - return readStateManager.getPosition(tag); - } - - private void showTimelineNotification(SharedPreferences preferences, AccountPreferences pref, long position) { - final UserKey accountKey = pref.getAccountKey(); - final Context context = getContext(); - if (context == null) return; - final Resources resources = context.getResources(); - final NotificationManagerWrapper nm = notificationManager; - final Expression selection = Expression.and(Expression.equalsArgs(Statuses.ACCOUNT_KEY), - Expression.greaterThan(Statuses.POSITION_KEY, position)); - final String filteredSelection = DataStoreFunctionsKt.buildStatusFilterWhereClause(preferences, - Statuses.TABLE_NAME, selection).getSQL(); - final String[] selectionArgs = {accountKey.toString()}; - final String[] userProjection = {Statuses.USER_KEY, Statuses.USER_NAME, Statuses.USER_SCREEN_NAME}; - final String[] statusProjection = {Statuses.POSITION_KEY}; - final Cursor statusCursor = databaseWrapper.query(Statuses.TABLE_NAME, statusProjection, - filteredSelection, selectionArgs, null, null, Statuses.DEFAULT_SORT_ORDER); - final Cursor userCursor = databaseWrapper.query(Statuses.TABLE_NAME, userProjection, - filteredSelection, selectionArgs, Statuses.USER_KEY, null, Statuses.DEFAULT_SORT_ORDER); - //noinspection TryFinallyCanBeTryWithResources - try { - final int usersCount = userCursor.getCount(); - final int statusesCount = statusCursor.getCount(); - if (statusesCount == 0 || usersCount == 0) return; - final ParcelableStatusCursorIndices statusIndices = new ParcelableStatusCursorIndices(statusCursor), - userIndices = new ParcelableStatusCursorIndices(userCursor); - final long positionKey = statusCursor.moveToFirst() ? statusCursor.getLong(statusIndices.position_key) : -1; - final String notificationTitle = resources.getQuantityString(R.plurals.N_new_statuses, - statusesCount, statusesCount); - final String notificationContent; - userCursor.moveToFirst(); - final String displayName = userColorNameManager.getDisplayName(userCursor.getString(userIndices.user_key), - userCursor.getString(userIndices.user_name), userCursor.getString(userIndices.user_screen_name), - nameFirst); - if (usersCount == 1) { - notificationContent = context.getString(R.string.from_name, displayName); - } else if (usersCount == 2) { - userCursor.moveToPosition(1); - final String othersName = userColorNameManager.getDisplayName(userCursor.getString(userIndices.user_key), - userCursor.getString(userIndices.user_name), userCursor.getString(userIndices.user_screen_name), - nameFirst); - notificationContent = resources.getString(R.string.from_name_and_name, displayName, othersName); - } else { - notificationContent = resources.getString(R.string.from_name_and_N_others, displayName, usersCount - 1); - } - - // Setup notification - final NotificationCompat.Builder builder = new NotificationCompat.Builder(context); - builder.setAutoCancel(true); - builder.setSmallIcon(R.drawable.ic_stat_twitter); - builder.setTicker(notificationTitle); - builder.setContentTitle(notificationTitle); - builder.setContentText(notificationContent); - builder.setCategory(NotificationCompat.CATEGORY_SOCIAL); - builder.setContentIntent(getContentIntent(context, CustomTabType.HOME_TIMELINE, - NotificationType.HOME_TIMELINE, accountKey, positionKey)); - builder.setDeleteIntent(getMarkReadDeleteIntent(context, NotificationType.HOME_TIMELINE, - accountKey, positionKey, false)); - builder.setNumber(statusesCount); - builder.setCategory(NotificationCompat.CATEGORY_SOCIAL); - applyNotificationPreferences(builder, pref, pref.getHomeTimelineNotificationType()); - try { - nm.notify("home_" + accountKey, Utils.getNotificationId(NOTIFICATION_ID_HOME_TIMELINE, accountKey), builder.build()); - Utils.sendPebbleNotification(context, null, notificationContent); - } catch (SecurityException e) { - // Silently ignore - } - } finally { - statusCursor.close(); - userCursor.close(); - } - } - - private void showInteractionsNotification(AccountPreferences pref, long position, boolean combined) { - final Context context = getContext(); - if (context == null) return; - final SQLiteDatabase db = databaseWrapper.getSQLiteDatabase(); - final UserKey accountKey = pref.getAccountKey(); - final String where = Expression.and( - Expression.equalsArgs(Activities.ACCOUNT_KEY), - Expression.greaterThanArgs(Activities.POSITION_KEY) - ).getSQL(); - final String[] whereArgs = {accountKey.toString(), String.valueOf(position)}; - Cursor c = query(Activities.AboutMe.CONTENT_URI, Activities.COLUMNS, where, whereArgs, - new OrderBy(Activities.TIMESTAMP, false).getSQL()); - if (c == null) return; - final NotificationCompat.Builder builder = new NotificationCompat.Builder(context); - final StringBuilder pebbleNotificationStringBuilder = new StringBuilder(); - try { - final int count = c.getCount(); - if (count == 0) return; - builder.setSmallIcon(R.drawable.ic_stat_notification); - builder.setCategory(NotificationCompat.CATEGORY_SOCIAL); - applyNotificationPreferences(builder, pref, pref.getMentionsNotificationType()); - - final Resources resources = context.getResources(); - final String accountName = DataStoreUtils.INSTANCE.getAccountDisplayName(context, accountKey, nameFirst); - builder.setContentText(accountName); - final InboxStyle style = new InboxStyle(); - builder.setStyle(style); - builder.setAutoCancel(true); - style.setSummaryText(accountName); - final ParcelableActivityCursorIndices ci = new ParcelableActivityCursorIndices(c); - int messageLines = 0; - - long timestamp = -1; - c.moveToPosition(-1); - while (c.moveToNext()) { - if (messageLines == 5) { - style.addLine(resources.getString(R.string.and_N_more, count - c.getPosition())); - pebbleNotificationStringBuilder.append(resources.getString(R.string.and_N_more, count - c.getPosition())); - break; - } - final ParcelableActivity activity = ci.newObject(c); - if (pref.isNotificationMentionsOnly() && !ArrayUtils.contains(Activity.Action.MENTION_ACTIONS, - activity.action)) { - continue; - } - if (activity.status_id != null && InternalTwitterContentUtils.isFiltered(db, - activity.status_user_key, activity.status_text_plain, - activity.status_quote_text_plain, activity.status_spans, - activity.status_quote_spans, activity.status_source, - activity.status_quote_source, activity.status_retweeted_by_user_key, - activity.status_quoted_user_key)) { - continue; - } - final UserKey[] filteredUserIds = DataStoreUtils.INSTANCE.getFilteredUserIds(context); - if (timestamp == -1) { - timestamp = activity.timestamp; - } - ParcelableActivityUtils.INSTANCE.initAfterFilteredSourceIds(activity, filteredUserIds, - pref.isNotificationFollowingOnly()); - final ParcelableUser[] sources = ParcelableActivityUtils.INSTANCE.getAfterFilteredSources(activity); - if (ArrayUtils.isEmpty(sources)) continue; - final ActivityTitleSummaryMessage message = ActivityTitleSummaryMessage.get(context, - userColorNameManager, activity, sources, - 0, useStarForLikes, nameFirst); - if (message != null) { - final CharSequence summary = message.getSummary(); - if (TextUtils.isEmpty(summary)) { - style.addLine(message.getTitle()); - pebbleNotificationStringBuilder.append(message.getTitle()); - pebbleNotificationStringBuilder.append("\n"); - } else { - style.addLine(SpanFormatter.format(resources.getString(R.string.title_summary_line_format), - message.getTitle(), summary)); - pebbleNotificationStringBuilder.append(message.getTitle()); - pebbleNotificationStringBuilder.append(": "); - pebbleNotificationStringBuilder.append(message.getSummary()); - pebbleNotificationStringBuilder.append("\n"); - } - messageLines++; - } - } - if (messageLines == 0) return; - final int displayCount = messageLines + count - c.getPosition(); - final String title = resources.getQuantityString(R.plurals.N_new_interactions, - displayCount, displayCount); - builder.setContentTitle(title); - style.setBigContentTitle(title); - builder.setNumber(displayCount); - builder.setContentIntent(getContentIntent(context, CustomTabType.NOTIFICATIONS_TIMELINE, - NotificationType.INTERACTIONS, accountKey, timestamp)); - if (timestamp != -1) { - builder.setDeleteIntent(getMarkReadDeleteIntent(context, - NotificationType.INTERACTIONS, accountKey, timestamp, false)); - } - } catch (IOException e) { - return; - } finally { - c.close(); - } - final int notificationId = Utils.getNotificationId(NOTIFICATION_ID_INTERACTIONS_TIMELINE, - accountKey); - notificationManager.notify("interactions", notificationId, builder.build()); - - Utils.sendPebbleNotification(context, context.getResources().getString(R.string.interactions), pebbleNotificationStringBuilder.toString()); - - } - - private PendingIntent getContentIntent(final Context context, @CustomTabType final String type, - @NotificationType final String notificationType, - @Nullable final UserKey accountKey, final long readPosition) { - // Setup click intent - final Intent homeIntent = new Intent(context, HomeActivity.class); - final Uri.Builder homeLinkBuilder = new Uri.Builder(); - homeLinkBuilder.scheme(SCHEME_TWIDERE); - homeLinkBuilder.authority(type); - if (accountKey != null) - homeLinkBuilder.appendQueryParameter(QUERY_PARAM_ACCOUNT_KEY, accountKey.toString()); - homeLinkBuilder.appendQueryParameter(QUERY_PARAM_FROM_NOTIFICATION, String.valueOf(true)); - homeLinkBuilder.appendQueryParameter(QUERY_PARAM_TIMESTAMP, String.valueOf(System.currentTimeMillis())); - homeLinkBuilder.appendQueryParameter(QUERY_PARAM_NOTIFICATION_TYPE, notificationType); - if (readPosition > 0) { - homeLinkBuilder.appendQueryParameter(QUERY_PARAM_READ_POSITION, String.valueOf(readPosition)); - } - homeIntent.setData(homeLinkBuilder.build()); - homeIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); - return PendingIntent.getActivity(context, 0, homeIntent, 0); - } - - private void applyNotificationPreferences(NotificationCompat.Builder builder, AccountPreferences pref, int defaultFlags) { - int notificationDefaults = 0; - if (AccountPreferences.isNotificationHasLight(defaultFlags)) { - notificationDefaults |= NotificationCompat.DEFAULT_LIGHTS; - } - if (isNotificationAudible()) { - if (AccountPreferences.isNotificationHasVibration(defaultFlags)) { - notificationDefaults |= NotificationCompat.DEFAULT_VIBRATE; - } else { - notificationDefaults &= ~NotificationCompat.DEFAULT_VIBRATE; - } - if (AccountPreferences.isNotificationHasRingtone(defaultFlags)) { - builder.setSound(pref.getNotificationRingtone(), AudioManager.STREAM_NOTIFICATION); - } - } else { - notificationDefaults &= ~(NotificationCompat.DEFAULT_VIBRATE | NotificationCompat.DEFAULT_SOUND); - } - builder.setColor(pref.getNotificationLightColor()); - builder.setDefaults(notificationDefaults); - builder.setOnlyAlertOnce(true); - } - - 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 void updatePreferences() { - nameFirst = preferences.getBoolean(KEY_NAME_FIRST); - useStarForLikes = preferences.getBoolean(KEY_I_WANT_MY_STARS_BACK); - } - -} diff --git a/twidere/src/main/java/org/mariotaku/twidere/task/RemoveUnreadCountsTask.java b/twidere/src/main/java/org/mariotaku/twidere/task/RemoveUnreadCountsTask.java deleted file mode 100644 index 03393f16e..000000000 --- a/twidere/src/main/java/org/mariotaku/twidere/task/RemoveUnreadCountsTask.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Twidere - Twitter client for Android - * - * Copyright (C) 2012-2017 Mariotaku Lee - * - * 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 . - */ - -package org.mariotaku.twidere.task; - -import android.content.Context; -import android.os.AsyncTask; -import android.support.v4.util.SimpleArrayMap; - -import org.mariotaku.twidere.model.UserKey; -import org.mariotaku.twidere.util.TwitterWrapper; - -import java.util.Set; - -/** - * Created by mariotaku on 2017/2/10. - */ -public final class RemoveUnreadCountsTask extends AsyncTask { - private final Context context; - private final int position; - private final SimpleArrayMap> counts; - - public RemoveUnreadCountsTask(Context context, final int position, final SimpleArrayMap> counts) { - this.context = context; - this.position = position; - this.counts = counts; - } - - @Override - protected Integer doInBackground(final Object... params) { - return TwitterWrapper.removeUnreadCounts(context, position, counts); - } - -} diff --git a/twidere/src/main/java/org/mariotaku/twidere/util/InternalTwitterContentUtils.java b/twidere/src/main/java/org/mariotaku/twidere/util/InternalTwitterContentUtils.java index 16edd14b9..d1817fbef 100644 --- a/twidere/src/main/java/org/mariotaku/twidere/util/InternalTwitterContentUtils.java +++ b/twidere/src/main/java/org/mariotaku/twidere/util/InternalTwitterContentUtils.java @@ -21,7 +21,7 @@ import org.mariotaku.microblog.library.twitter.model.User; import org.mariotaku.twidere.model.ParcelableStatus; import org.mariotaku.twidere.model.SpanItem; import org.mariotaku.twidere.model.UserKey; -import org.mariotaku.twidere.provider.TwidereDataStore.Filters; +import org.mariotaku.twidere.util.database.FilterQueryBuilder; import java.util.ArrayList; import java.util.List; @@ -46,83 +46,25 @@ public class InternalTwitterContentUtils { final String textPlain, final String quotedTextPlain, final SpanItem[] spans, final SpanItem[] quotedSpans, final String source, final String quotedSource, - final UserKey retweetedById, final UserKey quotedUserId) { + final UserKey retweetedByKey, final UserKey quotedUserKey) { return isFiltered(database, userKey, textPlain, quotedTextPlain, spans, quotedSpans, source, - quotedSource, retweetedById, quotedUserId, true); + quotedSource, retweetedByKey, quotedUserKey, true); } + public static boolean isFiltered(final SQLiteDatabase database, final UserKey userKey, final String textPlain, final String quotedTextPlain, final SpanItem[] spans, final SpanItem[] quotedSpans, final String source, final String quotedSource, final UserKey retweetedByKey, final UserKey quotedUserKey, final boolean filterRts) { - if (database == null) return false; if (textPlain == null && spans == null && userKey == null && source == null) return false; - final StringBuilder builder = new StringBuilder(); - final List selectionArgs = new ArrayList<>(); - builder.append("SELECT "); - if (textPlain != null) { - selectionArgs.add(textPlain); - addTextPlainStatement(builder); - } - if (quotedTextPlain != null) { - if (!selectionArgs.isEmpty()) { - builder.append(" OR "); - } - selectionArgs.add(quotedTextPlain); - addTextPlainStatement(builder); - } - if (spans != null) { - if (!selectionArgs.isEmpty()) { - builder.append(" OR "); - } - addSpansStatement(spans, builder, selectionArgs); - } - if (quotedSpans != null) { - if (!selectionArgs.isEmpty()) { - builder.append(" OR "); - } - addSpansStatement(quotedSpans, builder, selectionArgs); - } - if (userKey != null) { - if (!selectionArgs.isEmpty()) { - builder.append(" OR "); - } - selectionArgs.add(String.valueOf(userKey)); - createUserKeyStatement(builder); - } - if (retweetedByKey != null) { - if (!selectionArgs.isEmpty()) { - builder.append(" OR "); - } - selectionArgs.add(String.valueOf(retweetedByKey)); - createUserKeyStatement(builder); - } - if (quotedUserKey != null) { - if (!selectionArgs.isEmpty()) { - builder.append(" OR "); - } - selectionArgs.add(String.valueOf(quotedUserKey)); - createUserKeyStatement(builder); - } - if (source != null) { - if (!selectionArgs.isEmpty()) { - builder.append(" OR "); - } - selectionArgs.add(source); - appendSourceStatement(builder); - } - if (quotedSource != null) { - if (!selectionArgs.isEmpty()) { - builder.append(" OR "); - } - selectionArgs.add(quotedSource); - appendSourceStatement(builder); - } - final Cursor cur = database.rawQuery(builder.toString(), - selectionArgs.toArray(new String[selectionArgs.size()])); + + final Pair query = FilterQueryBuilder.INSTANCE.isFilteredQuery(userKey, + textPlain, quotedTextPlain, spans, quotedSpans, source, quotedSource, retweetedByKey, + quotedUserKey, filterRts); + final Cursor cur = database.rawQuery(query.getFirst(), query.getSecond()); if (cur == null) return false; try { return cur.moveToFirst() && cur.getInt(0) != 0; @@ -131,38 +73,6 @@ public class InternalTwitterContentUtils { } } - static void addTextPlainStatement(StringBuilder builder) { - builder.append("(SELECT 1 IN (SELECT ? LIKE '%'||").append(Filters.Keywords.TABLE_NAME).append(".").append(Filters.VALUE).append("||'%' FROM ").append(Filters.Keywords.TABLE_NAME).append("))"); - } - - static void addSpansStatement(SpanItem[] spans, StringBuilder builder, List selectionArgs) { - StringBuilder spansFlat = new StringBuilder(); - for (SpanItem span : spans) { - spansFlat.append(span.link); - spansFlat.append(' '); - } - selectionArgs.add(spansFlat.toString()); - builder.append("(SELECT 1 IN (SELECT ? LIKE '%'||") - .append(Filters.Links.TABLE_NAME) - .append(".") - .append(Filters.VALUE) - .append("||'%' FROM ") - .append(Filters.Links.TABLE_NAME) - .append("))"); - } - - static void createUserKeyStatement(StringBuilder builder) { - builder.append("(SELECT ").append("?").append(" IN (SELECT ").append(Filters.Users.USER_KEY).append(" FROM ").append(Filters.Users.TABLE_NAME).append("))"); - } - - static void appendSourceStatement(StringBuilder builder) { - builder.append("(SELECT 1 IN (SELECT ? LIKE '%>'||") - .append(Filters.Sources.TABLE_NAME).append(".") - .append(Filters.VALUE).append("||'%' FROM ") - .append(Filters.Sources.TABLE_NAME) - .append("))"); - } - public static boolean isFiltered(final SQLiteDatabase database, final ParcelableStatus status, final boolean filterRTs) { if (database == null || status == null) return false; diff --git a/twidere/src/main/java/org/mariotaku/twidere/util/TwidereQueryBuilder.java b/twidere/src/main/java/org/mariotaku/twidere/util/TwidereQueryBuilder.java index 8a1a758de..d585fa699 100644 --- a/twidere/src/main/java/org/mariotaku/twidere/util/TwidereQueryBuilder.java +++ b/twidere/src/main/java/org/mariotaku/twidere/util/TwidereQueryBuilder.java @@ -19,7 +19,6 @@ package org.mariotaku.twidere.util; -import android.util.Pair; import org.mariotaku.sqliteqb.library.Columns; import org.mariotaku.sqliteqb.library.Columns.Column; @@ -27,7 +26,6 @@ import org.mariotaku.sqliteqb.library.Expression; import org.mariotaku.sqliteqb.library.Join; import org.mariotaku.sqliteqb.library.Join.Operation; import org.mariotaku.sqliteqb.library.OrderBy; -import org.mariotaku.sqliteqb.library.SQLQueryBuilder; import org.mariotaku.sqliteqb.library.Selectable; import org.mariotaku.sqliteqb.library.Table; import org.mariotaku.sqliteqb.library.Tables; @@ -35,14 +33,11 @@ import org.mariotaku.sqliteqb.library.query.SQLSelectQuery; import org.mariotaku.twidere.model.UserKey; import org.mariotaku.twidere.provider.TwidereDataStore.CachedRelationships; import org.mariotaku.twidere.provider.TwidereDataStore.CachedUsers; -import org.mariotaku.twidere.provider.TwidereDataStore.DirectMessages; -import org.mariotaku.twidere.provider.TwidereDataStore.DirectMessages.Conversation; -import org.mariotaku.twidere.provider.TwidereDataStore.DirectMessages.ConversationEntries; -import org.mariotaku.twidere.provider.TwidereDataStore.DirectMessages.Inbox; -import org.mariotaku.twidere.provider.TwidereDataStore.DirectMessages.Outbox; import java.util.Locale; +import kotlin.Pair; + public class TwidereQueryBuilder { public static final class CachedUsersQueryBuilder { @@ -51,19 +46,19 @@ public class TwidereQueryBuilder { } public static Pair withRelationship(final String[] projection, - final String selection, - final String[] selectionArgs, - final String sortOrder, - final UserKey accountKey) { + final String selection, + final String[] selectionArgs, + final String sortOrder, + final UserKey accountKey) { return withRelationship(Utils.getColumnsFromProjection(projection), selection, selectionArgs, sortOrder, accountKey); } public static Pair withRelationship(final Selectable select, - final String selection, - final String[] selectionArgs, - final String sortOrder, - final UserKey accountKey) { + final String selection, + final String[] selectionArgs, + final String sortOrder, + final UserKey accountKey) { final SQLSelectQuery.Builder qb = new SQLSelectQuery.Builder(); qb.select(select).from(new Tables(CachedUsers.TABLE_NAME)); final Column relationshipsUserId = new Column(new Table(CachedRelationships.TABLE_NAME), @@ -99,15 +94,15 @@ public class TwidereQueryBuilder { } final String[] mergedArgs = new String[TwidereArrayUtils.arraysLength(accountKeyArgs, selectionArgs)]; TwidereArrayUtils.mergeArray(mergedArgs, accountKeyArgs, selectionArgs); - return Pair.create(qb.build(), mergedArgs); + return new Pair<>(qb.build(), mergedArgs); } public static Pair withScore(final String[] projection, - final String selection, - final String[] selectionArgs, - final String sortOrder, - final UserKey accountKey, - final int limit) { + final String selection, + final String[] selectionArgs, + final String sortOrder, + final UserKey accountKey, + final int limit) { final SQLSelectQuery.Builder qb = new SQLSelectQuery.Builder(); final Selectable select = Utils.getColumnsFromProjection(projection); final Column[] columns = new Column[CachedUsers.COLUMNS.length + 1]; @@ -127,9 +122,9 @@ public class TwidereQueryBuilder { qb.select(select); final Pair pair = withRelationship(new Columns(columns), null, null, null, accountKey); - qb.from(pair.first); - final String[] mergedArgs = new String[TwidereArrayUtils.arraysLength(pair.second, selectionArgs)]; - TwidereArrayUtils.mergeArray(mergedArgs, pair.second, selectionArgs); + qb.from(pair.getFirst()); + final String[] mergedArgs = new String[TwidereArrayUtils.arraysLength(pair.getSecond(), selectionArgs)]; + TwidereArrayUtils.mergeArray(mergedArgs, pair.getSecond(), selectionArgs); if (selection != null) { qb.where(new Expression(selection)); } @@ -139,7 +134,7 @@ public class TwidereQueryBuilder { if (limit > 0) { qb.limit(limit); } - return Pair.create(qb.build(), mergedArgs); + return new Pair<>(qb.build(), mergedArgs); } private static Object[] valueOrZero(String... columns) { diff --git a/twidere/src/main/java/org/mariotaku/twidere/util/TwitterWrapper.java b/twidere/src/main/java/org/mariotaku/twidere/util/TwitterWrapper.java index e311b4a50..ef1c79806 100644 --- a/twidere/src/main/java/org/mariotaku/twidere/util/TwitterWrapper.java +++ b/twidere/src/main/java/org/mariotaku/twidere/util/TwitterWrapper.java @@ -23,7 +23,6 @@ import android.content.ContentResolver; import android.content.Context; import android.net.Uri; import android.support.annotation.NonNull; -import android.support.v4.util.SimpleArrayMap; import android.text.TextUtils; import android.webkit.MimeTypeMap; @@ -41,34 +40,16 @@ import org.mariotaku.twidere.annotation.AccountType; import org.mariotaku.twidere.model.ListResponse; import org.mariotaku.twidere.model.SingleResponse; import org.mariotaku.twidere.model.UserKey; -import org.mariotaku.twidere.provider.TwidereDataStore.Notifications; -import org.mariotaku.twidere.provider.TwidereDataStore.UnreadCounts; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.util.List; -import java.util.Set; public class TwitterWrapper implements Constants { - public static int clearNotification(final Context context, final int notificationType, final UserKey accountId) { - final Uri.Builder builder = Notifications.CONTENT_URI.buildUpon(); - builder.appendPath(String.valueOf(notificationType)); - if (accountId != null) { - builder.appendPath(String.valueOf(accountId)); - } - return context.getContentResolver().delete(builder.build(), null, null); - } - - public static int clearUnreadCount(final Context context, final int position) { - if (context == null || position < 0) return 0; - final Uri uri = UnreadCounts.CONTENT_URI.buildUpon().appendPath(String.valueOf(position)).build(); - return context.getContentResolver().delete(uri, null, null); - } - public static SingleResponse deleteProfileBannerImage(final Context context, - final UserKey accountKey) { + final UserKey accountKey) { final MicroBlog twitter = MicroBlogAPIFactory.getInstance(context, accountKey); if (twitter == null) return SingleResponse.Companion.getInstance(false); try { @@ -79,38 +60,9 @@ public class TwitterWrapper implements Constants { } } - public static int removeUnreadCounts(final Context context, final int position, final long account_id, - final long... statusIds) { - if (context == null || position < 0 || statusIds == null || statusIds.length == 0) - return 0; - int result = 0; - final Uri.Builder builder = UnreadCounts.CONTENT_URI.buildUpon(); - builder.appendPath(String.valueOf(position)); - builder.appendPath(String.valueOf(account_id)); - builder.appendPath(TwidereArrayUtils.toString(statusIds, ',', false)); - result += context.getContentResolver().delete(builder.build(), null, null); - return result; - } - - public static int removeUnreadCounts(final Context context, final int position, - final SimpleArrayMap> counts) { - if (context == null || position < 0 || counts == null) return 0; - int result = 0; - for (int i = 0, j = counts.size(); i < j; i++) { - final UserKey key = counts.keyAt(i); - final Set value = counts.valueAt(i); - final Uri.Builder builder = UnreadCounts.CONTENT_URI.buildUpon(); - builder.appendPath(String.valueOf(position)); - builder.appendPath(String.valueOf(key)); - builder.appendPath(CollectionUtils.toString(value, ',', false)); - result += context.getContentResolver().delete(builder.build(), null, null); - } - return result; - } - @NonNull public static User showUser(@NonNull final MicroBlog twitter, final String id, final String screenName, - final String accountType) throws MicroBlogException { + final String accountType) throws MicroBlogException { if (id != null) { if (AccountType.FANFOU.equals(accountType)) { return twitter.showFanfouUser(id); @@ -127,7 +79,7 @@ public class TwitterWrapper implements Constants { @NonNull public static User showUserAlternative(@NonNull final MicroBlog twitter, final String id, - final String screenName) + final String screenName) throws MicroBlogException { final String searchScreenName; if (screenName != null) { @@ -161,7 +113,7 @@ public class TwitterWrapper implements Constants { @NonNull public static User tryShowUser(@NonNull final MicroBlog twitter, final String id, final String screenName, - String accountType) + String accountType) throws MicroBlogException { try { return showUser(twitter, id, screenName, accountType); @@ -175,9 +127,9 @@ public class TwitterWrapper implements Constants { } public static void updateProfileBannerImage(@NonNull final Context context, - @NonNull final MicroBlog twitter, - @NonNull final Uri imageUri, - final boolean deleteImage) + @NonNull final MicroBlog twitter, + @NonNull final Uri imageUri, + final boolean deleteImage) throws IOException, MicroBlogException { FileBody fileBody = null; try { @@ -192,10 +144,10 @@ public class TwitterWrapper implements Constants { } public static void updateProfileBackgroundImage(@NonNull final Context context, - @NonNull final MicroBlog twitter, - @NonNull final Uri imageUri, - final boolean tile, - final boolean deleteImage) + @NonNull final MicroBlog twitter, + @NonNull final Uri imageUri, + final boolean tile, + final boolean deleteImage) throws IOException, MicroBlogException { FileBody fileBody = null; try { @@ -210,7 +162,7 @@ public class TwitterWrapper implements Constants { } public static User updateProfileImage(@NonNull final Context context, @NonNull final MicroBlog twitter, - @NonNull final Uri imageUri, final boolean deleteImage) + @NonNull final Uri imageUri, final boolean deleteImage) throws IOException, MicroBlogException { FileBody fileBody = null; try { @@ -262,12 +214,12 @@ public class TwitterWrapper implements Constants { } public MessageListResponse(final UserKey accountKey, final String maxId, final String sinceId, - final List list) { + final List list) { this(accountKey, maxId, sinceId, list, null); } MessageListResponse(final UserKey accountKey, final String maxId, final String sinceId, - final List list, final Exception exception) { + final List list, final Exception exception) { super(accountKey, maxId, sinceId, list, exception); } @@ -286,12 +238,12 @@ public class TwitterWrapper implements Constants { } public StatusListResponse(final UserKey accountKey, final String maxId, final String sinceId, - final List list, final boolean truncated) { + final List list, final boolean truncated) { this(accountKey, maxId, sinceId, list, truncated, null); } StatusListResponse(final UserKey accountKey, final String maxId, final String sinceId, final List list, - final boolean truncated, final Exception exception) { + final boolean truncated, final Exception exception) { super(accountKey, maxId, sinceId, list, exception); this.truncated = truncated; } @@ -305,17 +257,17 @@ public class TwitterWrapper implements Constants { public final String sinceId; public TwitterListResponse(final UserKey accountKey, - final Exception exception) { + final Exception exception) { this(accountKey, null, null, null, exception); } public TwitterListResponse(final UserKey accountKey, final String maxId, - final String sinceId, final List list) { + final String sinceId, final List list) { this(accountKey, maxId, sinceId, list, null); } TwitterListResponse(final UserKey accountKey, final String maxId, final String sinceId, - final List list, final Exception exception) { + final List list, final Exception exception) { super(list, exception); this.accountKey = accountKey; this.maxId = maxId; diff --git a/twidere/src/main/java/org/mariotaku/twidere/util/Utils.java b/twidere/src/main/java/org/mariotaku/twidere/util/Utils.java index 829be5cae..5a6758e8a 100644 --- a/twidere/src/main/java/org/mariotaku/twidere/util/Utils.java +++ b/twidere/src/main/java/org/mariotaku/twidere/util/Utils.java @@ -99,7 +99,7 @@ import org.mariotaku.twidere.model.PebbleMessage; import org.mariotaku.twidere.model.UserKey; import org.mariotaku.twidere.model.util.AccountUtils; import org.mariotaku.twidere.provider.TwidereDataStore.CachedUsers; -import org.mariotaku.twidere.provider.TwidereDataStore.DirectMessages; +import org.mariotaku.twidere.provider.TwidereDataStore.Messages; import org.mariotaku.twidere.view.TabPagerIndicator; import java.io.Closeable; @@ -509,7 +509,7 @@ public final class Utils implements Constants { switch (tableId) { case TABLE_ID_MESSAGES: case TABLE_ID_MESSAGES_CONVERSATIONS: - return DirectMessages.CONTENT_URI; + return Messages.CONTENT_URI; } return def; } diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/extension/ContentResolverExtensions.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/extension/ContentResolverExtensions.kt new file mode 100644 index 000000000..51e1cb1d1 --- /dev/null +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/extension/ContentResolverExtensions.kt @@ -0,0 +1,33 @@ +/* + * Twidere - Twitter client for Android + * + * Copyright (C) 2012-2017 Mariotaku Lee + * + * 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 . + */ + +package org.mariotaku.twidere.extension + +import android.annotation.SuppressLint +import android.content.ContentResolver +import android.database.Cursor +import android.net.Uri +import org.mariotaku.twidere.provider.TwidereDataStore + +@SuppressLint("Recycle") +fun ContentResolver.rawQuery(sql: String, selectionArgs: Array?): Cursor { + val rawUri = Uri.withAppendedPath(TwidereDataStore.CONTENT_URI_RAW_QUERY, sql) + return query(rawUri, selectionArgs, null, null, null) +} + diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/preference/ClearDatabasesPreference.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/preference/ClearDatabasesPreference.kt index 3e82c50bf..9dd2ad984 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/preference/ClearDatabasesPreference.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/preference/ClearDatabasesPreference.kt @@ -26,7 +26,10 @@ import org.mariotaku.twidere.TwidereConstants.TIMELINE_POSITIONS_PREFERENCES_NAM import org.mariotaku.twidere.provider.TwidereDataStore.* import org.mariotaku.twidere.util.DataStoreUtils -class ClearDatabasesPreference(context: Context, attrs: AttributeSet? = null) : AsyncTaskPreference(context, attrs, R.attr.preferenceStyle) { +class ClearDatabasesPreference( + context: Context, + attrs: AttributeSet? = null +) : AsyncTaskPreference(context, attrs, R.attr.preferenceStyle) { override fun doInBackground() { val context = context ?: return @@ -45,9 +48,8 @@ class ClearDatabasesPreference(context: Context, attrs: AttributeSet? = null) : } resolver.delete(Activities.AboutMe.CONTENT_URI, null, null) resolver.delete(Activities.ByFriends.CONTENT_URI, null, null) - resolver.delete(Notifications.CONTENT_URI, null, null) - resolver.delete(UnreadCounts.CONTENT_URI, null, null) resolver.delete(SavedSearches.CONTENT_URI, null, null) + // TODO clear all notifications val prefs = context.getSharedPreferences(TIMELINE_POSITIONS_PREFERENCES_NAME, Context.MODE_PRIVATE) val editor = prefs.edit() diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/provider/TwidereDataProvider.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/provider/TwidereDataProvider.kt new file mode 100644 index 000000000..7ff59d26a --- /dev/null +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/provider/TwidereDataProvider.kt @@ -0,0 +1,573 @@ +/* + * Twidere - Twitter client for Android + * + * Copyright (C) 2012-2017 Mariotaku Lee + * + * 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 . + */ + +package org.mariotaku.twidere.provider + +import android.app.PendingIntent +import android.content.ContentProvider +import android.content.ContentValues +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.SQLiteFullException +import android.net.Uri +import android.os.Binder +import android.os.Handler +import android.os.Looper +import android.os.Process +import android.provider.BaseColumns +import android.support.v4.app.NotificationCompat +import android.support.v4.text.BidiFormatter +import com.nostra13.universalimageloader.core.ImageLoader +import com.squareup.otto.Bus +import org.apache.commons.lang3.ArrayUtils +import org.mariotaku.ktextension.isNullOrEmpty +import org.mariotaku.ktextension.toNulls +import org.mariotaku.sqliteqb.library.Columns.Column +import org.mariotaku.sqliteqb.library.Expression +import org.mariotaku.sqliteqb.library.RawItemArray +import org.mariotaku.twidere.BuildConfig +import org.mariotaku.twidere.R +import org.mariotaku.twidere.TwidereConstants.* +import org.mariotaku.twidere.annotation.CustomTabType +import org.mariotaku.twidere.annotation.ReadPositionTag +import org.mariotaku.twidere.app.TwidereApplication +import org.mariotaku.twidere.model.AccountPreferences +import org.mariotaku.twidere.model.Draft +import org.mariotaku.twidere.model.DraftCursorIndices +import org.mariotaku.twidere.model.UserKey +import org.mariotaku.twidere.model.event.UnreadCountUpdatedEvent +import org.mariotaku.twidere.provider.TwidereDataStore.* +import org.mariotaku.twidere.service.LengthyOperationsService +import org.mariotaku.twidere.util.* +import org.mariotaku.twidere.util.SQLiteDatabaseWrapper.LazyLoadCallback +import org.mariotaku.twidere.util.TwidereQueryBuilder.CachedUsersQueryBuilder +import org.mariotaku.twidere.util.dagger.GeneralComponentHelper +import org.mariotaku.twidere.util.database.SuggestionsCursorCreator +import org.mariotaku.twidere.util.net.TwidereDns +import java.io.IOException +import java.util.concurrent.Executor +import java.util.concurrent.Executors +import javax.inject.Inject + +class TwidereDataProvider : ContentProvider(), LazyLoadCallback { + @Inject + lateinit internal var readStateManager: ReadStateManager + @Inject + lateinit internal var twitterWrapper: AsyncTwitterWrapper + @Inject + lateinit internal var mediaLoader: ImageLoader + @Inject + lateinit internal var notificationManager: NotificationManagerWrapper + @Inject + lateinit internal var preferences: SharedPreferencesWrapper + @Inject + lateinit internal var dns: TwidereDns + @Inject + lateinit internal var bus: Bus + @Inject + lateinit internal var userColorNameManager: UserColorNameManager + @Inject + lateinit internal var bidiFormatter: BidiFormatter + @Inject + lateinit internal var permissionsManager: PermissionsManager + @Inject + lateinit internal var contentNotificationManager: ContentNotificationManager + + private lateinit var databaseWrapper: SQLiteDatabaseWrapper + + private var handler: Handler? = null + private var backgroundExecutor: Executor? = null + + override fun bulkInsert(uri: Uri, valuesArray: Array): Int { + try { + return bulkInsertInternal(uri, valuesArray) + } catch (e: SQLException) { + if (handleSQLException(e)) { + try { + return bulkInsertInternal(uri, valuesArray) + } catch (e1: SQLException) { + throw IllegalStateException(e1) + } + + } + throw IllegalStateException(e) + } + + } + + private fun handleSQLException(e: SQLException): Boolean { + try { + if (e is SQLiteFullException) { + // Drop cached databases + databaseWrapper.delete(CachedUsers.TABLE_NAME, null, null) + databaseWrapper.delete(CachedStatuses.TABLE_NAME, null, null) + databaseWrapper.delete(CachedHashtags.TABLE_NAME, null, null) + databaseWrapper.execSQL("VACUUM") + return true + } + } catch (ee: SQLException) { + throw IllegalStateException(ee) + } + + throw IllegalStateException(e) + } + + private fun bulkInsertInternal(uri: Uri, valuesArray: Array): Int { + val tableId = DataStoreUtils.getTableId(uri) + val table = DataStoreUtils.getTableNameById(tableId) + var result = 0 + val newIds = LongArray(valuesArray.size) + if (table != null && valuesArray.isNotEmpty()) { + databaseWrapper.beginTransaction() + if (tableId == TABLE_ID_CACHED_USERS) { + for (values in valuesArray) { + val where = Expression.equalsArgs(CachedUsers.USER_KEY) + databaseWrapper.update(table, values, where.sql, arrayOf(values.getAsString(CachedUsers.USER_KEY))) + newIds[result++] = databaseWrapper.insertWithOnConflict(table, null, + values, SQLiteDatabase.CONFLICT_REPLACE) + } + } else if (tableId == TABLE_ID_SEARCH_HISTORY) { + for (values in valuesArray) { + values.put(SearchHistory.RECENT_QUERY, System.currentTimeMillis()) + val where = Expression.equalsArgs(SearchHistory.QUERY) + val args = arrayOf(values.getAsString(SearchHistory.QUERY)) + databaseWrapper.update(table, values, where.sql, args) + newIds[result++] = databaseWrapper.insertWithOnConflict(table, null, + values, SQLiteDatabase.CONFLICT_IGNORE) + } + } else { + val conflictAlgorithm = getConflictAlgorithm(tableId) + if (conflictAlgorithm != SQLiteDatabase.CONFLICT_NONE) { + for (values in valuesArray) { + newIds[result++] = databaseWrapper.insertWithOnConflict(table, null, + values, conflictAlgorithm) + } + } else { + for (values in valuesArray) { + newIds[result++] = databaseWrapper.insert(table, null, values) + } + } + } + databaseWrapper.setTransactionSuccessful() + databaseWrapper.endTransaction() + } + if (result > 0) { + onDatabaseUpdated(tableId, uri) + } + onNewItemsInserted(uri, tableId, valuesArray.toNulls()) + return result + } + + override fun delete(uri: Uri, selection: String?, selectionArgs: Array?): Int { + try { + return deleteInternal(uri, selection, selectionArgs) + } catch (e: SQLException) { + if (handleSQLException(e)) { + try { + return deleteInternal(uri, selection, selectionArgs) + } catch (e1: SQLException) { + throw IllegalStateException(e1) + } + + } + throw IllegalStateException(e) + } + + } + + private fun deleteInternal(uri: Uri, selection: String?, selectionArgs: Array?): Int { + val tableId = DataStoreUtils.getTableId(uri) + val table = DataStoreUtils.getTableNameById(tableId) ?: return 0 + val result = databaseWrapper.delete(table, selection, selectionArgs) + if (result > 0) { + onDatabaseUpdated(tableId, uri) + } + return result + } + + override fun getType(uri: Uri): String? { + return null + } + + override fun insert(uri: Uri, values: ContentValues?): Uri? { + try { + return insertInternal(uri, values) + } catch (e: SQLException) { + if (handleSQLException(e)) { + try { + return insertInternal(uri, values) + } catch (e1: SQLException) { + throw IllegalStateException(e1) + } + + } + throw IllegalStateException(e) + } + + } + + private fun insertInternal(uri: Uri, values: ContentValues?): Uri? { + val tableId = DataStoreUtils.getTableId(uri) + val table = DataStoreUtils.getTableNameById(tableId) + var rowId: Long = -1 + when (tableId) { + TABLE_ID_CACHED_USERS -> { + if (values != null) { + val where = Expression.equalsArgs(CachedUsers.USER_KEY) + val whereArgs = arrayOf(values.getAsString(CachedUsers.USER_KEY)) + databaseWrapper.update(table, values, where.sql, whereArgs) + } + rowId = databaseWrapper.insertWithOnConflict(table, null, values, + SQLiteDatabase.CONFLICT_IGNORE) + } + TABLE_ID_SEARCH_HISTORY -> { + if (values != null) { + values.put(SearchHistory.RECENT_QUERY, System.currentTimeMillis()) + val where = Expression.equalsArgs(SearchHistory.QUERY) + val args = arrayOf(values.getAsString(SearchHistory.QUERY)) + databaseWrapper.update(table, values, where.sql, args) + } + rowId = databaseWrapper.insertWithOnConflict(table, null, values, + SQLiteDatabase.CONFLICT_IGNORE) + } + TABLE_ID_CACHED_RELATIONSHIPS -> { + var updated = false + if (values != null) { + val accountKey = values.getAsString(CachedRelationships.ACCOUNT_KEY) + val userId = values.getAsString(CachedRelationships.USER_KEY) + val where = Expression.and(Expression.equalsArgs(CachedRelationships.ACCOUNT_KEY), + Expression.equalsArgs(CachedRelationships.USER_KEY)) + if (databaseWrapper.update(table, values, where.sql, arrayOf(accountKey, userId)) > 0) { + val projection = arrayOf(CachedRelationships._ID) + val c = databaseWrapper.query(table, projection, where.sql, null, + null, null, null) + if (c.moveToFirst()) { + rowId = c.getLong(0) + } + c.close() + updated = true + } + } + if (!updated) { + rowId = databaseWrapper.insertWithOnConflict(table, null, values, + SQLiteDatabase.CONFLICT_IGNORE) + } + } + VIRTUAL_TABLE_ID_DRAFTS_NOTIFICATIONS -> { + rowId = showDraftNotification(values) + } + else -> { + val conflictAlgorithm = getConflictAlgorithm(tableId) + if (conflictAlgorithm != SQLiteDatabase.CONFLICT_NONE) { + rowId = databaseWrapper.insertWithOnConflict(table, null, values, + conflictAlgorithm) + } else if (table != null) { + rowId = databaseWrapper.insert(table, null, values) + } else { + return null + } + } + } + onDatabaseUpdated(tableId, uri) + onNewItemsInserted(uri, tableId, arrayOf(values)) + return Uri.withAppendedPath(uri, rowId.toString()) + } + + private fun showDraftNotification(values: ContentValues?): Long { + val context = context + if (values == null || context == null) return -1 + val draftId = values.getAsLong(BaseColumns._ID) ?: return -1 + val where = Expression.equals(Drafts._ID, draftId) + val c = context.contentResolver.query(Drafts.CONTENT_URI, Drafts.COLUMNS, where.sql, null, null) ?: return -1 + val i = DraftCursorIndices(c) + val item: Draft + try { + if (!c.moveToFirst()) return -1 + item = i.newObject(c) + } catch (e: IOException) { + return -1 + } finally { + c.close() + } + val title = context.getString(R.string.status_not_updated) + val message = context.getString(R.string.status_not_updated_summary) + val intent = Intent() + intent.`package` = BuildConfig.APPLICATION_ID + val uriBuilder = Uri.Builder() + uriBuilder.scheme(SCHEME_TWIDERE) + uriBuilder.authority(AUTHORITY_DRAFTS) + intent.data = uriBuilder.build() + val nb = NotificationCompat.Builder(context) + nb.setTicker(message) + nb.setContentTitle(title) + nb.setContentText(item.text) + nb.setAutoCancel(true) + nb.setWhen(System.currentTimeMillis()) + nb.setSmallIcon(R.drawable.ic_stat_draft) + val discardIntent = Intent(context, LengthyOperationsService::class.java) + discardIntent.action = INTENT_ACTION_DISCARD_DRAFT + val draftUri = Uri.withAppendedPath(Drafts.CONTENT_URI, draftId.toString()) + discardIntent.data = draftUri + nb.addAction(R.drawable.ic_action_delete, context.getString(R.string.discard), PendingIntent.getService(context, 0, + discardIntent, PendingIntent.FLAG_ONE_SHOT)) + + val sendIntent = Intent(context, LengthyOperationsService::class.java) + sendIntent.action = INTENT_ACTION_SEND_DRAFT + sendIntent.data = draftUri + nb.addAction(R.drawable.ic_action_send, context.getString(R.string.action_send), + PendingIntent.getService(context, 0, sendIntent, PendingIntent.FLAG_ONE_SHOT)) + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS) + nb.setContentIntent(PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_ONE_SHOT)) + notificationManager.notify(draftUri.toString(), NOTIFICATION_ID_DRAFTS, + nb.build()) + return draftId + } + + override fun onCreate(): Boolean { + val context = context!! + GeneralComponentHelper.build(context).inject(this) + handler = Handler(Looper.getMainLooper()) + databaseWrapper = SQLiteDatabaseWrapper(this) + backgroundExecutor = Executors.newSingleThreadExecutor() + // final GetWritableDatabaseTask task = new + // GetWritableDatabaseTask(context, helper, mDatabaseWrapper); + // task.executeTask(); + return true + } + + override fun onCreateSQLiteDatabase(): SQLiteDatabase { + val app = TwidereApplication.getInstance(context!!) + val helper = app.sqLiteOpenHelper + return helper.writableDatabase + } + + + override fun query(uri: Uri, projection: Array?, selection: String?, selectionArgs: Array?, + sortOrder: String?): Cursor? { + try { + val tableId = DataStoreUtils.getTableId(uri) + val table = DataStoreUtils.getTableNameById(tableId) + when (tableId) { + VIRTUAL_TABLE_ID_DATABASE_PREPARE -> { + databaseWrapper.prepare() + return MatrixCursor(projection ?: arrayOfNulls(0)) + } + VIRTUAL_TABLE_ID_PERMISSIONS -> { + val context = context ?: return null + val c = MatrixCursor(Permissions.MATRIX_COLUMNS) + val pm = context.packageManager + if (Binder.getCallingUid() == Process.myUid()) { + val map = permissionsManager.all + for ((key, value) in map) { + c.addRow(arrayOf(key, value)) + } + } else { + val map = permissionsManager.all + val callingPackages = pm.getPackagesForUid(Binder.getCallingUid()) + for ((key, value) in map) { + if (ArrayUtils.contains(callingPackages, key)) { + c.addRow(arrayOf(key, value)) + } + } + } + return c + } + VIRTUAL_TABLE_ID_CACHED_USERS_WITH_RELATIONSHIP -> { + val accountKey = UserKey.valueOf(uri.lastPathSegment) + val query = CachedUsersQueryBuilder.withRelationship(projection, + selection, selectionArgs, sortOrder, accountKey) + val c = databaseWrapper.rawQuery(query.first.sql, query.second) + contentNotificationManager.setNotificationUri(c, CachedUsers.CONTENT_URI) + return c + } + VIRTUAL_TABLE_ID_CACHED_USERS_WITH_SCORE -> { + val accountKey = UserKey.valueOf(uri.lastPathSegment) + val query = CachedUsersQueryBuilder.withScore(projection, + selection, selectionArgs, sortOrder, accountKey, 0) + val c = databaseWrapper.rawQuery(query.first.sql, query.second) + contentNotificationManager.setNotificationUri(c, CachedUsers.CONTENT_URI) + return c + } + VIRTUAL_TABLE_ID_DRAFTS_UNSENT -> { + val twitter = twitterWrapper + val sendingIds = RawItemArray(twitter.getSendingDraftIds()) + val where: Expression + if (selection != null) { + where = Expression.and(Expression(selection), + Expression.notIn(Column(Drafts._ID), sendingIds)) + } else { + where = Expression.and(Expression.notIn(Column(Drafts._ID), sendingIds)) + } + val c = databaseWrapper.query(Drafts.TABLE_NAME, projection, + where.sql, selectionArgs, null, null, sortOrder) + contentNotificationManager.setNotificationUri(c, Utils.getNotificationUri(tableId, uri)) + return c + } + VIRTUAL_TABLE_ID_SUGGESTIONS_AUTO_COMPLETE -> { + return SuggestionsCursorCreator.forAutoComplete(databaseWrapper, + userColorNameManager, uri, projection) + } + VIRTUAL_TABLE_ID_SUGGESTIONS_SEARCH -> { + return SuggestionsCursorCreator.forSearch(databaseWrapper, + userColorNameManager, uri, projection) + } + VIRTUAL_TABLE_ID_NULL -> { + return null + } + VIRTUAL_TABLE_ID_EMPTY -> { + return MatrixCursor(projection ?: arrayOfNulls(0)) + } + VIRTUAL_TABLE_ID_RAW_QUERY -> { + if (projection != null || selection != null || sortOrder != null) { + throw IllegalArgumentException() + } + return databaseWrapper.rawQuery(uri.lastPathSegment, selectionArgs) + } + } + if (table == null) return null + val c = databaseWrapper.query(table, projection, selection, selectionArgs, + null, null, sortOrder) + contentNotificationManager.setNotificationUri(c, Utils.getNotificationUri(tableId, uri)) + return c + } catch (e: SQLException) { + throw IllegalStateException(e) + } + + } + + + override fun update(uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array?): Int { + try { + return updateInternal(uri, values, selection, selectionArgs) + } catch (e: SQLException) { + if (handleSQLException(e)) { + try { + return updateInternal(uri, values, selection, selectionArgs) + } catch (e1: SQLException) { + throw IllegalStateException(e1) + } + + } + throw IllegalStateException(e) + } + + } + + private fun updateInternal(uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array?): Int { + val tableId = DataStoreUtils.getTableId(uri) + val table = DataStoreUtils.getTableNameById(tableId) + var result = 0 + if (table != null) { + result = databaseWrapper.update(table, values, selection, selectionArgs) + } + if (result > 0) { + onDatabaseUpdated(tableId, uri) + } + return result + } + + private fun clearNotification() { + notificationManager.cancelAll() + } + + private fun clearNotification(notificationType: Int, accountId: UserKey) { + notificationManager.cancelById(Utils.getNotificationId(notificationType, accountId)) + } + + + private fun notifyContentObserver(uri: Uri) { + if (!uri.getBooleanQueryParameter(QUERY_PARAM_NOTIFY, true)) return + handler!!.post(Runnable { + val cr = context.contentResolver ?: return@Runnable + cr.notifyChange(uri, null) + }) + } + + private fun notifyUnreadCountChanged(position: Int) { + handler!!.post { bus.post(UnreadCountUpdatedEvent(position)) } + } + + private fun onDatabaseUpdated(tableId: Int, uri: Uri?) { + if (uri == null) return + notifyContentObserver(Utils.getNotificationUri(tableId, uri)) + } + + private fun onNewItemsInserted(uri: Uri, tableId: Int, valuesArray: Array?) { + val context = context ?: return + if (valuesArray.isNullOrEmpty()) return + when (tableId) { + TABLE_ID_STATUSES -> { + backgroundExecutor!!.execute { + val prefs = AccountPreferences.getNotificationEnabledPreferences(context, + DataStoreUtils.getAccountKeys(context)) + prefs.filter(AccountPreferences::isHomeTimelineNotificationEnabled).forEach { + val positionTag = getPositionTag(CustomTabType.HOME_TIMELINE, it.accountKey) + contentNotificationManager.showTimelineNotification(it, positionTag) + } + notifyUnreadCountChanged(NOTIFICATION_ID_HOME_TIMELINE) + } + } + TABLE_ID_ACTIVITIES_ABOUT_ME -> { + backgroundExecutor!!.execute { + val prefs = AccountPreferences.getNotificationEnabledPreferences(context, + DataStoreUtils.getAccountKeys(context)) + prefs.filter(AccountPreferences::isInteractionsNotificationEnabled).forEach { + contentNotificationManager.showInteractionsNotification(it, getPositionTag(ReadPositionTag.ACTIVITIES_ABOUT_ME, + it.accountKey)) + } + notifyUnreadCountChanged(NOTIFICATION_ID_INTERACTIONS_TIMELINE) + } + } + TABLE_ID_MESSAGES -> { + val prefs = AccountPreferences.getNotificationEnabledPreferences(context, + DataStoreUtils.getAccountKeys(context)) + prefs.filter(AccountPreferences::isDirectMessagesNotificationEnabled).forEach { + val pairs = readStateManager.getPositionPairs(CustomTabType.DIRECT_MESSAGES) + // TODO show messages notifications + } + notifyUnreadCountChanged(NOTIFICATION_ID_DIRECT_MESSAGES) + } + TABLE_ID_DRAFTS -> { + } + } + } + + private fun getPositionTag(tag: String, accountKey: UserKey): Long { + val position = readStateManager.getPosition(Utils.getReadPositionTagWithAccount(tag, + accountKey)) + if (position != -1L) return position + return readStateManager.getPosition(tag) + } + + companion object { + + private fun getConflictAlgorithm(tableId: Int): Int { + when (tableId) { + TABLE_ID_CACHED_HASHTAGS, TABLE_ID_CACHED_STATUSES, TABLE_ID_CACHED_USERS, TABLE_ID_CACHED_RELATIONSHIPS, TABLE_ID_SEARCH_HISTORY, TABLE_ID_MESSAGES, TABLE_ID_MESSAGES_CONVERSATIONS -> return SQLiteDatabase.CONFLICT_REPLACE + TABLE_ID_FILTERED_USERS, TABLE_ID_FILTERED_KEYWORDS, TABLE_ID_FILTERED_SOURCES, TABLE_ID_FILTERED_LINKS -> return SQLiteDatabase.CONFLICT_IGNORE + } + return SQLiteDatabase.CONFLICT_NONE + } + } + + +} diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/service/StreamingService.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/service/StreamingService.kt index 3f3db4bb8..d56b385be 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/service/StreamingService.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/service/StreamingService.kt @@ -30,7 +30,6 @@ import org.mariotaku.twidere.model.UserKey import org.mariotaku.twidere.model.account.cred.OAuthCredentials import org.mariotaku.twidere.model.util.AccountUtils import org.mariotaku.twidere.provider.TwidereDataStore.* -import org.mariotaku.twidere.util.ContentValuesCreator import org.mariotaku.twidere.util.DataStoreUtils import org.mariotaku.twidere.util.DebugLog import org.mariotaku.twidere.util.TwidereArrayUtils @@ -164,11 +163,9 @@ class StreamingService : Service() { } override fun onDirectMessageDeleted(event: DeletionEvent) { - val where = Expression.equalsArgs(DirectMessages.MESSAGE_ID).sql + val where = Expression.equalsArgs(Messages.MESSAGE_ID).sql val whereArgs = arrayOf(event.id) - for (uri in MESSAGES_URIS) { - context.contentResolver.delete(uri, where, whereArgs) - } + context.contentResolver.delete(Messages.CONTENT_URI, where, whereArgs) } override fun onStatusDeleted(event: DeletionEvent) { @@ -183,15 +180,6 @@ class StreamingService : Service() { override fun onDirectMessage(directMessage: DirectMessage) { if (directMessage.id == null) return - val resolver = context.contentResolver - val where = Expression.and(Expression.equalsArgs(DirectMessages.ACCOUNT_KEY), - Expression.equalsArgs(DirectMessages.MESSAGE_ID)).sql - val whereArgs = arrayOf(account.key.toString(), directMessage.id) - for (uri in MESSAGES_URIS) { - resolver.delete(uri, where, whereArgs) - } - - } override fun onException(ex: Throwable) { @@ -261,24 +249,7 @@ class StreamingService : Service() { @Throws(IOException::class) override fun onStatus(status: Status) { - val resolver = context.contentResolver - val values = ContentValuesCreator.createStatus(status, account.key) - if (!statusStreamStarted) { - statusStreamStarted = true - values.put(Statuses.IS_GAP, true) - } - val where = Expression.and(Expression.equalsArgs(AccountSupportColumns.ACCOUNT_KEY), - Expression.equalsArgs(Statuses.STATUS_ID)).sql - val whereArgs = arrayOf(account.key.toString(), status.id.toString()) - resolver.delete(Statuses.CONTENT_URI, where, whereArgs) - resolver.delete(Mentions.CONTENT_URI, where, whereArgs) - resolver.insert(Statuses.CONTENT_URI, values) - val rt = status.retweetedStatus - if (rt != null && rt.extendedText.contains("@" + account.user.screen_name) || - rt == null && status.extendedText.contains("@" + account.user.screen_name)) { - resolver.insert(Mentions.CONTENT_URI, values) - } } override fun onTrackLimitationNotice(numberOfLimitedStatuses: Int) { @@ -334,7 +305,6 @@ class StreamingService : Service() { private val NOTIFICATION_SERVICE_STARTED = 1 - private val MESSAGES_URIS = arrayOf(DirectMessages.Inbox.CONTENT_URI, DirectMessages.Outbox.CONTENT_URI) } } diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/task/UpdateAccountInfoTask.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/task/UpdateAccountInfoTask.kt index 290aa1998..8021453b0 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/task/UpdateAccountInfoTask.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/task/UpdateAccountInfoTask.kt @@ -47,8 +47,8 @@ class UpdateAccountInfoTask( resolver.update(Statuses.CONTENT_URI, accountKeyValues, accountKeyWhere, accountKeyWhereArgs) resolver.update(Activities.AboutMe.CONTENT_URI, accountKeyValues, accountKeyWhere, accountKeyWhereArgs) - resolver.update(DirectMessages.Inbox.CONTENT_URI, accountKeyValues, accountKeyWhere, accountKeyWhereArgs) - resolver.update(DirectMessages.Outbox.CONTENT_URI, accountKeyValues, accountKeyWhere, accountKeyWhereArgs) + resolver.update(Messages.CONTENT_URI, accountKeyValues, accountKeyWhere, accountKeyWhereArgs) + resolver.update(Messages.Conversations.CONTENT_URI, accountKeyValues, accountKeyWhere, accountKeyWhereArgs) resolver.update(CachedRelationships.CONTENT_URI, accountKeyValues, accountKeyWhere, accountKeyWhereArgs) updateTabs(resolver, user.key) diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/task/twitter/message/temp2.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/task/twitter/message/temp2.kt index 4ca8f41e8..0ee859747 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/task/twitter/message/temp2.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/task/twitter/message/temp2.kt @@ -40,6 +40,7 @@ import org.mariotaku.twidere.model.util.ParcelableUserUtils import org.mariotaku.twidere.model.util.UserKeyUtils import org.mariotaku.twidere.provider.TwidereDataStore.Messages import org.mariotaku.twidere.provider.TwidereDataStore.Messages.Conversations +import org.mariotaku.twidere.task.BaseAbstractTask import org.mariotaku.twidere.util.content.ContentResolverUtils import java.util.* @@ -48,9 +49,9 @@ import java.util.* */ class GetMessagesTask( - context: android.content.Context -) : org.mariotaku.twidere.task.BaseAbstractTask Unit>(context) { - override fun doLongOperation(param: org.mariotaku.twidere.task.twitter.message.GetMessagesTask.RefreshMessagesTaskParam) { + context: Context +) : BaseAbstractTask Unit>(context) { + override fun doLongOperation(param: RefreshMessagesTaskParam) { val accountKeys = param.accountKeys val am = android.accounts.AccountManager.get(context) accountKeys.forEachIndexed { i, accountKey -> @@ -61,16 +62,16 @@ class GetMessagesTask( } catch (e: org.mariotaku.microblog.library.MicroBlogException) { return@forEachIndexed } - org.mariotaku.twidere.task.twitter.message.GetMessagesTask.Companion.storeMessages(context, messages, details) + Companion.storeMessages(context, messages, details) } } override fun afterExecute(callback: ((Boolean) -> Unit)?, result: Unit) { callback?.invoke(true) - bus.post(org.mariotaku.twidere.model.event.GetMessagesTaskEvent(org.mariotaku.twidere.provider.TwidereDataStore.Messages.CONTENT_URI, params?.taskTag, false, null)) + bus.post(org.mariotaku.twidere.model.event.GetMessagesTaskEvent(Messages.CONTENT_URI, params?.taskTag, false, null)) } - private fun getMessages(microBlog: org.mariotaku.microblog.library.MicroBlog, details: org.mariotaku.twidere.model.AccountDetails, param: org.mariotaku.twidere.task.twitter.message.GetMessagesTask.RefreshMessagesTaskParam, index: Int): org.mariotaku.twidere.task.twitter.message.GetMessagesTask.DatabaseUpdateData { + private fun getMessages(microBlog: org.mariotaku.microblog.library.MicroBlog, details: AccountDetails, param: RefreshMessagesTaskParam, index: Int): DatabaseUpdateData { when (details.type) { org.mariotaku.twidere.annotation.AccountType.FANFOU -> { // Use fanfou DM api, disabled since it's conversation api is not suitable for paging @@ -87,8 +88,8 @@ class GetMessagesTask( return getDefaultMessages(microBlog, details, param, index) } - private fun getTwitterOfficialMessages(microBlog: org.mariotaku.microblog.library.MicroBlog, details: org.mariotaku.twidere.model.AccountDetails, - param: org.mariotaku.twidere.task.twitter.message.GetMessagesTask.RefreshMessagesTaskParam, index: Int): org.mariotaku.twidere.task.twitter.message.GetMessagesTask.DatabaseUpdateData { + private fun getTwitterOfficialMessages(microBlog: org.mariotaku.microblog.library.MicroBlog, details: AccountDetails, + param: RefreshMessagesTaskParam, index: Int): DatabaseUpdateData { val conversationId = param.conversationId if (conversationId == null) { return getTwitterOfficialUserInbox(microBlog, details, param, index) @@ -97,16 +98,16 @@ class GetMessagesTask( } } - private fun getFanfouMessages(microBlog: org.mariotaku.microblog.library.MicroBlog, details: org.mariotaku.twidere.model.AccountDetails, param: org.mariotaku.twidere.task.twitter.message.GetMessagesTask.RefreshMessagesTaskParam, index: Int): org.mariotaku.twidere.task.twitter.message.GetMessagesTask.DatabaseUpdateData { + private fun getFanfouMessages(microBlog: org.mariotaku.microblog.library.MicroBlog, details: AccountDetails, param: RefreshMessagesTaskParam, index: Int): DatabaseUpdateData { val conversationId = param.conversationId if (conversationId == null) { return getFanfouConversations(microBlog, details, param, index) } else { - return org.mariotaku.twidere.task.twitter.message.GetMessagesTask.DatabaseUpdateData(emptyList(), emptyList()) + return DatabaseUpdateData(emptyList(), emptyList()) } } - private fun getDefaultMessages(microBlog: org.mariotaku.microblog.library.MicroBlog, details: org.mariotaku.twidere.model.AccountDetails, param: org.mariotaku.twidere.task.twitter.message.GetMessagesTask.RefreshMessagesTaskParam, index: Int): org.mariotaku.twidere.task.twitter.message.GetMessagesTask.DatabaseUpdateData { + private fun getDefaultMessages(microBlog: org.mariotaku.microblog.library.MicroBlog, details: AccountDetails, param: RefreshMessagesTaskParam, index: Int): DatabaseUpdateData { val accountKey = details.key val sinceIds = if (param.hasSinceIds) param.sinceIds else null @@ -137,8 +138,8 @@ class GetMessagesTask( }) - val insertMessages = arrayListOf() - val conversations = hashMapOf() + val insertMessages = arrayListOf() + val conversations = hashMapOf() val conversationIds = hashSetOf() received.forEach { @@ -162,23 +163,23 @@ class GetMessagesTask( insertMessages.add(message) conversations.addConversation(message.conversation_id, details, message, setOf(dm.sender, dm.recipient)) } - return org.mariotaku.twidere.task.twitter.message.GetMessagesTask.DatabaseUpdateData(conversations.values, insertMessages) + return DatabaseUpdateData(conversations.values, insertMessages) } - private fun getTwitterOfficialConversation(microBlog: org.mariotaku.microblog.library.MicroBlog, details: org.mariotaku.twidere.model.AccountDetails, - conversationId: String, param: org.mariotaku.twidere.task.twitter.message.GetMessagesTask.RefreshMessagesTaskParam, index: Int): org.mariotaku.twidere.task.twitter.message.GetMessagesTask.DatabaseUpdateData { - val maxId = param.maxIds?.get(index) ?: return org.mariotaku.twidere.task.twitter.message.GetMessagesTask.DatabaseUpdateData(emptyList(), emptyList()) + private fun getTwitterOfficialConversation(microBlog: org.mariotaku.microblog.library.MicroBlog, details: AccountDetails, + conversationId: String, param: RefreshMessagesTaskParam, index: Int): DatabaseUpdateData { + val maxId = param.maxIds?.get(index) ?: return DatabaseUpdateData(emptyList(), emptyList()) val paging = org.mariotaku.microblog.library.twitter.model.Paging().apply { maxId(maxId) } val response = microBlog.getDmConversation(conversationId, paging).conversationTimeline response.fixMedia(microBlog) - return org.mariotaku.twidere.task.twitter.message.GetMessagesTask.Companion.createDatabaseUpdateData(context, details, response) + return Companion.createDatabaseUpdateData(context, details, response) } - private fun getTwitterOfficialUserInbox(microBlog: org.mariotaku.microblog.library.MicroBlog, details: org.mariotaku.twidere.model.AccountDetails, - param: org.mariotaku.twidere.task.twitter.message.GetMessagesTask.RefreshMessagesTaskParam, index: Int): org.mariotaku.twidere.task.twitter.message.GetMessagesTask.DatabaseUpdateData { + private fun getTwitterOfficialUserInbox(microBlog: org.mariotaku.microblog.library.MicroBlog, details: AccountDetails, + param: RefreshMessagesTaskParam, index: Int): DatabaseUpdateData { val maxId = if (param.hasMaxIds) param.maxIds?.get(index) else null val cursor = if (param.hasCursors) param.cursors?.get(index) else null val response = if (cursor != null) { @@ -191,11 +192,11 @@ class GetMessagesTask( }).userInbox } response.fixMedia(microBlog) - return org.mariotaku.twidere.task.twitter.message.GetMessagesTask.Companion.createDatabaseUpdateData(context, details, response) + return Companion.createDatabaseUpdateData(context, details, response) } - private fun getFanfouConversations(microBlog: org.mariotaku.microblog.library.MicroBlog, details: org.mariotaku.twidere.model.AccountDetails, param: org.mariotaku.twidere.task.twitter.message.GetMessagesTask.RefreshMessagesTaskParam, index: Int): org.mariotaku.twidere.task.twitter.message.GetMessagesTask.DatabaseUpdateData { + private fun getFanfouConversations(microBlog: org.mariotaku.microblog.library.MicroBlog, details: AccountDetails, param: RefreshMessagesTaskParam, index: Int): DatabaseUpdateData { val accountKey = details.key val cursor = param.cursors?.get(index) val page = cursor?.substringAfter("page:").toInt(-1) @@ -205,7 +206,7 @@ class GetMessagesTask( page(page) } }) - val conversations = hashMapOf() + val conversations = hashMapOf() val conversationIds = hashSetOf() result.mapTo(conversationIds) { "${accountKey.id}-${it.otherId}" } @@ -219,26 +220,26 @@ class GetMessagesTask( setOf(dm.sender, dm.recipient)) mc.request_cursor = "page:$page" } - return org.mariotaku.twidere.task.twitter.message.GetMessagesTask.DatabaseUpdateData(conversations.values, emptyList()) + return DatabaseUpdateData(conversations.values, emptyList()) } data class DatabaseUpdateData( - val conversations: Collection, - val messages: Collection, + val conversations: Collection, + val messages: Collection, val deleteConversations: List = emptyList(), val deleteMessages: Map> = emptyMap(), val conversationRequestCursor: String? = null ) abstract class RefreshNewTaskParam( - context: android.content.Context - ) : org.mariotaku.twidere.task.twitter.message.GetMessagesTask.RefreshMessagesTaskParam(context) { + context: Context + ) : RefreshMessagesTaskParam(context) { override val sinceIds: Array? get() { - val incomingIds = org.mariotaku.twidere.util.DataStoreUtils.getNewestMessageIds(context, org.mariotaku.twidere.provider.TwidereDataStore.Messages.CONTENT_URI, + val incomingIds = org.mariotaku.twidere.util.DataStoreUtils.getNewestMessageIds(context, Messages.CONTENT_URI, defaultKeys, false) - val outgoingIds = org.mariotaku.twidere.util.DataStoreUtils.getNewestMessageIds(context, org.mariotaku.twidere.provider.TwidereDataStore.Messages.CONTENT_URI, + val outgoingIds = org.mariotaku.twidere.util.DataStoreUtils.getNewestMessageIds(context, Messages.CONTENT_URI, defaultKeys, true) return incomingIds + outgoingIds } @@ -247,7 +248,7 @@ class GetMessagesTask( get() { val cursors = arrayOfNulls(defaultKeys.size) val newestConversations = org.mariotaku.twidere.util.DataStoreUtils.getNewestConversations(context, - org.mariotaku.twidere.provider.TwidereDataStore.Messages.Conversations.CONTENT_URI, twitterOfficialKeys) + Conversations.CONTENT_URI, twitterOfficialKeys) newestConversations.forEachIndexed { i, conversation -> cursors[i] = conversation?.request_cursor } @@ -260,18 +261,18 @@ class GetMessagesTask( } abstract class LoadMoreEntriesTaskParam( - context: android.content.Context - ) : org.mariotaku.twidere.task.twitter.message.GetMessagesTask.RefreshMessagesTaskParam(context) { + context: Context + ) : RefreshMessagesTaskParam(context) { override val maxIds: Array? by lazy { - val incomingIds = org.mariotaku.twidere.util.DataStoreUtils.getOldestMessageIds(context, org.mariotaku.twidere.provider.TwidereDataStore.Messages.CONTENT_URI, + val incomingIds = org.mariotaku.twidere.util.DataStoreUtils.getOldestMessageIds(context, Messages.CONTENT_URI, defaultKeys, false) - val outgoingIds = org.mariotaku.twidere.util.DataStoreUtils.getOldestMessageIds(context, org.mariotaku.twidere.provider.TwidereDataStore.Messages.CONTENT_URI, + val outgoingIds = org.mariotaku.twidere.util.DataStoreUtils.getOldestMessageIds(context, Messages.CONTENT_URI, defaultKeys, true) val oldestConversations = org.mariotaku.twidere.util.DataStoreUtils.getOldestConversations(context, - org.mariotaku.twidere.provider.TwidereDataStore.Messages.Conversations.CONTENT_URI, twitterOfficialKeys) + Conversations.CONTENT_URI, twitterOfficialKeys) oldestConversations.forEachIndexed { i, conversation -> - val extras = conversation?.conversation_extras as? org.mariotaku.twidere.model.message.conversation.TwitterOfficialConversationExtras ?: return@forEachIndexed + val extras = conversation?.conversation_extras as? TwitterOfficialConversationExtras ?: return@forEachIndexed incomingIds[i] = extras.maxEntryId } return@lazy incomingIds + outgoingIds @@ -282,19 +283,19 @@ class GetMessagesTask( } class LoadMoreMessageTaskParam( - context: android.content.Context, - accountKey: org.mariotaku.twidere.model.UserKey, + context: Context, + accountKey: UserKey, override val conversationId: String, maxId: String - ) : org.mariotaku.twidere.task.twitter.message.GetMessagesTask.RefreshMessagesTaskParam(context) { - override val accountKeys: Array = arrayOf(accountKey) + ) : RefreshMessagesTaskParam(context) { + override val accountKeys: Array = arrayOf(accountKey) override val maxIds: Array? = arrayOf(maxId) override val hasMaxIds: Boolean = true } abstract class RefreshMessagesTaskParam( - val context: android.content.Context - ) : org.mariotaku.twidere.model.SimpleRefreshTaskParam() { + val context: Context + ) : SimpleRefreshTaskParam() { /** * If `conversationId` has value, load messages in conversationId @@ -303,11 +304,11 @@ class GetMessagesTask( var taskTag: String? = null - protected val accounts: Array by lazy { + protected val accounts: Array by lazy { org.mariotaku.twidere.model.util.AccountUtils.getAllAccountDetails(android.accounts.AccountManager.get(context), accountKeys, false) } - protected val defaultKeys: Arrayby lazy { + protected val defaultKeys: Arrayby lazy { return@lazy accounts.map { account -> account ?: return@map null if (account.isOfficial(context) || account.type == org.mariotaku.twidere.annotation.AccountType.FANFOU) { @@ -317,7 +318,7 @@ class GetMessagesTask( }.toTypedArray() } - protected val twitterOfficialKeys: Array by lazy { + protected val twitterOfficialKeys: Array by lazy { return@lazy accounts.map { account -> account ?: return@map null if (!account.isOfficial(context)) { @@ -331,22 +332,22 @@ class GetMessagesTask( companion object { - fun createDatabaseUpdateData(context: android.content.Context, account: org.mariotaku.twidere.model.AccountDetails, response: org.mariotaku.microblog.library.twitter.model.DMResponse): - org.mariotaku.twidere.task.twitter.message.GetMessagesTask.DatabaseUpdateData { + fun createDatabaseUpdateData(context: Context, account: AccountDetails, response: DMResponse): + DatabaseUpdateData { val respConversations = response.conversations.orEmpty() val respEntries = response.entries.orEmpty() val respUsers = response.users.orEmpty() - val conversations = hashMapOf() + val conversations = hashMapOf() conversations.addLocalConversations(context, account.key, respConversations.keys) - val messages = java.util.ArrayList() - val messageDeletionsMap = java.util.HashMap>() - val conversationDeletions = java.util.ArrayList() + val messages = ArrayList() + val messageDeletionsMap = HashMap>() + val conversationDeletions = ArrayList() respEntries.mapNotNullTo(messages) { entry -> when { entry.messageDelete != null -> { - val list = messageDeletionsMap.getOrPut(entry.messageDelete.conversationId) { java.util.ArrayList() } + val list = messageDeletionsMap.getOrPut(entry.messageDelete.conversationId) { ArrayList() } entry.messageDelete.messages?.forEach { list.add(it.messageId) } @@ -361,7 +362,7 @@ class GetMessagesTask( } } } - val messagesMap = messages.groupBy(org.mariotaku.twidere.model.ParcelableMessage::conversation_id) + val messagesMap = messages.groupBy(ParcelableMessage::conversation_id) for ((k, v) in respConversations) { val message = messagesMap[k]?.maxBy(ParcelableMessage::message_timestamp) ?: continue val participants = respUsers.filterKeys { userId -> diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/util/AsyncTwitterWrapper.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/util/AsyncTwitterWrapper.kt index dfaee02c4..9a6a7ce75 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/util/AsyncTwitterWrapper.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/util/AsyncTwitterWrapper.kt @@ -22,8 +22,6 @@ package org.mariotaku.twidere.util import android.content.ContentValues import android.content.Context import android.net.Uri -import android.os.AsyncTask -import android.support.v4.util.SimpleArrayMap import com.squareup.otto.Bus import com.squareup.otto.Subscribe import org.apache.commons.collections.primitives.ArrayIntList @@ -33,7 +31,6 @@ import org.mariotaku.abstask.library.TaskStarter import org.mariotaku.kpreferences.get import org.mariotaku.ktextension.toNulls import org.mariotaku.microblog.library.MicroBlogException -import org.mariotaku.microblog.library.twitter.http.HttpResponseCode import org.mariotaku.microblog.library.twitter.model.* import org.mariotaku.sqliteqb.library.Expression import org.mariotaku.twidere.R @@ -57,7 +54,8 @@ class AsyncTwitterWrapper( val context: Context, private val bus: Bus, private val preferences: SharedPreferencesWrapper, - private val asyncTaskManager: AsyncTaskManager + private val asyncTaskManager: AsyncTaskManager, + private val notificationManager: NotificationManagerWrapper ) { private val resolver = context.contentResolver @@ -123,13 +121,7 @@ class AsyncTwitterWrapper( } fun clearNotificationAsync(notificationId: Int, accountKey: UserKey?) { - val task = ClearNotificationTask(context, notificationId, accountKey) - AsyncTaskUtils.executeTask(task) - } - - fun clearUnreadCountAsync(position: Int) { - val task = ClearUnreadCountTask(position) - AsyncTaskUtils.executeTask(task) + notificationManager.cancelById(Utils.getNotificationId(notificationId, accountKey)) } fun createBlockAsync(accountKey: UserKey, userKey: UserKey, filterEverywhere: Boolean) { @@ -313,11 +305,6 @@ class AsyncTwitterWrapper( } } - fun removeUnreadCountsAsync(position: Int, counts: SimpleArrayMap>) { - val task = RemoveUnreadCountsTask(context, position, counts) - AsyncTaskUtils.executeTask(task) - } - fun reportMultiSpam(accountKey: UserKey, userIds: Array) { // TODO implementation } @@ -398,26 +385,6 @@ class AsyncTwitterWrapper( return updatingRelationshipIds.contains(ParcelableUser.calculateHashCode(accountId, userId)) } - internal inner class ClearNotificationTask( - private val context: Context, - private val notificationType: Int, - private val accountKey: UserKey? - ) : AsyncTask() { - - override fun doInBackground(vararg params: Any): Int { - return TwitterWrapper.clearNotification(context, notificationType, accountKey) - } - - } - - internal inner class ClearUnreadCountTask(private val position: Int) : AsyncTask() { - - override fun doInBackground(vararg params: Any): Int { - return TwitterWrapper.clearUnreadCount(context, position) - } - - } - internal inner class CreateMultiBlockTask( context: Context, private val accountKey: UserKey, @@ -611,49 +578,6 @@ class AsyncTwitterWrapper( } - internal inner class DestroyDirectMessageTask(private val mAccountKey: UserKey, private val mMessageId: String) : ManagedAsyncTask>(context) { - - private fun deleteMessages() { - val where = Expression.and(Expression.equalsArgs(DirectMessages.ACCOUNT_KEY), - Expression.equalsArgs(DirectMessages.MESSAGE_ID)).sql - val whereArgs = arrayOf(mAccountKey.toString(), mMessageId) - resolver.delete(DirectMessages.Inbox.CONTENT_URI, where, whereArgs) - resolver.delete(DirectMessages.Outbox.CONTENT_URI, where, whereArgs) - } - - private fun isMessageNotFound(e: Exception?): Boolean { - if (e !is MicroBlogException) return false - return e.errorCode == ErrorInfo.PAGE_NOT_FOUND || e.statusCode == HttpResponseCode.NOT_FOUND - } - - override fun doInBackground(vararg args: Any): SingleResponse { - val microBlog = MicroBlogAPIFactory.getInstance(context, mAccountKey) ?: return SingleResponse.getInstance() - try { - val message = microBlog.destroyDirectMessage(mMessageId) - deleteMessages() - return SingleResponse.getInstance(message) - } catch (e: MicroBlogException) { - if (isMessageNotFound(e)) { - deleteMessages() - } - return SingleResponse.getInstance(e) - } - - } - - - override fun onPostExecute(result: SingleResponse) { - super.onPostExecute(result) - if (result.hasData() || isMessageNotFound(result.exception)) { - Utils.showInfoMessage(context, R.string.message_direct_message_deleted, false) - } else { - Utils.showErrorMessage(context, R.string.action_deleting, result.exception, true) - } - } - - - } - internal inner class DestroySavedSearchTask( context: Context, private val accountKey: UserKey, diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/util/ContentNotificationManager.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/util/ContentNotificationManager.kt new file mode 100644 index 000000000..60420a469 --- /dev/null +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/util/ContentNotificationManager.kt @@ -0,0 +1,355 @@ +/* + * Twidere - Twitter client for Android + * + * Copyright (C) 2012-2017 Mariotaku Lee + * + * 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 . + */ + +package org.mariotaku.twidere.util + +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.content.SharedPreferences +import android.database.Cursor +import android.media.AudioManager +import android.net.Uri +import android.support.v4.app.NotificationCompat +import android.text.TextUtils +import org.apache.commons.lang3.ArrayUtils +import org.mariotaku.kpreferences.get +import org.mariotaku.microblog.library.twitter.model.Activity +import org.mariotaku.sqliteqb.library.* +import org.mariotaku.twidere.R +import org.mariotaku.twidere.TwidereConstants.* +import org.mariotaku.twidere.activity.HomeActivity +import org.mariotaku.twidere.annotation.CustomTabType +import org.mariotaku.twidere.annotation.NotificationType +import org.mariotaku.twidere.constant.IntentConstants +import org.mariotaku.twidere.constant.iWantMyStarsBackKey +import org.mariotaku.twidere.constant.nameFirstKey +import org.mariotaku.twidere.extension.rawQuery +import org.mariotaku.twidere.model.* +import org.mariotaku.twidere.model.util.ParcelableActivityUtils +import org.mariotaku.twidere.provider.TwidereDataStore +import org.mariotaku.twidere.provider.TwidereDataStore.Activities +import org.mariotaku.twidere.provider.TwidereDataStore.Statuses +import org.mariotaku.twidere.receiver.NotificationReceiver +import org.mariotaku.twidere.util.database.FilterQueryBuilder +import org.oshkimaadziig.george.androidutils.SpanFormatter +import java.io.IOException + +/** + * Created by mariotaku on 2017/2/16. + */ +class ContentNotificationManager( + val context: Context, + val activityTracker: ActivityTracker, + val userColorNameManager: UserColorNameManager, + val notificationManager: NotificationManagerWrapper, + val preferences: SharedPreferences +) { + + private var nameFirst: Boolean = false + private var useStarForLikes: Boolean = false + + fun showInteractionsNotification(pref: AccountPreferences, position: Long) { + val cr = context.contentResolver + val accountKey = pref.accountKey + val where = Expression.and( + Expression.equalsArgs(Activities.ACCOUNT_KEY), + Expression.greaterThanArgs(Activities.POSITION_KEY) + ).sql + val whereArgs = arrayOf(accountKey.toString(), position.toString()) + val c = cr.query(Activities.AboutMe.CONTENT_URI, Activities.COLUMNS, where, whereArgs, + OrderBy(Activities.TIMESTAMP, false).sql) ?: return + val builder = NotificationCompat.Builder(context) + val pebbleNotificationStringBuilder = StringBuilder() + try { + val count = c.count + if (count == 0) return + builder.setSmallIcon(R.drawable.ic_stat_notification) + builder.setCategory(NotificationCompat.CATEGORY_SOCIAL) + applyNotificationPreferences(builder, pref, pref.mentionsNotificationType) + + val resources = context.resources + val accountName = DataStoreUtils.getAccountDisplayName(context, accountKey, nameFirst) + builder.setContentText(accountName) + val style = NotificationCompat.InboxStyle() + builder.setStyle(style) + builder.setAutoCancel(true) + style.setSummaryText(accountName) + val ci = ParcelableActivityCursorIndices(c) + var messageLines = 0 + + var timestamp: Long = -1 + c.moveToPosition(-1) + while (c.moveToNext()) { + if (messageLines == 5) { + style.addLine(resources.getString(R.string.and_N_more, count - c.position)) + pebbleNotificationStringBuilder.append(resources.getString(R.string.and_N_more, count - c.position)) + break + } + val activity = ci.newObject(c) + if (pref.isNotificationMentionsOnly && !ArrayUtils.contains(Activity.Action.MENTION_ACTIONS, + activity.action)) { + continue + } + if (activity.status_id != null && FilterQueryBuilder.isFiltered(cr, + activity.status_user_key, activity.status_text_plain, + activity.status_quote_text_plain, activity.status_spans, + activity.status_quote_spans, activity.status_source, + activity.status_quote_source, activity.status_retweeted_by_user_key, + activity.status_quoted_user_key)) { + continue + } + val filteredUserIds = DataStoreUtils.getFilteredUserIds(context) + if (timestamp == -1L) { + timestamp = activity.timestamp + } + ParcelableActivityUtils.initAfterFilteredSourceIds(activity, filteredUserIds, + pref.isNotificationFollowingOnly) + val sources = ParcelableActivityUtils.getAfterFilteredSources(activity) + if (ArrayUtils.isEmpty(sources)) continue + val message = ActivityTitleSummaryMessage.get(context, + userColorNameManager, activity, sources, + 0, useStarForLikes, nameFirst) + if (message != null) { + val summary = message.summary + if (TextUtils.isEmpty(summary)) { + style.addLine(message.title) + pebbleNotificationStringBuilder.append(message.title) + pebbleNotificationStringBuilder.append("\n") + } else { + style.addLine(SpanFormatter.format(resources.getString(R.string.title_summary_line_format), + message.title, summary)) + pebbleNotificationStringBuilder.append(message.title) + pebbleNotificationStringBuilder.append(": ") + pebbleNotificationStringBuilder.append(message.summary) + pebbleNotificationStringBuilder.append("\n") + } + messageLines++ + } + } + if (messageLines == 0) return + val displayCount = messageLines + count - c.position + val title = resources.getQuantityString(R.plurals.N_new_interactions, + displayCount, displayCount) + builder.setContentTitle(title) + style.setBigContentTitle(title) + builder.setNumber(displayCount) + builder.setContentIntent(getContentIntent(context, CustomTabType.NOTIFICATIONS_TIMELINE, + NotificationType.INTERACTIONS, accountKey, timestamp)) + if (timestamp != -1L) { + builder.setDeleteIntent(getMarkReadDeleteIntent(context, + NotificationType.INTERACTIONS, accountKey, timestamp, false)) + } + } catch (e: IOException) { + return + } finally { + c.close() + } + val notificationId = Utils.getNotificationId(NOTIFICATION_ID_INTERACTIONS_TIMELINE, accountKey) + notificationManager.notify("interactions", notificationId, builder.build()) + + Utils.sendPebbleNotification(context, context.resources.getString(R.string.interactions), pebbleNotificationStringBuilder.toString()) + + } + + + private fun applyNotificationPreferences(builder: NotificationCompat.Builder, pref: AccountPreferences, defaultFlags: Int) { + var notificationDefaults = 0 + if (AccountPreferences.isNotificationHasLight(defaultFlags)) { + notificationDefaults = notificationDefaults or NotificationCompat.DEFAULT_LIGHTS + } + if (isNotificationAudible()) { + if (AccountPreferences.isNotificationHasVibration(defaultFlags)) { + notificationDefaults = notificationDefaults or NotificationCompat.DEFAULT_VIBRATE + } else { + notificationDefaults = notificationDefaults and NotificationCompat.DEFAULT_VIBRATE.inv() + } + if (AccountPreferences.isNotificationHasRingtone(defaultFlags)) { + builder.setSound(pref.notificationRingtone, AudioManager.STREAM_NOTIFICATION) + } + } else { + notificationDefaults = notificationDefaults and (NotificationCompat.DEFAULT_VIBRATE or NotificationCompat.DEFAULT_SOUND).inv() + } + builder.color = pref.notificationLightColor + builder.setDefaults(notificationDefaults) + builder.setOnlyAlertOnce(true) + } + + private fun isNotificationAudible(): Boolean { + return !activityTracker.isHomeActivityStarted + } + + fun showTimelineNotification(pref: AccountPreferences, position: Long) { + val accountKey = pref.accountKey + val resources = context.resources + val nm = notificationManager + val selection = Expression.and(Expression.equalsArgs(Statuses.ACCOUNT_KEY), + Expression.greaterThan(Statuses.POSITION_KEY, position)) + val filteredSelection = buildStatusFilterWhereClause(preferences, + Statuses.TABLE_NAME, selection) + val selectionArgs = arrayOf(accountKey.toString()) + val userProjection = arrayOf(Statuses.USER_KEY, Statuses.USER_NAME, Statuses.USER_SCREEN_NAME) + val statusProjection = arrayOf(Statuses.POSITION_KEY) + val statusCursor = context.contentResolver.query(Statuses.CONTENT_URI, statusProjection, + filteredSelection.sql, selectionArgs, Statuses.DEFAULT_SORT_ORDER) + + val userCursor = context.contentResolver.rawQuery(SQLQueryBuilder.select(Columns(*userProjection)) + .from(Table(Statuses.TABLE_NAME)) + .where(filteredSelection) + .groupBy(Columns.Column(Statuses.USER_KEY)) + .orderBy(OrderBy(Statuses.DEFAULT_SORT_ORDER)).buildSQL(), selectionArgs) + + try { + val usersCount = userCursor.count + val statusesCount = statusCursor.count + if (statusesCount == 0 || usersCount == 0) return + val statusIndices = ParcelableStatusCursorIndices(statusCursor) + val userIndices = ParcelableStatusCursorIndices(userCursor) + val positionKey = if (statusCursor.moveToFirst()) statusCursor.getLong(statusIndices.position_key) else -1L + val notificationTitle = resources.getQuantityString(R.plurals.N_new_statuses, + statusesCount, statusesCount) + val notificationContent: String + userCursor.moveToFirst() + val displayName = userColorNameManager.getDisplayName(userCursor.getString(userIndices.user_key), + userCursor.getString(userIndices.user_name), userCursor.getString(userIndices.user_screen_name), + nameFirst) + if (usersCount == 1) { + notificationContent = context.getString(R.string.from_name, displayName) + } else if (usersCount == 2) { + userCursor.moveToPosition(1) + val othersName = userColorNameManager.getDisplayName(userCursor.getString(userIndices.user_key), + userCursor.getString(userIndices.user_name), userCursor.getString(userIndices.user_screen_name), + nameFirst) + notificationContent = resources.getString(R.string.from_name_and_name, displayName, othersName) + } else { + notificationContent = resources.getString(R.string.from_name_and_N_others, displayName, usersCount - 1) + } + + // Setup notification + val builder = NotificationCompat.Builder(context) + builder.setAutoCancel(true) + builder.setSmallIcon(R.drawable.ic_stat_twitter) + builder.setTicker(notificationTitle) + builder.setContentTitle(notificationTitle) + builder.setContentText(notificationContent) + builder.setCategory(NotificationCompat.CATEGORY_SOCIAL) + builder.setContentIntent(getContentIntent(context, CustomTabType.HOME_TIMELINE, + NotificationType.HOME_TIMELINE, accountKey, positionKey)) + builder.setDeleteIntent(getMarkReadDeleteIntent(context, NotificationType.HOME_TIMELINE, + accountKey, positionKey, false)) + builder.setNumber(statusesCount) + builder.setCategory(NotificationCompat.CATEGORY_SOCIAL) + applyNotificationPreferences(builder, pref, pref.homeTimelineNotificationType) + try { + nm.notify("home_" + accountKey, Utils.getNotificationId(NOTIFICATION_ID_HOME_TIMELINE, accountKey), builder.build()) + Utils.sendPebbleNotification(context, null, notificationContent) + } catch (e: SecurityException) { + // Silently ignore + } + + } finally { + statusCursor.close() + userCursor.close() + } + } + + + private fun getContentIntent(context: Context, @CustomTabType type: String, + @NotificationType notificationType: String, accountKey: UserKey?, readPosition: Long): PendingIntent { + // Setup click intent + val homeIntent = Intent(context, HomeActivity::class.java) + val homeLinkBuilder = Uri.Builder() + homeLinkBuilder.scheme(SCHEME_TWIDERE) + homeLinkBuilder.authority(type) + if (accountKey != null) + homeLinkBuilder.appendQueryParameter(QUERY_PARAM_ACCOUNT_KEY, accountKey.toString()) + homeLinkBuilder.appendQueryParameter(QUERY_PARAM_FROM_NOTIFICATION, true.toString()) + homeLinkBuilder.appendQueryParameter(QUERY_PARAM_TIMESTAMP, System.currentTimeMillis().toString()) + homeLinkBuilder.appendQueryParameter(QUERY_PARAM_NOTIFICATION_TYPE, notificationType) + if (readPosition > 0) { + homeLinkBuilder.appendQueryParameter(QUERY_PARAM_READ_POSITION, readPosition.toString()) + } + homeIntent.data = homeLinkBuilder.build() + homeIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK) + return PendingIntent.getActivity(context, 0, homeIntent, 0) + } + + + fun setNotificationUri(c: Cursor?, uri: Uri?) { + val cr = context.contentResolver + if (cr == null || c == null || uri == null) return + c.setNotificationUri(cr, uri) + } + + private fun updatePreferences() { + nameFirst = preferences[nameFirstKey] + useStarForLikes = preferences[iWantMyStarsBackKey] + } + + + private fun getMarkReadDeleteIntent(context: Context, @NotificationType notificationType: String, + accountKey: UserKey?, positions: Array): PendingIntent { + // Setup delete intent + val intent = Intent(context, NotificationReceiver::class.java) + val linkBuilder = Uri.Builder() + linkBuilder.scheme(SCHEME_TWIDERE) + linkBuilder.authority(AUTHORITY_INTERACTIONS) + linkBuilder.appendPath(notificationType) + if (accountKey != null) { + linkBuilder.appendQueryParameter(QUERY_PARAM_ACCOUNT_KEY, accountKey.toString()) + } + linkBuilder.appendQueryParameter(QUERY_PARAM_READ_POSITIONS, StringLongPair.toString(positions)) + linkBuilder.appendQueryParameter(QUERY_PARAM_TIMESTAMP, System.currentTimeMillis().toString()) + linkBuilder.appendQueryParameter(QUERY_PARAM_NOTIFICATION_TYPE, notificationType) + intent.data = linkBuilder.build() + return PendingIntent.getBroadcast(context, 0, intent, 0) + } + + + private fun getMarkReadDeleteIntent(context: Context, @NotificationType type: String, + accountKey: UserKey?, position: Long, + extraUserFollowing: Boolean): PendingIntent { + return getMarkReadDeleteIntent(context, type, accountKey, position, -1, -1, extraUserFollowing) + } + + private fun getMarkReadDeleteIntent(context: Context, @NotificationType type: String, + accountKey: UserKey?, position: Long, + extraId: Long, extraUserId: Long, + extraUserFollowing: Boolean): PendingIntent { + // Setup delete intent + val intent = Intent(context, NotificationReceiver::class.java) + intent.action = IntentConstants.BROADCAST_NOTIFICATION_DELETED + val linkBuilder = Uri.Builder() + linkBuilder.scheme(SCHEME_TWIDERE) + linkBuilder.authority(AUTHORITY_INTERACTIONS) + linkBuilder.appendPath(type) + if (accountKey != null) { + linkBuilder.appendQueryParameter(QUERY_PARAM_ACCOUNT_KEY, accountKey.toString()) + } + linkBuilder.appendQueryParameter(QUERY_PARAM_READ_POSITION, position.toString()) + linkBuilder.appendQueryParameter(QUERY_PARAM_TIMESTAMP, System.currentTimeMillis().toString()) + linkBuilder.appendQueryParameter(QUERY_PARAM_NOTIFICATION_TYPE, type) + + UriExtraUtils.addExtra(linkBuilder, "item_id", extraId) + UriExtraUtils.addExtra(linkBuilder, "item_user_id", extraUserId) + UriExtraUtils.addExtra(linkBuilder, "item_user_following", extraUserFollowing) + intent.data = linkBuilder.build() + return PendingIntent.getBroadcast(context, 0, intent, 0) + } +} \ No newline at end of file diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/util/DataStoreFunctions.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/util/DataStoreFunctions.kt index 4203b53c4..41a3b631b 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/util/DataStoreFunctions.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/util/DataStoreFunctions.kt @@ -24,8 +24,8 @@ import java.io.IOException */ fun buildStatusFilterWhereClause(preferences: SharedPreferences, - table: String, - extraSelection: Expression?): Expression { + table: String, + extraSelection: Expression?): Expression { val filteredUsersQuery = SQLQueryBuilder .select(Columns.Column(Table(Filters.Users.TABLE_NAME), Filters.Users.USER_KEY)) .from(Tables(Filters.Users.TABLE_NAME)) @@ -113,7 +113,6 @@ fun deleteAccountData(resolver: ContentResolver, accountKey: UserKey) { // Also delete tweets related to the account we previously // deleted. resolver.delete(Statuses.CONTENT_URI, where, whereArgs) - resolver.delete(Mentions.CONTENT_URI, where, whereArgs) resolver.delete(Activities.AboutMe.CONTENT_URI, where, whereArgs) resolver.delete(Messages.CONTENT_URI, where, whereArgs) resolver.delete(Messages.Conversations.CONTENT_URI, where, whereArgs) @@ -121,7 +120,7 @@ fun deleteAccountData(resolver: ContentResolver, accountKey: UserKey) { fun deleteActivityStatus(cr: ContentResolver, accountKey: UserKey, - statusId: String, result: ParcelableStatus?) { + statusId: String, result: ParcelableStatus?) { val host = accountKey.host val deleteWhere: String @@ -171,9 +170,9 @@ fun deleteActivityStatus(cr: ContentResolver, accountKey: UserKey, } fun updateActivityStatus(resolver: ContentResolver, - accountKey: UserKey, - statusId: String, - action: (ParcelableActivity) -> Unit) { + accountKey: UserKey, + statusId: String, + action: (ParcelableActivity) -> Unit) { val activityWhere = Expression.and( Expression.equalsArgs(Activities.ACCOUNT_KEY), Expression.or( @@ -190,8 +189,8 @@ fun updateActivityStatus(resolver: ContentResolver, @WorkerThread fun updateActivity(cr: ContentResolver, uri: Uri, - where: String?, whereArgs: Array?, - action: (ParcelableActivity) -> Unit) { + where: String?, whereArgs: Array?, + action: (ParcelableActivity) -> Unit) { val c = cr.query(uri, Activities.COLUMNS, where, whereArgs, null) ?: return val values = LongSparseArray() try { diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/util/DataStoreUtils.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/util/DataStoreUtils.kt index 36dae08ea..d1496a955 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/util/DataStoreUtils.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/util/DataStoreUtils.kt @@ -47,6 +47,7 @@ import org.mariotaku.twidere.extension.model.getAccountKey import org.mariotaku.twidere.extension.model.getAccountUser import org.mariotaku.twidere.extension.model.getColor import org.mariotaku.twidere.extension.model.isActivated +import org.mariotaku.twidere.extension.rawQuery import org.mariotaku.twidere.model.* import org.mariotaku.twidere.model.tab.extra.HomeTabExtras import org.mariotaku.twidere.model.tab.extra.InteractionsTabExtras @@ -112,32 +113,8 @@ object DataStoreUtils { CONTENT_PROVIDER_URI_MATCHER.addURI(TwidereDataStore.AUTHORITY, SearchHistory.CONTENT_PATH, TABLE_ID_SEARCH_HISTORY) - CONTENT_PROVIDER_URI_MATCHER.addURI(TwidereDataStore.AUTHORITY, Notifications.CONTENT_PATH, - VIRTUAL_TABLE_ID_NOTIFICATIONS) - CONTENT_PROVIDER_URI_MATCHER.addURI(TwidereDataStore.AUTHORITY, Notifications.CONTENT_PATH + "/#", - VIRTUAL_TABLE_ID_NOTIFICATIONS) - CONTENT_PROVIDER_URI_MATCHER.addURI(TwidereDataStore.AUTHORITY, Notifications.CONTENT_PATH + "/#/*", - VIRTUAL_TABLE_ID_NOTIFICATIONS) CONTENT_PROVIDER_URI_MATCHER.addURI(TwidereDataStore.AUTHORITY, Permissions.CONTENT_PATH, VIRTUAL_TABLE_ID_PERMISSIONS) - CONTENT_PROVIDER_URI_MATCHER.addURI(TwidereDataStore.AUTHORITY, DNS.CONTENT_PATH + "/*", - VIRTUAL_TABLE_ID_DNS) - CONTENT_PROVIDER_URI_MATCHER.addURI(TwidereDataStore.AUTHORITY, CachedImages.CONTENT_PATH, - VIRTUAL_TABLE_ID_CACHED_IMAGES) - CONTENT_PROVIDER_URI_MATCHER.addURI(TwidereDataStore.AUTHORITY, CacheFiles.CONTENT_PATH + "/*", - VIRTUAL_TABLE_ID_CACHE_FILES) - CONTENT_PROVIDER_URI_MATCHER.addURI(TwidereDataStore.AUTHORITY, Preferences.CONTENT_PATH, - VIRTUAL_TABLE_ID_ALL_PREFERENCES) - CONTENT_PROVIDER_URI_MATCHER.addURI(TwidereDataStore.AUTHORITY, Preferences.CONTENT_PATH + "/*", - VIRTUAL_TABLE_ID_PREFERENCES) - CONTENT_PROVIDER_URI_MATCHER.addURI(TwidereDataStore.AUTHORITY, UnreadCounts.CONTENT_PATH, - VIRTUAL_TABLE_ID_UNREAD_COUNTS) - CONTENT_PROVIDER_URI_MATCHER.addURI(TwidereDataStore.AUTHORITY, UnreadCounts.CONTENT_PATH + "/#", - VIRTUAL_TABLE_ID_UNREAD_COUNTS) - CONTENT_PROVIDER_URI_MATCHER.addURI(TwidereDataStore.AUTHORITY, UnreadCounts.CONTENT_PATH + "/#/#/*", - VIRTUAL_TABLE_ID_UNREAD_COUNTS) - CONTENT_PROVIDER_URI_MATCHER.addURI(TwidereDataStore.AUTHORITY, UnreadCounts.ByType.CONTENT_PATH + "/*", - VIRTUAL_TABLE_ID_UNREAD_COUNTS_BY_TYPE) CONTENT_PROVIDER_URI_MATCHER.addURI(TwidereDataStore.AUTHORITY, CachedUsers.CONTENT_PATH_WITH_RELATIONSHIP + "/*", VIRTUAL_TABLE_ID_CACHED_USERS_WITH_RELATIONSHIP) CONTENT_PROVIDER_URI_MATCHER.addURI(TwidereDataStore.AUTHORITY, CachedUsers.CONTENT_PATH_WITH_SCORE + "/*", @@ -698,8 +675,7 @@ object DataStoreUtils { if (sortExpression != null) { builder.orderBy(sortExpression) } - val rawUri = Uri.withAppendedPath(TwidereDataStore.CONTENT_URI_RAW_QUERY, builder.buildSQL()) - resolver.query(rawUri, null, null, bindingArgs, null)?.useCursor { cur -> + resolver.rawQuery(builder.buildSQL(), bindingArgs)?.useCursor { cur -> cur.moveToFirst() val colIdx = creator.newIndex(cur) while (!cur.isAfterLast) { @@ -788,8 +764,7 @@ object DataStoreUtils { fun assign(array: T, arrayIdx: Int, cur: Cursor, colIdx: I) } - fun queryCount(context: Context, uri: Uri, - selection: String?, selectionArgs: Array?): Int { + fun queryCount(context: Context, uri: Uri, selection: String?, selectionArgs: Array?): Int { val resolver = context.contentResolver val projection = arrayOf(SQLFunctions.COUNT()) val cur = resolver.query(uri, projection, selection, selectionArgs, null) ?: return -1 diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/util/content/TwidereSQLiteOpenHelper.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/util/content/TwidereSQLiteOpenHelper.kt index 3a2734814..445707dd5 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/util/content/TwidereSQLiteOpenHelper.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/util/content/TwidereSQLiteOpenHelper.kt @@ -82,8 +82,8 @@ class TwidereSQLiteOpenHelper( db.beginTransaction() db.execSQL(createTable(Messages.TABLE_NAME, Messages.COLUMNS, Messages.TYPES, true, messagesConstraint())) - db.execSQL(createTable(Conversations.TABLE_NAME, Conversations.COLUMNS, - Conversations.TYPES, true, messageConversationsConstraint())) + db.execSQL(createTable(Conversations.TABLE_NAME, Conversations.COLUMNS, Conversations.TYPES, + true, messageConversationsConstraint())) db.setTransactionSuccessful() db.endTransaction() @@ -210,13 +210,13 @@ class TwidereSQLiteOpenHelper( } } if (oldVersion <= 164) { - db.execSQL(SQLQueryBuilder.dropView(true, DirectMessages.TABLE_NAME).sql) - db.execSQL(SQLQueryBuilder.dropView(true, DirectMessages.ConversationEntries.TABLE_NAME).sql) + db.execSQL(SQLQueryBuilder.dropView(true, "messages").sql) + db.execSQL(SQLQueryBuilder.dropView(true, "messages_conversation_entries").sql) db.execSQL(SQLQueryBuilder.dropTrigger(true, "delete_old_received_messages").sql) db.execSQL(SQLQueryBuilder.dropTrigger(true, "delete_old_sent_messages").sql) - db.execSQL(SQLQueryBuilder.dropTable(true, DirectMessages.Inbox.TABLE_NAME).sql) - db.execSQL(SQLQueryBuilder.dropTable(true, DirectMessages.Outbox.TABLE_NAME).sql) + db.execSQL(SQLQueryBuilder.dropTable(true, "messages_inbox").sql) + db.execSQL(SQLQueryBuilder.dropTable(true, "messages_outbox").sql) db.execSQL(SQLQueryBuilder.dropIndex(true, "messages_inbox_index").sql) db.execSQL(SQLQueryBuilder.dropIndex(true, "messages_outbox_index").sql) @@ -301,7 +301,7 @@ class TwidereSQLiteOpenHelper( } private fun createTable(tableName: String, columns: Array, types: Array, - createIfNotExists: Boolean, vararg constraints: Constraint): String { + createIfNotExists: Boolean, vararg constraints: Constraint): String { val qb = SQLQueryBuilder.createTable(createIfNotExists, tableName) qb.columns(*NewColumn.createNewColumns(columns, types)) qb.constraint(*constraints) @@ -309,7 +309,7 @@ class TwidereSQLiteOpenHelper( } private fun createIndex(indexName: String, tableName: String, columns: Array, - createIfNotExists: Boolean): String { + createIfNotExists: Boolean): String { val qb = SQLQueryBuilder.createIndex(false, createIfNotExists) qb.name(indexName) qb.on(Table(tableName), Columns(*columns)) diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/util/dagger/ApplicationModule.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/util/dagger/ApplicationModule.kt index 80df575ed..4cecf7feb 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/util/dagger/ApplicationModule.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/util/dagger/ApplicationModule.kt @@ -132,7 +132,7 @@ class ApplicationModule(private val application: Application) { @Provides @Singleton fun restHttpClient(prefs: SharedPreferencesWrapper, dns: TwidereDns, - connectionPool: ConnectionPool): RestHttpClient { + connectionPool: ConnectionPool): RestHttpClient { val conf = HttpClientFactory.HttpClientConfiguration(prefs) return HttpClientFactory.createRestHttpClient(conf, dns, connectionPool) } @@ -180,8 +180,8 @@ class ApplicationModule(private val application: Application) { @Provides @Singleton fun asyncTwitterWrapper(bus: Bus, preferences: SharedPreferencesWrapper, - asyncTaskManager: AsyncTaskManager): AsyncTwitterWrapper { - return AsyncTwitterWrapper(application, bus, preferences, asyncTaskManager) + asyncTaskManager: AsyncTaskManager, notificationManagerWrapper: NotificationManagerWrapper): AsyncTwitterWrapper { + return AsyncTwitterWrapper(application, bus, preferences, asyncTaskManager, notificationManagerWrapper) } @Provides @@ -190,6 +190,13 @@ class ApplicationModule(private val application: Application) { return ReadStateManager(application) } + @Provides + @Singleton + fun contentNotificationManager(activityTracker: ActivityTracker, userColorNameManager: UserColorNameManager, + notificationManagerWrapper: NotificationManagerWrapper, preferences: SharedPreferencesWrapper): ContentNotificationManager { + return ContentNotificationManager(application, activityTracker, userColorNameManager, notificationManagerWrapper, preferences) + } + @Provides @Singleton fun mediaLoaderWrapper(loader: ImageLoader, preferences: SharedPreferencesWrapper): MediaLoaderWrapper { diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/util/database/FilterQueryBuilder.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/util/database/FilterQueryBuilder.kt new file mode 100644 index 000000000..1f187996f --- /dev/null +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/util/database/FilterQueryBuilder.kt @@ -0,0 +1,161 @@ +/* + * Twidere - Twitter client for Android + * + * Copyright (C) 2012-2017 Mariotaku Lee + * + * 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 . + */ + +package org.mariotaku.twidere.util.database + +import android.content.ContentResolver +import org.mariotaku.twidere.extension.rawQuery +import org.mariotaku.twidere.model.SpanItem +import org.mariotaku.twidere.model.UserKey +import org.mariotaku.twidere.provider.TwidereDataStore.Filters +import java.util.* + +/** + * Created by mariotaku on 2017/2/16. + */ + +object FilterQueryBuilder { + + fun isFiltered(cr: ContentResolver, userKey: UserKey?, textPlain: String?, quotedTextPlain: String?, + spans: Array?, quotedSpans: Array?, source: String?, quotedSource: String?, + retweetedByKey: UserKey?, quotedUserKey: UserKey?): Boolean { + val query = FilterQueryBuilder.isFilteredQuery(userKey, + textPlain, quotedTextPlain, spans, quotedSpans, source, quotedSource, retweetedByKey, + quotedUserKey, true) + val cur = cr.rawQuery(query.first, query.second) + try { + return cur.moveToFirst() && cur.getInt(0) != 0 + } finally { + cur.close() + } + } + + fun isFilteredQuery(userKey: UserKey?, textPlain: String?, quotedTextPlain: String?, + spans: Array?, quotedSpans: Array?, source: String?, quotedSource: String?, + retweetedByKey: UserKey?, quotedUserKey: UserKey?, filterRts: Boolean): Pair> { + val builder = StringBuilder() + val selectionArgs = ArrayList() + builder.append("SELECT ") + if (textPlain != null) { + selectionArgs.add(textPlain) + addTextPlainStatement(builder) + } + if (quotedTextPlain != null) { + if (!selectionArgs.isEmpty()) { + builder.append(" OR ") + } + selectionArgs.add(quotedTextPlain) + addTextPlainStatement(builder) + } + if (spans != null) { + if (!selectionArgs.isEmpty()) { + builder.append(" OR ") + } + addSpansStatement(spans, builder, selectionArgs) + } + if (quotedSpans != null) { + if (!selectionArgs.isEmpty()) { + builder.append(" OR ") + } + addSpansStatement(quotedSpans, builder, selectionArgs) + } + if (userKey != null) { + if (!selectionArgs.isEmpty()) { + builder.append(" OR ") + } + selectionArgs.add(userKey.toString()) + createUserKeyStatement(builder) + } + if (retweetedByKey != null) { + if (!selectionArgs.isEmpty()) { + builder.append(" OR ") + } + selectionArgs.add(retweetedByKey.toString()) + createUserKeyStatement(builder) + } + if (quotedUserKey != null) { + if (!selectionArgs.isEmpty()) { + builder.append(" OR ") + } + selectionArgs.add(quotedUserKey.toString()) + createUserKeyStatement(builder) + } + if (source != null) { + if (!selectionArgs.isEmpty()) { + builder.append(" OR ") + } + selectionArgs.add(source) + appendSourceStatement(builder) + } + if (quotedSource != null) { + if (!selectionArgs.isEmpty()) { + builder.append(" OR ") + } + selectionArgs.add(quotedSource) + appendSourceStatement(builder) + } + return Pair(builder.toString(), selectionArgs.toTypedArray()) + } + + + private fun createUserKeyStatement(builder: StringBuilder) { + builder.append("(SELECT ") + .append("?") + .append(" IN (SELECT ") + .append(Filters.Users.USER_KEY) + .append(" FROM ") + .append(Filters.Users.TABLE_NAME) + .append("))") + } + + private fun appendSourceStatement(builder: StringBuilder) { + builder.append("(SELECT 1 IN (SELECT ? LIKE '%>'||") + .append(Filters.Sources.TABLE_NAME).append(".") + .append(Filters.VALUE).append("||'%' FROM ") + .append(Filters.Sources.TABLE_NAME) + .append("))") + } + + private fun addTextPlainStatement(builder: StringBuilder) { + builder.append("(SELECT 1 IN (SELECT ? LIKE '%'||") + .append(Filters.Keywords.TABLE_NAME) + .append(".") + .append(Filters.VALUE) + .append("||'%' FROM ") + .append(Filters.Keywords.TABLE_NAME) + .append("))") + } + + private fun addSpansStatement(spans: Array, builder: StringBuilder, selectionArgs: MutableList) { + val spansFlat = StringBuilder() + for (span in spans) { + spansFlat.append(span.link) + spansFlat.append(' ') + } + selectionArgs.add(spansFlat.toString()) + builder.append("(SELECT 1 IN (SELECT ? LIKE '%'||") + .append(Filters.Links.TABLE_NAME) + .append(".") + .append(Filters.VALUE) + .append("||'%' FROM ") + .append(Filters.Links.TABLE_NAME) + .append("))") + } + +} diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/util/database/SuggestionsCursorCreator.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/util/database/SuggestionsCursorCreator.kt new file mode 100644 index 000000000..b1e4c5779 --- /dev/null +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/util/database/SuggestionsCursorCreator.kt @@ -0,0 +1,215 @@ +/* + * Twidere - Twitter client for Android + * + * Copyright (C) 2012-2017 Mariotaku Lee + * + * 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 . + */ + +package org.mariotaku.twidere.util.database + +import android.database.Cursor +import android.database.MatrixCursor +import android.database.MergeCursor +import android.net.Uri +import android.text.TextUtils +import org.mariotaku.ktextension.convert +import org.mariotaku.sqliteqb.library.* +import org.mariotaku.twidere.TwidereConstants.* +import org.mariotaku.twidere.model.UserKey +import org.mariotaku.twidere.provider.TwidereDataStore.* +import org.mariotaku.twidere.util.SQLiteDatabaseWrapper +import org.mariotaku.twidere.util.TwidereQueryBuilder +import org.mariotaku.twidere.util.UserColorNameManager +import org.mariotaku.twidere.util.Utils +import java.util.regex.Pattern + +/** + * Created by mariotaku on 2017/2/16. + */ + +object SuggestionsCursorCreator { + + private val PATTERN_SCREEN_NAME = Pattern.compile("(?i)[@\uFF20]?([a-z0-9_]{1,20})") + + private val historyProjectionMap = mapOf( + Suggestions._ID to Columns.Column(SearchHistory._ID, Suggestions._ID).sql, + Suggestions.TYPE to Columns.Column("'${Suggestions.Search.TYPE_SEARCH_HISTORY}'", Suggestions.TYPE).sql, + Suggestions.TITLE to Columns.Column(SearchHistory.QUERY, Suggestions.TITLE).sql, + Suggestions.SUMMARY to Columns.Column(SQLConstants.NULL, Suggestions.SUMMARY).sql, + Suggestions.ICON to Columns.Column(SQLConstants.NULL, Suggestions.ICON).sql, + Suggestions.EXTRA_ID to Columns.Column("0", Suggestions.EXTRA_ID).sql, + Suggestions.EXTRA to Columns.Column(SQLConstants.NULL, Suggestions.EXTRA).sql, + Suggestions.VALUE to Columns.Column(SearchHistory.QUERY, Suggestions.VALUE).sql + ) + + private val savedSearchesProjectionMap = mapOf(Suggestions._ID to Columns.Column(SavedSearches._ID, Suggestions._ID).sql, + Suggestions.TYPE to Columns.Column("'${Suggestions.Search.TYPE_SAVED_SEARCH}'", Suggestions.TYPE).sql, + Suggestions.TITLE to Columns.Column(SavedSearches.QUERY, Suggestions.TITLE).sql, + Suggestions.SUMMARY to Columns.Column(SQLConstants.NULL, Suggestions.SUMMARY).sql, + Suggestions.ICON to Columns.Column(SQLConstants.NULL, Suggestions.ICON).sql, + Suggestions.EXTRA_ID to Columns.Column("0", Suggestions.EXTRA_ID).sql, + Suggestions.EXTRA to Columns.Column(SQLConstants.NULL, Suggestions.EXTRA).sql, + Suggestions.VALUE to Columns.Column(SavedSearches.QUERY, Suggestions.VALUE).sql + ) + private val suggestionUsersProjectionMap = mapOf(Suggestions._ID to Columns.Column(CachedUsers._ID, Suggestions._ID).sql, + Suggestions.TYPE to Columns.Column("'${Suggestions.Search.TYPE_USER}'", Suggestions.TYPE).sql, + Suggestions.TITLE to Columns.Column(CachedUsers.NAME, Suggestions.TITLE).sql, + Suggestions.SUMMARY to Columns.Column(CachedUsers.SCREEN_NAME, Suggestions.SUMMARY).sql, + Suggestions.ICON to Columns.Column(CachedUsers.PROFILE_IMAGE_URL, Suggestions.ICON).sql, + Suggestions.EXTRA_ID to Columns.Column(CachedUsers.USER_KEY, Suggestions.EXTRA_ID).sql, + Suggestions.EXTRA to Columns.Column(SQLConstants.NULL, Suggestions.EXTRA).sql, + Suggestions.VALUE to Columns.Column(CachedUsers.SCREEN_NAME, Suggestions.VALUE).sql + ) + + private val autoCompleteUsersProjectionMap = mapOf(Suggestions._ID to Columns.Column(CachedUsers._ID, Suggestions._ID).sql, + Suggestions.TYPE to Columns.Column("'${Suggestions.AutoComplete.TYPE_USERS}'", Suggestions.TYPE).sql, + Suggestions.TITLE to Columns.Column(CachedUsers.NAME, Suggestions.TITLE).sql, + Suggestions.SUMMARY to Columns.Column(CachedUsers.SCREEN_NAME, Suggestions.SUMMARY).sql, + Suggestions.ICON to Columns.Column(CachedUsers.PROFILE_IMAGE_URL, Suggestions.ICON).sql, + Suggestions.EXTRA_ID to Columns.Column(CachedUsers.USER_KEY, Suggestions.EXTRA_ID).sql, + Suggestions.EXTRA to Columns.Column(SQLConstants.NULL, Suggestions.EXTRA).sql, + Suggestions.VALUE to Columns.Column(CachedUsers.SCREEN_NAME, Suggestions.VALUE).sql + ) + + private val hashtagsProjectionMap = mapOf(Suggestions._ID to Columns.Column(CachedHashtags._ID, Suggestions._ID).sql, + Suggestions.TYPE to Columns.Column("'${Suggestions.AutoComplete.TYPE_HASHTAGS}'", Suggestions.TYPE).sql, + Suggestions.TITLE to Columns.Column(CachedHashtags.NAME, Suggestions.TITLE).sql, + Suggestions.SUMMARY to Columns.Column(SQLConstants.NULL, Suggestions.SUMMARY).sql, + Suggestions.ICON to Columns.Column(SQLConstants.NULL, Suggestions.ICON).sql, + Suggestions.EXTRA_ID to Columns.Column("0", Suggestions.EXTRA_ID).sql, + Suggestions.EXTRA to Columns.Column(SQLConstants.NULL, Suggestions.EXTRA).sql, + Suggestions.VALUE to Columns.Column(CachedHashtags.NAME, Suggestions.VALUE).sql + ) + + + fun forSearch(db: SQLiteDatabaseWrapper, manager: UserColorNameManager, + uri: Uri, projection: Array?): Cursor? { + val nonNullProjection = projection ?: Suggestions.COLUMNS + val query = uri.getQueryParameter(QUERY_PARAM_QUERY) ?: return null + val accountKey = uri.getQueryParameter(QUERY_PARAM_ACCOUNT_KEY)?.convert(UserKey::valueOf) ?: return null + val emptyQuery = TextUtils.isEmpty(query) + val cursors = mutableListOf(getHistoryCursor(db, nonNullProjection, query)) + if (emptyQuery) { + cursors.add(getSavedSearchCursor(db, nonNullProjection, accountKey)) + } else { + val queryTrimmed = query.replace("_", "^_").substringAfter("@") + + if (!hasName(db, queryTrimmed)) { + val m = PATTERN_SCREEN_NAME.matcher(query) + if (m.matches()) { + val screenName = m.group(1) + cursors.add(getScreenNameCursor(nonNullProjection, screenName)) + } + } + cursors.add(getUsersCursor(db, manager, accountKey, nonNullProjection, query, queryTrimmed)) + } + return MergeCursor(cursors.toTypedArray()) + } + + + fun forAutoComplete(db: SQLiteDatabaseWrapper, manager: UserColorNameManager, + uri: Uri, projection: Array?): Cursor? { + val nonNullProjection = projection ?: Suggestions.COLUMNS + val query = uri.getQueryParameter(QUERY_PARAM_QUERY) ?: return null + val type = uri.getQueryParameter(QUERY_PARAM_TYPE) ?: return null + val accountKey = uri.getQueryParameter(QUERY_PARAM_ACCOUNT_KEY)?.convert(UserKey::valueOf) ?: return null + val queryEscaped = query.replace("_", "^_") + when (type) { + Suggestions.AutoComplete.TYPE_USERS -> { + val nicknameKeys = Utils.getMatchedNicknameKeys(query, manager) + val where = Expression.or(Expression.inArgs(Columns.Column(CachedUsers.USER_KEY), nicknameKeys.size), + Expression.likeRaw(Columns.Column(CachedUsers.SCREEN_NAME), "?||'%'", "^"), + Expression.likeRaw(Columns.Column(CachedUsers.NAME), "?||'%'", "^")) + val whereArgs = nicknameKeys + queryEscaped + queryEscaped + val orderBy = arrayOf(CachedUsers.SCORE, CachedUsers.LAST_SEEN, CachedUsers.SCREEN_NAME, CachedUsers.NAME) + val ascending = booleanArrayOf(false, false, true, true) + val mappedProjection = nonNullProjection.map { autoCompleteUsersProjectionMap[it] }.toTypedArray() + val (sql, bindingArgs) = TwidereQueryBuilder.CachedUsersQueryBuilder.withScore(mappedProjection, + where.sql, whereArgs, OrderBy(orderBy, ascending).sql, accountKey, 0) + return db.rawQuery(sql.sql, bindingArgs) + } + Suggestions.AutoComplete.TYPE_HASHTAGS -> { + val where = Expression.likeRaw(Columns.Column(CachedHashtags.NAME), "?||'%'", "^") + val whereArgs = arrayOf(queryEscaped) + val mappedProjection = nonNullProjection.map { hashtagsProjectionMap[it] }.toTypedArray() + return db.query(CachedHashtags.TABLE_NAME, mappedProjection, where.sql, whereArgs, null, + null, null) + } + else -> return null + } + } + + private fun getUsersCursor(db: SQLiteDatabaseWrapper, manager: UserColorNameManager, accountKey: UserKey, + selection: Array, query: String, queryTrimmed: String): Cursor { + val nicknameKeys = Utils.getMatchedNicknameKeys(query, manager) + val usersSelection = Expression.or( + Expression.inArgs(Columns.Column(CachedUsers.USER_KEY), nicknameKeys.size), + Expression.likeRaw(Columns.Column(CachedUsers.SCREEN_NAME), "?||'%'", "^"), + Expression.likeRaw(Columns.Column(CachedUsers.NAME), "?||'%'", "^") + ) + val selectionArgs = nicknameKeys + queryTrimmed + queryTrimmed + val order = arrayOf(CachedUsers.LAST_SEEN, CachedUsers.SCORE, CachedUsers.SCREEN_NAME, CachedUsers.NAME) + val ascending = booleanArrayOf(false, false, true, true) + val orderBy = OrderBy(order, ascending) + val usersProjection = selection.map { suggestionUsersProjectionMap[it] }.toTypedArray() + val usersQuery = TwidereQueryBuilder.CachedUsersQueryBuilder.withScore(usersProjection, + usersSelection.sql, selectionArgs, orderBy.sql, accountKey, 0) + return db.rawQuery(usersQuery.first.sql, usersQuery.second) + } + + private fun getSavedSearchCursor(db: SQLiteDatabaseWrapper, projection: Array, accountKey: UserKey): Cursor { + val savedSearchesWhere = Expression.equalsArgs(SavedSearches.ACCOUNT_KEY) + val whereArgs = arrayOf(accountKey.toString()) + val savedSearchesProjection = projection.map { savedSearchesProjectionMap[it] }.toTypedArray() + return db.query(true, SavedSearches.TABLE_NAME, savedSearchesProjection, savedSearchesWhere.sql, + whereArgs, null, null, SavedSearches.DEFAULT_SORT_ORDER, null) + } + + private fun getHistoryCursor(db: SQLiteDatabaseWrapper, projection: Array, query: String): Cursor { + val queryEscaped = query.replace("_", "^_") + val historySelection = Expression.likeRaw(Columns.Column(SearchHistory.QUERY), "?||'%'", "^") + val historySelectionArgs = arrayOf(queryEscaped) + val historyProjection = projection.map { historyProjectionMap[it] }.toTypedArray() + val cursorLimit = if (TextUtils.isEmpty(query)) "3" else "2" + return db.query(true, SearchHistory.TABLE_NAME, historyProjection, historySelection.sql, + historySelectionArgs, null, null, SearchHistory.DEFAULT_SORT_ORDER, cursorLimit) + } + + private fun getScreenNameCursor(projection: Array, screenName: String): Cursor { + fun mapSelectionToValue(column: String) = when (column) { + Suggestions._ID -> "0" + Suggestions.TYPE -> Suggestions.Search.TYPE_SCREEN_NAME + Suggestions.TITLE -> screenName + Suggestions.EXTRA_ID -> "0" + Suggestions.VALUE -> screenName + else -> null + } + + val cursor = MatrixCursor(projection) + cursor.addRow(projection.map(::mapSelectionToValue)) + return cursor + } + + private fun hasName(db: SQLiteDatabaseWrapper, queryTrimmed: String): Boolean { + val exactUserSelection = Expression.or(Expression.likeRaw(Columns.Column(CachedUsers.SCREEN_NAME), "?", "^")) + val exactUserCursor = db.query(CachedUsers.TABLE_NAME, arrayOf(SQLFunctions.COUNT()), + exactUserSelection.sql, arrayOf(queryTrimmed), null, null, null, "1") + try { + return exactUserCursor.moveToPosition(0) && exactUserCursor.getInt(0) > 0 + } finally { + exactUserCursor.close() + } + } +}