Java tutorial
/* * Everychan 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 com.nttec.everychan.ui.presentation; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.TimeZone; import org.apache.commons.lang3.tuple.Triple; import com.nttec.everychan.api.interfaces.CancellableTask; import com.nttec.everychan.api.models.AttachmentModel; import com.nttec.everychan.api.models.PostModel; import com.nttec.everychan.api.models.ThreadModel; import com.nttec.everychan.api.models.UrlPageModel; import com.nttec.everychan.api.util.ChanModels; import com.nttec.everychan.cache.SerializablePage; import com.nttec.everychan.common.Logger; import com.nttec.everychan.common.MainApplication; import com.nttec.everychan.lib.org_json.JSONArray; import com.nttec.everychan.ui.Database; import com.nttec.everychan.ui.Database.IsHiddenDelegate; import com.nttec.everychan.ui.presentation.ClickableURLSpan.URLSpanClickListener; import com.nttec.everychan.ui.presentation.FlowTextHelper.FloatingModel; import com.nttec.everychan.ui.presentation.HtmlParser.ImageGetter; import com.nttec.everychan.ui.settings.AutohideActivity; import com.nttec.everychan.ui.theme.ThemeUtils; import android.content.res.Resources.Theme; /** * - ? ( ?? ?) ?.<br> * ? {@link PresentationItemModel} - {@link #presentationList} ?? ? ? ?. * @author miku-nyan * */ public class PresentationModel { private static final String TAG = "PresentationModel"; /** ?? ? - {@link SerializablePage} */ public final SerializablePage source; /** ?? URL ?, ?? ?, ?? e-mail (mailto) */ public final URLSpanClickListener spanClickListener; /** , ??? ? ? (, ?) */ public final ImageGetter imageGetter; private final Theme theme; private FloatingModel[] floatingModels; private final DateFormat dateFormat; private final boolean reduceNames; private final IsHiddenDelegate isHiddenDelegate; private final List<AutohideActivity.CompiledAutohideRule> autohideRules; /** * ? . * ?? , ??? - ? . * ? ? ?, LRU ? ? , * ? ? {@link #PresentationModel(PresentationModel)} */ public final int size; /** * ? ? . * ? (?) ?? {@link #updateViewModels()} ?, * ?? (?) {@link #source} */ public List<PresentationItemModel> presentationList = null; private ArrayList<Triple<AttachmentModel, String, String>> attachments = null; private Object lock = new Object(); private HashMap<String, Integer> postNumbersMap = new HashMap<String, Integer>(); private volatile boolean notReady; /** * ? * @param source ?? ? - {@link SerializablePage} * @param localTime ?, ??? ? (? true), ? ?? (? false) * @param reduceNames ? true, ? (. ?) ?? * @param spanClickListener ?? URL ?, ?? ?, ?? e-mail (mailto) * @param imageGetter , ??? ? ? (, ?) * @param theme ? * @param floatingModels ?? ? ?. ? - ? * , - ? ? ? (gif, , ). * ? null, ? . */ public PresentationModel(SerializablePage source, boolean localTime, boolean reduceNames, URLSpanClickListener spanClickListener, ImageGetter imageGetter, Theme theme, FloatingModel[] floatingModels) { if (source.pageModel.type == UrlPageModel.TYPE_OTHERPAGE) throw new IllegalArgumentException(); this.source = source; this.spanClickListener = spanClickListener; this.imageGetter = imageGetter; this.theme = theme; this.floatingModels = floatingModels; this.reduceNames = reduceNames; Database database = MainApplication.getInstance().database; this.isHiddenDelegate = source.pageModel.type == UrlPageModel.TYPE_THREADPAGE ? database.getCachedIsHiddenDelegate(source.pageModel.chanName, source.pageModel.boardName, source.pageModel.threadNumber) : database.getDefaultIsHiddenDelegate(); this.autohideRules = new ArrayList<AutohideActivity.CompiledAutohideRule>(); try { JSONArray autohideJson = new JSONArray(MainApplication.getInstance().settings.getAutohideRulesJson()); for (int i = 0; i < autohideJson.length(); ++i) { AutohideActivity.AutohideRule rule = AutohideActivity.AutohideRule .fromJson(autohideJson.getJSONObject(i)); if (rule.matches(source.pageModel.chanName, source.pageModel.boardName, source.pageModel.threadNumber)) { this.autohideRules.add(new AutohideActivity.CompiledAutohideRule(rule)); } } } catch (Exception e) { Logger.e(TAG, "error while processing regex autohide rules", e); } AndroidDateFormat.initPattern(); String datePattern = AndroidDateFormat.getPattern(); DateFormat dateFormat = datePattern == null ? DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT) : new SimpleDateFormat(datePattern, Locale.US); dateFormat.setTimeZone( localTime ? TimeZone.getDefault() : TimeZone.getTimeZone(source.boardModel.timeZoneId)); this.dateFormat = dateFormat; this.size = getSerializablePageSize(source); } /** * ? ? (? ??), ? {@link #size} * @param model ? */ public PresentationModel(PresentationModel model) { source = model.source; spanClickListener = model.spanClickListener; imageGetter = model.imageGetter; theme = model.theme; floatingModels = model.floatingModels; dateFormat = model.dateFormat; reduceNames = model.reduceNames; isHiddenDelegate = model.isHiddenDelegate; autohideRules = model.autohideRules; size = getSerializablePageSize(source); presentationList = model.presentationList; attachments = model.attachments; postNumbersMap = model.postNumbersMap; notReady = model.notReady; } public interface RebuildCallback { void onRebuild(); } /** * ? ? */ private static int getSerializablePageSize(SerializablePage page) { int size = 32, noPresentationSize = 0; if (page.posts != null) { size += (12 + (page.posts.length * 4)); for (PostModel post : page.posts) size += ChanModels.getPostModelSize(post); } if (page.threads != null) { size += (12 + (page.threads.length * 4)); for (ThreadModel threadModel : page.threads) { size += (32 + (threadModel.threadNumber == null ? 0 : (40 + (threadModel.threadNumber.length() * 2)))); size += (12 + (threadModel.posts.length * 4)); if (threadModel.posts.length > 0) size += ChanModels.getPostModelSize(threadModel.posts[0]); for (int i = 1; i < threadModel.posts.length; ++i) noPresentationSize += ChanModels.getPostModelSize(threadModel.posts[i]); } } return size * 3 + noPresentationSize; } /** * ? ( ?) * @param showIndex ? ? ( ?) ? * @param task ?? * @param rebuildCallback ? () ?, ? ?? ??? */ public synchronized void updateViewModels(boolean showIndex, CancellableTask task, RebuildCallback rebuildCallback) { PostModel[] posts = source.posts; if (posts == null) { posts = new PostModel[source.threads.length]; for (int i = 0; i < source.threads.length; ++i) { posts[i] = source.threads[i].posts[0]; } } try { updateViewModels(posts, showIndex, task, rebuildCallback); } catch (OutOfMemoryError oom) { MainApplication.freeMemory(); Logger.e(TAG, oom); if (task != null && task.isCancelled()) return; try { updateViewModels(posts, showIndex, task, rebuildCallback); } catch (OutOfMemoryError oom1) { MainApplication.freeMemory(); Logger.e(TAG, oom1); } } } /** * ? ? ? ( ? ? ? ? ?) * @param floatingModels ?? ? ? */ public void setFloatingModels(FloatingModel[] models) { if (!FlowTextHelper.IS_AVAILABLE) return; if (models == null || models.length != 2 || models[0] == null || models[1] == null) return; if (floatingModels == null || floatingModels.length != 2 || floatingModels[0] == null || floatingModels[1] == null) return; if (models[0].equals(floatingModels[0]) && models[1].equals(floatingModels[1])) return; try { this.floatingModels = models; int size = presentationList.size(); for (int i = 0; i < size; ++i) presentationList.get(i).changeFloatingModels(models); } catch (Exception e) { Logger.e(TAG, e); } } private synchronized void updateViewModels(PostModel[] posts, boolean showIndex, CancellableTask task, RebuildCallback rebuildCallback) { if (task == null) task = CancellableTask.NOT_CANCELLABLE; synchronized (lock) { notReady = true; } if (presentationList == null) { presentationList = new ArrayList<PresentationItemModel>(posts.length); // ? ?(?) attachments = new ArrayList<Triple<AttachmentModel, String, String>>(); } if (task.isCancelled()) return; String[] subscriptions = null; if (source.pageModel.type == UrlPageModel.TYPE_THREADPAGE && MainApplication.getInstance().settings.highlightSubscriptions()) { subscriptions = MainApplication.getInstance().subscriptions.getSubscriptions(source.pageModel.chanName, source.pageModel.boardName, source.pageModel.threadNumber); } boolean headersRebuilding = false; int indexCounter = 0; boolean rebuild = false; if (posts.length < presentationList.size()) { rebuild = true; Logger.d(TAG, "rebuild: new list is shorter"); } else { for (int i = 0, size = presentationList.size(); i < size; ++i) { if (!presentationList.get(i).sourceModel.number.equals(posts[i].number) || ChanModels.hashPostModel(posts[i]) != presentationList.get(i).sourceModelHash) { rebuild = true; Logger.d(TAG, "rebuild: changed item " + i); break; } if (showIndex) { if (!posts[i].deleted) ++indexCounter; if (headersRebuilding |= presentationList.get(i).isDeleted != posts[i].deleted) { presentationList.get(i).buildSpannedHeader(!posts[i].deleted ? indexCounter : -1, source.boardModel.bumpLimit, reduceNames ? source.boardModel.defaultUserName : null, source.pageModel.type == UrlPageModel.TYPE_SEARCHPAGE ? posts[i].parentThread : null, subscriptions != null ? Arrays.binarySearch(subscriptions, posts[i].number) >= 0 : false); } } presentationList.get(i).isDeleted = posts[i].deleted; } } if (task.isCancelled()) return; if (rebuild) { if (rebuildCallback != null) rebuildCallback.onRebuild(); presentationList.clear(); postNumbersMap.clear(); attachments.clear(); indexCounter = 0; } final boolean openSpoilers = MainApplication.getInstance().settings.openSpoilers(); for (int i = presentationList.size(); i < posts.length; ++i) { if (task.isCancelled()) return; PresentationItemModel model = new PresentationItemModel(posts[i], source.pageModel.chanName, source.pageModel.boardName, source.pageModel.type == UrlPageModel.TYPE_THREADPAGE ? source.pageModel.threadNumber : null, dateFormat, spanClickListener, imageGetter, ThemeUtils.ThemeColors.getInstance(theme), openSpoilers, floatingModels, subscriptions); postNumbersMap.put(posts[i].number, i); if (source.pageModel.type == UrlPageModel.TYPE_THREADPAGE) { for (String ref : model.referencesTo) { Integer postPosition = postNumbersMap.get(ref); if (postPosition != null && postPosition < presentationList.size()) { presentationList.get(postPosition).addReferenceFrom(model.sourceModel.number); } } } presentationList.add(model); for (int j = 0; j < model.attachmentHashes.length; ++j) { attachments.add(Triple.of(posts[i].attachments[j], model.attachmentHashes[j], posts[i].number)); } model.buildSpannedHeader(showIndex && !posts[i].deleted ? ++indexCounter : -1, source.boardModel.bumpLimit, reduceNames ? source.boardModel.defaultUserName : null, source.pageModel.type == UrlPageModel.TYPE_SEARCHPAGE ? posts[i].parentThread : null, subscriptions != null ? Arrays.binarySearch(subscriptions, posts[i].number) >= 0 : false); if (source.pageModel.type == UrlPageModel.TYPE_THREADPAGE) { model.hidden = isHiddenDelegate.isHidden(source.pageModel.chanName, source.pageModel.boardName, source.pageModel.threadNumber, posts[i].number); } else if (source.pageModel.type == UrlPageModel.TYPE_BOARDPAGE || source.pageModel.type == UrlPageModel.TYPE_CATALOGPAGE) { model.hidden = isHiddenDelegate.isHidden(source.pageModel.chanName, source.pageModel.boardName, posts[i].number, null); } if (!model.hidden && ( //? source.pageModel.type == UrlPageModel.TYPE_THREADPAGE || source.pageModel.type == UrlPageModel.TYPE_BOARDPAGE || source.pageModel.type == UrlPageModel.TYPE_CATALOGPAGE)) { for (AutohideActivity.CompiledAutohideRule rule : autohideRules) { if ((rule.inComment && model.spannedComment != null && rule.pattern.matcher(model.spannedComment).find()) || (rule.inSubject && posts[i].subject != null && rule.pattern.matcher(posts[i].subject).find()) || (rule.inName && (posts[i].name != null && rule.pattern.matcher(posts[i].name).find()) || (posts[i].trip != null && rule.pattern.matcher(posts[i].trip).find()))) { model.hidden = true; model.autohideReason = rule.regex; } } } } if (source.pageModel.type == UrlPageModel.TYPE_THREADPAGE) { for (PresentationItemModel model : presentationList) { if (task.isCancelled()) return; model.buildReferencesString(); } } if (source.threads != null) { for (int i = 0; i < source.threads.length; ++i) { if (task.isCancelled()) return; presentationList.get(i).buildPostsCountString(source.threads[i].postsCount, source.threads[i].attachmentsCount); presentationList.get(i).buildStickyClosedString(source.threads[i].isSticky, source.threads[i].isClosed); } } notReady = false; } /** * true, ? ? ( ? ) */ public boolean isNotReady() { return notReady; } /** * ? notReady true (, , ? ) */ public void setNotReady() { notReady = true; } /** * ?? ?, {@link Triple} ?, ? ? ?, ??? * @return ?? null, ? ? ??? */ public List<Triple<AttachmentModel, String, String>> getAttachments() { if (notReady || attachments == null) return null; synchronized (lock) { if (notReady) return null; return new ArrayList<Triple<AttachmentModel, String, String>>(attachments); } } /** * ?? ? ({@link #presentationList}), ?, ? ? * ( ? ? null) */ public List<PresentationItemModel> getSafePresentationList() { if (notReady || presentationList == null) return null; synchronized (lock) { if (notReady) return null; return new ArrayList<PresentationItemModel>(presentationList); } } }