/** * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. */ package com.ubergeek42.WeechatAndroid.service; import android.content.Context; import android.content.SharedPreferences; import android.graphics.Paint; import android.graphics.Typeface; import android.preference.PreferenceManager; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v7.preference.FilePreference; import android.support.v7.preference.ThemeManager; import android.text.TextUtils; import com.ubergeek42.WeechatAndroid.R; import com.ubergeek42.WeechatAndroid.relay.Buffer; import com.ubergeek42.WeechatAndroid.relay.BufferList; import com.ubergeek42.WeechatAndroid.utils.Utils; import com.ubergeek42.weechat.Color; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.Serializable; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.Locale; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSocketFactory; import static com.ubergeek42.WeechatAndroid.utils.Constants.*; public class P implements SharedPreferences.OnSharedPreferenceChangeListener{ final private static Logger logger = LoggerFactory.getLogger("P"); final private static boolean DEBUG_PREFS = true; final private static boolean DEBUG_SAVE_RESTORE = true; private static Context context; private static SharedPreferences p; // we need to keep a reference, huh @SuppressWarnings({"FieldCanBeLocal", "unused"}) private static P instance; public static void init(@NonNull Context context) { if (P.context != null) return; P.context = context; p = PreferenceManager.getDefaultSharedPreferences(context); loadUIPreferences(); p.registerOnSharedPreferenceChangeListener(instance = new P()); } ///////////////////////////////////////////////////////////////////////////////////////////// ui public static boolean sortBuffers, showTitle, filterBuffers, optimizeTraffic; public static boolean filterLines, autoHideActionbar; public static int maxWidth; public static boolean encloseNick, dimDownNonHumanLines; public static @Nullable DateFormat dateFormat; public static int align; public static float textSize, letterWidth; public static boolean notificationEnable, notificationTicker, notificationLight, notificationVibrate; public static String notificationSound; public static boolean showSend, showTab, hotlistSync, volumeBtnSize; public static String bufferFont; public static boolean showBufferFilter; public static void loadUIPreferences() { // buffer list preferences sortBuffers = p.getBoolean(PREF_SORT_BUFFERS, PREF_SORT_BUFFERS_D); showTitle = p.getBoolean(PREF_SHOW_BUFFER_TITLES, PREF_SHOW_BUFFER_TITLES_D); filterBuffers = p.getBoolean(PREF_FILTER_NONHUMAN_BUFFERS, PREF_FILTER_NONHUMAN_BUFFERS_D); optimizeTraffic = p.getBoolean(PREF_OPTIMIZE_TRAFFIC, PREF_OPTIMIZE_TRAFFIC_D); // okay this is out of sync with onChanged stuff—used for the bell icon // buffer-wide preferences filterLines = p.getBoolean(PREF_FILTER_LINES, PREF_FILTER_LINES_D); autoHideActionbar = p.getBoolean(PREF_AUTO_HIDE_ACTIONBAR, PREF_AUTO_HIDE_ACTIONBAR_D); maxWidth = Integer.parseInt(p.getString(PREF_MAX_WIDTH, PREF_MAX_WIDTH_D)); encloseNick = p.getBoolean(PREF_ENCLOSE_NICK, PREF_ENCLOSE_NICK_D); dimDownNonHumanLines = p.getBoolean(PREF_DIM_DOWN, PREF_DIM_DOWN_D); setTimestampFormat(); setAlignment(); setTextSizeAndLetterWidth(); ThemeManager.loadColorSchemeFromPreferences(context); // notifications notificationEnable = p.getBoolean(PREF_NOTIFICATION_ENABLE, PREF_NOTIFICATION_ENABLE_D); notificationSound = p.getString(PREF_NOTIFICATION_SOUND, PREF_NOTIFICATION_SOUND_D); notificationTicker = p.getBoolean(PREF_NOTIFICATION_TICKER, PREF_NOTIFICATION_TICKER_D); notificationLight = p.getBoolean(PREF_NOTIFICATION_LIGHT, PREF_NOTIFICATION_LIGHT_D); notificationVibrate = p.getBoolean(PREF_NOTIFICATION_VIBRATE, PREF_NOTIFICATION_VIBRATE_D); // buffer fragment showSend = p.getBoolean(PREF_SHOW_SEND, PREF_SHOW_SEND_D); showTab = p.getBoolean(PREF_SHOW_TAB, PREF_SHOW_TAB_D); hotlistSync = p.getBoolean(PREF_HOTLIST_SYNC, PREF_HOTLIST_SYNC_D); volumeBtnSize = p.getBoolean(PREF_VOLUME_BTN_SIZE, PREF_VOLUME_BTN_SIZE_D); // buffer list filter showBufferFilter = p.getBoolean(PREF_SHOW_BUFFER_FILTER, PREF_SHOW_BUFFER_FILTER_D); } ///////////////////////////////////////////////////////////////////////////////////// connection public static String host, wsPath, pass, connectionType, sshHost, sshUser, sshPass; public static byte[] sshKey, sshKnownHosts; public static int port, sshPort; public static SSLSocketFactory sslSocketFactory; public static boolean reconnect; public static boolean pingEnabled; public static long pingIdleTime, pingTimeout; public static int lineIncrement; public static String printableHost; public static void loadConnectionPreferences() { host = p.getString(PREF_HOST, PREF_HOST_D); pass = p.getString(PREF_PASSWORD, PREF_PASSWORD_D); port = Integer.parseInt(p.getString(PREF_PORT, PREF_PORT_D)); wsPath = p.getString(PREF_WS_PATH, PREF_WS_PATH_D); connectionType = p.getString(PREF_CONNECTION_TYPE, PREF_CONNECTION_TYPE_D); sshHost = p.getString(PREF_SSH_HOST, PREF_SSH_HOST_D); sshPort = Integer.valueOf(p.getString(PREF_SSH_PORT, PREF_SSH_PORT_D)); sshUser = p.getString(PREF_SSH_USER, PREF_SSH_USER_D); sshPass = p.getString(PREF_SSH_PASS, PREF_SSH_PASS_D); sshKey = FilePreference.getData(p.getString(PREF_SSH_KEY, PREF_SSH_KEY_D)); sshKnownHosts = FilePreference.getData(p.getString(PREF_SSH_KNOWN_HOSTS, PREF_SSH_KNOWN_HOSTS_D)); lineIncrement = Integer.parseInt(p.getString(PREF_LINE_INCREMENT, PREF_LINE_INCREMENT_D)); reconnect = p.getBoolean(PREF_RECONNECT, PREF_RECONNECT_D); optimizeTraffic = p.getBoolean(PREF_OPTIMIZE_TRAFFIC, PREF_OPTIMIZE_TRAFFIC_D); pingEnabled = p.getBoolean(PREF_PING_ENABLED, PREF_PING_ENABLED_D); pingIdleTime = Integer.parseInt(p.getString(PREF_PING_IDLE, PREF_PING_IDLE_D)) * 1000; pingTimeout = Integer.parseInt(p.getString(PREF_PING_TIMEOUT, PREF_PING_TIMEOUT_D)) * 1000; if (Utils.isAnyOf(connectionType, PREF_TYPE_SSL, PREF_TYPE_WEBSOCKET_SSL)) { sslSocketFactory = SSLHandler.getInstance(context).getSSLSocketFactory(); } else { sslSocketFactory = null; } printableHost = connectionType.equals(PREF_TYPE_SSH) ? sshHost + "/" + host : host; } public static @Nullable String validateConnectionPreferences() { if (TextUtils.isEmpty(host)) return context.getString(R.string.pref_error_relay_host_not_set); if (TextUtils.isEmpty(pass)) return context.getString(R.string.pref_error_relay_password_not_set); if (connectionType.equals(PREF_TYPE_SSH)) { if (TextUtils.isEmpty(sshHost)) return context.getString(R.string.pref_error_ssh_host_not_set); if (Utils.isEmpty(sshKey) && TextUtils.isEmpty(sshPass)) return context.getString(R.string.pref_error_no_ssh_key); if (Utils.isEmpty(sshKnownHosts)) return context.getString(R.string.pref_error_no_ssh_known_hosts); } return null; } //////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////// @Override public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { if (DEBUG_PREFS) logger.debug("onSharedPreferenceChanged()"); switch (key) { // buffer list preferences case PREF_SORT_BUFFERS: sortBuffers = p.getBoolean(key, PREF_SORT_BUFFERS_D); break; case PREF_SHOW_BUFFER_TITLES: showTitle = p.getBoolean(key, PREF_SHOW_BUFFER_TITLES_D); break; case PREF_FILTER_NONHUMAN_BUFFERS: filterBuffers = p.getBoolean(key, PREF_FILTER_NONHUMAN_BUFFERS_D); break; case PREF_AUTO_HIDE_ACTIONBAR: autoHideActionbar = p.getBoolean(key, PREF_AUTO_HIDE_ACTIONBAR_D); break; // buffer-wide preferences case PREF_FILTER_LINES: filterLines = p.getBoolean(key, PREF_FILTER_LINES_D); break; case PREF_MAX_WIDTH: maxWidth = Integer.parseInt(p.getString(key, PREF_MAX_WIDTH_D)); BufferList.notifyOpenBuffersMustBeProcessed(false); break; case PREF_ENCLOSE_NICK: encloseNick = p.getBoolean(key, PREF_ENCLOSE_NICK_D); BufferList.notifyOpenBuffersMustBeProcessed(false); break; case PREF_DIM_DOWN: dimDownNonHumanLines = p.getBoolean(key, PREF_DIM_DOWN_D); BufferList.notifyOpenBuffersMustBeProcessed(true); break; case PREF_TIMESTAMP_FORMAT: setTimestampFormat(); BufferList.notifyOpenBuffersMustBeProcessed(false); break; case PREF_PREFIX_ALIGN: setAlignment(); BufferList.notifyOpenBuffersMustBeProcessed(false); break; case PREF_TEXT_SIZE: case PREF_BUFFER_FONT: setTextSizeAndLetterWidth(); BufferList.notifyOpenBuffersMustBeProcessed(true); break; case PREF_COLOR_SCHEME: ThemeManager.loadColorSchemeFromPreferences(context); BufferList.notifyOpenBuffersMustBeProcessed(true); break; // notifications case PREF_NOTIFICATION_ENABLE: notificationEnable = p.getBoolean(key, PREF_NOTIFICATION_ENABLE_D); break; case PREF_NOTIFICATION_SOUND: notificationSound = p.getString(key, PREF_NOTIFICATION_SOUND_D); break; case PREF_NOTIFICATION_TICKER: notificationTicker = p.getBoolean(key, PREF_NOTIFICATION_TICKER_D); break; case PREF_NOTIFICATION_LIGHT: notificationLight = p.getBoolean(key, PREF_NOTIFICATION_LIGHT_D); break; case PREF_NOTIFICATION_VIBRATE: notificationVibrate = p.getBoolean(key, PREF_NOTIFICATION_VIBRATE_D); break; // buffer fragment case PREF_SHOW_SEND: showSend = p.getBoolean(key, PREF_SHOW_SEND_D); break; case PREF_SHOW_TAB: showTab = p.getBoolean(key, PREF_SHOW_TAB_D); break; case PREF_HOTLIST_SYNC: hotlistSync = p.getBoolean(key, PREF_HOTLIST_SYNC_D); break; case PREF_VOLUME_BTN_SIZE: volumeBtnSize = p.getBoolean(key, PREF_VOLUME_BTN_SIZE_D); break; // buffer list fragment case PREF_SHOW_BUFFER_FILTER: showBufferFilter = p.getBoolean(key, PREF_SHOW_BUFFER_FILTER_D); break; } } //////////////////////////////////////////////////////////////////////////////////////////////// private static void setTimestampFormat() { String t = p.getString(PREF_TIMESTAMP_FORMAT, PREF_TIMESTAMP_FORMAT_D); dateFormat = (TextUtils.isEmpty(t)) ? null : new SimpleDateFormat(t, Locale.US); } private static void setAlignment() { String alignment = p.getString(PREF_PREFIX_ALIGN, PREF_PREFIX_ALIGN_D); switch (alignment) { case "right": align = Color.ALIGN_RIGHT; break; case "left": align = Color.ALIGN_LEFT; break; case "timestamp": align = Color.ALIGN_TIMESTAMP; break; default: align = Color.ALIGN_NONE; break; } } private static void setTextSizeAndLetterWidth() { textSize = Float.parseFloat(p.getString(PREF_TEXT_SIZE, PREF_TEXT_SIZE_D)); bufferFont = p.getString(PREF_BUFFER_FONT, PREF_BUFFER_FONT_D); Paint paint = new Paint(); Typeface tf = Typeface.MONOSPACE; try {tf = Typeface.createFromFile(bufferFont);} catch (Exception ignored) {} paint.setTypeface(tf); paint.setTextSize(textSize * context.getResources().getDisplayMetrics().scaledDensity); letterWidth = (paint.measureText("m")); } public static void setTextSizeAndLetterWidth(float size) { p.edit().putString(PREF_TEXT_SIZE, Float.toString(size)).apply(); } //////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////// final private static String ALIVE = "alive"; public static boolean isServiceAlive() { return p.getBoolean(ALIVE, false); } public static void setServiceAlive(boolean alive) { p.edit().putBoolean(ALIVE, alive).apply(); } //////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////// save/restore //////////////////////////////////////////////////////////////////////////////////////////////// private final static String PREF_DATA = "sb"; private final static String PREF_PROTOCOL_ID = "pid"; // protocol must be changed each time anything that uses the following function changes // needed to make sure nothing crashes if we cannot restore the data public static final int PROTOCOL_ID = 9; public static void saveStuff() { if (DEBUG_SAVE_RESTORE) logger.debug("saveStuff()"); for (Buffer buffer : BufferList.buffers) saveLastReadLine(buffer); String data = Utils.serialize(new Object[]{openBuffers, bufferToLastReadLine, sentMessages}); p.edit().putString(PREF_DATA, data).putInt(PREF_PROTOCOL_ID, PROTOCOL_ID).apply(); } @SuppressWarnings("unchecked") public static void restoreStuff() { if (DEBUG_SAVE_RESTORE) logger.debug("restoreStuff()"); if (p.getInt(PREF_PROTOCOL_ID, -1) != PROTOCOL_ID) return; Object o = Utils.deserialize(p.getString(PREF_DATA, null)); if (!(o instanceof Object[])) return; Object[] array = (Object[]) o; if (array[0] instanceof LinkedHashSet) openBuffers = (LinkedHashSet<String>) array[0]; if (array[1] instanceof LinkedHashMap) bufferToLastReadLine = (LinkedHashMap<String, BufferHotData>) array[1]; if (array[2] instanceof LinkedList) sentMessages = (LinkedList<String>) array[2]; } //////////////////////////////////////////////////////////////////////////////////////////////// // contains names of open buffers. needs more synchronization? static public @NonNull LinkedHashSet<String> openBuffers = new LinkedHashSet<>(); // this stores information about last read line (in `desktop` weechat) and according number of // read lines/highlights. this is subtracted from highlight counts client receives from the server static private @NonNull LinkedHashMap<String, BufferHotData> bufferToLastReadLine = new LinkedHashMap<>(); synchronized public static boolean isBufferOpen(String name) { return openBuffers.contains(name); } synchronized public static void setBufferOpen(String name, boolean open) { if (open) openBuffers.add(name); else openBuffers.remove(name); } //////////////////////////////////////////////////////////////////////////////////////////////// private static class BufferHotData implements Serializable { long readMarkerLine = -1; long lastReadLineServer = -1; int totalOldUnreads = 0; int totalOldHighlights = 0; } //////////////////////////////////////////////////////////////////////////////////////////////// // restore buffer's stuff. this is called for every buffer upon buffer creation synchronized public static void restoreLastReadLine(Buffer buffer) { BufferHotData data = bufferToLastReadLine.get(buffer.fullName); if (data != null) { buffer.readMarkerLine = data.readMarkerLine; buffer.lastReadLineServer = data.lastReadLineServer; buffer.totalReadUnreads = data.totalOldUnreads; buffer.totalReadHighlights = data.totalOldHighlights; } } // save buffer's stuff. this is called when information is about to be written to disk synchronized static void saveLastReadLine(Buffer buffer) { BufferHotData data = bufferToLastReadLine.get(buffer.fullName); if (data == null) { data = new BufferHotData(); bufferToLastReadLine.put(buffer.fullName, data); } data.readMarkerLine = buffer.readMarkerLine; data.lastReadLineServer = buffer.lastReadLineServer; data.totalOldUnreads = buffer.totalReadUnreads; data.totalOldHighlights = buffer.totalReadHighlights; } //////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////// saving messages //////////////////////////////////////////////////////////////////////////////////////////////// public static LinkedList<String> sentMessages = new LinkedList<>(); public static void addSentMessage(String line) { for (Iterator<String> it = sentMessages.iterator(); it.hasNext();) { String s = it.next(); if (line.equals(s)) it.remove(); } sentMessages.add(Utils.cut(line, 2000)); if (sentMessages.size() > 40) sentMessages.pop(); } }