/* * 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.api; import java.io.InputStream; import java.io.OutputStream; import nya.miku.wishmaster.R; import nya.miku.wishmaster.api.interfaces.CancellableTask; import nya.miku.wishmaster.api.interfaces.ProgressListener; import nya.miku.wishmaster.api.models.CaptchaModel; import nya.miku.wishmaster.api.models.DeletePostModel; import nya.miku.wishmaster.api.models.PostModel; import nya.miku.wishmaster.api.models.SendPostModel; import nya.miku.wishmaster.api.models.ThreadModel; import nya.miku.wishmaster.api.models.UrlPageModel; import nya.miku.wishmaster.api.util.CryptoUtils; import nya.miku.wishmaster.api.util.LazyPreferences; import nya.miku.wishmaster.common.Logger; import nya.miku.wishmaster.http.client.ExtendedHttpClient; import nya.miku.wishmaster.http.streamer.HttpRequestModel; import nya.miku.wishmaster.http.streamer.HttpResponseModel; import nya.miku.wishmaster.http.streamer.HttpStreamer; import nya.miku.wishmaster.lib.org_json.JSONArray; import nya.miku.wishmaster.lib.org_json.JSONObject; import cz.msebera.android.httpclient.HttpHost; import cz.msebera.android.httpclient.client.HttpClient; import cz.msebera.android.httpclient.cookie.Cookie; import android.content.Context; import android.content.SharedPreferences; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.net.Uri; import android.os.Bundle; import android.preference.CheckBoxPreference; import android.preference.EditTextPreference; import android.preference.Preference; import android.preference.PreferenceCategory; import android.preference.PreferenceGroup; import android.preference.Preference.OnPreferenceChangeListener; import android.text.InputFilter; import android.text.InputType; public abstract class AbstractChanModule implements HttpChanModule { private static final String TAG = "AbstractChanModule"; private static final String preferenceKeySplit = "_"; protected static final String DEFAULT_PROXY_HOST = "127.0.0.1"; protected static final String DEFAULT_PROXY_PORT = "8118"; protected static final String PREF_KEY_USE_PROXY = "PREF_KEY_USE_PROXY"; protected static final String PREF_KEY_PROXY_HOST = "PREF_KEY_PROXY_HOST"; protected static final String PREF_KEY_PROXY_PORT = "PREF_KEY_PROXY_PORT"; protected static final String PREF_KEY_PASSWORD = "PREF_KEY_PASSWORD"; protected static final String PREF_KEY_USE_HTTPS = "PREF_KEY_USE_HTTPS"; protected static final String PREF_KEY_ONLY_NEW_POSTS = "PREF_KEY_ONLY_NEW_POSTS"; /** * Основной HTTP-клиент */ protected ExtendedHttpClient httpClient; /** * Объект ресурсов */ protected final Resources resources; /** * Объект общих параметров. * При установке/чтении параметров (в т.ч. добавлении на экран настроек), во избежание конфликта между параметрами разных модулей, * используйте ключи, полученные методом {@link #getSharedKey(String)} */ protected final SharedPreferences preferences; /** * Слушатель, следящий за изменением настроек, при изменении которых необходимо обновить (создать новый) HTTP клиент */ private OnPreferenceChangeListener updateHttpListener = new OnPreferenceChangeListener() { @Override public boolean onPreferenceChange(Preference preference, Object newValue) { boolean useProxy = preferences.getBoolean(getSharedKey(PREF_KEY_USE_PROXY), false); String proxyHost = preferences.getString(getSharedKey(PREF_KEY_PROXY_HOST), DEFAULT_PROXY_HOST); String proxyPort = preferences.getString(getSharedKey(PREF_KEY_PROXY_PORT), DEFAULT_PROXY_PORT); if (preference.getKey().equals(getSharedKey(PREF_KEY_USE_PROXY))) { useProxy = (boolean)newValue; updateHttpClient(useProxy, proxyHost, proxyPort); return true; } else if (preference.getKey().equals(getSharedKey(PREF_KEY_PROXY_HOST))) { if (!proxyHost.equals((String)newValue)) { proxyHost = (String)newValue; updateHttpClient(useProxy, proxyHost, proxyPort); } return true; } else if (preference.getKey().equals(getSharedKey(PREF_KEY_PROXY_PORT))) { if (!proxyPort.equals((String)newValue)) { proxyPort = (String)newValue; updateHttpClient(useProxy, proxyHost, proxyPort); } return true; } return false; } }; public AbstractChanModule(SharedPreferences preferences, Resources resources) { this.preferences = preferences; this.resources = resources; updateHttpClient( preferences.getBoolean(getSharedKey(PREF_KEY_USE_PROXY), false), preferences.getString(getSharedKey(PREF_KEY_PROXY_HOST), DEFAULT_PROXY_HOST), preferences.getString(getSharedKey(PREF_KEY_PROXY_PORT), DEFAULT_PROXY_PORT)); } /** * Получить ключ общих параметров, относящийся конкретно к данному модулю (ChanModule) * @param key внутренний ключ параметра * @return ключ общих параметров */ protected final String getSharedKey(String key) { return getChanName() + preferenceKeySplit + key; } /** * Обновить (создать новый) HTTP-клиент * @param useProxy использовать прокси * @param proxyHost адрес прокси-сервера, если useProxy true * @param proxyPort порт прокси-сервера, если useProxy true */ private void updateHttpClient(boolean useProxy, String proxyHost, String proxyPort) { HttpHost proxy = null; if (useProxy) { try { int port = Integer.parseInt(proxyPort); proxy = new HttpHost(proxyHost, port); } catch (Exception e) { Logger.e(TAG, e); } } if (httpClient != null) { try { httpClient.close(); } catch (Exception e) { Logger.e(TAG, e); } } httpClient = new ExtendedHttpClient(proxy); initHttpClient(); } /** * Метод вызывается после создания (нового) объекта HttpClient, эта реализация не делает ничего (пустой метод). * Может быть переопределёт в подклассе, например, устанавливать сохранённые cookies. * Необходимо учитывать, что метод вызывается в т.ч. из конструктора, поэтому не следует использовать нестатические поля, определённые в подклассе. */ protected void initHttpClient() {} /** * Добавить в группу параметров (на экран/в категорию) новую категорию настроек прокси-сервера * @param group группа, на которую добавляются параметры */ protected void addProxyPreferences(PreferenceGroup group) { final Context context = group.getContext(); PreferenceCategory proxyCat = new PreferenceCategory(context); //категория настроек прокси proxyCat.setTitle(R.string.pref_cat_proxy); group.addPreference(proxyCat); CheckBoxPreference useProxyPref = new LazyPreferences.CheckBoxPreference(context); //чекбокс "использовать ли прокси вообще" useProxyPref.setTitle(R.string.pref_use_proxy); useProxyPref.setSummary(R.string.pref_use_proxy_summary); useProxyPref.setKey(getSharedKey(PREF_KEY_USE_PROXY)); useProxyPref.setDefaultValue(false); useProxyPref.setOnPreferenceChangeListener(updateHttpListener); proxyCat.addPreference(useProxyPref); EditTextPreference proxyHostPref = new LazyPreferences.EditTextPreference(context); //поле ввода адреса прокси-сервера proxyHostPref.setTitle(R.string.pref_proxy_host); proxyHostPref.setDialogTitle(R.string.pref_proxy_host); proxyHostPref.setSummary(R.string.pref_proxy_host_summary); proxyHostPref.setKey(getSharedKey(PREF_KEY_PROXY_HOST)); proxyHostPref.setDefaultValue(DEFAULT_PROXY_HOST); proxyHostPref.getEditText().setSingleLine(); proxyHostPref.getEditText().setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_URI); proxyHostPref.setOnPreferenceChangeListener(updateHttpListener); proxyCat.addPreference(proxyHostPref); proxyHostPref.setDependency(getSharedKey(PREF_KEY_USE_PROXY)); EditTextPreference proxyHostPort = new LazyPreferences.EditTextPreference(context); //поле ввода порта прокси-сервера proxyHostPort.setTitle(R.string.pref_proxy_port); proxyHostPort.setDialogTitle(R.string.pref_proxy_port); proxyHostPort.setSummary(R.string.pref_proxy_port_summary); proxyHostPort.setKey(getSharedKey(PREF_KEY_PROXY_PORT)); proxyHostPort.setDefaultValue(DEFAULT_PROXY_PORT); proxyHostPort.getEditText().setSingleLine(); proxyHostPort.getEditText().setInputType(InputType.TYPE_CLASS_NUMBER); proxyHostPort.setOnPreferenceChangeListener(updateHttpListener); proxyCat.addPreference(proxyHostPort); proxyHostPort.setDependency(getSharedKey(PREF_KEY_USE_PROXY)); } /** * Добавить в группу параметров (на экран/в категорию) параметр задания пароля для удаления постов/файлов * @param group группа, на которую добавляется параметр */ protected void addPasswordPreference(PreferenceGroup group) { final Context context = group.getContext(); EditTextPreference passwordPref = new EditTextPreference(context) { @Override protected void showDialog(Bundle state) { if (createPassword()) { setText(getDefaultPassword()); } super.showDialog(state); } }; passwordPref.setTitle(R.string.pref_password_title); passwordPref.setDialogTitle(R.string.pref_password_title); passwordPref.setSummary(R.string.pref_password_summary); passwordPref.setKey(getSharedKey(PREF_KEY_PASSWORD)); passwordPref.getEditText().setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD); passwordPref.getEditText().setSingleLine(); passwordPref.getEditText().setFilters(new InputFilter[] { new InputFilter.LengthFilter(255) }); group.addPreference(passwordPref); } @Override public HttpClient getHttpClient() { return httpClient; } @Override public void saveCookie(Cookie cookie) { if (cookie != null) { httpClient.getCookieStore().addCookie(cookie); } } @Override public void addPreferencesOnScreen(PreferenceGroup preferenceGroup) { addPasswordPreference(preferenceGroup); addProxyPreferences(preferenceGroup); } /** * Добавить в группу параметров (на экран/в категорию) настройку выбора HTTPS (защищённого соединения). * Для хранения используется ключ общих параметров {@link #PREF_KEY_USE_HTTPS} ({@link #getSharedKey(String)}). * См. также: {@link #useHttps(boolean)} - для получения значения параметра. * @param group группа, на которую добавляется параметр * @param defaultValue значение параметра по умолчанию * return объект {@link CheckBoxPreference} с параметром */ protected CheckBoxPreference addHttpsPreference(PreferenceGroup group, boolean defaultValue) { final Context context = group.getContext(); CheckBoxPreference httpsPref = new LazyPreferences.CheckBoxPreference(context); httpsPref.setTitle(R.string.pref_use_https); httpsPref.setSummary(R.string.pref_use_https_summary); httpsPref.setKey(getSharedKey(PREF_KEY_USE_HTTPS)); httpsPref.setDefaultValue(defaultValue); group.addPreference(httpsPref); return httpsPref; } /** * Определить значение параметра использования HTTPS (защищённого соединения) из ключа общих настроек {@link #PREF_KEY_USE_HTTPS}. * Настройка добавляется на экран (в группу) настроек методом {@link #addHttpsPreference(PreferenceGroup, boolean)}. * @param defaultValue значение параметра по умолчанию * @return значение параметра */ protected boolean useHttps(boolean defaultValue) { return preferences.getBoolean(getSharedKey(PREF_KEY_USE_HTTPS), defaultValue); } /** * Добавить в группу параметров (на экран/в категорию) настройку выбора использования инкрементальной загрузки (загрузки только новых постов). * Для хранения используется ключ общих параметров {@link #PREF_KEY_ONLY_NEW_POSTS} ({@link #getSharedKey(String)}). * См. также: {@link #loadOnlyNewPosts(boolean)} - для получения значения параметра. * @param group группа, на которую добавляется параметр * @param defaultValue значение параметра по умолчанию * return объект {@link CheckBoxPreference} с параметром */ protected CheckBoxPreference addOnlyNewPostsPreference(PreferenceGroup group, boolean defaultValue) { final Context context = group.getContext(); CheckBoxPreference onlyNewPostsPref = new LazyPreferences.CheckBoxPreference(context); onlyNewPostsPref.setTitle(R.string.pref_only_new_posts); onlyNewPostsPref.setSummary(R.string.pref_only_new_posts_summary); onlyNewPostsPref.setKey(getSharedKey(PREF_KEY_ONLY_NEW_POSTS)); onlyNewPostsPref.setDefaultValue(defaultValue); group.addPreference(onlyNewPostsPref); return onlyNewPostsPref; } /** * Определить значение параметра использования инкрементальной загрузки из ключа общих настроек {@link #PREF_KEY_ONLY_NEW_POSTS}. * Настройка добавляется на экран (в группу) настроек методом {@link #addOnlyNewPostsPreference(PreferenceGroup, boolean)}. * @param defaultValue значение параметра по умолчанию * @return значение параметра */ protected boolean loadOnlyNewPosts(boolean defaultValue) { return preferences.getBoolean(getSharedKey(PREF_KEY_ONLY_NEW_POSTS), defaultValue); } private boolean createPassword() { if (!preferences.contains(getSharedKey(PREF_KEY_PASSWORD))) { preferences.edit().putString(getSharedKey(PREF_KEY_PASSWORD), CryptoUtils.genPassword()).commit(); return true; } return false; } @Override public String getDefaultPassword() { createPassword(); return preferences.getString(getSharedKey(PREF_KEY_PASSWORD), ""); } @Override public String fixRelativeUrl(String url) { if (url == null) return null; if (Uri.parse(url).getScheme() != null) return url; UrlPageModel model = new UrlPageModel(); model.chanName = getChanName(); model.type = UrlPageModel.TYPE_OTHERPAGE; model.otherPath = url; return buildUrl(model); } @Override public ThreadModel[] getCatalog( String boardName, int catalogType, ProgressListener listener, CancellableTask task, ThreadModel[] oldList) throws Exception { throw new UnsupportedOperationException(); } @Override public PostModel[] search(String boardName, String searchRequest, ProgressListener listener, CancellableTask task) throws Exception { throw new UnsupportedOperationException(); } @Override public CaptchaModel getNewCaptcha(String boardName, String threadNumber, ProgressListener listener, CancellableTask task) throws Exception { return null; } @Override public String sendPost(SendPostModel model, ProgressListener listener, CancellableTask task) throws Exception { throw new UnsupportedOperationException(); } @Override public String deletePost(DeletePostModel model, ProgressListener listener, CancellableTask task) throws Exception { throw new UnsupportedOperationException(); } @Override public String reportPost(DeletePostModel model, ProgressListener listener, CancellableTask task) throws Exception { throw new UnsupportedOperationException(); } @Override public void downloadFile(String url, OutputStream out, ProgressListener listener, CancellableTask task) throws Exception { String fixedUrl = fixRelativeUrl(url); HttpStreamer.getInstance().downloadFileFromUrl(fixedUrl, out, HttpRequestModel.DEFAULT_GET, httpClient, listener, task, false); } /** * Скачать JSON-объект по ссылке * @param url абсолютный URL * @param checkIfModidied не загружать, если данные не изменились с прошлого запроса (HTTP 304), в этом случае вернёт null * @param listener интерфейс отслеживания прогресса (может принимать null) * @param task задача, отмена которой прервёт поток (может принимать null) * @return объект JSONObject, или NULL, если страница не была изменена (HTTP 304) */ protected JSONObject downloadJSONObject(String url, boolean checkIfModidied, ProgressListener listener, CancellableTask task) throws Exception { HttpRequestModel rqModel = HttpRequestModel.builder().setGET().setCheckIfModified(checkIfModidied).build(); JSONObject object = HttpStreamer.getInstance().getJSONObjectFromUrl(url, rqModel, httpClient, listener, task, false); if (task != null && task.isCancelled()) throw new Exception("interrupted"); if (listener != null) listener.setIndeterminate(); return object; } /** * Скачать JSON-массив по ссылке * @param url абсолютный URL * @param checkIfModidied не загружать, если данные не изменились с прошлого запроса (HTTP 304), в этом случае вернёт null * @param listener интерфейс отслеживания прогресса (может принимать null) * @param task задача, отмена которой прервёт поток (может принимать null) * @return объект JSONArray, или NULL, если страница не была изменена (HTTP 304) */ protected JSONArray downloadJSONArray(String url, boolean checkIfModidied, ProgressListener listener, CancellableTask task) throws Exception { HttpRequestModel rqModel = HttpRequestModel.builder().setGET().setCheckIfModified(checkIfModidied).build(); JSONArray array = HttpStreamer.getInstance().getJSONArrayFromUrl(url, rqModel, httpClient, listener, task, false); if (task != null && task.isCancelled()) throw new Exception("interrupted"); if (listener != null) listener.setIndeterminate(); return array; } /** * Загрузить капчу по ссылке. * Использется GET-запрос по умолчанию, код состояния HTTP не учитывается (загружается, даже если сервер не вернул HTTP 200). * Тип модели капчи устанавливается: {@link CaptchaModel#TYPE_NORMAL} - допустимы все символы (а не только цифры). * @param captchaUrl абсолютный URL * @param listener интерфейс отслеживания прогресса (может принимать null) * @param task задача, отмена которой прервёт поток (может принимать null) * @return объект CaptchaModel с загруженной картинкой и типом {@link CaptchaModel#TYPE_NORMAL} */ protected CaptchaModel downloadCaptcha(String captchaUrl, ProgressListener listener, CancellableTask task) throws Exception { Bitmap captchaBitmap = null; HttpRequestModel requestModel = HttpRequestModel.DEFAULT_GET; HttpResponseModel responseModel = HttpStreamer.getInstance().getFromUrl(captchaUrl, requestModel, httpClient, listener, task); try { InputStream imageStream = responseModel.stream; captchaBitmap = BitmapFactory.decodeStream(imageStream); } finally { responseModel.release(); } CaptchaModel captchaModel = new CaptchaModel(); captchaModel.type = CaptchaModel.TYPE_NORMAL; captchaModel.bitmap = captchaBitmap; return captchaModel; } }