/*
* Kontalk Android client
* Copyright (C) 2017 Kontalk Devteam <devteam@kontalk.org>
* 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 org.kontalk.util;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import org.kontalk.Kontalk;
import org.kontalk.R;
import org.kontalk.authenticator.Authenticator;
import org.kontalk.client.EndpointServer;
import org.kontalk.client.ServerList;
import org.kontalk.crypto.PersonalKey;
import org.kontalk.provider.Keyring;
import org.kontalk.service.ServerListUpdater;
import org.kontalk.service.msgcenter.MessageCenterService;
import android.annotation.SuppressLint;
import android.content.ContentValues;
import android.content.Context;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Point;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.preference.Preference;
import android.preference.PreferenceManager;
import android.provider.BaseColumns;
import android.support.annotation.VisibleForTesting;
import android.text.TextUtils;
import android.util.Base64;
import android.view.Display;
import android.view.WindowManager;
/**
* Access to application preferences.
* @author Daniele Ricci
*/
public final class Preferences {
private static SharedPreferences sPreferences;
private static Drawable sCustomBackground;
private static String sBalloonTheme;
private static String sBalloonGroupsTheme;
@SuppressLint("CommitPrefEdits")
public static void init(Context context) {
sPreferences = PreferenceManager.getDefaultSharedPreferences(context);
// set the new default theme if this is the first upgrade
String newTheme = context.getString(R.string.pref_default_balloons);
if (!getBooleanOnce("has_new_theme." + newTheme))
sPreferences.edit().putString("pref_balloons", newTheme)
.commit();
}
public static void setCachedCustomBackground(Drawable customBackground) {
sCustomBackground = customBackground;
}
public static void setCachedBalloonTheme(String balloonTheme) {
sBalloonTheme = balloonTheme;
}
public static void setCachedBalloonGroupsTheme(String balloonTheme) {
sBalloonGroupsTheme = balloonTheme;
}
public static void updateServerListLastUpdate(Preference pref, ServerList list) {
Context context = pref.getContext();
String timestamp = MessageUtils.formatTimeStampString(context, list.getDate().getTime(), true);
pref.setSummary(context.getString(R.string.server_list_last_update, timestamp));
}
private static String getString(String key, String defaultValue) {
return sPreferences.getString(key, defaultValue);
}
private static int getInt(String key, int defaultValue) {
return sPreferences.getInt(key, defaultValue);
}
private static int getIntMinValue(String key, int minValue, int defaultValue) {
String val = getString(key, null);
int nval;
try {
nval = Integer.valueOf(val);
}
catch (Exception e) {
nval = defaultValue;
}
return (nval < minValue) ? minValue : nval;
}
private static long getLong(String key, long defaultValue) {
return sPreferences.getLong(key, defaultValue);
}
/** Retrieves a long and if >= 0 it sets it to -1. */
@SuppressLint("CommitPrefEdits")
private static long getLongOnce(String key) {
long value = sPreferences.getLong(key, -1);
if (value >= 0)
sPreferences.edit().putLong(key, -1).commit();
return value;
}
private static boolean getBoolean(String key, boolean defaultValue) {
return sPreferences.getBoolean(key, defaultValue);
}
/** Retrieve a boolean and if false set it to true. */
@SuppressLint("CommitPrefEdits")
private static boolean getBooleanOnce(String key) {
boolean value = sPreferences.getBoolean(key, false);
if (!value)
sPreferences.edit().putBoolean(key, true).commit();
return value;
}
public static boolean setRingtone(String uri) {
return sPreferences.edit()
.putString("pref_ringtone", uri)
.commit();
}
public static String getServerURI() {
return getString("pref_network_uri", null);
}
public static boolean setServerURI(String serverURI) {
return sPreferences.edit()
.putString("pref_network_uri", serverURI)
.commit();
}
/** Returns a random server from the cached list or the user-defined server. */
public static EndpointServer getEndpointServer(Context context) {
String customUri = getServerURI();
if (!TextUtils.isEmpty(customUri)) {
try {
return new EndpointServer(customUri);
}
catch (Exception e) {
// custom is not valid - take one from list
}
}
// return server stored in the default account
return Authenticator.getDefaultServer(context);
}
/** Returns a server provider reflecting the current settings. */
public static EndpointServer.EndpointServerProvider getEndpointServerProvider(Context context) {
final String customUri = getServerURI();
if (!TextUtils.isEmpty(customUri)) {
return new EndpointServer.SingleServerProvider(customUri);
}
else {
ServerList list = ServerListUpdater.getCurrentList(context);
return new ServerList.ServerListProvider(list);
}
}
public static boolean getEncryptionEnabled(Context context) {
return getBoolean("pref_encrypt", context
.getResources().getBoolean(R.bool.pref_default_encrypt));
}
public static boolean getSyncSIMContacts(Context context) {
return getBoolean("pref_sync_sim_contacts", context
.getResources().getBoolean(R.bool.pref_default_sync_sim_contacts));
}
public static boolean getSyncInvisibleContacts(Context context) {
return getBoolean("pref_sync_invisible_contacts", context
.getResources().getBoolean(R.bool.pref_default_sync_invisible_contacts));
}
public static boolean getAutoAcceptSubscriptions(Context context) {
return getBoolean("pref_auto_accept_subscriptions", context
.getResources().getBoolean(R.bool.pref_default_auto_accept_subscriptions));
}
public static boolean getPushNotificationsEnabled(Context context) {
return getBoolean("pref_push_notifications", context
.getResources().getBoolean(R.bool.pref_default_push_notifications));
}
public static boolean getNotificationsEnabled(Context context) {
return getBoolean("pref_enable_notifications", context
.getResources().getBoolean(R.bool.pref_default_enable_notifications));
}
public static String getNotificationVibrate(Context context) {
return getString("pref_vibrate", context
.getString(R.string.pref_default_vibrate));
}
public static String getNotificationRingtone(Context context) {
return getString("pref_ringtone", context
.getString(R.string.pref_default_ringtone));
}
public static boolean getNotificationLED(Context context) {
return getBoolean("pref_enable_notification_led",
context.getResources().getBoolean(R.bool.pref_default_enable_notification_led));
}
public static int getNotificationLEDColor(Context context) {
return getInt("pref_notification_led_color",
context.getResources().getInteger(R.integer.pref_default_notification_led_color));
}
public static boolean setNotificationLEDColor(int color) {
return sPreferences.edit()
.putInt("pref_notification_led_color", color)
.commit();
}
public static int getImageCompression(Context context) {
return Integer.parseInt(getString("pref_image_resize", String
.valueOf(context.getResources().getInteger(R.integer.pref_default_image_resize))));
}
/** Returns true if, as per settings, we can autodownload a file of the given size. */
public static boolean canAutodownloadMedia(Context context, long size) {
int threshold = Integer.parseInt(getString("pref_media_autodownload_threshold", String
.valueOf(context.getResources().getInteger(R.integer.pref_default_media_autodownload_threshold))));
String autodownload = getString("pref_media_autodownload",
context.getResources().getString(R.string.pref_default_media_autodownload));
return (size / 1024) < threshold || "always".equals(autodownload) ||
("wifi".equals(autodownload) && SystemUtils.isOnWifi(context));
}
public static boolean getContactsListVisited() {
return getBooleanOnce("pref_contacts_visited");
}
public static boolean getGroupChatCreateDisclaimer() {
return getBoolean("pref_create_group_disclaimer", true);
}
public static void setGroupChatCreateDisclaimer() {
sPreferences.edit()
.putBoolean("pref_create_group_disclaimer", false)
.apply();
}
public static long getLastSyncTimestamp() {
return getLong("pref_last_sync", -1);
}
public static boolean setLastSyncTimestamp(long timestamp) {
return sPreferences.edit()
.putLong("pref_last_sync", timestamp)
.commit();
}
public static boolean setLastPushNotification(long timestamp) {
return sPreferences.edit()
.putLong("pref_last_push_notification", timestamp)
.commit();
}
public static long getLastPushNotification() {
return getLongOnce("pref_last_push_notification");
}
/** TODO cache value */
public static String getFontSize(Context context) {
return getString("pref_font_size", context
.getString(R.string.pref_default_font_size));
}
public static String getBalloonTheme(Context context) {
if (sBalloonTheme == null)
sBalloonTheme = getString("pref_balloons", context
.getString(R.string.pref_default_balloons));
return sBalloonTheme;
}
public static String getBalloonGroupsTheme(Context context) {
if (sBalloonGroupsTheme == null)
sBalloonGroupsTheme = getString("pref_balloons_groups", context
.getString(R.string.pref_default_balloons_groups));
return sBalloonGroupsTheme;
}
/** Still unused. */
public static boolean getEmojiConverter(Context context) {
return getBoolean("pref_emoji_converter",
context.getResources().getBoolean(R.bool.pref_default_emoji_converter));
}
public static String getStatusMessage() {
return getString("pref_status_message", null);
}
public static void setStatusMessage(String message) {
sPreferences.edit()
.putString("pref_status_message", message)
.apply();
}
/** Loads and stores a cached version of the given conversation background. */
@SuppressWarnings("deprecation")
public static File cacheConversationBackground(Context context, Uri uri) throws IOException {
InputStream in = null;
OutputStream out = null;
try {
WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
Display display = wm.getDefaultDisplay();
int width;
int height;
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.HONEYCOMB_MR2) {
Point size = new Point();
display.getSize(size);
width = size.x;
height = size.y;
}
else {
width = display.getWidth();
height = display.getHeight();
}
BitmapFactory.Options options;
try {
in = context.getContentResolver().openInputStream(uri);
options = MediaStorage.preloadBitmap(in, width, height);
}
catch (Exception e) {
throw new IOException(e);
}
finally {
SystemUtils.closeStream(in);
}
Bitmap bitmap;
try {
// open again
in = context.getContentResolver().openInputStream(uri);
bitmap = BitmapFactory.decodeStream(in, null, options);
}
catch (Exception e) {
throw new IOException(e);
}
finally {
SystemUtils.closeStream(in);
}
Bitmap tn = ThumbnailUtils.extractThumbnail(bitmap, width, height);
bitmap.recycle();
// check for rotation data
tn = MediaStorage.bitmapOrientation(context, uri, tn);
File outFile = new File(context.getFilesDir(), "background.png");
out = new FileOutputStream(outFile);
tn.compress(Bitmap.CompressFormat.PNG, 90, out);
tn.recycle();
return outFile;
}
finally {
SystemUtils.closeStream(out);
}
}
public static Drawable getConversationBackground(Context context) {
InputStream in = null;
try {
if (getBoolean("pref_custom_background", false)) {
if (sCustomBackground == null) {
String _customBg = getString("pref_background_uri", null);
in = context.getContentResolver().openInputStream(Uri.parse(_customBg));
if (in != null) {
Bitmap bmap = BitmapFactory.decodeStream(in, null, null);
sCustomBackground = new BitmapDrawable(context.getResources(), bmap);
}
}
return sCustomBackground;
}
}
catch (Exception e) {
// ignored
}
finally {
SystemUtils.closeStream(in);
}
return null;
}
/**
* Switches offline mode on or off.
* @return offline mode status before the switch
*/
public static boolean switchOfflineMode(Context context) {
boolean old = sPreferences.getBoolean("offline_mode", false);
// set flag again!
boolean offline = !old;
sPreferences.edit()
.putBoolean("offline_mode", offline)
.apply();
if (offline) {
// stop the message center and never start it again
MessageCenterService.stop(context);
Kontalk.setBackendEnabled(context, false);
}
else {
Kontalk.setBackendEnabled(context, true);
MessageCenterService.start(context);
}
return old;
}
/** Enable/disable offline mode. */
public static void setOfflineMode(Context context, boolean enabled) {
sPreferences.edit()
.putBoolean("offline_mode", enabled)
.apply();
if (enabled) {
// stop the message center and never start it again
MessageCenterService.stop(context);
}
else {
MessageCenterService.start(context);
}
}
public static boolean getOfflineMode() {
return getBoolean("offline_mode", false);
}
public static boolean getOfflineModeUsed() {
return getBoolean("offline_mode_used", false);
}
public static void setOfflineModeUsed() {
sPreferences.edit()
.putBoolean("offline_mode_used", true)
.apply();
}
public static boolean getSendTyping(Context context) {
return getBoolean("pref_send_typing", context.getResources()
.getBoolean(R.bool.pref_default_send_typing));
}
public static String getDialPrefix() {
String pref = getString("pref_remove_prefix", null);
return (pref != null && !TextUtils.isEmpty(pref.trim())) ? pref: null;
}
public static String getPushSenderId() {
return getString("pref_push_sender", null);
}
public static boolean setPushSenderId(String senderId) {
return sPreferences.edit()
.putString("pref_push_sender", senderId)
.commit();
}
public static boolean getAcceptAnyCertificate(Context context) {
return getBoolean("pref_accept_any_certificate", context.getResources()
.getBoolean(R.bool.pref_default_accept_any_certificate));
}
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
public static boolean setAcceptAnyCertificate(boolean acceptAnyCertificate) {
return sPreferences.edit()
.putBoolean("pref_accept_any_certificate", acceptAnyCertificate)
.commit();
}
public static int getIdleTimeMillis(Context context, int minValue) {
return getIntMinValue("pref_idle_time", minValue, context
.getResources().getInteger(R.integer.pref_default_idle_time));
}
public static int getWakeupTimeMillis(Context context, int minValue) {
return getIntMinValue("pref_wakeup_time", minValue, context
.getResources().getInteger(R.integer.pref_default_wakeup_time));
}
public static long getLastConnection() {
return getLong("pref_last_connection", -1);
}
// TODO why isn't this used?
public static void setLastConnection() {
sPreferences.edit()
.putLong("pref_last_connection", System.currentTimeMillis())
.apply();
}
public static String getEnterKeyMode(Context context) {
try {
return getString("pref_text_enter", context
.getString(R.string.pref_default_text_enter));
}
catch (ClassCastException e) {
// legacy mode
return getBoolean("pref_text_enter", false) ?
"newline" : "default";
}
}
public static boolean getShowBlockedUsers(Context context) {
return getBoolean("pref_show_blocked_users", context
.getResources().getBoolean(R.bool.pref_default_show_blocked_users));
}
public static String getRosterVersion() {
return getString("roster_version", "");
}
public static boolean setRosterVersion(String version) {
return sPreferences.edit()
.putString("roster_version", version)
.commit();
}
public static boolean isSkipHuaweiProtectedApps() {
return getBoolean("huawei_skip_protected_apps", false);
}
public static boolean setSkipHuaweiProtectedApps(boolean value) {
return sPreferences.edit()
.putBoolean("huawei_skip_protected_apps", value)
.commit();
}
public static boolean isReportingEnabled(Context context) {
return getBoolean("pref_reporting", context
.getResources().getBoolean(R.bool.pref_default_reporting));
}
public static boolean isDebugLogEnabled(Context context) {
return getBoolean("pref_debug_log", context
.getResources().getBoolean(R.bool.pref_default_debug_log));
}
public static long getPingAlarmInterval(Context context, long defaultValue) {
String networkType = SystemUtils.getCurrentNetworkName(context);
return (networkType != null) ?
getLong("ping_alarm_interval_" + networkType, defaultValue) :
defaultValue;
}
public static boolean setPingAlarmInterval(Context context, long intervalMillis) {
String networkType = SystemUtils.getCurrentNetworkName(context);
return networkType != null && sPreferences.edit()
.putLong("ping_alarm_interval_" + networkType, intervalMillis)
.commit();
}
public static long getPingAlarmBackoff(Context context, long defaultValue) {
String networkType = SystemUtils.getCurrentNetworkName(context);
return (networkType != null) ?
getLong("ping_alarm_backoff_" + networkType, defaultValue) :
defaultValue;
}
public static boolean setPingAlarmBackoff(Context context, long intervalMillis) {
String networkType = SystemUtils.getCurrentNetworkName(context);
return networkType != null && sPreferences.edit()
.putLong("ping_alarm_backoff_" + networkType, intervalMillis)
.commit();
}
/**
* Saves the current registration progress data. Used for recoverying a
* registration after a restart or in very low memory situations.
*/
public static boolean saveRegistrationProgress(String name,
String phoneNumber, PersonalKey key, String passphrase,
byte[] importedPublicKey, byte[] importedPrivateKey, String serverUri,
String sender, String challenge, String brandImage, String brandLink,
boolean force, Map<String, Keyring.TrustedFingerprint> trustedKeys) {
ByteArrayOutputStream trustedKeysOut = null;
if (trustedKeys != null) {
trustedKeysOut = new ByteArrayOutputStream();
Properties prop = new Properties();
for (Map.Entry<String, Keyring.TrustedFingerprint> e : trustedKeys.entrySet()) {
Keyring.TrustedFingerprint fingerprint = e.getValue();
if (fingerprint != null) {
prop.put(e.getKey(), fingerprint.toString());
}
}
try {
prop.store(trustedKeysOut, null);
}
catch (IOException e) {
// something went wrong
// we can't have IOExceptions from byte buffers anyway
trustedKeysOut = null;
}
}
return sPreferences.edit()
.putString("registration_name", name)
.putString("registration_phone", phoneNumber)
.putString("registration_key", key != null ? key.toBase64() : null)
.putString("registration_importedpublickey", importedPublicKey != null ?
Base64.encodeToString(importedPublicKey, Base64.NO_WRAP) : null)
.putString("registration_importedprivatekey", importedPrivateKey != null ?
Base64.encodeToString(importedPrivateKey, Base64.NO_WRAP) : null)
.putString("registration_passphrase", passphrase)
.putString("registration_server", serverUri)
.putString("registration_sender", sender)
.putString("registration_challenge", challenge)
.putString("registration_brandimage", brandImage)
.putString("registration_brandlink", brandLink)
.putBoolean("registration_force", force)
.putString("registration_trustedkeys", trustedKeysOut != null ?
Base64.encodeToString(trustedKeysOut.toByteArray(), Base64.NO_WRAP) : null)
.commit();
}
@SuppressWarnings({"unchecked"})
public static RegistrationProgress getRegistrationProgress() {
String name = getString("registration_name", null);
if (name != null) {
RegistrationProgress p = new RegistrationProgress();
p.name = name;
p.phone = getString("registration_phone", null);
String serverUri = getString("registration_server", null);
p.server = serverUri != null ? new EndpointServer(serverUri) : null;
String key = getString("registration_key", null);
p.key = !TextUtils.isEmpty(key) ? PersonalKey.fromBase64(key) : null;
p.passphrase = getString("registration_passphrase", null);
String importedPublicKey = getString("registration_importedpublickey", null);
if (importedPublicKey != null)
p.importedPublicKey = Base64.decode(importedPublicKey, Base64.NO_WRAP);
String importedPrivateKey = getString("registration_importedprivatekey", null);
if (importedPrivateKey != null)
p.importedPrivateKey = Base64.decode(importedPrivateKey, Base64.NO_WRAP);
p.sender = getString("registration_sender", null);
p.challenge = getString("registration_challenge", null);
p.brandImage = getString("registration_brandimage", null);
p.brandLink = getString("registration_brandlink", null);
p.force = getBoolean("registration_force", false);
String trustedKeys = getString("registration_trustedkeys", null);
if (trustedKeys != null) {
ByteArrayInputStream trustedKeysProp =
new ByteArrayInputStream(Base64.decode(trustedKeys, Base64.NO_WRAP));
try {
Properties prop = new Properties();
prop.load(trustedKeysProp);
p.trustedKeys = new HashMap<>(prop.size());
for (Map.Entry e : prop.entrySet()) {
Keyring.TrustedFingerprint fingerprint =
Keyring.TrustedFingerprint.fromString((String) e.getValue());
if (fingerprint != null) {
p.trustedKeys.put((String) e.getKey(), fingerprint);
}
}
}
catch (IOException ignored) {
}
}
return p;
}
return null;
}
public static void clearRegistrationProgress() {
sPreferences.edit()
.remove("registration_name")
.remove("registration_phone")
.remove("registration_key")
.remove("registration_importedpublickey")
.remove("registration_importedprivatekey")
.remove("registration_passphrase")
.remove("registration_server")
.remove("registration_sender")
.remove("registration_challenge")
.remove("registration_brandimage")
.remove("registration_brandlink")
.remove("registration_force")
.remove("registration_trustedkeys")
.apply();
}
public static final class RegistrationProgress {
public String name;
public String phone;
public PersonalKey key;
public String passphrase;
public byte[] importedPublicKey;
public byte[] importedPrivateKey;
public EndpointServer server;
public String sender;
public String challenge;
public String brandImage;
public String brandLink;
public boolean force;
public Map<String, Keyring.TrustedFingerprint> trustedKeys;
}
/** Recent statuses database helper. */
private static final class RecentStatusDbHelper extends SQLiteOpenHelper {
private static final String DATABASE_NAME = "status.db";
private static final int DATABASE_VERSION = 1;
private static final String TABLE_STATUS = "status";
private static final String SCHEMA_STATUS = "CREATE TABLE " + TABLE_STATUS + " (" +
"_id INTEGER PRIMARY KEY," +
"status TEXT UNIQUE," +
"timestamp INTEGER" +
")";
RecentStatusDbHelper(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(SCHEMA_STATUS);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
// no upgrade for version 1
}
public Cursor query() {
SQLiteDatabase db = getReadableDatabase();
return db.query(TABLE_STATUS, new String[] { BaseColumns._ID, "status" },
null, null, null, null, "timestamp DESC");
}
public void insert(String status) {
SQLiteDatabase db = getWritableDatabase();
ContentValues v = new ContentValues(2);
v.put("status", status);
v.put("timestamp", System.currentTimeMillis());
db.replace(TABLE_STATUS, null, v);
// delete old entries
db.delete(TABLE_STATUS, "_id NOT IN (SELECT _id FROM " +
TABLE_STATUS + " ORDER BY timestamp DESC LIMIT 10)", null);
}
}
private static RecentStatusDbHelper recentStatusDb;
private static void _recentStatusDbHelper(Context context) {
if (recentStatusDb == null)
recentStatusDb = new RecentStatusDbHelper(context.getApplicationContext());
}
/** Retrieves the list of recently used status messages. */
public static Cursor getRecentStatusMessages(Context context) {
_recentStatusDbHelper(context);
return recentStatusDb.query();
}
public static void addRecentStatusMessage(Context context, String status) {
_recentStatusDbHelper(context);
recentStatusDb.insert(status);
recentStatusDb.close();
}
}