diff --git a/android/app/src/main/java/org/fossasia/openevent/common/api/JWTUtils.java b/android/app/src/main/java/org/fossasia/openevent/common/api/JWTUtils.java deleted file mode 100644 index 19bd03b123..0000000000 --- a/android/app/src/main/java/org/fossasia/openevent/common/api/JWTUtils.java +++ /dev/null @@ -1,95 +0,0 @@ -package org.fossasia.openevent.common.api; - -import android.support.v4.util.SparseArrayCompat; - -import org.json.JSONException; -import org.json.JSONObject; - -public class JWTUtils { - - public static SparseArrayCompat decode(String token) { - SparseArrayCompat decoded = new SparseArrayCompat<>(2); - - String[] split = token.split("\\."); - decoded.append(0, getJson(split[0])); - decoded.append(1, getJson(split[1])); - - return decoded; - } - - public static long getExpiry(String token) throws JSONException { - SparseArrayCompat decoded = decode(token); - - // We are using JSONObject instead of GSON as it takes about 5 ms instead of 150 ms taken by GSON - return Long.parseLong(new JSONObject(decoded.get(1)).get("exp").toString()); - } - - public static int getIdentity(String token) throws JSONException { - SparseArrayCompat decoded = decode(token); - - return Integer.parseInt(new JSONObject(decoded.get(1)).get("identity").toString()); - } - - public static boolean isExpired(String token) { - long expiry; - - try { - expiry = getExpiry(token); - } catch (JSONException jse) { - return true; - } - - return System.currentTimeMillis() / 1000 >= expiry; - } - - private static String getJson(String strEncoded) { - byte[] decodedBytes = Base64Utils.decode(strEncoded); - return new String(decodedBytes); - } - - /** - * Base64 class because we can't test Android class and this is faster - */ - private static class Base64Utils { - - private final static char[] ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".toCharArray(); - - private static int[] toInt = new int[128]; - - static { - for (int i = 0; i < ALPHABET.length; i++) { - toInt[ALPHABET[i]] = i; - } - } - - /** - * Translates the specified Base64 string into a byte array. - * - * @param s the Base64 string (not null) - * @return the byte array (not null) - */ - private static byte[] decode(String s) { - int delta = s.endsWith("==") ? 2 : s.endsWith("=") ? 1 : 0; - byte[] buffer = new byte[s.length() * 3 / 4 - delta]; - int mask = 0xFF; - int index = 0; - for (int i = 0; i < s.length(); i += 4) { - int c0 = toInt[s.charAt(i)]; - int c1 = toInt[s.charAt(i + 1)]; - buffer[index++] = (byte) (((c0 << 2) | (c1 >> 4)) & mask); - if (index >= buffer.length) { - return buffer; - } - int c2 = toInt[s.charAt(i + 2)]; - buffer[index++] = (byte) (((c1 << 4) | (c2 >> 2)) & mask); - if (index >= buffer.length) { - return buffer; - } - int c3 = toInt[s.charAt(i + 3)]; - buffer[index++] = (byte) (((c2 << 6) | c3) & mask); - } - return buffer; - } - - } -} diff --git a/android/app/src/main/java/org/fossasia/openevent/common/api/JWTUtils.kt b/android/app/src/main/java/org/fossasia/openevent/common/api/JWTUtils.kt new file mode 100644 index 0000000000..f0d1ddf38e --- /dev/null +++ b/android/app/src/main/java/org/fossasia/openevent/common/api/JWTUtils.kt @@ -0,0 +1,101 @@ +package org.fossasia.openevent.common.api + +import android.support.v4.util.SparseArrayCompat + +import org.json.JSONException +import org.json.JSONObject + +object JWTUtils { + + private fun decode(token: String): SparseArrayCompat { + val decoded = SparseArrayCompat(2) + + val split = token.split("\\.".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() + decoded.append(0, getJson(split[0])) + decoded.append(1, getJson(split[1])) + + return decoded + } + + @Throws(JSONException::class) + private fun getExpiry(token: String): Long { + val decoded = decode(token) + + // We are using JSONObject instead of GSON as it takes about 5 ms instead of 150 ms taken by GSON + return JSONObject(decoded.get(1)).get("exp").toString().toLong() + } + + @Throws(JSONException::class) + @JvmStatic + fun getIdentity(token: String): Int { + val decoded = decode(token) + + return JSONObject(decoded.get(1)).get("identity").toString().toInt() + } + + @JvmStatic + fun isExpired(token: String): Boolean { + val expiry: Long + + try { + expiry = getExpiry(token) + } catch (jse: JSONException) { + return true + } + + return System.currentTimeMillis() / 1000 >= expiry + } + + private fun getJson(strEncoded: String): String { + val decodedBytes = Base64Utils.decode(strEncoded) + return String(decodedBytes) + } + + /** + * Base64 class because we can't test Android class and this is faster + */ + private object Base64Utils { + + private val ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".toCharArray() + + private val toInt = IntArray(128) + + init { + for (i in ALPHABET.indices) { + toInt[ALPHABET[i].toInt()] = i + } + } + + /** + * Translates the specified Base64 string into a byte array. + * + * @param s the Base64 string (not null) + * @return the byte array (not null) + */ + fun decode(s: String): ByteArray { + val delta = if (s.endsWith("==")) 2 else if (s.endsWith("=")) 1 else 0 + val buffer = ByteArray(s.length * 3 / 4 - delta) + val mask = 0xFF + var index = 0 + var i = 0 + while (i < s.length) { + val c0 = toInt[s[i].toInt()] + val c1 = toInt[s[i + 1].toInt()] + buffer[index++] = (c0 shl 2 or (c1 shr 4) and mask).toByte() + if (index >= buffer.size) { + return buffer + } + val c2 = toInt[s[i + 2].toInt()] + buffer[index++] = (c1 shl 4 or (c2 shr 2) and mask).toByte() + if (index >= buffer.size) { + return buffer + } + val c3 = toInt[s[i + 3].toInt()] + buffer[index++] = (c2 shl 6 or c3 and mask).toByte() + i += 4 + } + return buffer + } + + } +} diff --git a/android/app/src/main/java/org/fossasia/openevent/common/api/Urls.java b/android/app/src/main/java/org/fossasia/openevent/common/api/Urls.java deleted file mode 100644 index e3e2f3de77..0000000000 --- a/android/app/src/main/java/org/fossasia/openevent/common/api/Urls.java +++ /dev/null @@ -1,85 +0,0 @@ -package org.fossasia.openevent.common.api; - -import android.webkit.URLUtil; - -import org.fossasia.openevent.common.utils.Utils; - -public abstract class Urls { - - public static final String API_VERSION = "v1"; - - /** - * Change EVENT Id Here * - */ - public static final int EVENT_ID = 1; - - public static final String WEB_APP_URL_BASIC = "http://fossasia.github.io/open-event-webapp/#/"; - - public static final String EVENT = "event"; - - public static final String SPEAKERS = "speakers"; - - public static final String TRACKS = "tracks"; - - public static final String SESSIONS = "sessions"; - - public static final String SPONSORS = "sponsors"; - - public static final String BOOKMARKS = "bookmarks"; - - public static final String MICROLOCATIONS = "microlocations"; - - public static final String SESSION_TYPES = "session_types"; - - public static final String MAP = "map"; - - public static String BASE_URL = "https://eventyay.com/api/v1/events/6"; - - public static String FACEBOOK_BASE_URL = "https://graph.facebook.com"; - - public static String LOKLAK_BASE_URL = "https://api.loklak.org"; - - public static final String BASE_GET_URL = BASE_URL + "/api/" + API_VERSION; - - public static final String BASE_GET_URL_ALT = "https://raw.githubusercontent.com/fossasia/open-event/master/testapi/"; - - // Replace the template in getAppLink() if changing it - public static final String APP_LINK = "https://app_link_goes_here.com"; - - public static final String INVALID_LINK = "http://abc//"; - - public static final String EMPTY_LINK = "http://xyz//"; - - public static final String GOOGLE_PLAY_HOME = "https://play.google.com/store"; - - - public static String getBaseUrl() { - return BASE_URL; - } - - public static void setBaseUrl(String baseUrl) { - if (URLUtil.isValidUrl(baseUrl)) { - if (!baseUrl.endsWith("/")) { - BASE_URL = baseUrl + "/"; - } else { - BASE_URL = baseUrl; - } - } else { - if (!Utils.isEmpty(baseUrl)) { - BASE_URL = INVALID_LINK; - } else { - BASE_URL = EMPTY_LINK; - } - } - } - - /** - * Checks if the app link is replaced by the generator, if not - * returns null which is to be checked by caller to make decision - * accordingly. - * @return String - */ - public static String getAppLink() { - return APP_LINK.equals("https://app_link_goes_here.com")?GOOGLE_PLAY_HOME:APP_LINK; - } -} diff --git a/android/app/src/main/java/org/fossasia/openevent/common/api/Urls.kt b/android/app/src/main/java/org/fossasia/openevent/common/api/Urls.kt new file mode 100644 index 0000000000..7cd98e6596 --- /dev/null +++ b/android/app/src/main/java/org/fossasia/openevent/common/api/Urls.kt @@ -0,0 +1,85 @@ +package org.fossasia.openevent.common.api + +import android.webkit.URLUtil + +import org.fossasia.openevent.common.utils.Utils + +object Urls { + + const val API_VERSION = "v1" + + /** + * Change EVENT Id Here * + */ + const val EVENT_ID = 1 + + const val WEB_APP_URL_BASIC = "http://fossasia.github.io/open-event-webapp/#/" + + const val EVENT = "event" + + const val SPEAKERS = "speakers" + + const val TRACKS = "tracks" + + const val SESSIONS = "sessions" + + const val SPONSORS = "sponsors" + + const val BOOKMARKS = "bookmarks" + + const val MICROLOCATIONS = "microlocations" + + const val SESSION_TYPES = "session_types" + + const val MAP = "map" + + @JvmField + var BASE_URL = "https://eventyay.com/api/v1/events/6" + + const val FACEBOOK_BASE_URL = "https://graph.facebook.com" + + const val LOKLAK_BASE_URL = "https://api.loklak.org" + + @JvmField + val BASE_GET_URL = "$BASE_URL/api/$API_VERSION" + + const val BASE_GET_URL_ALT = "https://raw.githubusercontent.com/fossasia/open-event/master/testapi/" + + // Replace the template in getAppLink() if changing it + private const val APP_LINK = "https://app_link_goes_here.com" + + const val INVALID_LINK = "http://abc//" + + const val EMPTY_LINK = "http://xyz//" + + private const val GOOGLE_PLAY_HOME = "https://play.google.com/store" + + var baseUrl: String + @JvmStatic + get() = BASE_URL + @JvmStatic + set(baseUrl) = if (URLUtil.isValidUrl(baseUrl)) { + BASE_URL = if (!baseUrl.endsWith("/")) { + "$baseUrl/" + } else { + baseUrl + } + } else { + BASE_URL = if (!Utils.isEmpty(baseUrl)) { + INVALID_LINK + } else { + EMPTY_LINK + } + } + + /** + * Checks if the app link is replaced by the generator, if not + * returns null which is to be checked by caller to make decision + * accordingly. + * @return String + */ + val appLink: String + @JvmStatic + get() = if (APP_LINK == "https://app_link_goes_here.com") GOOGLE_PLAY_HOME else APP_LINK + +} diff --git a/android/app/src/main/java/org/fossasia/openevent/common/utils/Utils.kt b/android/app/src/main/java/org/fossasia/openevent/common/utils/Utils.kt index d4f6f41e16..90864023b6 100644 --- a/android/app/src/main/java/org/fossasia/openevent/common/utils/Utils.kt +++ b/android/app/src/main/java/org/fossasia/openevent/common/utils/Utils.kt @@ -29,7 +29,7 @@ object Utils { @JvmStatic val isBaseUrlEmpty: Boolean - get() = Urls.getBaseUrl() == Urls.EMPTY_LINK + get() = Urls.baseUrl == Urls.EMPTY_LINK @JvmStatic fun isEmpty(string: String?): Boolean { @@ -85,7 +85,7 @@ object Utils { if (isBaseUrlEmpty) { swipeRefreshLayout.isEnabled = false } else { - StrategyRegistry.instance.eventBusStrategy!!.eventBus.register(`object`) + StrategyRegistry.instance.eventBusStrategy?.eventBus?.register(`object`) swipeRefreshLayout.setOnRefreshListener(onRefreshListener) } } @@ -93,7 +93,7 @@ object Utils { @JvmStatic fun unregisterIfUrlValid(`object`: Any) { if (!isBaseUrlEmpty) { - StrategyRegistry.instance.eventBusStrategy!!.eventBus.unregister(`object`) + StrategyRegistry.instance.eventBusStrategy?.eventBus?.unregister(`object`) } } diff --git a/android/app/src/main/java/org/fossasia/openevent/config/AppConfigurer.java b/android/app/src/main/java/org/fossasia/openevent/config/AppConfigurer.java deleted file mode 100644 index ed697a00fe..0000000000 --- a/android/app/src/main/java/org/fossasia/openevent/config/AppConfigurer.java +++ /dev/null @@ -1,37 +0,0 @@ -package org.fossasia.openevent.config; - -import android.content.Context; - -/** - * Holds application context and a strategy holder and configures each strategy in order - * - * If any strategy returns true to halt, all further strategies are skipped - * Uses {@link StrategyRegistry} singleton to get default holder if not provided - */ -public class AppConfigurer { - - private final Context context; - private final ConfigStrategyHolder configStrategyHolder; - - public AppConfigurer(Context context, ConfigStrategyHolder configStrategyHolder) { - this.context = context; - this.configStrategyHolder = configStrategyHolder; - } - - public void configure() { - for (ConfigStrategy configStrategy : configStrategyHolder.getStrategies()) { - boolean shouldHalt = configStrategy.configure(context); - if (shouldHalt) - break; - } - } - - public static void configure(Context context, ConfigStrategyHolder configStrategyHolder) { - new AppConfigurer(context, configStrategyHolder).configure(); - } - - public static void configure(Context context) { - configure(context, StrategyRegistry.getInstance().getDefaultHolder()); - } - -} diff --git a/android/app/src/main/java/org/fossasia/openevent/config/AppConfigurer.kt b/android/app/src/main/java/org/fossasia/openevent/config/AppConfigurer.kt new file mode 100644 index 0000000000..4666a157cb --- /dev/null +++ b/android/app/src/main/java/org/fossasia/openevent/config/AppConfigurer.kt @@ -0,0 +1,30 @@ +package org.fossasia.openevent.config + +import android.content.Context + +/** + * Holds application context and a strategy holder and configures each strategy in order + * + * If any strategy returns true to halt, all further strategies are skipped + * Uses [StrategyRegistry] singleton to get default holder if not provided + */ +class AppConfigurer(private val context: Context?, private val configStrategyHolder: ConfigStrategyHolder) { + + fun configure() { + for (configStrategy in configStrategyHolder.strategies) { + val shouldHalt = configStrategy.configure(context) + if (shouldHalt) + break + } + } + + companion object { + + @JvmOverloads + @JvmStatic + fun configure(context: Context?, configStrategyHolder: ConfigStrategyHolder = StrategyRegistry.instance.getDefaultHolder()) { + AppConfigurer(context, configStrategyHolder).configure() + } + } + +} diff --git a/android/app/src/main/java/org/fossasia/openevent/config/strategies/AppConfigStrategy.java b/android/app/src/main/java/org/fossasia/openevent/config/strategies/AppConfigStrategy.java deleted file mode 100644 index a2287f0072..0000000000 --- a/android/app/src/main/java/org/fossasia/openevent/config/strategies/AppConfigStrategy.java +++ /dev/null @@ -1,102 +0,0 @@ -package org.fossasia.openevent.config.strategies; - -import android.content.Context; -import android.content.IntentFilter; -import android.net.ConnectivityManager; - -import org.fossasia.openevent.common.ConstantStrings; -import org.fossasia.openevent.common.api.Urls; -import org.fossasia.openevent.common.network.NetworkConnectivityChangeReceiver; -import org.fossasia.openevent.common.utils.SharedPreferencesUtil; -import org.fossasia.openevent.common.utils.Utils; -import org.fossasia.openevent.config.ConfigStrategy; -import org.json.JSONException; -import org.json.JSONObject; - -import java.io.IOException; -import java.io.InputStream; - -/** - * Parses application configuration and populates necessary data application wide - */ -public class AppConfigStrategy implements ConfigStrategy { - - private static final String API_LINK = "api-link"; - private static final String EMAIL = "email"; - private static final String APP_NAME = "app-name"; - private static final String AUTH_OPTION = "is-auth-enabled"; - - @Override - public boolean configure(Context context) { - String config_json = null; - String event_json = null; - //getting config.json data - try { - InputStream inputStream = context.getAssets().open("config.json"); - int size = inputStream.available(); - byte[] buffer = new byte[size]; - inputStream.read(buffer); - inputStream.close(); - config_json = new String(buffer, "UTF-8"); - } catch(IOException e) { - e.printStackTrace(); - } - - try { - JSONObject jsonObject = new JSONObject(config_json); - String email = jsonObject.has(EMAIL) ? jsonObject.getString(EMAIL) : ""; - String appName = jsonObject.has(APP_NAME) ? jsonObject.getString(APP_NAME) : ""; - String apiLink = jsonObject.has(API_LINK) ? jsonObject.getString(API_LINK) : ""; - boolean isAuthEnabled = jsonObject.has(AUTH_OPTION) && jsonObject.getBoolean(AUTH_OPTION); - - Urls.setBaseUrl(apiLink); - - SharedPreferencesUtil.putString(ConstantStrings.EMAIL, email); - SharedPreferencesUtil.putString(ConstantStrings.APP_NAME, appName); - SharedPreferencesUtil.putString(ConstantStrings.BASE_API_URL, apiLink); - SharedPreferencesUtil.putBoolean(ConstantStrings.IS_AUTH_ENABLED, isAuthEnabled); - - if (extractEventIdFromApiLink(apiLink) != 0) - SharedPreferencesUtil.putInt(ConstantStrings.EVENT_ID, extractEventIdFromApiLink(apiLink)); - } catch (JSONException e) { - e.printStackTrace(); - } - - //getting event data - try { - InputStream inputStream = context.getAssets().open("event"); - int size = inputStream.available(); - byte[] buffer = new byte[size]; - inputStream.read(buffer); - inputStream.close(); - event_json = new String(buffer, "UTF-8"); - } catch(IOException e) { - e.printStackTrace(); - } - - try { - JSONObject jsonObject = new JSONObject(event_json); - String org_description = jsonObject.has(ConstantStrings.ORG_DESCRIPTION) ? - jsonObject.getString(ConstantStrings.ORG_DESCRIPTION) : ""; - String eventTimeZone = jsonObject.has(ConstantStrings.TIMEZONE) ? jsonObject.getString(ConstantStrings.TIMEZONE) : ""; - SharedPreferencesUtil.putString(ConstantStrings.ORG_DESCRIPTION, org_description); - SharedPreferencesUtil.putString(ConstantStrings.TIMEZONE, eventTimeZone); - } catch (JSONException e) { - e.printStackTrace(); - } - - if (!Utils.isBaseUrlEmpty()) { - context.registerReceiver(new NetworkConnectivityChangeReceiver(), new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)); - } - - return false; - } - - private int extractEventIdFromApiLink(String apiLink){ - if(Utils.isEmpty(apiLink)) - return 0; - - return Integer.parseInt(apiLink.split("/v1/events/")[1].replace("/","")); - } - -} diff --git a/android/app/src/main/java/org/fossasia/openevent/config/strategies/AppConfigStrategy.kt b/android/app/src/main/java/org/fossasia/openevent/config/strategies/AppConfigStrategy.kt new file mode 100644 index 0000000000..391b9693ee --- /dev/null +++ b/android/app/src/main/java/org/fossasia/openevent/config/strategies/AppConfigStrategy.kt @@ -0,0 +1,102 @@ +package org.fossasia.openevent.config.strategies + +import android.content.Context +import android.content.IntentFilter +import android.net.ConnectivityManager +import org.fossasia.openevent.common.ConstantStrings +import org.fossasia.openevent.common.api.Urls +import org.fossasia.openevent.common.network.NetworkConnectivityChangeReceiver +import org.fossasia.openevent.common.utils.SharedPreferencesUtil +import org.fossasia.openevent.common.utils.Utils +import org.fossasia.openevent.config.ConfigStrategy +import org.json.JSONException +import org.json.JSONObject +import java.io.IOException +import java.nio.charset.Charset + +/** + * Parses application configuration and populates necessary data application wide + */ +class AppConfigStrategy : ConfigStrategy { + + override fun configure(context: Context): Boolean { + var config_json: String? = null + var event_json: String? = null + //getting config.json data + try { + val inputStream = context.assets.open("config.json") + val size = inputStream.available() + val buffer = ByteArray(size) + inputStream.read(buffer) + inputStream.close() + config_json = String(buffer, Charset.forName("UTF-8")) + } catch (e: IOException) { + e.printStackTrace() + } + + try { + val jsonObject = JSONObject(config_json) + val email = if (jsonObject.has(EMAIL)) jsonObject.getString(EMAIL) else "" + val appName = if (jsonObject.has(APP_NAME)) jsonObject.getString(APP_NAME) else "" + val apiLink = if (jsonObject.has(API_LINK)) jsonObject.getString(API_LINK) else "" + val isAuthEnabled = jsonObject.has(AUTH_OPTION) && jsonObject.getBoolean(AUTH_OPTION) + + Urls.baseUrl = apiLink + + SharedPreferencesUtil.putString(ConstantStrings.EMAIL, email) + SharedPreferencesUtil.putString(ConstantStrings.APP_NAME, appName) + SharedPreferencesUtil.putString(ConstantStrings.BASE_API_URL, apiLink) + SharedPreferencesUtil.putBoolean(ConstantStrings.IS_AUTH_ENABLED, isAuthEnabled) + + if (extractEventIdFromApiLink(apiLink) != 0) + SharedPreferencesUtil.putInt(ConstantStrings.EVENT_ID, extractEventIdFromApiLink(apiLink)) + } catch (e: JSONException) { + e.printStackTrace() + } + + //getting event data + try { + val inputStream = context.assets.open("event") + val size = inputStream.available() + val buffer = ByteArray(size) + inputStream.read(buffer) + inputStream.close() + event_json = String(buffer, Charset.forName("UTF-8")) + } catch (e: IOException) { + e.printStackTrace() + } + + try { + val jsonObject = JSONObject(event_json) + val orgDescription = if (jsonObject.has(ConstantStrings.ORG_DESCRIPTION)) + jsonObject.getString(ConstantStrings.ORG_DESCRIPTION) + else + "" + val eventTimeZone = if (jsonObject.has(ConstantStrings.TIMEZONE)) jsonObject.getString(ConstantStrings.TIMEZONE) else "" + SharedPreferencesUtil.putString(ConstantStrings.ORG_DESCRIPTION, orgDescription) + SharedPreferencesUtil.putString(ConstantStrings.TIMEZONE, eventTimeZone) + } catch (e: JSONException) { + e.printStackTrace() + } + + if (!Utils.isBaseUrlEmpty) { + context.registerReceiver(NetworkConnectivityChangeReceiver(), IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)) + } + + return false + } + + private fun extractEventIdFromApiLink(apiLink: String): Int { + return if (Utils.isEmpty(apiLink)) 0 else Integer.parseInt(apiLink.split("/v1/events/".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()[1].replace("/", "")) + + } + + companion object { + + private const val API_LINK = "api-link" + private const val EMAIL = "email" + private const val APP_NAME = "app-name" + private const val AUTH_OPTION = "is-auth-enabled" + } + +} diff --git a/android/app/src/main/java/org/fossasia/openevent/config/strategies/EventBusStrategy.java b/android/app/src/main/java/org/fossasia/openevent/config/strategies/EventBusStrategy.java deleted file mode 100644 index 92d4db0c52..0000000000 --- a/android/app/src/main/java/org/fossasia/openevent/config/strategies/EventBusStrategy.java +++ /dev/null @@ -1,44 +0,0 @@ -package org.fossasia.openevent.config.strategies; - -import android.content.Context; -import android.os.Handler; -import android.os.Looper; - -import com.squareup.otto.Bus; - -import org.fossasia.openevent.config.ConfigStrategy; - -/** - * Configures and provides EventBus singleton for use throughout app - * - * To be used via {@link org.fossasia.openevent.config.StrategyRegistry} - * - * TODO: Remove event bus from project - */ -public class EventBusStrategy implements ConfigStrategy { - - private Bus eventBus; - private Handler handler; - - public EventBusStrategy() { - handler = new Handler(Looper.getMainLooper()); - } - - public Bus getEventBus() { - if (eventBus == null) { - eventBus = new Bus(); - } - return eventBus; - } - - @Override - public boolean configure(Context context) { - getEventBus().register(context); - return false; - } - - public void postEventOnUIThread(final Object event) { - handler.post(() -> getEventBus().post(event)); - } - -} diff --git a/android/app/src/main/java/org/fossasia/openevent/config/strategies/EventBusStrategy.kt b/android/app/src/main/java/org/fossasia/openevent/config/strategies/EventBusStrategy.kt new file mode 100644 index 0000000000..81d48cad87 --- /dev/null +++ b/android/app/src/main/java/org/fossasia/openevent/config/strategies/EventBusStrategy.kt @@ -0,0 +1,39 @@ +package org.fossasia.openevent.config.strategies + +import android.content.Context +import android.os.Handler +import android.os.Looper + +import com.squareup.otto.Bus + +import org.fossasia.openevent.config.ConfigStrategy + +/** + * Configures and provides EventBus singleton for use throughout app + * + * To be used via [org.fossasia.openevent.config.StrategyRegistry] + * + * TODO: Remove event bus from project + */ +class EventBusStrategy : ConfigStrategy { + + var eventBus: Bus? = null + get() { + if (field == null) { + field = Bus() + } + return field + } + + private val handler: Handler = Handler(Looper.getMainLooper()) + + override fun configure(context: Context): Boolean { + eventBus?.register(context) + return false + } + + fun postEventOnUIThread(event: Any) { + handler.post { eventBus?.post(event) } + } + +} diff --git a/android/app/src/main/java/org/fossasia/openevent/config/strategies/RealmStrategy.java b/android/app/src/main/java/org/fossasia/openevent/config/strategies/RealmStrategy.java deleted file mode 100644 index 23383f43a4..0000000000 --- a/android/app/src/main/java/org/fossasia/openevent/config/strategies/RealmStrategy.java +++ /dev/null @@ -1,31 +0,0 @@ -package org.fossasia.openevent.config.strategies; - -import android.content.Context; - -import org.fossasia.openevent.config.ConfigStrategy; -import org.fossasia.openevent.data.module.DataModule; -import org.fossasia.openevent.data.repository.RealmDatabaseMigration; - -import io.realm.Realm; - -/** - * Configures Realm Database - */ -public class RealmStrategy implements ConfigStrategy { - - @Override - public boolean configure(Context context) { - Realm.init(context); - io.realm.RealmConfiguration config = new io.realm.RealmConfiguration.Builder() - .schemaVersion(RealmDatabaseMigration.DB_VERSION) // Must be bumped when the schema changes - .modules(new DataModule()) - //TODO: Re-add migration once DB is locked/finalized - .deleteRealmIfMigrationNeeded() - .build(); - - Realm.setDefaultConfiguration(config); - - return false; - } - -} diff --git a/android/app/src/main/java/org/fossasia/openevent/config/strategies/RealmStrategy.kt b/android/app/src/main/java/org/fossasia/openevent/config/strategies/RealmStrategy.kt new file mode 100644 index 0000000000..8735b4b718 --- /dev/null +++ b/android/app/src/main/java/org/fossasia/openevent/config/strategies/RealmStrategy.kt @@ -0,0 +1,31 @@ +package org.fossasia.openevent.config.strategies + +import android.content.Context + +import org.fossasia.openevent.config.ConfigStrategy +import org.fossasia.openevent.data.module.DataModule +import org.fossasia.openevent.data.repository.RealmDatabaseMigration + +import io.realm.Realm +import io.realm.RealmConfiguration + +/** + * Configures Realm Database + */ +class RealmStrategy : ConfigStrategy { + + override fun configure(context: Context): Boolean { + Realm.init(context) + val config = RealmConfiguration.Builder() + .schemaVersion(RealmDatabaseMigration.DB_VERSION) // Must be bumped when the schema changes + .modules(DataModule()) + //TODO: Re-add migration once DB is locked/finalized + .deleteRealmIfMigrationNeeded() + .build() + + Realm.setDefaultConfiguration(config) + + return false + } + +} diff --git a/android/app/src/main/java/org/fossasia/openevent/config/strategies/TimberStrategy.java b/android/app/src/main/java/org/fossasia/openevent/config/strategies/TimberStrategy.java deleted file mode 100644 index d4a81a2265..0000000000 --- a/android/app/src/main/java/org/fossasia/openevent/config/strategies/TimberStrategy.java +++ /dev/null @@ -1,68 +0,0 @@ -package org.fossasia.openevent.config.strategies; - -import android.content.Context; -import android.util.Log; - -import org.fossasia.openevent.BuildConfig; -import org.fossasia.openevent.config.ConfigStrategy; - -import timber.log.Timber; - -public class TimberStrategy implements ConfigStrategy { - - @Override - public boolean configure(Context context) { - if (BuildConfig.DEBUG) { - Timber.plant(new Timber.DebugTree()); - } else { - Timber.plant(new CrashReportingTree()); - } - - return false; - } - - /** - * A tree which logs important information for crash reporting. - */ - private static class CrashReportingTree extends Timber.Tree { - @Override - protected void log(int priority, String tag, String message, Throwable t) { - if (priority == Log.VERBOSE || priority == Log.DEBUG) { - return; - } - - FakeCrashLibrary.log(priority, tag, message); - - if (t != null) { - if (priority == Log.ERROR) { - FakeCrashLibrary.logError(t); - } else if (priority == Log.WARN) { - FakeCrashLibrary.logWarning(t); - } - } - } - } - - private static final class FakeCrashLibrary { - - /** - * Not a real crash reporting library! - */ - private FakeCrashLibrary() { - throw new AssertionError("No instances."); - } - - public static void log(int priority, String tag, String message) { - // TODO add log entry to circular buffer. - } - - public static void logWarning(Throwable t) { - // TODO report non-fatal warning. - } - - public static void logError(Throwable t) { - // TODO report non-fatal error. - } - } - -} diff --git a/android/app/src/main/java/org/fossasia/openevent/config/strategies/TimberStrategy.kt b/android/app/src/main/java/org/fossasia/openevent/config/strategies/TimberStrategy.kt new file mode 100644 index 0000000000..f9e0717ade --- /dev/null +++ b/android/app/src/main/java/org/fossasia/openevent/config/strategies/TimberStrategy.kt @@ -0,0 +1,70 @@ +package org.fossasia.openevent.config.strategies + +import android.content.Context +import android.util.Log + +import org.fossasia.openevent.BuildConfig +import org.fossasia.openevent.config.ConfigStrategy + +import timber.log.Timber + +class TimberStrategy : ConfigStrategy { + + override fun configure(context: Context): Boolean { + if (BuildConfig.DEBUG) { + Timber.plant(Timber.DebugTree()) + } else { + Timber.plant(CrashReportingTree()) + } + + return false + } + + /** + * A tree which logs important information for crash reporting. + */ + private class CrashReportingTree : Timber.Tree() { + override fun log(priority: Int, tag: String?, message: String, t: Throwable?) { + if (priority == Log.VERBOSE || priority == Log.DEBUG) { + return + } + + FakeCrashLibrary.log(priority, tag, message) + + if (t != null) { + if (priority == Log.ERROR) { + FakeCrashLibrary.logError(t) + } else if (priority == Log.WARN) { + FakeCrashLibrary.logWarning(t) + } + } + } + } + + private class FakeCrashLibrary private constructor() { + + /** + * Not a real crash reporting library! + */ + + init { + throw AssertionError("No instances.") + } + + companion object { + + fun log(priority: Int, tag: String?, message: String) { + // TODO add log entry to circular buffer. + } + + fun logWarning(t: Throwable) { + // TODO report non-fatal warning. + } + + fun logError(t: Throwable) { + // TODO report non-fatal error. + } + } + } + +} diff --git a/android/app/src/main/java/org/fossasia/openevent/config/strategies/TimeConfigStrategy.java b/android/app/src/main/java/org/fossasia/openevent/config/strategies/TimeConfigStrategy.java deleted file mode 100644 index 5e1f009ead..0000000000 --- a/android/app/src/main/java/org/fossasia/openevent/config/strategies/TimeConfigStrategy.java +++ /dev/null @@ -1,25 +0,0 @@ -package org.fossasia.openevent.config.strategies; - -import android.content.Context; - -import com.jakewharton.threetenabp.AndroidThreeTen; - -import org.fossasia.openevent.R; -import org.fossasia.openevent.common.date.DateConverter; -import org.fossasia.openevent.common.utils.SharedPreferencesUtil; -import org.fossasia.openevent.config.ConfigStrategy; - -/** - * Configures Date Time library and timezone information of the project - */ -public class TimeConfigStrategy implements ConfigStrategy { - - @Override - public boolean configure(Context context) { - AndroidThreeTen.init(context); - DateConverter.setShowLocalTime(SharedPreferencesUtil.getBoolean(context.getResources() - .getString(R.string.timezone_mode_key), false)); - return false; - } - -} diff --git a/android/app/src/main/java/org/fossasia/openevent/config/strategies/TimeConfigStrategy.kt b/android/app/src/main/java/org/fossasia/openevent/config/strategies/TimeConfigStrategy.kt new file mode 100644 index 0000000000..4bea76fb3d --- /dev/null +++ b/android/app/src/main/java/org/fossasia/openevent/config/strategies/TimeConfigStrategy.kt @@ -0,0 +1,24 @@ +package org.fossasia.openevent.config.strategies + +import android.content.Context + +import com.jakewharton.threetenabp.AndroidThreeTen + +import org.fossasia.openevent.R +import org.fossasia.openevent.common.date.DateConverter +import org.fossasia.openevent.common.utils.SharedPreferencesUtil +import org.fossasia.openevent.config.ConfigStrategy + +/** + * Configures Date Time library and timezone information of the project + */ +class TimeConfigStrategy : ConfigStrategy { + + override fun configure(context: Context): Boolean { + AndroidThreeTen.init(context) + DateConverter.setShowLocalTime(SharedPreferencesUtil.getBoolean(context.resources + .getString(R.string.timezone_mode_key), false)) + return false + } + +} diff --git a/android/app/src/main/java/org/fossasia/openevent/core/auth/AuthUtil.kt b/android/app/src/main/java/org/fossasia/openevent/core/auth/AuthUtil.kt index 284c48c83c..2a6c510bbf 100644 --- a/android/app/src/main/java/org/fossasia/openevent/core/auth/AuthUtil.kt +++ b/android/app/src/main/java/org/fossasia/openevent/core/auth/AuthUtil.kt @@ -56,7 +56,7 @@ object AuthUtil { SharedPreferencesUtil.remove(ConstantStrings.USER_LAST_NAME) //Delete User data from realm - RealmDataRepository.getDefaultInstance().clearUserData() + RealmDataRepository.defaultInstance.clearUserData() goToMainActivity(context) showMessage(R.string.logged_out_successfully) diff --git a/android/app/src/main/java/org/fossasia/openevent/data/repository/RealmDataRepository.java b/android/app/src/main/java/org/fossasia/openevent/data/repository/RealmDataRepository.java deleted file mode 100644 index 2de6bba880..0000000000 --- a/android/app/src/main/java/org/fossasia/openevent/data/repository/RealmDataRepository.java +++ /dev/null @@ -1,707 +0,0 @@ -package org.fossasia.openevent.data.repository; - -import android.text.TextUtils; - -import org.fossasia.openevent.common.arch.FilterableRealmLiveData; -import org.fossasia.openevent.common.arch.LiveRealmData; -import org.fossasia.openevent.common.arch.LiveRealmDataObject; -import org.fossasia.openevent.common.events.BookmarkChangedEvent; -import org.fossasia.openevent.config.StrategyRegistry; -import org.fossasia.openevent.core.auth.model.User; -import org.fossasia.openevent.data.DiscountCode; -import org.fossasia.openevent.data.Event; -import org.fossasia.openevent.data.FAQ; -import org.fossasia.openevent.data.Microlocation; -import org.fossasia.openevent.data.Notification; -import org.fossasia.openevent.data.Session; -import org.fossasia.openevent.data.SessionType; -import org.fossasia.openevent.data.Speaker; -import org.fossasia.openevent.data.Sponsor; -import org.fossasia.openevent.data.Track; -import org.fossasia.openevent.data.extras.EventDates; - -import java.util.HashMap; -import java.util.List; - -import io.reactivex.Completable; -import io.reactivex.schedulers.Schedulers; -import io.realm.Case; -import io.realm.Realm; -import io.realm.RealmList; -import io.realm.RealmObject; -import io.realm.RealmResults; -import io.realm.Sort; -import timber.log.Timber; - -public class RealmDataRepository { - - private Realm realm; - - private static RealmDataRepository realmDataRepository; - - private static HashMap repoCache = new HashMap<>(); - - public static RealmDataRepository getDefaultInstance() { - if(realmDataRepository == null) - realmDataRepository = new RealmDataRepository(Realm.getDefaultInstance()); - - return realmDataRepository; - } - - /** - * For threaded operation, a separate Realm instance is needed, not the default - * instance, and thus all Realm objects can not pass through threads, extra care - * must be taken to close the Realm instance after use or else app will crash - * onDestroy of MainActivity. This is to ensure the database remains compact and - * application remains free of silent bugs - * @param realmInstance Separate Realm instance to be used - * @return Realm Data Repository - */ - public static RealmDataRepository getInstance(Realm realmInstance) { - if(!repoCache.containsKey(realmInstance)) { - repoCache.put(realmInstance, new RealmDataRepository(realmInstance)); - } - return repoCache.get(realmInstance); - } - - private RealmDataRepository(Realm realm) { - this.realm = realm; - } - - public Realm getRealmInstance() { - return realm; - } - - //User Section - - private void saveUserInRealm(User user) { - Realm realm = Realm.getDefaultInstance(); - realm.beginTransaction(); - realm.insertOrUpdate(user); - realm.commitTransaction(); - realm.close(); - } - - /** - * Saves the User object in database and returns Completable - * object for tracking the state of operation - * - * @param user User which is to be stored - * @return Completable object to be subscribed by caller - */ - public Completable saveUser(final User user) { - return Completable.fromAction(() -> { - saveUserInRealm(user); - Timber.d("Saved User"); - }); - } - - /** - * Returns Future style User which is null - * To get the contents of User, add an OnRealmChangeListener - * which notifies about the object state asynchronously - * - * @return User Returns User Future - */ - public User getUser() { - Realm realm = Realm.getDefaultInstance(); - User user = realm.where(User.class).findFirstAsync(); - realm.close(); - return user; - } - - /** - * Returns User synchronously - * - * @return User - */ - public User getUserSync() { - Realm realm = Realm.getDefaultInstance(); - User user = realm.where(User.class).findFirst(); - realm.close(); - return user; - } - - public void clearUserData() { - Realm realm = Realm.getDefaultInstance(); - realm.executeTransaction(realm1 -> realm1.delete(User.class)); - realm.close(); - } - - - // Events Section - - private void saveEventInRealm(Event event) { - realm.beginTransaction(); - realm.insertOrUpdate(event); - realm.commitTransaction(); - } - - /** - * Saves the Event object in database and returns Completable - * object for tracking the state of operation - * @param event Event which is to be stored - * @return Completable object to be subscribed by caller - */ - public Completable saveEvent(final Event event) { - return Completable.fromAction(() -> { - saveEventInRealm(event); - Timber.d("Saved Event"); - }); - } - - /** - * Returns Future style Event which is null - * To get the contents of Event, add an OnRealmChangeListener - * which notifies about the object state asynchronously - * @return Event Returns Event Future - */ - public Event getEvent() { - return realm.where(Event.class).findFirstAsync(); - } - - /** - * Returns Event synchronously - * @return Event - */ - public Event getEventSync() { - return realm.where(Event.class).findFirst(); - } - - // Tracks Section - - /** - * Saves tracks while merging with sessions asynchronously - * @param tracks Tracks to be saved - */ - private void saveTracksInRealm(final List tracks) { - // Since this is a threaded operation. We need our own instance of Realm - Realm realm = Realm.getDefaultInstance(); - - realm.executeTransaction(realm1 -> { - for(Track track : tracks) { - List sessions = track.getSessions(); - - if (sessions != null && !sessions.isEmpty()) { - - RealmList newSessions = new RealmList<>(); - - for (Session session : sessions) { - // To prevent overwriting of previously saved values - Session stored = realm1.where(Session.class).equalTo("id", session.getId()).findFirst(); - - if (stored != null) { - newSessions.add(stored); - } else { - newSessions.add(session); - } - } - - track.setSessions(newSessions); - track.setName(track.getName()); // Trimming the response - } - - realm1.insertOrUpdate(track); - } - }); - - realm.close(); - } - - /** - * Saves the list of Tracks in database and returns Completable - * object for tracking the state of operation - * @param tracks Tracks to be saved - * @return Completable object to be subscribed by caller - */ - public Completable saveTracks(final List tracks) { - return Completable.fromAction(() -> { - saveTracksInRealm(tracks); - Timber.d("Saved Tracks"); - }); - } - - /** - * Returns filtered tracks according to query - * @param query Query String WITHOUT wildcards - * @return List of Tracks following constraints - */ - public RealmResults getTracksFiltered(String query) { - String wildcardQuery = String.format("*%s*", query); - - return realm.where(Track.class) - .like("name", wildcardQuery, Case.INSENSITIVE) - .sort("name") - .findAll(); - } - - public RealmResults getTracks() { - return realm.where(Track.class) - .sort("name") - .findAllAsync(); - } - - public RealmResults getTracksSync() { - return realm.where(Track.class) - .sort("name") - .findAll(); - } - - public Track getTrack(int trackId) { - return realm.where(Track.class).equalTo("id", trackId).findFirstAsync(); - } - - public static boolean isNull(Track track) { - return track == null || TextUtils.isEmpty(track.getName()) || TextUtils.isEmpty(track.getColor()); - } - - // Session Section - - /** - * Saves sessions while merging with tracks and speakers asynchronously - * @param sessions Sessions to be saved - */ - private void saveSessionsInRealm(final List sessions) { - // Since this is a threaded operation. We need our own instance of Realm - Realm realm = Realm.getDefaultInstance(); - - realm.executeTransaction(transaction -> { - - for(Session session : sessions) { - // If session was previously bookmarked, set this one too - Session storedSession = transaction.where(Session.class).equalTo("id", session.getId()).findFirst(); - if(storedSession != null && storedSession.getIsBookmarked()) - session.setIsBookmarked(true); - - List speakers = session.getSpeakers(); - - if(speakers != null && !speakers.isEmpty()) { - - RealmList newSpeakers = new RealmList<>(); - - for (Speaker speaker : speakers) { - // To prevent overwriting of previously saved values - Speaker stored = transaction.where(Speaker.class).equalTo("id", speaker.getId()).findFirst(); - - if (stored != null) { - newSpeakers.add(stored); - } else { - newSpeakers.add(speaker); - } - } - - session.setSpeakers(newSpeakers); - } - - Track track = session.getTrack(); - - if (track != null) { - // To prevent overwriting of previously saved values - Track stored = transaction.where(Track.class).equalTo("id", track.getId()).findFirst(); - - if (stored != null) { - session.setTrack(stored); - } else { - // Set intermediate information for partial update - - if(TextUtils.isEmpty(track.getColor())) - track.setColor("#bbbbbb"); - - if(track.getName() == null) - track.setName(""); - else - track.setName(track.getName()); - } - } - - if(session.getTitle().contains("Create Full")) - Timber.d("Session " + session.toString()); - - transaction.insertOrUpdate(session); - } - }); - - realm.close(); - } - - public Completable saveSessions(final List sessions) { - return Completable.fromAction(() -> { - saveSessionsInRealm(sessions); - Timber.d("Saved Sessions"); - }); - } - - /** - * Sets bookmark of a session asynchronously - * @param sessionId Session ID whose bookmark is to be updated - * @param bookmark boolean value of bookmark to be set - * @return Completable denoting action completion - */ - public Completable setBookmark(final int sessionId, final boolean bookmark) { - - return Completable.fromAction(() -> { - - Realm realm1 = Realm.getDefaultInstance(); - - realm1.beginTransaction(); - realm1.where(Session.class) - .equalTo("id", sessionId) - .findFirst() - .setIsBookmarked(bookmark); - - StrategyRegistry.getInstance() - .getEventBusStrategy() - .postEventOnUIThread(new BookmarkChangedEvent()); - realm1.commitTransaction(); - - realm1.close(); - }).subscribeOn(Schedulers.io()); - } - - public Session getSession(int sessionId) { - return realm.where(Session.class).equalTo("id", sessionId).findFirstAsync(); - } - - public Session getSessionSync(int sessionId) { - return realm.where(Session.class).equalTo("id", sessionId).findFirst(); - } - - public Session getSession(String title) { - return realm.where(Session.class).equalTo("title", title).findFirstAsync(); - } - - /** - * Returns sessions belonging to a specific track filtered by - * a query string. - * @param trackId ID of Track which Sessions should belong to - * @param query Query of search WITHOUT wildcards - * @return List of Sessions following constraints - */ - public RealmResults getSessionsFiltered(int trackId, String query) { - String wildcardQuery = String.format("*%s*", query); - - return realm.where(Session.class) - .equalTo("track.id", trackId) - .like("title", wildcardQuery, Case.INSENSITIVE) - .sort("startsAt") - .findAll(); - } - - public RealmResults getSessionsByLocation(String location) { - return realm.where(Session.class) - .equalTo("microlocation.name", location) - .sort(Session.START_TIME) - .findAllAsync(); - } - - public RealmResults getSessionsByDate(String date) { - return realm.where(Session.class).equalTo("startDate", date).findAllAsync(); - } - - public RealmResults getSessionsByDateFiltered(String date, String query, String sortCriteria) { - String wildcardQuery = String.format("*%s*", query); - - return realm.where(Session.class) - .equalTo("startDate", date) - .like("title", wildcardQuery, Case.INSENSITIVE) - .sort(sortCriteria) - .findAllAsync(); - } - - public RealmResults getBookMarkedSessions() { - return realm.where(Session.class).equalTo("isBookmarked", true).findAllAsync(); - } - - public List getBookMarkedSessionsSync() { - Realm realm = Realm.getDefaultInstance(); - RealmResults sessions = realm.where(Session.class).equalTo("isBookmarked", true).findAll(); - List list = realm.copyFromRealm(sessions); - realm.close(); - return list; - } - - // Speakers Section - - /** - * Saves speakers while merging with sessions asynchronously - * @param speakers Speakers to be saved - */ - private void saveSpeakersInRealm(final List speakers) { - - // Since this is a threaded operation. We need our own instance of Realm - Realm realm = Realm.getDefaultInstance(); - - realm.executeTransaction(transaction -> { - for(Speaker speaker : speakers) { - List sessions = speaker.getSessions(); - - if(sessions != null && !sessions.isEmpty()) { - RealmList newSessions = new RealmList<>(); - - for (Session session : sessions) { - // To prevent overwriting of previously saved values - Session stored = transaction.where(Session.class).equalTo("id", session.getId()).findFirst(); - - if (stored != null) { - newSessions.add(stored); - } else { - newSessions.add(session); - } - } - - speaker.setSessions(newSessions); - } - - transaction.insertOrUpdate(speaker); - } - }); - - realm.close(); - } - - public Completable saveSpeakers(final List speakers) { - return Completable.fromAction(() -> { - saveSpeakersInRealm(speakers); - Timber.d("Saved Speakers"); - }); - } - - public Speaker getSpeaker(String speakerName) { - return realm.where(Speaker.class).equalTo("name", speakerName).findFirstAsync(); - } - - public RealmResults getSpeakersForName(String speakerName){ - return realm.where(Speaker.class).equalTo("name", speakerName).findAllAsync(); - } - - public RealmResults getSpeakers(String sortCriteria) { - return realm.where(Speaker.class) - .sort(sortCriteria) - .findAllAsync(); - } - - public RealmResults getSpeakersSync(String sortCriteria) { - return realm.where(Speaker.class) - .sort(sortCriteria) - .findAllAsync(); - } - - public RealmResults getSpeakersFiltered(String query, String sortCriteria) { - String wildcardQuery = String.format("*%s*", query); - - return realm.where(Speaker.class) - .like("name", wildcardQuery, Case.INSENSITIVE) - .sort(sortCriteria) - .findAllAsync(); - } - - public RealmResults getFeaturedSpeakers() { - return realm.where(Speaker.class) - .equalTo("isFeatured", true) - .findAllAsync(); - } - - // Sponsors Section - - private void saveSponsorsInRealm(List sponsors) { - Realm realm = Realm.getDefaultInstance(); - realm.executeTransaction(realm1 -> realm1.insertOrUpdate(sponsors)); - realm.close(); - } - - public Completable saveSponsors(final List sponsors) { - return Completable.fromAction(() -> { - saveSponsorsInRealm(sponsors); - Timber.d("Saved Sponsors"); - }); - } - - public RealmResults getSponsors() { - return realm.where(Sponsor.class) - .sort("level", Sort.DESCENDING, "name", Sort.ASCENDING) - .findAllAsync(); - } - - //DiscountCode section - - private void saveDiscountCodesinRealm(List discountCodes) { - Realm realm = Realm.getDefaultInstance(); - realm.executeTransaction(realm1 -> realm1.insertOrUpdate(discountCodes)); - realm.close(); - } - - public Completable saveDiscountCodes(final List discountCodes) { - return Completable.fromAction(() -> { - saveDiscountCodesinRealm(discountCodes); - Timber.d("Saved DiscountCodes"); - }); - } - - public RealmResults getDiscountCodes() { - return realm.where(DiscountCode.class) - .sort("code") - .findAllAsync(); - } - - // Location Section - - private void saveLocationsInRealm(List locations) { - Realm realm = Realm.getDefaultInstance(); - realm.executeTransaction(realm1 -> realm1.insertOrUpdate(locations)); - realm.close(); - } - - public Completable saveLocations(final List locations) { - return Completable.fromAction(() -> { - saveLocationsInRealm(locations); - Timber.d("Saved Locations"); - }); - } - - public RealmResults getLocations() { - return realm.where(Microlocation.class) - .sort("name") - .findAllAsync(); - } - - public RealmResults getLocationsSync() { - return realm.where(Microlocation.class) - .sort("name") - .findAll(); - } - - //Session Types Section - - private void saveSessionTypesInRealm(List sessionTypes) { - Realm realm = Realm.getDefaultInstance(); - realm.executeTransaction(realm1 -> realm1.insertOrUpdate(sessionTypes)); - realm.close(); - } - - public Completable saveSessionTypes(final List sessionTypes) { - return Completable.fromAction(() -> { - saveSessionTypesInRealm(sessionTypes); - Timber.d("Saved Session Types"); - }); - } - - public RealmResults getSessionTypes() { - return realm.where(SessionType.class) - .sort("name") - .findAllAsync(); - } - - public RealmResults getSessionTypesSync() { - return realm.where(SessionType.class) - .sort("name") - .findAll(); - } - - // Dates Section - - /** - * Saves Event Dates Synchronously - * @param eventDates List of dates of entire event span (inclusive) - */ - private void saveEventDatesInRealm(List eventDates) { - Realm realm = Realm.getDefaultInstance(); - realm.executeTransaction(transaction -> { - transaction.delete(EventDates.class); - transaction.insertOrUpdate(eventDates); - }); - realm.close(); - } - - public Completable saveEventDates(List eventDates) { - return Completable.fromAction(() -> saveEventDatesInRealm(eventDates)); - } - - public RealmResults getEventDates() { - return realm.where(EventDates.class).findAllAsync(); - } - - public RealmResults getEventDatesSync(){ - return realm.where(EventDates.class).findAll(); - } - - // Notifications section - - public Completable saveNotifications(final List notifications) { - return Completable.fromAction(() -> { - saveNotificationsInRealm(notifications); - Timber.d("Saved notifications"); - }); - } - - private void saveNotificationsInRealm(List notifications) { - realm.executeTransaction(realm1 -> { - realm1.delete(Notification.class); - realm1.insertOrUpdate(notifications); - }); - } - - public RealmResults getNotifications() { - return realm.where(Notification.class) - .sort("receivedAt") - .findAllAsync(); - } - - // FAQ Section - public RealmResults getEventFAQs() { - return realm.where(FAQ.class).findAllAsync(); - } - - public Completable saveFAQs(final List faqs) { - return Completable.fromAction(() -> { - saveFAQsInRealm(faqs); - Timber.d("Saved FAQs"); - }); - } - - private void saveFAQsInRealm(List faqs) { - Realm realm = Realm.getDefaultInstance(); - realm.executeTransaction(transaction -> { - // Using a threaded instance now to handle relationship with FAQ types in the future. - transaction.delete(FAQ.class); - transaction.insertOrUpdate(faqs); - }); - realm.close(); - } - - /** - * Convert RealmResults to LiveRealmData - */ - public static LiveRealmData asLiveData(RealmResults data) { - return new LiveRealmData<>(data); - } - - /** - * Convert RealmObject to LiveRealmDataObject - */ - public static LiveRealmDataObject asLiveDataForObject(K data) { - return new LiveRealmDataObject<>(data); - } - - /** - * Convert RealmResults to FilterableRealmLiveData - */ - public static FilterableRealmLiveData asFilterableLiveData(RealmResults data) { - return new FilterableRealmLiveData<>(data); - } - - /** - * Compacts the database to save space - * Should be called when exiting application to ensure - * all Realm instances are ready to be closed. - * - * Closing the repoCache instances is the responsibility - * of caller - */ - public static void compactDatabase() { - Realm realm = realmDataRepository.getRealmInstance(); - - Timber.d("Vacuuming the database"); - Realm.compactRealm(realm.getConfiguration()); - } - -} diff --git a/android/app/src/main/java/org/fossasia/openevent/data/repository/RealmDataRepository.kt b/android/app/src/main/java/org/fossasia/openevent/data/repository/RealmDataRepository.kt new file mode 100644 index 0000000000..2a988456ac --- /dev/null +++ b/android/app/src/main/java/org/fossasia/openevent/data/repository/RealmDataRepository.kt @@ -0,0 +1,673 @@ +package org.fossasia.openevent.data.repository + +import android.text.TextUtils +import io.reactivex.Completable +import io.reactivex.schedulers.Schedulers +import io.realm.* +import org.fossasia.openevent.common.arch.FilterableRealmLiveData +import org.fossasia.openevent.common.arch.LiveRealmData +import org.fossasia.openevent.common.arch.LiveRealmDataObject +import org.fossasia.openevent.common.events.BookmarkChangedEvent +import org.fossasia.openevent.config.StrategyRegistry +import org.fossasia.openevent.core.auth.model.User +import org.fossasia.openevent.data.* +import org.fossasia.openevent.data.extras.EventDates +import timber.log.Timber + +class RealmDataRepository private constructor(val realmInstance: Realm) { + + /** + * Returns Future style User which is null + * To get the contents of User, add an OnRealmChangeListener + * which notifies about the object state asynchronously + * + * @return User Returns User Future + */ + val user: User + get() { + val realm = Realm.getDefaultInstance() + val user = realm.where(User::class.java).findFirstAsync() + realm.close() + return user + } + + /** + * Returns User synchronously + * + * @return User + */ + val userSync: User? + get() { + val realm = Realm.getDefaultInstance() + val user = realm.where(User::class.java).findFirst() + realm.close() + return user + } + + /** + * Returns Future style Event which is null + * To get the contents of Event, add an OnRealmChangeListener + * which notifies about the object state asynchronously + * @return Event Returns Event Future + */ + val event: Event + get() = realmInstance.where(Event::class.java).findFirstAsync() + + /** + * Returns Event synchronously + * @return Event + */ + val eventSync: Event? + get() = realmInstance.where(Event::class.java).findFirst() + + val tracks: RealmResults + get() = realmInstance.where(Track::class.java) + .sort("name") + .findAllAsync() + + val tracksSync: RealmResults + get() = realmInstance.where(Track::class.java) + .sort("name") + .findAll() + + val bookMarkedSessions: RealmResults + get() = realmInstance.where(Session::class.java).equalTo("isBookmarked", true).findAllAsync() + + val bookMarkedSessionsSync: List + get() { + val realm = Realm.getDefaultInstance() + val sessions = realm.where(Session::class.java).equalTo("isBookmarked", true).findAll() + val list = realm.copyFromRealm(sessions) + realm.close() + return list + } + + val featuredSpeakers: RealmResults + get() = realmInstance.where(Speaker::class.java) + .equalTo("isFeatured", true) + .findAllAsync() + + val sponsors: RealmResults + get() = realmInstance.where(Sponsor::class.java) + .sort("level", Sort.DESCENDING, "name", Sort.ASCENDING) + .findAllAsync() + + val discountCodes: RealmResults + get() = realmInstance.where(DiscountCode::class.java) + .sort("code") + .findAllAsync() + + val locations: RealmResults + get() = realmInstance.where(Microlocation::class.java) + .sort("name") + .findAllAsync() + + val locationsSync: RealmResults + get() = realmInstance.where(Microlocation::class.java) + .sort("name") + .findAll() + + val sessionTypes: RealmResults + get() = realmInstance.where(SessionType::class.java) + .sort("name") + .findAllAsync() + + val sessionTypesSync: RealmResults + get() = realmInstance.where(SessionType::class.java) + .sort("name") + .findAll() + + val eventDates: RealmResults + get() = realmInstance.where(EventDates::class.java).findAllAsync() + + val eventDatesSync: RealmResults + get() = realmInstance.where(EventDates::class.java).findAll() + + val notifications: RealmResults + get() = realmInstance.where(Notification::class.java) + .sort("receivedAt") + .findAllAsync() + + // FAQ Section + val eventFAQs: RealmResults + get() = realmInstance.where(FAQ::class.java).findAllAsync() + + //User Section + private fun saveUserInRealm(user: User) { + val realm = Realm.getDefaultInstance() + realm.beginTransaction() + realm.insertOrUpdate(user) + realm.commitTransaction() + realm.close() + } + + /** + * Saves the User object in database and returns Completable + * object for tracking the state of operation + * + * @param user User which is to be stored + * @return Completable object to be subscribed by caller + */ + fun saveUser(user: User): Completable { + return Completable.fromAction { + saveUserInRealm(user) + Timber.d("Saved User") + } + } + + fun clearUserData() { + val realm = Realm.getDefaultInstance() + realm.executeTransaction({ realm1 -> realm1.delete(User::class.java) }) + realm.close() + } + + + // Events Section + private fun saveEventInRealm(event: Event) { + realmInstance.beginTransaction() + realmInstance.insertOrUpdate(event) + realmInstance.commitTransaction() + } + + /** + * Saves the Event object in database and returns Completable + * object for tracking the state of operation + * @param event Event which is to be stored + * @return Completable object to be subscribed by caller + */ + fun saveEvent(event: Event): Completable { + return Completable.fromAction { + saveEventInRealm(event) + Timber.d("Saved Event") + } + } + + // Tracks Section + + /** + * Saves tracks while merging with sessions asynchronously + * @param tracks Tracks to be saved + */ + private fun saveTracksInRealm(tracks: List) { + // Since this is a threaded operation. We need our own instance of Realm + val realm = Realm.getDefaultInstance() + + realm.executeTransaction({ realm1 -> + for (track in tracks) { + val sessions = track.sessions + + if (sessions != null && !sessions.isEmpty()) { + + val newSessions = RealmList() + + for (session in sessions) { + // To prevent overwriting of previously saved values + val stored = realm1.where(Session::class.java).equalTo("id", session.id).findFirst() + + if (stored != null) { + newSessions.add(stored) + } else { + newSessions.add(session) + } + } + + track.sessions = newSessions + track.name = track.name // Trimming the response + } + + realm1.insertOrUpdate(track) + } + }) + + realm.close() + } + + /** + * Saves the list of Tracks in database and returns Completable + * object for tracking the state of operation + * @param tracks Tracks to be saved + * @return Completable object to be subscribed by caller + */ + fun saveTracks(tracks: List): Completable { + return Completable.fromAction { + saveTracksInRealm(tracks) + Timber.d("Saved Tracks") + } + } + + /** + * Returns filtered tracks according to query + * @param query Query String WITHOUT wildcards + * @return List of Tracks following constraints + */ + fun getTracksFiltered(query: String): RealmResults { + val wildcardQuery = String.format("*%s*", query) + + return realmInstance.where(Track::class.java) + .like("name", wildcardQuery, Case.INSENSITIVE) + .sort("name") + .findAll() + } + + fun getTrack(trackId: Int): Track { + return realmInstance.where(Track::class.java).equalTo("id", trackId).findFirstAsync() + } + + // Session Section + + /** + * Saves sessions while merging with tracks and speakers asynchronously + * @param sessions Sessions to be saved + */ + private fun saveSessionsInRealm(sessions: List) { + // Since this is a threaded operation. We need our own instance of Realm + val realm = Realm.getDefaultInstance() + + realm.executeTransaction({ transaction -> + + for (session in sessions) { + // If session was previously bookmarked, set this one too + val storedSession = transaction.where(Session::class.java).equalTo("id", session.id).findFirst() + if (storedSession != null && storedSession.isBookmarked) + session.isBookmarked = true + + val speakers = session.speakers + + if (speakers != null && !speakers.isEmpty()) { + + val newSpeakers = RealmList() + + for (speaker in speakers) { + // To prevent overwriting of previously saved values + val stored = transaction.where(Speaker::class.java).equalTo("id", speaker.id).findFirst() + + if (stored != null) { + newSpeakers.add(stored) + } else { + newSpeakers.add(speaker) + } + } + + session.speakers = newSpeakers + } + + val track = session.track + + if (track != null) { + // To prevent overwriting of previously saved values + val stored = transaction.where(Track::class.java).equalTo("id", track.id).findFirst() + + if (stored != null) { + session.track = stored + } else { + // Set intermediate information for partial update + + if (TextUtils.isEmpty(track.color)) + track.color = "#bbbbbb" + + if (track.name == null) + track.name = "" + else + track.name = track.name + } + } + + if (session.title.contains("Create Full")) + Timber.d("Session $session") + + transaction.insertOrUpdate(session) + } + }) + + realm.close() + } + + fun saveSessions(sessions: List): Completable { + return Completable.fromAction { + saveSessionsInRealm(sessions) + Timber.d("Saved Sessions") + } + } + + /** + * Sets bookmark of a session asynchronously + * @param sessionId Session ID whose bookmark is to be updated + * @param bookmark boolean value of bookmark to be set + * @return Completable denoting action completion + */ + fun setBookmark(sessionId: Int, bookmark: Boolean): Completable { + + return Completable.fromAction { + + val realm1 = Realm.getDefaultInstance() + + realm1.beginTransaction() + realm1.where(Session::class.java) + .equalTo("id", sessionId) + .findFirst()?.isBookmarked = bookmark + + StrategyRegistry.instance + .eventBusStrategy + ?.postEventOnUIThread(BookmarkChangedEvent()) + realm1.commitTransaction() + + realm1.close() + }.subscribeOn(Schedulers.io()) + } + + fun getSession(sessionId: Int): Session { + return realmInstance.where(Session::class.java).equalTo("id", sessionId).findFirstAsync() + } + + fun getSessionSync(sessionId: Int): Session? { + return realmInstance.where(Session::class.java).equalTo("id", sessionId).findFirst() + } + + fun getSession(title: String): Session { + return realmInstance.where(Session::class.java).equalTo("title", title).findFirstAsync() + } + + /** + * Returns sessions belonging to a specific track filtered by + * a query string. + * @param trackId ID of Track which Sessions should belong to + * @param query Query of search WITHOUT wildcards + * @return List of Sessions following constraints + */ + fun getSessionsFiltered(trackId: Int, query: String): RealmResults { + val wildcardQuery = String.format("*%s*", query) + + return realmInstance.where(Session::class.java) + .equalTo("track.id", trackId) + .like("title", wildcardQuery, Case.INSENSITIVE) + .sort("startsAt") + .findAll() + } + + fun getSessionsByLocation(location: String): RealmResults { + return realmInstance.where(Session::class.java) + .equalTo("microlocation.name", location) + .sort(Session.START_TIME) + .findAllAsync() + } + + fun getSessionsByDate(date: String): RealmResults { + return realmInstance.where(Session::class.java).equalTo("startDate", date).findAllAsync() + } + + fun getSessionsByDateFiltered(date: String, query: String, sortCriteria: String): RealmResults { + val wildcardQuery = String.format("*%s*", query) + + return realmInstance.where(Session::class.java) + .equalTo("startDate", date) + .like("title", wildcardQuery, Case.INSENSITIVE) + .sort(sortCriteria) + .findAllAsync() + } + + // Speakers Section + + /** + * Saves speakers while merging with sessions asynchronously + * @param speakers Speakers to be saved + */ + private fun saveSpeakersInRealm(speakers: List) { + + // Since this is a threaded operation. We need our own instance of Realm + val realm = Realm.getDefaultInstance() + + realm.executeTransaction({ transaction -> + for (speaker in speakers) { + val sessions = speaker.sessions + + if (sessions != null && !sessions.isEmpty()) { + val newSessions = RealmList() + + for (session in sessions) { + // To prevent overwriting of previously saved values + val stored = transaction.where(Session::class.java).equalTo("id", session.id).findFirst() + + if (stored != null) { + newSessions.add(stored) + } else { + newSessions.add(session) + } + } + + speaker.sessions = newSessions + } + + transaction.insertOrUpdate(speaker) + } + }) + + realm.close() + } + + fun saveSpeakers(speakers: List): Completable { + return Completable.fromAction { + saveSpeakersInRealm(speakers) + Timber.d("Saved Speakers") + } + } + + fun getSpeaker(speakerName: String): Speaker { + return realmInstance.where(Speaker::class.java).equalTo("name", speakerName).findFirstAsync() + } + + fun getSpeakersForName(speakerName: String): RealmResults { + return realmInstance.where(Speaker::class.java).equalTo("name", speakerName).findAllAsync() + } + + fun getSpeakers(sortCriteria: String): RealmResults { + return realmInstance.where(Speaker::class.java) + .sort(sortCriteria) + .findAllAsync() + } + + fun getSpeakersSync(sortCriteria: String): RealmResults { + return realmInstance.where(Speaker::class.java) + .sort(sortCriteria) + .findAllAsync() + } + + fun getSpeakersFiltered(query: String, sortCriteria: String): RealmResults { + val wildcardQuery = String.format("*%s*", query) + + return realmInstance.where(Speaker::class.java) + .like("name", wildcardQuery, Case.INSENSITIVE) + .sort(sortCriteria) + .findAllAsync() + } + + // Sponsors Section + + private fun saveSponsorsInRealm(sponsors: List) { + val realm = Realm.getDefaultInstance() + realm.executeTransaction({ realm1 -> realm1.insertOrUpdate(sponsors) }) + realm.close() + } + + fun saveSponsors(sponsors: List): Completable { + return Completable.fromAction { + saveSponsorsInRealm(sponsors) + Timber.d("Saved Sponsors") + } + } + + //DiscountCode section + + private fun saveDiscountCodesinRealm(discountCodes: List) { + val realm = Realm.getDefaultInstance() + realm.executeTransaction({ realm1 -> realm1.insertOrUpdate(discountCodes) }) + realm.close() + } + + fun saveDiscountCodes(discountCodes: List): Completable { + return Completable.fromAction { + saveDiscountCodesinRealm(discountCodes) + Timber.d("Saved DiscountCodes") + } + } + + // Location Section + + private fun saveLocationsInRealm(locations: List) { + val realm = Realm.getDefaultInstance() + realm.executeTransaction({ realm1 -> realm1.insertOrUpdate(locations) }) + realm.close() + } + + fun saveLocations(locations: List): Completable { + return Completable.fromAction { + saveLocationsInRealm(locations) + Timber.d("Saved Locations") + } + } + + //Session Types Section + + private fun saveSessionTypesInRealm(sessionTypes: List) { + val realm = Realm.getDefaultInstance() + realm.executeTransaction({ realm1 -> realm1.insertOrUpdate(sessionTypes) }) + realm.close() + } + + fun saveSessionTypes(sessionTypes: List): Completable { + return Completable.fromAction { + saveSessionTypesInRealm(sessionTypes) + Timber.d("Saved Session Types") + } + } + + // Dates Section + + /** + * Saves Event Dates Synchronously + * @param eventDates List of dates of entire event span (inclusive) + */ + private fun saveEventDatesInRealm(eventDates: List) { + val realm = Realm.getDefaultInstance() + realm.executeTransaction({ transaction -> + transaction.delete(EventDates::class.java) + transaction.insertOrUpdate(eventDates) + }) + realm.close() + } + + fun saveEventDates(eventDates: List): Completable { + return Completable.fromAction { saveEventDatesInRealm(eventDates) } + } + + // Notifications section + + fun saveNotifications(notifications: List): Completable { + return Completable.fromAction { + saveNotificationsInRealm(notifications) + Timber.d("Saved notifications") + } + } + + private fun saveNotificationsInRealm(notifications: List) { + realmInstance.executeTransaction({ realm1 -> + realm1.delete(Notification::class.java) + realm1.insertOrUpdate(notifications) + }) + } + + fun saveFAQs(faqs: List): Completable { + return Completable.fromAction { + saveFAQsInRealm(faqs) + Timber.d("Saved FAQs") + } + } + + private fun saveFAQsInRealm(faqs: List) { + val realm = Realm.getDefaultInstance() + realm.executeTransaction({ transaction -> + // Using a threaded instance now to handle relationship with FAQ types in the future. + transaction.delete(FAQ::class.java) + transaction.insertOrUpdate(faqs) + }) + realm.close() + } + + companion object { + + private var realmDataRepository: RealmDataRepository? = null + + private val repoCache = HashMap() + + val defaultInstance: RealmDataRepository + @JvmStatic + get() { + if (realmDataRepository == null) + realmDataRepository = RealmDataRepository(Realm.getDefaultInstance()) + + return realmDataRepository as RealmDataRepository + } + + /** + * For threaded operation, a separate Realm instance is needed, not the default + * instance, and thus all Realm objects can not pass through threads, extra care + * must be taken to close the Realm instance after use or else app will crash + * onDestroy of MainActivity. This is to ensure the database remains compact and + * application remains free of silent bugs + * @param realmInstance Separate Realm instance to be used + * @return Realm Data Repository + */ + @JvmStatic + fun getInstance(realmInstance: Realm): RealmDataRepository? { + if (!repoCache.containsKey(realmInstance)) { + repoCache[realmInstance] = RealmDataRepository(realmInstance) + } + return repoCache[realmInstance] + } + + @JvmStatic + fun isNull(track: Track?): Boolean { + return track == null || TextUtils.isEmpty(track.name) || TextUtils.isEmpty(track.color) + } + + /** + * Convert RealmResults to LiveRealmData + */ + @JvmStatic + fun asLiveData(data: RealmResults): LiveRealmData { + return LiveRealmData(data) + } + + /** + * Convert RealmObject to LiveRealmDataObject + */ + @JvmStatic + fun asLiveDataForObject(data: K): LiveRealmDataObject { + return LiveRealmDataObject(data) + } + + /** + * Convert RealmResults to FilterableRealmLiveData + */ + @JvmStatic + fun asFilterableLiveData(data: RealmResults): FilterableRealmLiveData { + return FilterableRealmLiveData(data) + } + + /** + * Compacts the database to save space + * Should be called when exiting application to ensure + * all Realm instances are ready to be closed. + * + * Closing the repoCache instances is the responsibility + * of caller + */ + @JvmStatic + fun compactDatabase() { + val realm = realmDataRepository!!.realmInstance + + Timber.d("Vacuuming the database") + Realm.compactRealm(realm.configuration) + } + } + +} \ No newline at end of file