/* * Overchan Android (Meta Imageboard Client) * Copyright (C) 2014-2016 miku-nyan <https://github.com/miku-nyan> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package nya.miku.wishmaster.ui.presentation; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.HashSet; import java.util.List; import java.util.regex.Matcher; import org.apache.commons.lang3.StringEscapeUtils; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.database.DatabaseUtils; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import android.provider.BaseColumns; import nya.miku.wishmaster.api.models.UrlPageModel; import nya.miku.wishmaster.api.util.RegexUtils; import nya.miku.wishmaster.cache.SerializablePage; import nya.miku.wishmaster.common.Logger; import nya.miku.wishmaster.common.MainApplication; public class Subscriptions { private static final String TAG = "Subscriptions"; private SubscriptionsDB database; private Object[] cached; private Object[] waitingOwnPost; public Subscriptions(Context context) { database = new SubscriptionsDB(context); } /** * Получить текущее количество подписок (отслеживаемых постов) */ public long getCurrentCount() { return database.getNumEntries(); } /** * Проверить, есть ли на данной странице ответы на отслеживаемые посты * @param page страница * @param startPostIndex номер поста (по порядку) на странице, начиная с которого требуется проверять * @return индекс (номер по порядку) первого ответа на отслеживаемые посты, если таковые есть, в противном случае -1 */ public int checkSubscriptions(SerializablePage page, int startPostIndex) { if (!MainApplication.getInstance().settings.isSubscriptionsEnabled()) return -1; if (page.pageModel == null || page.pageModel.type != UrlPageModel.TYPE_THREADPAGE || page.posts == null) return -1; String[] subscriptions = getSubscriptions(page.pageModel.chanName, page.pageModel.boardName, page.pageModel.threadNumber); if (subscriptions == null) return -1; if (startPostIndex < page.posts.length && MainApplication.getInstance().settings.subscribeThreads() && Arrays.binarySearch(subscriptions, page.pageModel.threadNumber) >= 0) return startPostIndex; for (int i=startPostIndex; i<page.posts.length; ++i) { String comment = page.posts[i].comment; if (comment == null) continue; Matcher m = PresentationItemModel.REPLY_LINK_FULL_PATTERN.matcher(comment); while (m.find()) if (Arrays.binarySearch(subscriptions, m.group(1)) >= 0) return i; } return -1; } /** * Добавить отслеживаемый пост */ public void addSubscription(String chan, String board, String thread, String post) { database.put(chan, board, thread, post); Object[] tuple = cached; if (tuple != null && tuple[0].equals(chan) && tuple[1].equals(board) && tuple[2].equals(thread)) cached = null; } /** * Проверить, является ли пост отслеживаемым */ public boolean hasSubscription(String chan, String board, String thread, String post) { return database.hasSubscription(chan, board, thread, post); } /** * Удалить отслеживаемый пост */ public void removeSubscription(String chan, String board, String thread, String post) { database.remove(chan, board, thread, post); Object[] tuple = cached; if (tuple != null && tuple[0].equals(chan) && tuple[1].equals(board) && tuple[2].equals(thread)) cached = null; } /** * Получить отсортированный список отслеживаемых постов в данном треде * @return массив, отсортированный как массив java.lang.String */ public String[] getSubscriptions(String chan, String board, String thread) { Object[] tuple = cached; if (tuple != null && tuple[0].equals(chan) && tuple[1].equals(board) && tuple[2].equals(thread)) return (String[]) tuple[3]; String[] result = database.getSubscriptions(chan, board, thread); if (result == null || result.length == 0) result = null; else Arrays.sort(result); cached = new Object[] { chan, board, thread, result }; return result; } /** * Очистить все подписки (отслеживаемые посты) */ public void reset() { database.resetDB(); cached = null; } /** * Установить детектор своего поста (для борд, которые не отдают номер своего поста после отправки) */ public void detectOwnPost(String chan, String board, String thread, String comment) { if (chan == null || board == null || thread == null || comment == null) return; List<String> words = commentToWordsList(comment); //Logger.d(TAG, "set detector; words: " + words); waitingOwnPost = new Object[] { chan, board, thread, words }; } /** * Проверить, нет ли на странице своего поста, установленного в {@link #detectOwnPost(String, String, String, String)}, * если есть, добавить пост к отслеживаемым (подпискам) * @param page страница * @param startPostIndex номер поста (по порядку) на странице, начиная с которого требуется проверять */ @SuppressWarnings("unchecked") public void checkOwnPost(SerializablePage page, int startPostIndex) { if (page.pageModel == null || page.pageModel.type != UrlPageModel.TYPE_THREADPAGE || page.posts == null) return; String chan = page.pageModel.chanName; String board = page.pageModel.boardName; String thread = page.pageModel.threadNumber; Object[] tuple = waitingOwnPost; if (tuple != null && tuple[0].equals(chan) && tuple[1].equals(board) && tuple[2].equals(thread)) { waitingOwnPost = null; int postCount = page.posts.length - startPostIndex; if (postCount <= 1) { if (postCount == 1 && page.posts[startPostIndex] != null) addSubscription(chan, board, thread, page.posts[startPostIndex].number); return; } List<int[]> result = new ArrayList<>(postCount); List<String> waitingWords = (List<String>) tuple[3]; for (int i=startPostIndex; i<page.posts.length; ++i) { if (page.posts[i] == null || page.posts[i].comment == null) continue; HashSet<String> postWords = new HashSet<>(commentToWordsList(htmlToComment(page.posts[i].comment))); //Logger.d(TAG, "checking post i=" + i + "\ncomment: " + page.posts[i].comment+"\nwords:" + postWords); int wordsCount = 0; for (String waitingWord : waitingWords) if (postWords.remove(waitingWord)) ++wordsCount; result.add(new int[] { i, wordsCount, postWords.size() }); //Logger.d(TAG, "result: overlap=" + wordsCount + "; remained=" + postWords.size()); } if (result.size() == 0) return; Collections.sort(result, new Comparator<int[]>() { @Override public int compare(int[] lhs, int[] rhs) { int result = compareInt(rhs[1], lhs[1]); if (result == 0) result = compareInt(lhs[2], rhs[2]); if (result == 0) result = compareInt(lhs[0], rhs[0]); return result; } private int compareInt(int lhs, int rhs) { return lhs < rhs ? -1 : (lhs == rhs ? 0 : 1); } }); //for (int[] entry : result) Logger.d(TAG, "[" + entry[0] + ";" + entry[1] + ";" + entry[2] + "]"); addSubscription(chan, board, thread, page.posts[result.get(0)[0]].number); } } private static String htmlToComment(String html) { return StringEscapeUtils.unescapeHtml4(RegexUtils.removeHtmlTags(html.replaceAll("<(br|p)/?>", " "))); } private static List<String> commentToWordsList(String comment) { return Arrays.asList(comment.replaceAll("[\\*%_]", "").replaceAll("\\[[^\\]]*\\]", "").replaceAll("[^\\w\\d\\s]", " ").trim().split("\\s+")); } private static class SubscriptionsDB { private static final int DB_VERSION = 1000; private static final String DB_NAME = "subscriptions.db"; private static final String TABLE_NAME = "subscriptions"; private static final String COL_CHAN = "chan"; private static final String COL_BOARD = "board"; private static final String COL_THREAD = "thread"; private static final String COL_POST = "post"; private final DBHelper dbHelper; public SubscriptionsDB(Context context) { dbHelper = new DBHelper(context); } public boolean hasSubscription(String chan, String board, String thread, String post) { Cursor c = dbHelper.getReadableDatabase().query(TABLE_NAME, null, COL_CHAN + " = ? AND " + COL_BOARD + " = ? AND " + COL_THREAD + " = ? AND " + COL_POST + " = ?", new String[] { chan, board, thread, post }, null, null, null); boolean result = false; if (c != null && c.moveToFirst()) result = true; if (c != null) c.close(); return result; } public void put(String chan, String board, String thread, String post) { if (hasSubscription(chan, board, thread, post)) { Logger.d(TAG, "entry is already exists"); return; } ContentValues value = new ContentValues(4); value.put(COL_CHAN, chan); value.put(COL_BOARD, board); value.put(COL_THREAD, thread); value.put(COL_POST, post); dbHelper.getWritableDatabase().insert(TABLE_NAME, null, value); } public void remove(String chan, String board, String thread, String post) { dbHelper.getWritableDatabase().delete(TABLE_NAME, COL_CHAN + " = ? AND " + COL_BOARD + " = ? AND " + COL_THREAD + " = ? AND " + COL_POST + " = ?", new String[] { chan, board, thread, post }); } public String[] getSubscriptions(String chan, String board, String thread) { Cursor c = dbHelper.getReadableDatabase().query(TABLE_NAME, null, COL_CHAN + " = ? AND " + COL_BOARD + " = ? AND " + COL_THREAD + " = ?", new String[] { chan, board, thread }, null, null, null); String[] result = null; if (c != null && c.moveToFirst()) { int postIndex = c.getColumnIndex(COL_POST); int count = c.getCount(); result = new String[count]; int i = 0; do result[i++] = c.getString(postIndex); while (i < count && c.moveToNext()); if (i < count) { Logger.e(TAG, "result size < cursor getCount()"); String[] tmp = new String[i]; System.arraycopy(result, 0, tmp, 0, i); result = tmp; } } if (c != null) c.close(); return result; } public void resetDB() { dbHelper.resetDB(); } public long getNumEntries() { return DatabaseUtils.queryNumEntries(dbHelper.getReadableDatabase(), TABLE_NAME); } private static class DBHelper extends SQLiteOpenHelper implements BaseColumns { public DBHelper(Context context) { super(context, DB_NAME, null, DB_VERSION); } @Override public void onCreate(SQLiteDatabase db) { db.execSQL(createTable(TABLE_NAME, new String[] { COL_CHAN, COL_BOARD, COL_THREAD, COL_POST }, null)); } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { if (oldVersion < newVersion) { db.execSQL(dropTable(TABLE_NAME)); onCreate(db); } } @Override public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) { onUpgrade(db, oldVersion, newVersion); } private static String createTable(String tableName, String[] columns, String[] types) { StringBuilder sql = new StringBuilder(110).append("create table ").append(tableName).append(" ("). append(_ID).append(" integer primary key autoincrement,"); for (int i=0; i<columns.length; ++i) { sql.append(columns[i]).append(' ').append(types == null ? "text" : types[i]).append(','); } sql.setCharAt(sql.length()-1, ')'); return sql.append(';').toString(); } private static String dropTable(String tableName) { return "DROP TABLE IF EXISTS " + tableName; } private void resetDB() { SQLiteDatabase db = getWritableDatabase(); db.execSQL(dropTable(TABLE_NAME)); onCreate(db); } } } }