diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 53d9c8c6..00000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "hackertracker/src/main/assets/database"] - path = hackertracker/src/main/assets/database - url = https://github.com/BeezleLabs/conferences diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 00000000..88ea3aa1 --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,122 @@ + + + + + + + + + +
+ + + + xmlns:android + + ^$ + + + +
+
+ + + + xmlns:.* + + ^$ + + + BY_NAME + +
+
+ + + + .*:id + + http://schemas.android.com/apk/res/android + + + +
+
+ + + + .*:name + + http://schemas.android.com/apk/res/android + + + +
+
+ + + + name + + ^$ + + + +
+
+ + + + style + + ^$ + + + +
+
+ + + + .* + + ^$ + + + BY_NAME + +
+
+ + + + .* + + http://schemas.android.com/apk/res/android + + + ANDROID_ATTRIBUTE_ORDER + +
+
+ + + + .* + + .* + + + BY_NAME + +
+
+
+
+ + +
+
\ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml deleted file mode 100644 index 96cc43ef..00000000 --- a/.idea/compiler.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/copyright/profiles_settings.xml b/.idea/copyright/profiles_settings.xml deleted file mode 100644 index e7bedf33..00000000 --- a/.idea/copyright/profiles_settings.xml +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml deleted file mode 100644 index 97626ba4..00000000 --- a/.idea/encodings.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml deleted file mode 100644 index 8d7d28cd..00000000 --- a/.idea/gradle.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 0df579a3..703e5d4b 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,41 +1,11 @@ - - - + + + + - + diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index 4e176ce2..00000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 35eb1ddf..00000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/hackertracker/.gitignore b/app/.gitignore similarity index 100% rename from hackertracker/.gitignore rename to app/.gitignore diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 00000000..d84eca23 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,105 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-android-extensions' +apply plugin: 'maven' +apply plugin: 'kotlin-kapt' +apply plugin: 'com.google.gms.google-services' +apply plugin: 'com.google.firebase.crashlytics' + +android { + compileSdkVersion 29 + defaultConfig { + applicationId "com.shortstack.hackertracker" + minSdkVersion 16 + targetSdkVersion 29 + versionCode 191 + versionName "6.2.0" + vectorDrawables.useSupportLibrary = true + multiDexEnabled true + } + buildTypes { + release { + minifyEnabled true + useProguard true + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility = 1.8 + targetCompatibility = 1.8 + } + kotlinOptions { + jvmTarget = "1.8" + } +} + +dependencies { + implementation fileTree(include: ['*.jar'], dir: 'libs') + + // Kotlin + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" + + // Support Libraries + implementation 'com.google.android.material:material:1.3.0-alpha01' + implementation 'androidx.appcompat:appcompat:1.3.0-alpha01' + implementation 'androidx.recyclerview:recyclerview:1.2.0-alpha03' + implementation 'androidx.cardview:cardview:1.0.0' + implementation 'androidx.preference:preference:1.1.1' + + implementation 'androidx.constraintlayout:constraintlayout:1.1.3' + + // Koin + implementation 'org.koin:koin-android:2.1.6' + implementation 'org.koin:koin-androidx-scope:2.1.6' + implementation 'org.koin:koin-androidx-viewmodel:2.1.6' + + // Arch + implementation "androidx.lifecycle:lifecycle-extensions:2.2.0" + implementation "androidx.lifecycle:lifecycle-viewmodel:2.3.0-alpha04" + implementation "android.arch.work:work-runtime-ktx:1.0.1" + implementation "android.arch.work:work-firebase:1.0.0-alpha11" + + // Retrofit + implementation 'com.squareup.retrofit2:retrofit:2.9.0' + implementation 'com.squareup.retrofit2:converter-gson:2.7.0' + implementation 'com.squareup.retrofit2:adapter-rxjava2:2.2.0' + implementation 'com.squareup.okhttp3:okhttp:3.14.9' + implementation 'com.squareup.okhttp3:logging-interceptor:3.9.0' + + // Pretty Logger + implementation 'com.orhanobut:logger:1.15' + // PDF Viewer + implementation 'com.github.barteksc:android-pdf-viewer:2.8.2' + // Reviews + implementation 'com.github.stkent:amplify:2.2.0' + // Firebase Job Dispatcher + implementation 'com.firebase:firebase-jobdispatcher:0.8.5' + + implementation 'com.github.Advice-Dog:timehop:master-SNAPSHOT' + + + implementation "com.hendraanggrian.material:collapsingtoolbarlayout-subtitle:1.0.0-beta01" + + // Firebase + implementation 'com.google.firebase:firebase-analytics:17.4.4' + implementation 'com.google.firebase:firebase-firestore:21.5.0' + implementation 'com.google.firebase:firebase-storage:19.1.1' + implementation 'com.google.firebase:firebase-auth:19.3.2' + implementation 'com.google.firebase:firebase-messaging:20.2.3' + implementation 'com.google.firebase:firebase-crashlytics:17.1.1' + implementation 'com.google.guava:guava:27.0.1-android' + + + // Coroutines + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.2.1' + + implementation 'androidx.multidex:multidex:2.0.1' + + testImplementation "junit:junit:4.12" + testImplementation "io.mockk:mockk:1.9.3.kotlin12" + testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:2.1.0" + testImplementation 'org.mockito:mockito-inline:2.21.0' + testImplementation 'org.koin:koin-test:2.0.0-beta-4' +} + diff --git a/hackertracker/google-services.json b/app/google-services.json similarity index 100% rename from hackertracker/google-services.json rename to app/google-services.json diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 00000000..145c6d91 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,31 @@ +# Preserve annotations, line numbers, and source file names +-keepattributes *Annotation* +-keepattributes SourceFile,LineNumberTable +-keep public class * extends java.lang.Exception +-keep class com.crashlytics.** { *; } +-dontwarn com.crashlytics.** + +-keepattributes Signature +-keepattributes Exceptions + +-keep class com.shortstack.hackertracker.network.** { *; } +-keep class com.shortstack.hackertracker.models.** { *; } +-keep class com.shortstack.hackertracker.ui.themes.** { *; } + +-dontwarn com.shortstack.hackertracker.views.** + +-keep class com.shortstack.hackertracker.ui.home.HomeFragment { *; } +-keep class com.shortstack.hackertracker.ui.schedule.ScheduleFragment { *; } +-keep class com.shortstack.hackertracker.ui.maps.MapsFragment { *; } +-keep class com.shortstack.hackertracker.ui.information.faq.FAQFragment { *; } +-keep class com.shortstack.hackertracker.ui.information.vendors.VendorsFragment { *; } +-keep class com.shortstack.hackertracker.ui.search.SearchFragment { *; } +-keep class com.shortstack.hackertracker.ui.settings.SettingsFragment { *; } + +-keep class android.support.v7.widget.SearchView { *; } + +# Parceler configuration +-keep interface org.parceler.Parcel +-keep @org.parceler.Parcel class * { *; } +-keep class **$$Parcelable { *; } +-keep class org.parceler.Parceler$$Parcels \ No newline at end of file diff --git a/hackertracker/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml similarity index 88% rename from hackertracker/src/main/AndroidManifest.xml rename to app/src/main/AndroidManifest.xml index 92feaddd..f0168aea 100644 --- a/hackertracker/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -4,12 +4,13 @@ android:installLocation="auto"> + - + - - - - - - (val status: Status, val data: T?, val message: String fun loading(data: T?) = Resource(Status.LOADING, data, null) - fun init(data: T?) = Resource(Status.NOT_INITIALIZED, data, null) + fun init(data: T? = null) = Resource(Status.NOT_INITIALIZED, data, null) } } diff --git a/app/src/main/java/com/shortstack/hackertracker/database/DatabaseManager.kt b/app/src/main/java/com/shortstack/hackertracker/database/DatabaseManager.kt new file mode 100644 index 00000000..9f36b590 --- /dev/null +++ b/app/src/main/java/com/shortstack/hackertracker/database/DatabaseManager.kt @@ -0,0 +1,563 @@ +package com.shortstack.hackertracker.database + +import android.util.Log +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.work.OneTimeWorkRequestBuilder +import androidx.work.WorkManager +import androidx.work.workDataOf +import com.google.android.gms.tasks.OnCompleteListener +import com.google.firebase.auth.FirebaseAuth +import com.google.firebase.auth.FirebaseUser +import com.google.firebase.firestore.FirebaseFirestore +import com.google.firebase.firestore.FirebaseFirestoreSettings +import com.google.firebase.firestore.Query +import com.google.firebase.iid.FirebaseInstanceId +import com.google.firebase.storage.FirebaseStorage +import com.orhanobut.logger.Logger +import com.shortstack.hackertracker.* +import com.shortstack.hackertracker.models.firebase.* +import com.shortstack.hackertracker.models.local.* +import com.shortstack.hackertracker.network.task.ReminderWorker +import com.shortstack.hackertracker.utilities.MyClock +import com.shortstack.hackertracker.utilities.Storage +import com.shortstack.hackertracker.utilities.now +import kotlinx.coroutines.tasks.await +import java.io.File +import java.util.concurrent.TimeUnit + + +class DatabaseManager(private val preferences: Storage) { + + companion object { + private const val CONFERENCES = "conferences" + + private const val USERS = "users" + private const val BOOKMARKS = "bookmarks" + + private const val EVENTS = "events" + private const val TYPES = "types" + private const val FAQS = "faqs" + private const val VENDORS = "vendors" + private const val SPEAKERS = "speakers" + private const val LOCATIONS = "locations" + private const val ARTICLES = "articles" + + fun getNextConference(preferred: Int, conferences: List): Conference? { + if (preferred != -1) { + val pref = conferences.find { it.id == preferred && !it.hasFinished } + if (pref != null) return pref + } + + val list = conferences.sortedBy { it.startDate } + + val defcon = list.find { it.code == "DEFCON27" } + if (defcon?.hasFinished == false) { + return defcon + } + + return list.firstOrNull { !it.hasFinished } + ?: conferences.lastOrNull() + } + } + + private val code + get() = conference.value?.code ?: "DEFCON27" + + private val firestore = FirebaseFirestore.getInstance() + private val storage = FirebaseStorage.getInstance() + private val auth = FirebaseAuth.getInstance() + + + val conference = MutableLiveData() + val conferences = MutableLiveData>() + + private var user: FirebaseUser? = null + + init { + if (!BuildConfig.DEBUG) { + val settings = FirebaseFirestoreSettings.Builder() + .setPersistenceEnabled(true) + .build() + + firestore.firestoreSettings = settings + } + + auth.signInAnonymously().addOnCompleteListener { + if (it.isSuccessful) { + user = it.result?.user ?: return@addOnCompleteListener + } + + firestore.collection(CONFERENCES) + .get() + .addOnCompleteListener { + if (it.isSuccessful) { + val list = it.result?.toObjects(FirebaseConference::class.java) + ?.filter { !it.hidden || App.isDeveloper } + ?.map { it.toConference() } + ?.sortedBy { it.startDate } + + ?: emptyList() + + + val con = getNextConference(preferences.preferredConference, list) + conference.postValue(con) + conferences.postValue(list) + + if (con != null) + getFCMToken(con) + } + } + } + } + + + private fun getFCMToken(conference: Conference) { + FirebaseInstanceId.getInstance().instanceId + .addOnCompleteListener(OnCompleteListener { task -> + if (!task.isSuccessful) { + Logger.e(task.exception, "Could not get token.") + return@OnCompleteListener + } + + // Get new Instance ID token + val token = task.result?.token + Logger.d("Obtained token: $token") + updateFirebaseMessagingToken(conference, token) + }) + } + + fun changeConference(id: Int) { + preferences.preferredConference = id + + val current = conference.value + + if (current != null) { + current.isSelected = false + } + + firestore.collection(CONFERENCES) + .whereEqualTo("id", id) + .get() + .addOnCompleteListener { + if (it.isSuccessful) { + val selected = + it.result?.toObjects(FirebaseConference::class.java)?.firstOrNull() + ?.toConference() + conference.postValue(selected) + } + } + } + + fun getConferences(): LiveData> { + val mutableLiveData = MutableLiveData>() + + firestore.collection(CONFERENCES) + .addSnapshotListener { snapshot, exception -> + if (exception == null) { + val cons = snapshot?.toObjects(FirebaseConference::class.java) + ?.filter { !it.hidden || App.isDeveloper } + ?.map { it.toConference() } + + mutableLiveData.postValue(cons) + } + } + + return mutableLiveData + } + + fun getEvents(id: Conference, type: Type?): LiveData> { + return getSchedule() + } + + + fun getSchedule(): MutableLiveData> { + val mutableLiveData = MutableLiveData>() + + firestore.collection(CONFERENCES) + .document(code) + .collection(EVENTS) + .addSnapshotListener { snapshot, exception -> + if (exception == null) { + val events = snapshot?.toObjects(FirebaseEvent::class.java) + ?.filter { (!it.hidden || App.isDeveloper) } + ?.map { it.toEvent() } + + mutableLiveData.postValue(events) + + val id = user?.uid + if (id != null) { + firestore.collection(CONFERENCES) + .document(code) + .collection(USERS) + .document(id) + .collection(BOOKMARKS) + .addSnapshotListener { snapshot, exception -> + if (exception == null) { + val bookmarks = + snapshot?.toObjects(FirebaseBookmark::class.java) + + bookmarks?.forEach { bookmark -> + events?.find { it.id.toString() == bookmark.id } + ?.isBookmarked = bookmark.value + } + + mutableLiveData.postValue(events) + } + } + } + } + } + + return mutableLiveData + } + + fun getTypes(id: Conference): LiveData> { + val mutableLiveData = MutableLiveData>() + + firestore.collection(CONFERENCES) + .document(id.code) + .collection(TYPES) + .addSnapshotListener { snapshot, exception -> + if (exception == null) { + val types = snapshot?.toObjects(FirebaseType::class.java)?.map { it.toType() } + mutableLiveData.postValue(types) + + val id = user?.uid + if (id != null) { + firestore.collection(CONFERENCES) + .document(code) + .collection(USERS) + .document(id) + .collection(TYPES) + .addSnapshotListener { snapshot, exception -> + if (exception == null) { + val bookmarks = + snapshot?.toObjects(FirebaseBookmark::class.java) + + types?.forEach { type -> + type.isSelected = + bookmarks?.find { type.id.toString() == it.id }?.value + ?: false + } + + mutableLiveData.postValue(types) + } + } + } + } + } + + return mutableLiveData + } + + + fun getRecent(): LiveData> { + val mutableLiveData = MutableLiveData>() + + firestore.collection(CONFERENCES) + .document(code) + .collection(EVENTS) + .orderBy("updated_timestamp", Query.Direction.DESCENDING) + .limit(5) + .get() + .addOnSuccessListener { + val events = it.toObjects(FirebaseEvent::class.java) + .filter { !it.hidden || App.isDeveloper } + .map { it.toEvent() } + + mutableLiveData.postValue(events) + } + + return mutableLiveData + } + + fun getArticles(id: Conference? = null): LiveData> { + val results = MutableLiveData>() + + firestore.collection(CONFERENCES) + .document(id?.code ?: code) + .collection(ARTICLES) + .orderBy("updated_at", Query.Direction.DESCENDING) + .addSnapshotListener { snapshot, exception -> + if (exception == null) { + val articles = snapshot?.toObjects(FirebaseArticle::class.java) + ?.filter { !it.hidden || App.isDeveloper } + ?.map { it.toArticle() } ?: emptyList() + + results.postValue(articles) + } + + } + + return results + } + + fun getBookmarks(conference: Conference? = null): LiveData> { + val result = MutableLiveData>() + + val id = user?.uid ?: return result + + firestore.collection(CONFERENCES) + .document(conference?.code ?: code) + .collection(EVENTS) + .get() + .addOnSuccessListener { + val events = it.toObjects(FirebaseEvent::class.java) + .filter { !it.hidden || App.isDeveloper } + .map { it.toEvent() } + + firestore.collection(CONFERENCES) + .document(code) + .collection(USERS) + .document(id) + .collection(BOOKMARKS) + .get() + .addOnSuccessListener { + val bookmarks = it.toObjects(FirebaseBookmark::class.java).map { it.id } + + val bookmarked = events.filter { it.id.toString() in bookmarks }.take(3) + bookmarked.forEach { it.isBookmarked = true } + + result.postValue(bookmarked) + + + } + } + + return result + } + + + fun getFAQ(id: Conference): LiveData> { + val mutableLiveData = MutableLiveData>() + + firestore.collection(CONFERENCES) + .document(id.code) + .collection(FAQS) + .get() + .addOnSuccessListener { + val faqs = it.toObjects(FirebaseFAQ::class.java) + .map { it.toFAQ() } + mutableLiveData.postValue(faqs) + } + + return mutableLiveData + } + + fun getLocations(id: Conference? = null): MutableLiveData> { + val mutableLiveData = MutableLiveData>() + + firestore.collection(CONFERENCES) + .document(id?.code ?: code) + .collection(LOCATIONS) + .addSnapshotListener { snapshot, exception -> + if (exception == null) { + val list = + snapshot?.toObjects(FirebaseLocation::class.java)?.map { it.toLocation() } + mutableLiveData.postValue(list) + } + } + + return mutableLiveData + } + + fun getVendors(conference: Conference): LiveData> { + val mutableLiveData = MutableLiveData>() + + firestore.collection(CONFERENCES) + .document(conference.code) + .collection(VENDORS) + .get() + .addOnSuccessListener { + val vendors = it.toObjects(FirebaseVendor::class.java) + .filter { !it.hidden || App.isDeveloper } + .map { it.toVendor() } + + mutableLiveData.postValue(vendors) + } + return mutableLiveData + } + + suspend fun getEventById(conference: String, id: Int): Event? { + val snapshot = firestore.collection(CONFERENCES) + .document(conference) + .collection(EVENTS) + .document(id.toString()) + .get() + .await() + + return snapshot.toObject(FirebaseEvent::class.java)?.toEvent() + } + + fun updateBookmark(event: Event) { + val tag = "reminder_" + event.id + + if (event.isBookmarked) { + val delay = event.start.time - MyClock().now().time - (1000 * 20 * 60) + + if (delay > 0) { + val data = workDataOf( + ReminderWorker.INPUT_ID to event.id, + ReminderWorker.INPUT_CONFERENCE to event.conference + ) + + val notify = OneTimeWorkRequestBuilder() + .setInputData(data) + .setInitialDelay(delay, TimeUnit.MILLISECONDS) + .addTag(tag) + .build() + + WorkManager.getInstance().enqueue(notify) + } + + } else { + WorkManager.getInstance().cancelAllWorkByTag(tag) + } + + val id = user?.uid ?: return + + val document = firestore.collection(CONFERENCES) + .document(event.conference) + .collection(USERS) + .document(id) + .collection(BOOKMARKS) + .document(event.id.toString()) + + if (event.isBookmarked) { + document.set( + mapOf( + "id" to event.id.toString(), + "value" to true + ) + ) + } else { + document.delete() + } + } + + fun updateTypeIsSelected(type: Type) { + val id = user?.uid ?: return + + val document = firestore.collection(CONFERENCES) + .document(type.conference) + .collection(USERS) + .document(id) + .collection(TYPES) + .document(type.id.toString()) + + if (type.isSelected) { + document.set( + mapOf( + "id" to type.id.toString(), + "value" to true + ) + ) + } else { + document.delete() + } + } + + private fun updateFirebaseMessagingToken(conference: Conference?, token: String?) { + val id = user?.uid + + if (conference == null || token == null || id == null) { + Log.e("TAG", "Null, cannot update token.") + return + } + + val document = firestore.collection(CONFERENCES) + .document(conference.code) + .collection(USERS) + .document(id) + + + document.set(mapOf("token" to token)) + } + + fun clear() { + + } + + + fun getSpeakers(conference: Conference): LiveData> { + val mutableLiveData = MutableLiveData>() + + firestore.collection(CONFERENCES) + .document(conference.code) + .collection(SPEAKERS) + .addSnapshotListener { snapshot, exception -> + if (exception == null) { + val speakers = snapshot?.toObjects(FirebaseSpeaker::class.java) + ?.filter { !it.hidden || App.isDeveloper } + ?.map { it.toSpeaker() } + ?: emptyList() + + mutableLiveData.postValue(speakers) + } + } + + return mutableLiveData + } + + fun getEventsForSpeaker(speaker: Speaker): LiveData> { + val mutableLiveData = MutableLiveData>() + + firestore.collection(CONFERENCES) + .document(code) + .collection(EVENTS) + .addSnapshotListener { snapshot, exception -> + if (exception == null) { + val events = snapshot?.toObjects(FirebaseEvent::class.java) + val filtered = + events?.filter { it.speakers.firstOrNull { it.id == speaker.id } != null } + ?.map { it.toEvent() } + mutableLiveData.postValue(filtered) + } + } + + return mutableLiveData + } + + + fun getMaps(conference: Conference): MutableLiveData> { + val mutableLiveData = MutableLiveData>() + + val list = ArrayList() + + val maps = conference.maps + if (maps.isEmpty()) { + return mutableLiveData + } + + maps.forEach { + val map = FirebaseConferenceMap(it.name, it.file, null) + list.add(map) + } + + mutableLiveData.postValue(list) + + list.forEach { + val filename = "${conference.code}-${it.path}" + val file = File(App.instance.applicationContext.getExternalFilesDir(null), filename) + if (file.exists()) { + it.file = file + } else { + file.createNewFile() + + val map = storage.reference.child("/${conference.code}/${it.path}") + + map.getFile(file).addOnSuccessListener { task -> + it.file = file + + mutableLiveData.postValue(list.toList()) + }.addOnFailureListener { + // Handle any errors + } + } + } + return mutableLiveData + } + + +} \ No newline at end of file diff --git a/app/src/main/java/com/shortstack/hackertracker/database/ReminderManager.kt b/app/src/main/java/com/shortstack/hackertracker/database/ReminderManager.kt new file mode 100644 index 00000000..641d041d --- /dev/null +++ b/app/src/main/java/com/shortstack/hackertracker/database/ReminderManager.kt @@ -0,0 +1,53 @@ +package com.shortstack.hackertracker.database + +import androidx.work.OneTimeWorkRequestBuilder +import androidx.work.WorkManager +import androidx.work.workDataOf +import com.shortstack.hackertracker.models.local.Event +import com.shortstack.hackertracker.network.task.ReminderWorker +import com.shortstack.hackertracker.utilities.MyClock +import com.shortstack.hackertracker.utilities.now +import java.util.concurrent.TimeUnit + +class ReminderManager( + private val databaseManager: DatabaseManager, + private val workManager: WorkManager +) { + + companion object { + private const val TWENTY_MINUTES_BEFORE = 1000 * 20 * 60 + private const val TAG = "reminder_" + } + + suspend fun getEvent(conference: String, id: Int): Event? { + return databaseManager.getEventById(conference, id) + } + + fun setReminder(event: Event) { + val start = event.start + val now = MyClock().now() + + val delay = start.time - now.time - TWENTY_MINUTES_BEFORE + + if (delay < 0) { + return + } + + val data = workDataOf( + ReminderWorker.INPUT_ID to event.id, + ReminderWorker.INPUT_CONFERENCE to event.conference + ) + + val notify = OneTimeWorkRequestBuilder() + .setInputData(data) + .setInitialDelay(delay, TimeUnit.MILLISECONDS) + .addTag(TAG + event.id) + .build() + + workManager.enqueue(notify) + } + + fun cancel(event: Event) { + workManager.cancelAllWorkByTag(TAG + event.id) + } +} \ No newline at end of file diff --git a/hackertracker/src/main/java/com/shortstack/hackertracker/di/modules.kt b/app/src/main/java/com/shortstack/hackertracker/di/modules.kt similarity index 56% rename from hackertracker/src/main/java/com/shortstack/hackertracker/di/modules.kt rename to app/src/main/java/com/shortstack/hackertracker/di/modules.kt index e6d6ec78..6428c4a0 100644 --- a/hackertracker/src/main/java/com/shortstack/hackertracker/di/modules.kt +++ b/app/src/main/java/com/shortstack/hackertracker/di/modules.kt @@ -1,25 +1,31 @@ package com.shortstack.hackertracker.di +import androidx.work.WorkManager import com.firebase.jobdispatcher.FirebaseJobDispatcher import com.firebase.jobdispatcher.GooglePlayDriver import com.google.gson.FieldNamingPolicy import com.google.gson.GsonBuilder -import com.shortstack.hackertracker.utilities.Analytics import com.shortstack.hackertracker.database.DatabaseManager +import com.shortstack.hackertracker.database.ReminderManager +import com.shortstack.hackertracker.ui.themes.ThemesManager +import com.shortstack.hackertracker.utilities.Analytics import com.shortstack.hackertracker.utilities.NotificationHelper import com.shortstack.hackertracker.utilities.Storage -import com.shortstack.hackertracker.utilities.TickTimer -import org.koin.dsl.module.module +import org.koin.dsl.module val appModule = module { - single { TickTimer() } - single { Storage(get()) } + single { Storage(get(), get()) } single { NotificationHelper(get()) } - single { GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create() } + single { + GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create() + } single { FirebaseJobDispatcher(GooglePlayDriver(get())) } single { DatabaseManager(get()) } + single { ThemesManager() } - single { Analytics(get()) } + single { Analytics(get(), get()) } + single { WorkManager.getInstance()!! } + single { ReminderManager(get(), get()) } } \ No newline at end of file diff --git a/hackertracker/src/main/java/com/shortstack/hackertracker/models/Day.kt b/app/src/main/java/com/shortstack/hackertracker/models/Day.kt similarity index 100% rename from hackertracker/src/main/java/com/shortstack/hackertracker/models/Day.kt rename to app/src/main/java/com/shortstack/hackertracker/models/Day.kt diff --git a/hackertracker/src/main/java/com/shortstack/hackertracker/models/Information.kt b/app/src/main/java/com/shortstack/hackertracker/models/Information.kt similarity index 100% rename from hackertracker/src/main/java/com/shortstack/hackertracker/models/Information.kt rename to app/src/main/java/com/shortstack/hackertracker/models/Information.kt diff --git a/hackertracker/src/main/java/com/shortstack/hackertracker/models/Time.kt b/app/src/main/java/com/shortstack/hackertracker/models/Time.kt similarity index 100% rename from hackertracker/src/main/java/com/shortstack/hackertracker/models/Time.kt rename to app/src/main/java/com/shortstack/hackertracker/models/Time.kt diff --git a/hackertracker/src/main/java/com/shortstack/hackertracker/models/firebase/FirebaseArticle.kt b/app/src/main/java/com/shortstack/hackertracker/models/firebase/FirebaseArticle.kt similarity index 87% rename from hackertracker/src/main/java/com/shortstack/hackertracker/models/firebase/FirebaseArticle.kt rename to app/src/main/java/com/shortstack/hackertracker/models/firebase/FirebaseArticle.kt index 073bb489..efe4d470 100644 --- a/hackertracker/src/main/java/com/shortstack/hackertracker/models/firebase/FirebaseArticle.kt +++ b/app/src/main/java/com/shortstack/hackertracker/models/firebase/FirebaseArticle.kt @@ -1,6 +1,7 @@ package com.shortstack.hackertracker.models.firebase data class FirebaseArticle( + val id: Int = -1, val name: String = "", val text: String = "", val hidden: Boolean = false diff --git a/hackertracker/src/main/java/com/shortstack/hackertracker/models/firebase/FirebaseBookmark.kt b/app/src/main/java/com/shortstack/hackertracker/models/firebase/FirebaseBookmark.kt similarity index 100% rename from hackertracker/src/main/java/com/shortstack/hackertracker/models/firebase/FirebaseBookmark.kt rename to app/src/main/java/com/shortstack/hackertracker/models/firebase/FirebaseBookmark.kt diff --git a/hackertracker/src/main/java/com/shortstack/hackertracker/models/firebase/FirebaseConference.kt b/app/src/main/java/com/shortstack/hackertracker/models/firebase/FirebaseConference.kt similarity index 89% rename from hackertracker/src/main/java/com/shortstack/hackertracker/models/firebase/FirebaseConference.kt rename to app/src/main/java/com/shortstack/hackertracker/models/firebase/FirebaseConference.kt index 1a1d0bf1..3248f2d9 100644 --- a/hackertracker/src/main/java/com/shortstack/hackertracker/models/firebase/FirebaseConference.kt +++ b/app/src/main/java/com/shortstack/hackertracker/models/firebase/FirebaseConference.kt @@ -11,11 +11,13 @@ data class FirebaseConference( val id: Int = 0, val name: String = "", val description: String = "", + val codeofconduct: String? = null, val code: String = "", val maps: ArrayList = ArrayList(), val start_date: String = "", val end_date: String = "", val start_timestamp: Timestamp = Timestamp(Date()), val end_timestamp: Timestamp = Timestamp(Date()), + val timezone: String = "", val hidden: Boolean = false ) : Parcelable \ No newline at end of file diff --git a/app/src/main/java/com/shortstack/hackertracker/models/firebase/FirebaseConferenceMap.kt b/app/src/main/java/com/shortstack/hackertracker/models/firebase/FirebaseConferenceMap.kt new file mode 100644 index 00000000..7ef332ee --- /dev/null +++ b/app/src/main/java/com/shortstack/hackertracker/models/firebase/FirebaseConferenceMap.kt @@ -0,0 +1,5 @@ +package com.shortstack.hackertracker.models.firebase + +import java.io.File + +data class FirebaseConferenceMap(val title: String, val path: String, var file: File?) \ No newline at end of file diff --git a/hackertracker/src/main/java/com/shortstack/hackertracker/models/firebase/FirebaseEvent.kt b/app/src/main/java/com/shortstack/hackertracker/models/firebase/FirebaseEvent.kt similarity index 100% rename from hackertracker/src/main/java/com/shortstack/hackertracker/models/firebase/FirebaseEvent.kt rename to app/src/main/java/com/shortstack/hackertracker/models/firebase/FirebaseEvent.kt diff --git a/hackertracker/src/main/java/com/shortstack/hackertracker/models/firebase/FirebaseFAQ.kt b/app/src/main/java/com/shortstack/hackertracker/models/firebase/FirebaseFAQ.kt similarity index 100% rename from hackertracker/src/main/java/com/shortstack/hackertracker/models/firebase/FirebaseFAQ.kt rename to app/src/main/java/com/shortstack/hackertracker/models/firebase/FirebaseFAQ.kt diff --git a/hackertracker/src/main/java/com/shortstack/hackertracker/models/firebase/FirebaseLocation.kt b/app/src/main/java/com/shortstack/hackertracker/models/firebase/FirebaseLocation.kt similarity index 87% rename from hackertracker/src/main/java/com/shortstack/hackertracker/models/firebase/FirebaseLocation.kt rename to app/src/main/java/com/shortstack/hackertracker/models/firebase/FirebaseLocation.kt index 75144395..eaa5f76d 100644 --- a/hackertracker/src/main/java/com/shortstack/hackertracker/models/firebase/FirebaseLocation.kt +++ b/app/src/main/java/com/shortstack/hackertracker/models/firebase/FirebaseLocation.kt @@ -6,5 +6,6 @@ import kotlinx.android.parcel.Parcelize @Parcelize data class FirebaseLocation( val name: String = "", + val hotel: String? = null, val conference: String = "" ) : Parcelable \ No newline at end of file diff --git a/hackertracker/src/main/java/com/shortstack/hackertracker/models/firebase/FirebaseMap.kt b/app/src/main/java/com/shortstack/hackertracker/models/firebase/FirebaseMap.kt similarity index 100% rename from hackertracker/src/main/java/com/shortstack/hackertracker/models/firebase/FirebaseMap.kt rename to app/src/main/java/com/shortstack/hackertracker/models/firebase/FirebaseMap.kt diff --git a/hackertracker/src/main/java/com/shortstack/hackertracker/models/firebase/FirebaseSpeaker.kt b/app/src/main/java/com/shortstack/hackertracker/models/firebase/FirebaseSpeaker.kt similarity index 100% rename from hackertracker/src/main/java/com/shortstack/hackertracker/models/firebase/FirebaseSpeaker.kt rename to app/src/main/java/com/shortstack/hackertracker/models/firebase/FirebaseSpeaker.kt diff --git a/hackertracker/src/main/java/com/shortstack/hackertracker/models/firebase/FirebaseType.kt b/app/src/main/java/com/shortstack/hackertracker/models/firebase/FirebaseType.kt similarity index 69% rename from hackertracker/src/main/java/com/shortstack/hackertracker/models/firebase/FirebaseType.kt rename to app/src/main/java/com/shortstack/hackertracker/models/firebase/FirebaseType.kt index bae2df1d..79537149 100644 --- a/hackertracker/src/main/java/com/shortstack/hackertracker/models/firebase/FirebaseType.kt +++ b/app/src/main/java/com/shortstack/hackertracker/models/firebase/FirebaseType.kt @@ -9,9 +9,4 @@ data class FirebaseType( val name: String = "", val conference: String = "", val color: String = "#343434" -) : Parcelable { - - val filtered: Boolean - get() = name.contains("Workshop", true) || name.contains("Contest", true) - -} \ No newline at end of file +) : Parcelable \ No newline at end of file diff --git a/hackertracker/src/main/java/com/shortstack/hackertracker/models/firebase/FirebaseVendor.kt b/app/src/main/java/com/shortstack/hackertracker/models/firebase/FirebaseVendor.kt similarity index 100% rename from hackertracker/src/main/java/com/shortstack/hackertracker/models/firebase/FirebaseVendor.kt rename to app/src/main/java/com/shortstack/hackertracker/models/firebase/FirebaseVendor.kt diff --git a/app/src/main/java/com/shortstack/hackertracker/models/local/Article.kt b/app/src/main/java/com/shortstack/hackertracker/models/local/Article.kt new file mode 100644 index 00000000..e5a3634a --- /dev/null +++ b/app/src/main/java/com/shortstack/hackertracker/models/local/Article.kt @@ -0,0 +1,3 @@ +package com.shortstack.hackertracker.models.local + +data class Article(val id: Int, val name: String, val text: String) \ No newline at end of file diff --git a/hackertracker/src/main/java/com/shortstack/hackertracker/models/local/Conference.kt b/app/src/main/java/com/shortstack/hackertracker/models/local/Conference.kt similarity index 92% rename from hackertracker/src/main/java/com/shortstack/hackertracker/models/local/Conference.kt rename to app/src/main/java/com/shortstack/hackertracker/models/local/Conference.kt index 21cec436..6a53bcdb 100644 --- a/hackertracker/src/main/java/com/shortstack/hackertracker/models/local/Conference.kt +++ b/app/src/main/java/com/shortstack/hackertracker/models/local/Conference.kt @@ -13,10 +13,12 @@ data class Conference( val id: Int, val name: String, val description: String, + val conduct: String?, val code: String, val maps: ArrayList, val startDate: Date, val endDate: Date, + val timezone: String, var isSelected: Boolean = false ) : Parcelable { diff --git a/hackertracker/src/main/java/com/shortstack/hackertracker/models/local/Event.kt b/app/src/main/java/com/shortstack/hackertracker/models/local/Event.kt similarity index 80% rename from hackertracker/src/main/java/com/shortstack/hackertracker/models/local/Event.kt rename to app/src/main/java/com/shortstack/hackertracker/models/local/Event.kt index d0f6077a..2d2da696 100644 --- a/hackertracker/src/main/java/com/shortstack/hackertracker/models/local/Event.kt +++ b/app/src/main/java/com/shortstack/hackertracker/models/local/Event.kt @@ -7,6 +7,7 @@ import com.shortstack.hackertracker.utilities.MyClock import com.shortstack.hackertracker.utilities.TimeUtil import com.shortstack.hackertracker.utilities.now import kotlinx.android.parcel.Parcelize +import java.text.SimpleDateFormat import java.util.* @Parcelize @@ -22,7 +23,8 @@ data class Event( val speakers: List, val type: Type, val location: Location, - var isBookmarked: Boolean = false) : Parcelable { + var isBookmarked: Boolean = false, + var key: Long = -1) : Parcelable { val progress: Float get() { @@ -61,15 +63,14 @@ data class Event( } fun getFullTimeStamp(context: Context): String { - val (begin, end) = getTimeStamp(context) - val timestamp = TimeUtil.getRelativeDateStamp(context, start) + val date = TimeUtil.getDateStamp(start) - return String.format(context.getString(R.string.timestamp_full), timestamp, begin, end) - } + val time = if (android.text.format.DateFormat.is24HourFormat(context)) { + SimpleDateFormat("HH:mm").format(start) + } else { + SimpleDateFormat("h:mm aa").format(start) + } - private fun getTimeStamp(context: Context): Pair { - val begin = TimeUtil.getTimeStamp(context, start) - val end = TimeUtil.getTimeStamp(context, end) - return Pair(begin, end) + return String.format(context.getString(R.string.timestamp_start), date, time) } } \ No newline at end of file diff --git a/app/src/main/java/com/shortstack/hackertracker/models/local/FAQ.kt b/app/src/main/java/com/shortstack/hackertracker/models/local/FAQ.kt new file mode 100644 index 00000000..bb8b0447 --- /dev/null +++ b/app/src/main/java/com/shortstack/hackertracker/models/local/FAQ.kt @@ -0,0 +1,8 @@ +package com.shortstack.hackertracker.models.local + +data class FAQ( + val id: Int, + val question: String, + val answer: String, + var isExpanded: Boolean = false +) \ No newline at end of file diff --git a/hackertracker/src/main/java/com/shortstack/hackertracker/models/local/Location.kt b/app/src/main/java/com/shortstack/hackertracker/models/local/Location.kt similarity index 88% rename from hackertracker/src/main/java/com/shortstack/hackertracker/models/local/Location.kt rename to app/src/main/java/com/shortstack/hackertracker/models/local/Location.kt index 5acd65c9..f713b1ed 100644 --- a/hackertracker/src/main/java/com/shortstack/hackertracker/models/local/Location.kt +++ b/app/src/main/java/com/shortstack/hackertracker/models/local/Location.kt @@ -6,5 +6,6 @@ import kotlinx.android.parcel.Parcelize @Parcelize data class Location( val name: String, + val hotel: String?, val conference: String ) : Parcelable \ No newline at end of file diff --git a/hackertracker/src/main/java/com/shortstack/hackertracker/models/local/Speaker.kt b/app/src/main/java/com/shortstack/hackertracker/models/local/Speaker.kt similarity index 100% rename from hackertracker/src/main/java/com/shortstack/hackertracker/models/local/Speaker.kt rename to app/src/main/java/com/shortstack/hackertracker/models/local/Speaker.kt diff --git a/hackertracker/src/main/java/com/shortstack/hackertracker/models/local/Type.kt b/app/src/main/java/com/shortstack/hackertracker/models/local/Type.kt similarity index 78% rename from hackertracker/src/main/java/com/shortstack/hackertracker/models/local/Type.kt rename to app/src/main/java/com/shortstack/hackertracker/models/local/Type.kt index d7222293..2214bf50 100644 --- a/hackertracker/src/main/java/com/shortstack/hackertracker/models/local/Type.kt +++ b/app/src/main/java/com/shortstack/hackertracker/models/local/Type.kt @@ -14,6 +14,4 @@ data class Type( val isBookmark: Boolean get() = name.contains("bookmark", true) - val filtered: Boolean - get() = name.contains("Workshop", true) || name.contains("Contest", true) } \ No newline at end of file diff --git a/hackertracker/src/main/java/com/shortstack/hackertracker/models/local/Vendor.kt b/app/src/main/java/com/shortstack/hackertracker/models/local/Vendor.kt similarity index 75% rename from hackertracker/src/main/java/com/shortstack/hackertracker/models/local/Vendor.kt rename to app/src/main/java/com/shortstack/hackertracker/models/local/Vendor.kt index 6de399ab..3755dc56 100644 --- a/hackertracker/src/main/java/com/shortstack/hackertracker/models/local/Vendor.kt +++ b/app/src/main/java/com/shortstack/hackertracker/models/local/Vendor.kt @@ -12,18 +12,11 @@ data class Vendor( val partner: Boolean ) : Parcelable { - val colour: Int - get() { - if (id == -1) - return 0 - return id - } - val summary: String get() { if (description.isNullOrBlank()) return "Nothing to say." - return description + return description.replace("\\n", "\n") } } diff --git a/app/src/main/java/com/shortstack/hackertracker/network/task/ReminderWorker.kt b/app/src/main/java/com/shortstack/hackertracker/network/task/ReminderWorker.kt new file mode 100644 index 00000000..cc8b0352 --- /dev/null +++ b/app/src/main/java/com/shortstack/hackertracker/network/task/ReminderWorker.kt @@ -0,0 +1,40 @@ +package com.shortstack.hackertracker.network.task + +import android.content.Context +import androidx.work.Worker +import androidx.work.WorkerParameters +import com.shortstack.hackertracker.database.DatabaseManager +import com.shortstack.hackertracker.utilities.NotificationHelper +import kotlinx.coroutines.runBlocking +import org.koin.core.KoinComponent +import org.koin.core.inject + +class ReminderWorker(context: Context, params: WorkerParameters) : Worker(context, params), + KoinComponent { + + private val database: DatabaseManager by inject() + private val notifications: NotificationHelper by inject() + + override fun doWork(): Result { + val conference = inputData.getString(INPUT_CONFERENCE) + val id = inputData.getInt(INPUT_ID, -1) + + if (conference == null || id == -1) { + return Result.failure() + } + + runBlocking { + val event = database.getEventById(conference, id) + if (event != null) { + notifications.notifyStartingSoon(event) + } + } + + return Result.success() + } + + companion object { + const val INPUT_CONFERENCE = "INPUT_CONFERENCE" + const val INPUT_ID = "INPUT_ID" + } +} diff --git a/app/src/main/java/com/shortstack/hackertracker/ui/HackerTrackerViewModel.kt b/app/src/main/java/com/shortstack/hackertracker/ui/HackerTrackerViewModel.kt new file mode 100644 index 00000000..bfc00bc0 --- /dev/null +++ b/app/src/main/java/com/shortstack/hackertracker/ui/HackerTrackerViewModel.kt @@ -0,0 +1,337 @@ +package com.shortstack.hackertracker.ui + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MediatorLiveData +import androidx.lifecycle.Transformations +import androidx.lifecycle.ViewModel +import com.shortstack.hackertracker.Resource +import com.shortstack.hackertracker.database.DatabaseManager +import com.shortstack.hackertracker.models.firebase.FirebaseConferenceMap +import com.shortstack.hackertracker.models.local.* +import com.shortstack.hackertracker.ui.themes.ThemesManager +import com.shortstack.hackertracker.utilities.Storage +import org.koin.core.KoinComponent +import org.koin.core.inject + +class HackerTrackerViewModel : ViewModel(), KoinComponent { + + private val database: DatabaseManager by inject() + private val storage: Storage by inject() + private val themes: ThemesManager by inject() + + + val conference: LiveData> + val events: LiveData>> + val bookmarks: LiveData>> + val types: LiveData>> + val locations: LiveData>> + val speakers: LiveData>> + + val articles: LiveData>> + val faq: LiveData>> + val vendors: LiveData>> + + val maps: LiveData>> + + // Home + val home: LiveData>> + + // Schedule + val schedule: LiveData>> + + + // Search + private val query = MediatorLiveData() + val search: LiveData> + + + init { + conference = Transformations.switchMap(database.conference) { + val result = MediatorLiveData>() + + if (it == null) { + result.value = Resource.init() + } else { + result.value = Resource.success(it) + } + + + return@switchMap result + } + + types = Transformations.switchMap(database.conference) { + val result = MediatorLiveData>>() + + if (it == null) { + result.value = Resource.init() + } else { + result.addSource(database.getTypes(it)) { + result.value = Resource.success(it) + } + } + return@switchMap result + } + + locations = Transformations.switchMap(database.conference) { + val result = MediatorLiveData>>() + + if (it == null) { + result.value = Resource.init() + } else { + result.addSource(database.getLocations(it)) { + result.value = Resource.success(it) + } + } + return@switchMap result + } + + events = Transformations.switchMap(database.conference) { + val result = MediatorLiveData>>() + + if (it == null) { + result.value = Resource.init() + } else { + result.addSource(database.getSchedule()) { + result.value = Resource.success(it) + } + } + + + return@switchMap result + } + + schedule = Transformations.switchMap(database.conference) { id -> + val result = MediatorLiveData>>() + + if (id == null) { + result.value = Resource.init(null) + return@switchMap result + } + + result.value = Resource.loading(null) + + result.addSource(events) { + val types = types.value?.data ?: emptyList() + result.value = Resource.success(getSchedule(it?.data ?: emptyList(), types)) + } + + result.addSource(types) { types -> + val events = events.value?.data ?: return@addSource + result.value = Resource.success(getSchedule(events, types?.data ?: emptyList())) + } + + return@switchMap result + } + + bookmarks = Transformations.switchMap(database.conference) { + val result = MediatorLiveData>>() + + if (it == null) { + result.value = Resource.init(null) + return@switchMap result + } else { + result.addSource(database.getBookmarks(it)) { + result.value = Resource.success(it) + } + } + + + + return@switchMap result + } + + + speakers = Transformations.switchMap(database.conference) { + val result = MediatorLiveData>>() + + if (it == null) { + result.value = Resource.init() + } else { + result.addSource(database.getSpeakers(it)) { + result.value = Resource.success(it) + } + } + + + return@switchMap result + } + + articles = Transformations.switchMap(database.conference) { + val result = MediatorLiveData>>() + + if (it == null) { + result.value = Resource.init() + } else { + result.addSource(database.getArticles(it)) { + result.value = Resource.success(it) + } + } + + + return@switchMap result + } + + faq = Transformations.switchMap(database.conference) { + val result = MediatorLiveData>>() + + if (it == null) { + result.value = Resource.init() + } else { + result.addSource(database.getFAQ(it)) { + result.value = Resource.success(it) + } + } + + return@switchMap result + } + + vendors = Transformations.switchMap(database.conference) { + val result = MediatorLiveData>>() + + if (it == null) { + result.value = Resource.init() + } else { + result.addSource(database.getVendors(it)) { + result.value = Resource.success(it) + } + } + + return@switchMap result + } + + maps = Transformations.switchMap(database.conference) { + val result = MediatorLiveData>>() + + if (it == null) { + result.value = Resource.init() + } else { + result.addSource(database.getMaps(it)) { + result.value = Resource.success(it) + } + } + + return@switchMap result + } + + search = Transformations.switchMap(query) { text -> + val results = MediatorLiveData>() + + results.addSource(events) { + val locations = locations.value?.data ?: emptyList() + val speakers = speakers.value?.data ?: emptyList() + setValue(results, text, it?.data ?: emptyList(), locations, speakers) + } + + results.addSource(locations) { + val events = events.value?.data ?: emptyList() + val speakers = speakers.value?.data ?: emptyList() + setValue(results, text, events, it?.data ?: emptyList(), speakers) + } + + results.addSource(speakers) { + val events = events.value?.data ?: emptyList() + val locations = locations.value?.data ?: emptyList() + setValue(results, text, events, locations, it?.data ?: emptyList()) + } + + return@switchMap results + } + + home = Transformations.switchMap(database.conference) { id -> + val result = MediatorLiveData>>() + + if (id == null) { + result.value = Resource.init(null) + return@switchMap result + } + + result.value = Resource.loading(null) + + result.addSource(bookmarks) { + val articles = articles.value?.data?.take(4) ?: emptyList() + result.value = Resource.success(articles + (it.data?.take(3) ?: emptyList())) + } + + result.addSource(articles) { + val bookmarks = bookmarks.value?.data?.take(3) ?: emptyList() + result.value = Resource.success((it.data?.take(4) ?: emptyList()) + bookmarks) + } + + return@switchMap result + } + + } + + private fun getSchedule(events: List, types: List): List { + if (types.isEmpty()) + return events + + val requireBookmark = types.firstOrNull { it.isBookmark }?.isSelected ?: false + val filter = types.filter { !it.isBookmark && it.isSelected } + if (!requireBookmark && filter.isEmpty()) + return events + + if (requireBookmark && filter.isEmpty()) + return events.filter { it.isBookmarked } + + return events.filter { event -> isShown(event, requireBookmark, filter) } + } + + private fun isShown(event: Event, requireBookmark: Boolean, filter: List): Boolean { + val bookmark = if (requireBookmark) { + event.isBookmarked + } else { + true + } + + return bookmark && filter.find { it.id == event.type.id }?.isSelected == true + } + + private fun setValue( + results: MediatorLiveData>, + query: String, + events: List, + locations: List, + speakers: List + ) { + if (query.isBlank()) { + results.value = emptyList() + return + } + + val list = ArrayList() + + val speakers = speakers.filter { + it.name.contains(query, true) || it.description.contains( + query, + true + ) + } + if (speakers.isNotEmpty()) { + list.add("Speakers") + list.addAll(speakers) + } + + val locations = locations.filter { it.name.contains(query, true) } + locations.forEach { location -> + list.add(location) + // TODO: Should we add the filtered events, or all events for this location? + list.addAll(events.filter { it.location.name == location.name }.sortedBy { it.start }) + } + + val events = + events.filter { it.title.contains(query, true) || it.description.contains(query, true) } + if (events.isNotEmpty()) { + list.add("Events") + list.addAll(events) + } + + results.value = list + } + + + fun onQueryTextChange(text: String?) { + query.value = text + } + +} \ No newline at end of file diff --git a/hackertracker/src/main/java/com/shortstack/hackertracker/ui/ListAdapter.kt b/app/src/main/java/com/shortstack/hackertracker/ui/ListAdapter.kt similarity index 72% rename from hackertracker/src/main/java/com/shortstack/hackertracker/ui/ListAdapter.kt rename to app/src/main/java/com/shortstack/hackertracker/ui/ListAdapter.kt index 34ef8184..b8fffa96 100644 --- a/hackertracker/src/main/java/com/shortstack/hackertracker/ui/ListAdapter.kt +++ b/app/src/main/java/com/shortstack/hackertracker/ui/ListAdapter.kt @@ -3,19 +3,14 @@ package com.shortstack.hackertracker.ui import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView import com.shortstack.hackertracker.models.Day -import com.shortstack.hackertracker.models.Time -import com.shortstack.hackertracker.models.firebase.FirebaseFAQ -import com.shortstack.hackertracker.models.local.Location -import com.shortstack.hackertracker.models.local.Speaker -import com.shortstack.hackertracker.models.local.Event -import com.shortstack.hackertracker.models.local.Vendor -import com.shortstack.hackertracker.ui.information.FAQViewHolder +import com.shortstack.hackertracker.models.local.* +import com.shortstack.hackertracker.ui.information.faq.FAQViewHolder +import com.shortstack.hackertracker.ui.information.speakers.SpeakerViewHolder +import com.shortstack.hackertracker.ui.information.vendors.VendorViewHolder import com.shortstack.hackertracker.ui.schedule.DayViewHolder import com.shortstack.hackertracker.ui.schedule.EventViewHolder -import com.shortstack.hackertracker.ui.schedule.TimeViewHolder import com.shortstack.hackertracker.ui.search.LocationViewHolder -import com.shortstack.hackertracker.ui.speakers.SpeakerViewHolder -import com.shortstack.hackertracker.ui.vendors.VendorViewHolder +import com.shortstack.hackertracker.views.EventView class ListAdapter : RecyclerView.Adapter() { @@ -24,7 +19,6 @@ class ListAdapter : RecyclerView.Adapter() { private const val LOCATION = 1 private const val SPEAKER = 2 private const val DAY = 3 - private const val TIME = 4 private const val VENDOR = 5 private const val FAQ = 6 } @@ -33,11 +27,10 @@ class ListAdapter : RecyclerView.Adapter() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { return when (viewType) { - EVENT -> EventViewHolder.inflate(parent) + EVENT -> EventViewHolder.inflate(parent, EventView.DISPLAY_MODE_MIN) SPEAKER -> SpeakerViewHolder.inflate(parent) LOCATION -> LocationViewHolder.inflate(parent) DAY -> DayViewHolder.inflate(parent) - TIME -> TimeViewHolder.inflate(parent) VENDOR -> VendorViewHolder.inflate(parent) FAQ -> FAQViewHolder.inflate(parent) else -> throw IllegalStateException("Unknown viewType $viewType.") @@ -54,9 +47,8 @@ class ListAdapter : RecyclerView.Adapter() { is SpeakerViewHolder -> holder.render(item as Speaker) is LocationViewHolder -> holder.render(item as Location) is DayViewHolder -> holder.render(item as Day) - is TimeViewHolder -> holder.render(item as Time) is VendorViewHolder -> holder.render(item as Vendor) - is FAQViewHolder -> holder.render(item as FirebaseFAQ) + is FAQViewHolder -> holder.render(item as FAQ) } } @@ -66,9 +58,8 @@ class ListAdapter : RecyclerView.Adapter() { is Location -> LOCATION is Event -> EVENT is Day -> DAY - is Time -> TIME is Vendor -> VENDOR - is FirebaseFAQ -> FAQ + is FAQ -> FAQ else -> throw java.lang.IllegalStateException("Unknown viewType ${collection[position].javaClass}") } } diff --git a/hackertracker/src/main/java/com/shortstack/hackertracker/ui/ListFragment.kt b/app/src/main/java/com/shortstack/hackertracker/ui/ListFragment.kt similarity index 100% rename from hackertracker/src/main/java/com/shortstack/hackertracker/ui/ListFragment.kt rename to app/src/main/java/com/shortstack/hackertracker/ui/ListFragment.kt diff --git a/hackertracker/src/main/java/com/shortstack/hackertracker/ui/activities/MainActivity.kt b/app/src/main/java/com/shortstack/hackertracker/ui/activities/MainActivity.kt similarity index 54% rename from hackertracker/src/main/java/com/shortstack/hackertracker/ui/activities/MainActivity.kt rename to app/src/main/java/com/shortstack/hackertracker/ui/activities/MainActivity.kt index 00ab4e7c..af7b7f9a 100644 --- a/hackertracker/src/main/java/com/shortstack/hackertracker/ui/activities/MainActivity.kt +++ b/app/src/main/java/com/shortstack/hackertracker/ui/activities/MainActivity.kt @@ -1,126 +1,131 @@ package com.shortstack.hackertracker.ui.activities +import android.content.Context +import android.content.Intent import android.content.res.Resources +import android.os.Build import android.os.Bundle +import android.util.TypedValue import android.view.Menu import android.view.MenuItem import android.view.View -import androidx.appcompat.app.ActionBarDrawerToggle import androidx.appcompat.app.AppCompatActivity -import androidx.core.content.ContextCompat import androidx.core.view.GravityCompat -import androidx.core.view.ViewCompat import androidx.drawerlayout.widget.DrawerLayout import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProviders import com.github.stkent.amplify.tracking.Amplify -import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.navigation.NavigationView.OnNavigationItemSelectedListener import com.google.firebase.auth.FirebaseAuth import com.orhanobut.logger.Logger import com.shortstack.hackertracker.BuildConfig import com.shortstack.hackertracker.R -import com.shortstack.hackertracker.database.DatabaseManager -import com.shortstack.hackertracker.models.local.Speaker import com.shortstack.hackertracker.models.local.Event +import com.shortstack.hackertracker.models.local.Location +import com.shortstack.hackertracker.models.local.Speaker +import com.shortstack.hackertracker.models.local.Type import com.shortstack.hackertracker.replaceFragment -import com.shortstack.hackertracker.ui.SearchFragment -import com.shortstack.hackertracker.ui.settings.SettingsFragment -import com.shortstack.hackertracker.ui.contests.ContestsFragment +import com.shortstack.hackertracker.ui.HackerTrackerViewModel +import com.shortstack.hackertracker.ui.search.SearchFragment import com.shortstack.hackertracker.ui.events.EventFragment import com.shortstack.hackertracker.ui.home.HomeFragment import com.shortstack.hackertracker.ui.information.InformationFragment +import com.shortstack.hackertracker.ui.information.speakers.SpeakerFragment import com.shortstack.hackertracker.ui.maps.MapsFragment import com.shortstack.hackertracker.ui.schedule.ScheduleFragment -import com.shortstack.hackertracker.ui.speakers.SpeakerFragment -import com.shortstack.hackertracker.ui.speakers.SpeakersFragment -import com.shortstack.hackertracker.ui.vendors.VendorsFragment -import com.shortstack.hackertracker.ui.workshops.WorkshopFragment -import com.shortstack.hackertracker.utilities.TickTimer +import com.shortstack.hackertracker.ui.settings.SettingsFragment +import com.shortstack.hackertracker.ui.themes.ThemesManager.Theme.* +import com.shortstack.hackertracker.utilities.Storage import kotlinx.android.synthetic.main.activity_main.* import kotlinx.android.synthetic.main.app_bar_main.* import kotlinx.android.synthetic.main.nav_header_main.view.* import kotlinx.android.synthetic.main.row_nav_view.* -import kotlinx.android.synthetic.main.view_filter.* import org.koin.android.ext.android.inject class MainActivity : AppCompatActivity(), OnNavigationItemSelectedListener, FragmentManager.OnBackStackChangedListener { - private val database: DatabaseManager by inject() - - private val timer: TickTimer by inject() + private val storage: Storage by inject() - private lateinit var bottomSheet: BottomSheetBehavior - - private lateinit var viewModel: MainActivityViewModel + private lateinit var viewModel: HackerTrackerViewModel private val auth: FirebaseAuth by lazy { FirebaseAuth.getInstance() } private val map = HashMap() + private var secondaryVisible = false + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.activity_main) - setSupportActionBar(toolbar) initNavDrawer() - viewModel = ViewModelProviders.of(this).get(MainActivityViewModel::class.java) + viewModel = ViewModelProvider(this)[HackerTrackerViewModel::class.java] viewModel.conference.observe(this, Observer { if (it != null) { - nav_view.getHeaderView(0).nav_title.text = it.name - } - }) - - viewModel.types.observe(this, Observer { - - val hasContest = it.firstOrNull { it.name == "Contest" } != null - if (!hasContest) { - nav_view.menu.removeItem(NAV_CONTESTS) - } else if (nav_view.menu.findItem(NAV_CONTESTS) == null) { - nav_view.menu.add(R.id.nav_main, NAV_CONTESTS, 3, R.string.contests).apply { - icon = ContextCompat.getDrawable(this@MainActivity, R.drawable.ic_cake_white_24dp) - } - } - - val hasWorkshops = it.firstOrNull { it.name == "Workshop" } != null - if (!hasWorkshops) { - nav_view.menu.removeItem(NAV_WORKSHOPS) - } else if (nav_view.menu.findItem(NAV_WORKSHOPS) == null) { - nav_view.menu.add(R.id.nav_main, NAV_WORKSHOPS, 3, R.string.workshops).apply { - icon = ContextCompat.getDrawable(this@MainActivity, R.drawable.ic_computer_white_24dp) - } + nav_view.getHeaderView(0).nav_title.text = it.data?.name } - - filters.setTypes(it) }) - bottomSheet = BottomSheetBehavior.from(filters) - bottomSheet.state = BottomSheetBehavior.STATE_HIDDEN - - filter.setOnClickListener { expandFilters() } - close.setOnClickListener { hideFilters() } - if (savedInstanceState == null) { if (Amplify.getSharedInstance().shouldPrompt() && !BuildConfig.DEBUG) { val review = ReviewBottomSheet.newInstance() review.show(this.supportFragmentManager, review.tag) } + + + setMainFragment(R.id.nav_home, getString(R.string.home), false) } supportFragmentManager.addOnBackStackChangedListener(this) + } - setMainFragment(R.id.nav_schedule, getString(R.string.schedule), false) + override fun onNewIntent(intent: Intent?) { + super.onNewIntent(intent) - ViewCompat.setTranslationZ(filters, 10f) + val target = intent?.getIntExtra("target", -1) + if (target != null && target != -1) { + navigate(target) + } } + override fun onResume() { + super.onResume() + + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + val value = TypedValue() + theme.resolveAttribute(R.attr.dark_mode, value, true) + if (value.string == "dark") { + window.decorView.systemUiVisibility = 0 + window.statusBarColor = getThemeAccentColor(this) + } else { + window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR + window.statusBarColor = getThemeAccentColor(this) + } + } + } + + private fun getThemeAccentColor(context: Context, theme: Resources.Theme = context.theme): Int { + val colorAttr = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + android.R.attr.colorBackground + } else { + context.resources.getIdentifier("colorBackground", "attr", context.packageName) + } + val outValue = TypedValue() + theme.resolveAttribute(colorAttr, outValue, true) + return outValue.data + } + + + + override fun onStart() { super.onStart() auth.signInAnonymously().addOnCompleteListener(this) { @@ -133,51 +138,25 @@ class MainActivity : AppCompatActivity(), OnNavigationItemSelectedListener, Frag } } - override fun onResume() { - super.onResume() - timer.start() - } - - override fun onPause() { - timer.stop() - super.onPause() - } - - private lateinit var toggle: ActionBarDrawerToggle - private fun initNavDrawer() { - toggle = ActionBarDrawerToggle( - this, drawer_layout, toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close) - drawer_layout.addDrawerListener(toggle) - toggle.syncState() - nav_view.setNavigationItemSelectedListener(this) } override fun getTheme(): Resources.Theme { val theme = super.getTheme() - theme.applyStyle(R.style.AppTheme, true) - return theme - } - private fun setFABVisibility(visibility: Int) { - if (visibility == View.VISIBLE) { - filter.show() - } else { - filter.hide() + val style = when (storage.theme) { + Dark -> R.style.AppTheme_Dark + Light -> R.style.AppTheme + Developer -> R.style.AppTheme_Developer + null -> R.style.AppTheme_Dark } - } + theme.applyStyle(style, true) - private fun expandFilters() { - bottomSheet.state = BottomSheetBehavior.STATE_EXPANDED - } - - private fun hideFilters() { - bottomSheet.state = BottomSheetBehavior.STATE_HIDDEN + return theme } - override fun onCreateOptionsMenu(menu: Menu): Boolean { val inflater = menuInflater inflater.inflate(R.menu.search_menu, menu) @@ -185,9 +164,11 @@ class MainActivity : AppCompatActivity(), OnNavigationItemSelectedListener, Frag } override fun onBackPressed() { + val drawerOpen = drawer_layout.isDrawerOpen(GravityCompat.START) + when { - drawer_layout.isDrawerOpen(GravityCompat.START) -> drawer_layout.closeDrawers() - bottomSheet.state != BottomSheetBehavior.STATE_HIDDEN -> hideFilters() + drawerOpen -> drawer_layout.closeDrawers() + storage.navDrawerOnBack && !drawerOpen && !secondaryVisible -> drawer_layout.openDrawer(GravityCompat.START) else -> super.onBackPressed() } } @@ -202,9 +183,6 @@ class MainActivity : AppCompatActivity(), OnNavigationItemSelectedListener, Frag } private fun setMainFragment(id: Int, title: String? = null, addToBackStack: Boolean) { - val visibility = if (id == R.id.nav_schedule) View.VISIBLE else View.INVISIBLE - setFABVisibility(visibility) - replaceFragment(getFragment(id), R.id.container, backStack = addToBackStack) title?.let { @@ -214,26 +192,24 @@ class MainActivity : AppCompatActivity(), OnNavigationItemSelectedListener, Frag } override fun onNavigationItemSelected(item: MenuItem): Boolean { - if (item.groupId == R.id.nav_cons) { - viewModel.changeConference(item.itemId) - } else { - setMainFragment(item.itemId, item.title.toString(), false) - } - + setMainFragment(item.itemId, item.title.toString(), false) drawer_layout.closeDrawers() return true } private fun getFragment(id: Int): Fragment { + // TODO: Remove, this is a hacky solution for caching issue with InformationFragment's children fragments. + if (id == R.id.nav_information) + return InformationFragment.newInstance() + + if (id == R.id.nav_map) + return MapsFragment.newInstance() + if (map[id] == null) { map[id] = when (id) { R.id.nav_home -> HomeFragment.newInstance() R.id.nav_schedule -> ScheduleFragment.newInstance() R.id.nav_map -> MapsFragment.newInstance() - R.id.nav_speakers -> SpeakersFragment.newInstance() - R.id.nav_companies -> VendorsFragment.newInstance() - NAV_CONTESTS -> ContestsFragment.newInstance() - NAV_WORKSHOPS -> WorkshopFragment.newInstance() R.id.nav_settings -> SettingsFragment.newInstance() R.id.search -> SearchFragment.newInstance() else -> InformationFragment.newInstance() @@ -243,7 +219,11 @@ class MainActivity : AppCompatActivity(), OnNavigationItemSelectedListener, Frag } fun navigate(event: Event) { - replaceFragment(EventFragment.newInstance(event), R.id.container_above, hasAnimation = true) + navigate(event.id) + } + + fun navigate(id: Int) { + replaceFragment(EventFragment.newInstance(id), R.id.container_above, hasAnimation = true) } fun navigate(speaker: Speaker?) { @@ -260,24 +240,30 @@ class MainActivity : AppCompatActivity(), OnNavigationItemSelectedListener, Frag val last = fragments.lastOrNull() if (last is EventFragment || last is SpeakerFragment) { + secondaryVisible = true drawer_layout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED) - toolbar.visibility = View.INVISIBLE container.visibility = View.INVISIBLE - setFABVisibility(View.INVISIBLE) } else { + secondaryVisible = false drawer_layout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED) - toolbar.visibility = View.VISIBLE container.visibility = View.VISIBLE - if (last is ScheduleFragment) { - setFABVisibility(View.VISIBLE) - } else { - setFABVisibility(View.INVISIBLE) - } } } - companion object { - private const val NAV_WORKSHOPS = 1001 - private const val NAV_CONTESTS = 1002 + fun openNavDrawer() { + drawer_layout.openDrawer(GravityCompat.START) + } + + fun showSearch() { + setMainFragment(R.id.search, getString(R.string.search), true) + } + + fun showMap(location: Location) { + supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE) + + val fragment = MapsFragment.newInstance(location) + map[R.id.nav_map] = fragment + + setMainFragment(R.id.nav_map, getString(R.string.map), true) } } diff --git a/hackertracker/src/main/java/com/shortstack/hackertracker/ui/activities/ReviewBottomSheet.kt b/app/src/main/java/com/shortstack/hackertracker/ui/activities/ReviewBottomSheet.kt similarity index 100% rename from hackertracker/src/main/java/com/shortstack/hackertracker/ui/activities/ReviewBottomSheet.kt rename to app/src/main/java/com/shortstack/hackertracker/ui/activities/ReviewBottomSheet.kt diff --git a/hackertracker/src/main/java/com/shortstack/hackertracker/ui/events/EventFragment.kt b/app/src/main/java/com/shortstack/hackertracker/ui/events/EventFragment.kt similarity index 58% rename from hackertracker/src/main/java/com/shortstack/hackertracker/ui/events/EventFragment.kt rename to app/src/main/java/com/shortstack/hackertracker/ui/events/EventFragment.kt index 9cf464a1..c1aed0c2 100644 --- a/hackertracker/src/main/java/com/shortstack/hackertracker/ui/events/EventFragment.kt +++ b/app/src/main/java/com/shortstack/hackertracker/ui/events/EventFragment.kt @@ -6,21 +6,24 @@ import android.graphics.Color import android.net.Uri import android.os.Build import android.os.Bundle +import android.util.TypedValue import android.view.LayoutInflater import android.view.Menu import android.view.View import android.view.ViewGroup import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProvider import com.shortstack.hackertracker.R -import com.shortstack.hackertracker.utilities.Analytics import com.shortstack.hackertracker.database.DatabaseManager -import com.shortstack.hackertracker.models.local.Speaker +import com.shortstack.hackertracker.database.ReminderManager import com.shortstack.hackertracker.models.local.Event +import com.shortstack.hackertracker.ui.HackerTrackerViewModel import com.shortstack.hackertracker.ui.activities.MainActivity +import com.shortstack.hackertracker.utilities.Analytics import com.shortstack.hackertracker.utilities.TimeUtil import com.shortstack.hackertracker.views.SpeakerView -import com.shortstack.hackertracker.views.StatusBarSpacer import kotlinx.android.synthetic.main.empty_text.* import kotlinx.android.synthetic.main.fragment_event.* import org.koin.android.ext.android.inject @@ -31,34 +34,43 @@ class EventFragment : Fragment() { const val EXTRA_EVENT = "EXTRA_EVENT" - fun newInstance(event: Event): EventFragment { + fun newInstance(event: Int): EventFragment { val fragment = EventFragment() val bundle = Bundle() - bundle.putParcelable(EXTRA_EVENT, event) + bundle.putInt(EXTRA_EVENT, event) fragment.arguments = bundle return fragment } } - private val database: DatabaseManager by inject() private val analytics: Analytics by inject() + private val database: DatabaseManager by inject() + private val reminder: ReminderManager by inject() + private val viewModel: HackerTrackerViewModel by lazy { ViewModelProvider(context as MainActivity)[HackerTrackerViewModel::class.java] } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { return inflater.inflate(R.layout.fragment_event, container, false) } - override fun onPrepareOptionsMenu(menu: Menu?) { - menu?.clear() + override fun onPrepareOptionsMenu(menu: Menu) { + menu.clear() super.onPrepareOptionsMenu(menu) } override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) - val event = arguments?.getParcelable(EXTRA_EVENT) as? Event + val id = arguments?.getInt(EXTRA_EVENT) + + viewModel.events.observe(this, Observer { + val target = it.data?.find { it.id == id } + if (target != null) { + showEvent(target) + } + }) val drawable = ContextCompat.getDrawable(context @@ -69,60 +81,71 @@ class EventFragment : Fragment() { (activity as? MainActivity)?.popBackStack() } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - val context = context ?: return - val height = StatusBarSpacer.getStatusBarHeight(context, app_bar) - app_bar.setPadding(0, height, 0, 0) - } +// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { +// val context = context ?: return +// val height = StatusBarSpacer.getStatusBarHeight(context, app_bar) +// app_bar.setPadding(0, height, 0, 0) +// } + } + + private fun showEvent(event: Event) { + analytics.log("Viewing event ${event.title}") - event?.let { event -> + collapsing_toolbar.title = event.title - analytics.log("Viewing event ${event.title}") + val body = event.description - collapsing_toolbar.title = event.title + if (body.isNotBlank()) { + empty.visibility = View.GONE + description.text = body + } else { + empty.visibility = View.VISIBLE + } - val body = event.description + val url = event.link + if (url.isBlank()) { + link.visibility = View.GONE + } else { + link.visibility = View.VISIBLE - if (body.isNotBlank()) { - empty.visibility = View.GONE - description.text = body - } else { - empty.visibility = View.VISIBLE + link.setOnClickListener { + onLinkClick(url) + analytics.onEventAction(Analytics.EVENT_OPEN_URL, event) } + } - val url = event.link - if (url.isBlank()) { - link.visibility = View.GONE - } else { - link.visibility = View.VISIBLE + share.setOnClickListener { + onShareClick(event) + analytics.onEventAction(Analytics.EVENT_SHARE, event) + } - link.setOnClickListener { - onLinkClick(url) - analytics.onEventAction(Analytics.EVENT_OPEN_URL, event) - } - } + star.setOnClickListener { + onBookmarkClick(event) + } - share.setOnClickListener { - onShareClick(event) - analytics.onEventAction(Analytics.EVENT_SHARE, event) + if (event.location.hotel == null) { + map.visibility = View.GONE + } else { + map.visibility = View.VISIBLE + map.setOnClickListener { + onMapClick(event) } + } - star.setOnClickListener { - onBookmarkClick(event) - } + displayDescription(event) - displayDescription(event) + displayTypes(event) - displayTypes(event) + displayBookmark(event) - displayBookmark(event) + displaySpeakers(event) - val speakers = displaySpeakers(event) -// displayRelatedEvents(it, speakers) + analytics.onEventAction(Analytics.EVENT_VIEW, event) + } - analytics.onEventAction(Analytics.EVENT_VIEW, event) - } + private fun onMapClick(event: Event) { + (context as? MainActivity)?.showMap(event.location) } private fun onLinkClick(url: String?) { @@ -142,6 +165,12 @@ class EventFragment : Fragment() { event.isBookmarked = !event.isBookmarked database.updateBookmark(event) + if(event.isBookmarked) { + reminder.setReminder(event) + } else { + reminder.cancel(event) + } + val action = if (event.isBookmarked) Analytics.EVENT_BOOKMARK else Analytics.EVENT_UNBOOKMARK analytics.onEventAction(action, event) @@ -149,10 +178,8 @@ class EventFragment : Fragment() { } private fun getDetailsDescription(event: Event): String { -// val context = context ?: - return "" - -// return "Attending ${event.title} at ${getFullTimeStamp(context, event)} in ${event.location.firstOrNull()?.name} #hackertracker" + val context = context ?: return "" + return "Attending ${event.title} at ${getFullTimeStamp(context, event)} in ${event.location.name} #hackertracker" } private fun displayBookmark(event: Event) { @@ -168,13 +195,6 @@ class EventFragment : Fragment() { val image = ContextCompat.getDrawable(context, drawable)?.mutate() - if (isBookmarked && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - event.type.let { - val color = Color.parseColor(it.color) - image?.setTint(color) - } - } - star.setImageDrawable(image) } @@ -187,14 +207,14 @@ class EventFragment : Fragment() { } - fun getFullTimeStamp(context: Context, event: Event): String { + private fun getFullTimeStamp(context: Context, event: Event): String { val (begin, end) = getTimeStamp(context, event) - val timestamp = TimeUtil.getRelativeDateStamp(context, event.start) + val timestamp = TimeUtil.getDateStamp(event.start) return String.format(context.getString(R.string.timestamp_full), timestamp, begin, end) } - fun getTimeStamp(context: Context, event: Event): Pair { + private fun getTimeStamp(context: Context, event: Event): Pair { val begin = TimeUtil.getTimeStamp(context, event.start) val end = TimeUtil.getTimeStamp(context, event.end) return Pair(begin, end) @@ -206,13 +226,22 @@ class EventFragment : Fragment() { val type = event.type val context = context ?: return - val color = Color.parseColor(type.color) - app_bar.setBackgroundColor(color) + val value = TypedValue() + context.theme.resolveAttribute(R.attr.category_tint, value, true) + val id = value.resourceId + + val color = if (id > 0) { + ContextCompat.getColor(context, id) + } else { + Color.parseColor(type.color) + } + + app_bar.setBackgroundColor(Color.parseColor(type.color)) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { val drawable = ContextCompat.getDrawable(context, R.drawable.chip_background)?.mutate() drawable?.setTint(color) - category_text.background = drawable + category_dot.background = drawable } category_text.text = type.name @@ -234,20 +263,4 @@ class EventFragment : Fragment() { } } } - - - private fun displayRelatedEvents(event: Event, speakers: List) { - val context = context ?: return - -// val relatedEvents = database.getRelatedEvents(event.id, event.types, speakers) -// -// if (relatedEvents.isNotEmpty()) { -// related_events_header.visibility = View.VISIBLE -// relatedEvents.forEach { -// related_events.addView(EventView(context, it)) -// } -// } else { - related_events_header.visibility = View.GONE -// } - } } \ No newline at end of file diff --git a/hackertracker/src/main/java/com/shortstack/hackertracker/ui/home/ArticleViewHolder.kt b/app/src/main/java/com/shortstack/hackertracker/ui/home/ArticleViewHolder.kt similarity index 100% rename from hackertracker/src/main/java/com/shortstack/hackertracker/ui/home/ArticleViewHolder.kt rename to app/src/main/java/com/shortstack/hackertracker/ui/home/ArticleViewHolder.kt diff --git a/hackertracker/src/main/java/com/shortstack/hackertracker/ui/home/HeaderViewHolder.kt b/app/src/main/java/com/shortstack/hackertracker/ui/home/HeaderViewHolder.kt similarity index 100% rename from hackertracker/src/main/java/com/shortstack/hackertracker/ui/home/HeaderViewHolder.kt rename to app/src/main/java/com/shortstack/hackertracker/ui/home/HeaderViewHolder.kt diff --git a/app/src/main/java/com/shortstack/hackertracker/ui/home/HomeAdapter.kt b/app/src/main/java/com/shortstack/hackertracker/ui/home/HomeAdapter.kt new file mode 100644 index 00000000..6068b981 --- /dev/null +++ b/app/src/main/java/com/shortstack/hackertracker/ui/home/HomeAdapter.kt @@ -0,0 +1,112 @@ +package com.shortstack.hackertracker.ui.home + +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.RecyclerView +import com.shortstack.hackertracker.models.local.Article +import com.shortstack.hackertracker.models.local.Event +import com.shortstack.hackertracker.ui.schedule.EventViewHolder +import com.shortstack.hackertracker.views.EventView + +class HomeAdapter : RecyclerView.Adapter() { + + companion object { + private const val SKULL = 0 + private const val HEADER = 1 + private const val EVENT = 2 + private const val ARTICLE = 3 + } + + private val collection = ArrayList() + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + return when (viewType) { + SKULL -> SkullHeaderViewHolder.inflate(parent) + HEADER -> HeaderViewHolder.inflate(parent) + EVENT -> EventViewHolder.inflate(parent, EventView.DISPLAY_MODE_MIN) + ARTICLE -> ArticleViewHolder.inflate(parent) + else -> throw IllegalStateException("Unknown viewType $viewType") + } + } + + override fun getItemViewType(position: Int): Int { + if (position == 0) + return SKULL + + + return when (collection[position]) { + is Article -> ARTICLE + is String -> HEADER + else -> EVENT + } + } + + override fun getItemCount() = collection.size + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + when (holder) { + is EventViewHolder -> holder.render(collection[position] as Event) + is ArticleViewHolder -> holder.render(collection[position] as Article) + is HeaderViewHolder -> holder.render(collection[position] as String) + } + } + + fun setElements(list: List) { + val list = listOf(SKULL) + list + + val result = DiffUtil.calculateDiff(object : DiffUtil.Callback() { + override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { + if(oldItemPosition == 0 && newItemPosition == 0) + return true + + val lhs = collection[oldItemPosition] + val rhs = list[newItemPosition] + + if (lhs is Event && rhs is Event) { + return lhs.id == rhs.id + } + + if (lhs is Article && rhs is Article) { + return lhs.id == rhs.id + } + + if (lhs is String && rhs is String) { + return lhs == rhs + } + + return false + } + + override fun getOldListSize() = collection.size + + override fun getNewListSize() = list.size + + override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { + if(oldItemPosition == 0 && newItemPosition == 0) + return true + + val lhs = collection[oldItemPosition] + val rhs = list[newItemPosition] + + if (lhs is Event && rhs is Event) { + return lhs.updated == rhs.updated && lhs.isBookmarked == rhs.isBookmarked + } + + if (lhs is Article && rhs is Article) { + return lhs.name == rhs.name && lhs.text == rhs.text + } + + if (lhs is String && rhs is String) { + return lhs == rhs + } + + return false + } + + }) + + collection.clear() + collection.addAll(list) + result.dispatchUpdatesTo(this) + } +} \ No newline at end of file diff --git a/hackertracker/src/main/java/com/shortstack/hackertracker/ui/home/HomeFragment.kt b/app/src/main/java/com/shortstack/hackertracker/ui/home/HomeFragment.kt similarity index 58% rename from hackertracker/src/main/java/com/shortstack/hackertracker/ui/home/HomeFragment.kt rename to app/src/main/java/com/shortstack/hackertracker/ui/home/HomeFragment.kt index 6c55de29..fe1348aa 100644 --- a/hackertracker/src/main/java/com/shortstack/hackertracker/ui/home/HomeFragment.kt +++ b/app/src/main/java/com/shortstack/hackertracker/ui/home/HomeFragment.kt @@ -6,10 +6,12 @@ import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment import androidx.lifecycle.Observer -import androidx.lifecycle.ViewModelProviders +import androidx.lifecycle.ViewModelProvider import androidx.recyclerview.widget.LinearLayoutManager import com.shortstack.hackertracker.R -import kotlinx.android.synthetic.main.fragment_recyclerview.* +import com.shortstack.hackertracker.ui.HackerTrackerViewModel +import com.shortstack.hackertracker.ui.activities.MainActivity +import kotlinx.android.synthetic.main.fragment_home.* class HomeFragment : Fragment() { @@ -20,7 +22,7 @@ class HomeFragment : Fragment() { private val adapter = HomeAdapter() override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return inflater.inflate(R.layout.fragment_recyclerview, container, false) + return inflater.inflate(R.layout.fragment_home, container, false) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -29,9 +31,15 @@ class HomeFragment : Fragment() { list.adapter = adapter list.layoutManager = LinearLayoutManager(context) - val viewModel = ViewModelProviders.of(this).get(HomeViewModel::class.java) - viewModel.results.observe(this, Observer { - adapter.setElements(it) + toolbar.setNavigationOnClickListener { + (context as MainActivity).openNavDrawer() + } + + + val viewModel = ViewModelProvider(context as MainActivity)[HackerTrackerViewModel::class.java] + viewModel.home.observe(this, Observer { + if (it.data != null) + adapter.setElements(it.data) }) loading_progress.visibility = View.GONE diff --git a/hackertracker/src/main/java/com/shortstack/hackertracker/ui/home/SkullHeaderViewHolder.kt b/app/src/main/java/com/shortstack/hackertracker/ui/home/SkullHeaderViewHolder.kt similarity index 100% rename from hackertracker/src/main/java/com/shortstack/hackertracker/ui/home/SkullHeaderViewHolder.kt rename to app/src/main/java/com/shortstack/hackertracker/ui/home/SkullHeaderViewHolder.kt diff --git a/app/src/main/java/com/shortstack/hackertracker/ui/information/InformationFragment.kt b/app/src/main/java/com/shortstack/hackertracker/ui/information/InformationFragment.kt new file mode 100644 index 00000000..ddd79765 --- /dev/null +++ b/app/src/main/java/com/shortstack/hackertracker/ui/information/InformationFragment.kt @@ -0,0 +1,115 @@ +package com.shortstack.hackertracker.ui.information + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentManager +import androidx.fragment.app.FragmentStatePagerAdapter +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProvider +import com.google.android.material.tabs.TabLayout +import com.shortstack.hackertracker.R +import com.shortstack.hackertracker.Status +import com.shortstack.hackertracker.ui.HackerTrackerViewModel +import com.shortstack.hackertracker.ui.activities.MainActivity +import com.shortstack.hackertracker.ui.information.faq.FAQFragment +import com.shortstack.hackertracker.ui.information.info.InfoFragment +import com.shortstack.hackertracker.ui.information.speakers.SpeakersFragment +import com.shortstack.hackertracker.ui.information.vendors.VendorsFragment +import kotlinx.android.synthetic.main.fragment_information.* + +class InformationFragment : Fragment() { + + companion object { + private const val INFO = 0 + private const val FAQ = 1 + private const val SPEAKERS = 2 + private const val VENDORS = 3 + + + fun newInstance(): InformationFragment { + return InformationFragment() + } + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + return inflater.inflate(R.layout.fragment_information, container, false) + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + + toolbar.setNavigationOnClickListener { + (context as MainActivity).openNavDrawer() + } + + tabs.apply { + tabGravity = TabLayout.GRAVITY_FILL + setupWithViewPager(pager) + } + + pager.offscreenPageLimit = 4 + + tabs.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener { + override fun onTabReselected(tab: TabLayout.Tab) {} + + override fun onTabUnselected(tab: TabLayout.Tab) {} + + override fun onTabSelected(tab: TabLayout.Tab) { + pager.currentItem = tab.position + } + }) + + val viewModel = ViewModelProvider(this)[HackerTrackerViewModel::class.java] + viewModel.conference.observe(this, Observer { + val fm = activity?.supportFragmentManager ?: return@Observer + if (it.status == Status.SUCCESS) { + val adapter = PagerAdapter(fm, it.data!!.code) + pager.adapter = adapter + } + }) + } + + class PagerAdapter(fm: FragmentManager, private val conference: String) : FragmentStatePagerAdapter(fm) { + override fun getItem(position: Int): Fragment { + val index = if (conference == "DEFCON27") { + position + } else { + position + 1 + } + + return when (index) { + INFO -> InfoFragment.newInstance() + SPEAKERS -> SpeakersFragment.newInstance() + FAQ -> FAQFragment.newInstance() + VENDORS -> VendorsFragment.newInstance() + else -> throw IndexOutOfBoundsException("Position out of bounds: $index") + } + } + + override fun getPageTitle(position: Int): CharSequence? { + val index = if (conference == "DEFCON27") { + position + } else { + position + 1 + } + + return when (index) { + INFO -> "Event" + SPEAKERS -> "Speakers" + FAQ -> "FAQ" + VENDORS -> "Vendors" + else -> throw IndexOutOfBoundsException("Position out of bounds: $index") + } + } + + override fun getCount(): Int { + if (conference == "DEFCON27") + return 4 + return 3 + } + + } +} \ No newline at end of file diff --git a/app/src/main/java/com/shortstack/hackertracker/ui/information/faq/FAQFragment.kt b/app/src/main/java/com/shortstack/hackertracker/ui/information/faq/FAQFragment.kt new file mode 100644 index 00000000..c95b5a77 --- /dev/null +++ b/app/src/main/java/com/shortstack/hackertracker/ui/information/faq/FAQFragment.kt @@ -0,0 +1,26 @@ +package com.shortstack.hackertracker.ui.information.faq + +import android.os.Bundle +import android.view.View +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProvider +import com.shortstack.hackertracker.models.firebase.FirebaseFAQ +import com.shortstack.hackertracker.ui.HackerTrackerViewModel +import com.shortstack.hackertracker.ui.ListFragment +import com.shortstack.hackertracker.ui.activities.MainActivity + +class FAQFragment : ListFragment() { + + companion object { + fun newInstance() = FAQFragment() + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + val viewModel = ViewModelProvider(context as MainActivity)[HackerTrackerViewModel::class.java] + viewModel.faq.observe(this, Observer { + onResource(it) + }) + } +} diff --git a/hackertracker/src/main/java/com/shortstack/hackertracker/ui/information/FAQViewHolder.kt b/app/src/main/java/com/shortstack/hackertracker/ui/information/faq/FAQViewHolder.kt similarity index 76% rename from hackertracker/src/main/java/com/shortstack/hackertracker/ui/information/FAQViewHolder.kt rename to app/src/main/java/com/shortstack/hackertracker/ui/information/faq/FAQViewHolder.kt index b00214d7..27f4b4b2 100644 --- a/hackertracker/src/main/java/com/shortstack/hackertracker/ui/information/FAQViewHolder.kt +++ b/app/src/main/java/com/shortstack/hackertracker/ui/information/faq/FAQViewHolder.kt @@ -1,4 +1,4 @@ -package com.shortstack.hackertracker.ui.information +package com.shortstack.hackertracker.ui.information.faq import android.view.LayoutInflater import android.view.View @@ -8,18 +8,17 @@ import androidx.constraintlayout.widget.ConstraintSet import androidx.recyclerview.widget.RecyclerView import androidx.transition.ChangeBounds import androidx.transition.TransitionManager -import com.crashlytics.android.answers.CustomEvent import com.shortstack.hackertracker.R +import com.shortstack.hackertracker.models.local.FAQ import com.shortstack.hackertracker.utilities.Analytics -import com.shortstack.hackertracker.models.firebase.FirebaseFAQ import kotlinx.android.synthetic.main.row_faq.view.* -import org.koin.standalone.KoinComponent -import org.koin.standalone.inject +import org.koin.core.KoinComponent +import org.koin.core.inject class FAQViewHolder(val view: View) : RecyclerView.ViewHolder(view), KoinComponent { companion object { - fun inflate(parent: ViewGroup): RecyclerView.ViewHolder { + fun inflate(parent: ViewGroup): FAQViewHolder { val view = LayoutInflater.from(parent.context).inflate(R.layout.row_faq, parent, false) return FAQViewHolder(view) } @@ -27,7 +26,7 @@ class FAQViewHolder(val view: View) : RecyclerView.ViewHolder(view), KoinCompone private val analytics: Analytics by inject() - fun render(faq: FirebaseFAQ) { + fun render(faq: FAQ) { view.answer.visibility = View.GONE view.question.text = faq.question @@ -38,13 +37,15 @@ class FAQViewHolder(val view: View) : RecyclerView.ViewHolder(view), KoinCompone } } - private fun onFAQClick(faq: FirebaseFAQ) { + private fun onFAQClick(faq: FAQ) { val root = view.container - val isExpanded = view.answer.visibility == View.VISIBLE + val isExpanded = faq.isExpanded + + faq.isExpanded = !faq.isExpanded if (!isExpanded) { - val event = CustomEvent(Analytics.FAQ_VIEW).also { + val event = Analytics.CustomEvent(Analytics.FAQ_VIEW).also { it.putCustomAttribute("Question", faq.question) } analytics.logCustom(event) diff --git a/app/src/main/java/com/shortstack/hackertracker/ui/information/info/InfoFragment.kt b/app/src/main/java/com/shortstack/hackertracker/ui/information/info/InfoFragment.kt new file mode 100644 index 00000000..dc867b44 --- /dev/null +++ b/app/src/main/java/com/shortstack/hackertracker/ui/information/info/InfoFragment.kt @@ -0,0 +1,36 @@ +package com.shortstack.hackertracker.ui.information.info + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.lifecycle.Observer +import com.shortstack.hackertracker.R +import com.shortstack.hackertracker.database.DatabaseManager +import kotlinx.android.synthetic.main.fragment_info.* +import org.koin.android.ext.android.inject + +class InfoFragment : Fragment() { + + companion object { + fun newInstance() = InfoFragment() + } + + private val database: DatabaseManager by inject() + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + return inflater.inflate(R.layout.fragment_info, container, false) + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + + database.conference.observe(this, Observer { + conduct.setText(it.conduct) + }) + + + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/shortstack/hackertracker/ui/information/info/views/CodeOfConductView.kt b/app/src/main/java/com/shortstack/hackertracker/ui/information/info/views/CodeOfConductView.kt new file mode 100644 index 00000000..f4c52284 --- /dev/null +++ b/app/src/main/java/com/shortstack/hackertracker/ui/information/info/views/CodeOfConductView.kt @@ -0,0 +1,20 @@ +package com.shortstack.hackertracker.ui.information.info.views + +import android.content.Context +import android.util.AttributeSet +import android.widget.FrameLayout +import com.shortstack.hackertracker.R +import kotlinx.android.synthetic.main.view_code_of_conduct.view.* + + +class CodeOfConductView(context: Context, attrs: AttributeSet?) : FrameLayout(context, attrs) { + + init { + inflate(context, R.layout.view_code_of_conduct, this) + } + + fun setText(conduct: String?) { + content.text = conduct?.replace("\\n", "\n") + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/shortstack/hackertracker/ui/information/info/views/HelpLineView.kt b/app/src/main/java/com/shortstack/hackertracker/ui/information/info/views/HelpLineView.kt new file mode 100644 index 00000000..bcde5ec9 --- /dev/null +++ b/app/src/main/java/com/shortstack/hackertracker/ui/information/info/views/HelpLineView.kt @@ -0,0 +1,41 @@ +package com.shortstack.hackertracker.ui.information.info.views + +import android.app.AlertDialog +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.util.AttributeSet +import android.widget.FrameLayout +import com.shortstack.hackertracker.R +import kotlinx.android.synthetic.main.view_help_line.view.* + + +class HelpLineView(context: Context, attrs: AttributeSet?) : FrameLayout(context, attrs) { + + init { + inflate(context, R.layout.view_help_line, this) + + call.setOnClickListener { + showCallAlert() + } + } + + private fun showCallAlert() { + AlertDialog.Builder(context, R.style.MyAlertDialogStyle) + .setTitle(R.string.help_line_title) + .setMessage(R.string.help_line_message) + .setNegativeButton(R.string.cancel) { _, _ -> + // do nothing + } + .setPositiveButton(R.string.call) { _, _ -> + callHotline() + }.show() + } + + private fun callHotline() { + val intent = Intent(Intent.ACTION_DIAL) + intent.data = Uri.parse("tel:+1 (725) 222-0934") + context.startActivity(intent) + } + +} \ No newline at end of file diff --git a/hackertracker/src/main/java/com/shortstack/hackertracker/ui/speakers/SpeakerAdapter.kt b/app/src/main/java/com/shortstack/hackertracker/ui/information/speakers/SpeakerAdapter.kt similarity index 95% rename from hackertracker/src/main/java/com/shortstack/hackertracker/ui/speakers/SpeakerAdapter.kt rename to app/src/main/java/com/shortstack/hackertracker/ui/information/speakers/SpeakerAdapter.kt index 29cec8b7..b1cef863 100644 --- a/hackertracker/src/main/java/com/shortstack/hackertracker/ui/speakers/SpeakerAdapter.kt +++ b/app/src/main/java/com/shortstack/hackertracker/ui/information/speakers/SpeakerAdapter.kt @@ -1,4 +1,4 @@ -package com.shortstack.hackertracker.ui.speakers +package com.shortstack.hackertracker.ui.information.speakers import android.view.ViewGroup import androidx.recyclerview.widget.DiffUtil diff --git a/app/src/main/java/com/shortstack/hackertracker/ui/information/speakers/SpeakerFragment.kt b/app/src/main/java/com/shortstack/hackertracker/ui/information/speakers/SpeakerFragment.kt new file mode 100644 index 00000000..55b24efe --- /dev/null +++ b/app/src/main/java/com/shortstack/hackertracker/ui/information/speakers/SpeakerFragment.kt @@ -0,0 +1,133 @@ +package com.shortstack.hackertracker.ui.information.speakers + +import android.content.Intent +import android.graphics.Color +import android.net.Uri +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.content.ContextCompat +import androidx.fragment.app.Fragment +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProvider +import com.shortstack.hackertracker.R +import com.shortstack.hackertracker.utilities.Analytics +import com.shortstack.hackertracker.database.DatabaseManager +import com.shortstack.hackertracker.models.local.Speaker +import com.shortstack.hackertracker.ui.HackerTrackerViewModel +import com.shortstack.hackertracker.ui.activities.MainActivity +import com.shortstack.hackertracker.views.EventView +import kotlinx.android.synthetic.main.empty_text.* +import kotlinx.android.synthetic.main.fragment_speakers.* +import org.koin.android.ext.android.inject + +class SpeakerFragment : Fragment() { + + companion object { + private const val EXTRA_SPEAKER = "EXTRA_SPEAKER" + + fun newInstance(speaker: Speaker): SpeakerFragment { + val fragment = SpeakerFragment() + + val bundle = Bundle() + bundle.putParcelable(EXTRA_SPEAKER, speaker) + fragment.arguments = bundle + + return fragment + } + } + + private val database: DatabaseManager by inject() + private val analytics: Analytics by inject() + + private val viewModel by lazy { ViewModelProvider(context as MainActivity)[HackerTrackerViewModel::class.java] } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + return inflater.inflate(R.layout.fragment_speakers, container, false) + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + + val context = context ?: return + + + val drawable = ContextCompat.getDrawable(context, R.drawable.ic_arrow_back_white_24dp) + toolbar.navigationIcon = drawable + + toolbar.setNavigationOnClickListener { + (activity as? MainActivity)?.popBackStack() + } + +// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { +// val height = StatusBarSpacer.getStatusBarHeight(context, app_bar) +// app_bar.setPadding(0, height, 0, 0) +// } + + + + + val speaker = arguments?.getParcelable(EXTRA_SPEAKER) as? Speaker + + viewModel.speakers.observe(this, Observer { + val target = it.data?.find { it.id == speaker?.id } + if(target != null) { + showSpeaker(target) + } + }) + } + + private fun showSpeaker(speaker: Speaker) { + analytics.log("Viewing speaker ${speaker.name}") + + collapsing_toolbar.title = speaker.name + collapsing_toolbar.subtitle = if (speaker.title.isEmpty()) { + context?.getString(R.string.speaker_default_title) + } else { + speaker.title + } + + val url = speaker.twitter + if (url.isEmpty()) { + twitter.visibility = View.GONE + } else { + twitter.visibility = View.VISIBLE + + twitter.setOnClickListener { + val url = "https://twitter.com/" + url.replace("@", "") + + val intent = Intent(Intent.ACTION_VIEW).setData(Uri.parse(url)) + context?.startActivity(intent) + + analytics.onSpeakerEvent(Analytics.SPEAKER_TWITTER, speaker) + } + } + + + val body = speaker.description + + if (body.isNotBlank()) { + empty.visibility = View.GONE + description.text = body + } else { + empty.visibility = View.VISIBLE + } + + val colours = context!!.resources.getStringArray(R.array.colors) + val color = Color.parseColor(colours[speaker.id % colours.size]) + + app_bar.setBackgroundColor(color) + + database.getEventsForSpeaker(speaker).observe(this, Observer { list -> + + events_header.visibility = if (list.isEmpty()) View.GONE else View.VISIBLE + + list.forEach { + events.addView(EventView(context!!, it, EventView.DISPLAY_MODE_MIN)) + } + }) + + analytics.onSpeakerEvent(Analytics.SPEAKER_VIEW, speaker) + } +} \ No newline at end of file diff --git a/hackertracker/src/main/java/com/shortstack/hackertracker/ui/speakers/SpeakerViewHolder.kt b/app/src/main/java/com/shortstack/hackertracker/ui/information/speakers/SpeakerViewHolder.kt similarity index 65% rename from hackertracker/src/main/java/com/shortstack/hackertracker/ui/speakers/SpeakerViewHolder.kt rename to app/src/main/java/com/shortstack/hackertracker/ui/information/speakers/SpeakerViewHolder.kt index bc8ba8a7..d44732d3 100644 --- a/hackertracker/src/main/java/com/shortstack/hackertracker/ui/speakers/SpeakerViewHolder.kt +++ b/app/src/main/java/com/shortstack/hackertracker/ui/information/speakers/SpeakerViewHolder.kt @@ -1,9 +1,11 @@ -package com.shortstack.hackertracker.ui.speakers +package com.shortstack.hackertracker.ui.information.speakers import android.graphics.Color +import android.util.TypedValue import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.core.content.ContextCompat import androidx.recyclerview.widget.RecyclerView import com.shortstack.hackertracker.R import com.shortstack.hackertracker.models.local.Speaker @@ -28,9 +30,18 @@ class SpeakerViewHolder(private val view: View) : RecyclerView.ViewHolder(view) speaker.title } - val colours = context.resources.getStringArray(R.array.colors) - val color = Color.parseColor(colours[speaker.id % colours.size]) - card.setCardBackgroundColor(color) + val value = TypedValue() + context.theme.resolveAttribute(R.attr.category_tint, value, true) + val id = value.resourceId + + val color = if (id > 0) { + ContextCompat.getColor(context, id) + } else { + val colours = context.resources.getStringArray(R.array.colors) + Color.parseColor(colours[speaker.id % colours.size]) + } + + category.setBackgroundColor(color) setOnClickListener { (context as? MainActivity)?.navigate(speaker) diff --git a/app/src/main/java/com/shortstack/hackertracker/ui/information/speakers/SpeakersFragment.kt b/app/src/main/java/com/shortstack/hackertracker/ui/information/speakers/SpeakersFragment.kt new file mode 100644 index 00000000..6a905e76 --- /dev/null +++ b/app/src/main/java/com/shortstack/hackertracker/ui/information/speakers/SpeakersFragment.kt @@ -0,0 +1,28 @@ +package com.shortstack.hackertracker.ui.information.speakers + +import android.os.Bundle +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProvider +import com.shortstack.hackertracker.models.firebase.FirebaseSpeaker +import com.shortstack.hackertracker.ui.HackerTrackerViewModel +import com.shortstack.hackertracker.ui.ListFragment +import com.shortstack.hackertracker.ui.activities.MainActivity +import org.koin.androidx.viewmodel.ext.android.viewModel + +class SpeakersFragment : ListFragment() { + + companion object { + fun newInstance() = SpeakersFragment() + } + + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + + val viewModel = ViewModelProvider(context as MainActivity)[HackerTrackerViewModel::class.java] + + viewModel.speakers.observe(context as MainActivity, Observer { + onResource(it) + }) + } +} \ No newline at end of file diff --git a/hackertracker/src/main/java/com/shortstack/hackertracker/ui/vendors/VendorViewHolder.kt b/app/src/main/java/com/shortstack/hackertracker/ui/information/vendors/VendorViewHolder.kt similarity index 78% rename from hackertracker/src/main/java/com/shortstack/hackertracker/ui/vendors/VendorViewHolder.kt rename to app/src/main/java/com/shortstack/hackertracker/ui/information/vendors/VendorViewHolder.kt index 4f7d56ff..864cbd94 100644 --- a/hackertracker/src/main/java/com/shortstack/hackertracker/ui/vendors/VendorViewHolder.kt +++ b/app/src/main/java/com/shortstack/hackertracker/ui/information/vendors/VendorViewHolder.kt @@ -1,4 +1,4 @@ -package com.shortstack.hackertracker.ui.vendors +package com.shortstack.hackertracker.ui.information.vendors import android.content.Intent import android.graphics.Color @@ -8,7 +8,6 @@ import android.view.View import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView import com.shortstack.hackertracker.R -import com.shortstack.hackertracker.models.firebase.FirebaseVendor import com.shortstack.hackertracker.models.local.Vendor import kotlinx.android.synthetic.main.row_vendor.view.* @@ -31,10 +30,6 @@ class VendorViewHolder(val view: View) : RecyclerView.ViewHolder(view) { View.VISIBLE } - val colours = view.context.resources.getStringArray(R.array.colors) - val color = Color.parseColor(colours[vendor.colour % colours.size]) - view.card.setCardBackgroundColor(color) - view.link.setOnClickListener { val intent = Intent(Intent.ACTION_VIEW).setData(Uri.parse(vendor.link)) view.context.startActivity(intent) diff --git a/hackertracker/src/main/java/com/shortstack/hackertracker/ui/vendors/VendorsFragment.kt b/app/src/main/java/com/shortstack/hackertracker/ui/information/vendors/VendorsFragment.kt similarity index 57% rename from hackertracker/src/main/java/com/shortstack/hackertracker/ui/vendors/VendorsFragment.kt rename to app/src/main/java/com/shortstack/hackertracker/ui/information/vendors/VendorsFragment.kt index cad15b95..b7aa6f31 100644 --- a/hackertracker/src/main/java/com/shortstack/hackertracker/ui/vendors/VendorsFragment.kt +++ b/app/src/main/java/com/shortstack/hackertracker/ui/information/vendors/VendorsFragment.kt @@ -1,10 +1,13 @@ -package com.shortstack.hackertracker.ui.vendors +package com.shortstack.hackertracker.ui.information.vendors import android.os.Bundle import android.view.View import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProvider import com.shortstack.hackertracker.models.firebase.FirebaseVendor +import com.shortstack.hackertracker.ui.HackerTrackerViewModel import com.shortstack.hackertracker.ui.ListFragment +import com.shortstack.hackertracker.ui.activities.MainActivity class VendorsFragment : ListFragment() { @@ -16,7 +19,9 @@ class VendorsFragment : ListFragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - getViewModel().vendors.observe(this, Observer { + val viewModel = ViewModelProvider(context as MainActivity)[HackerTrackerViewModel::class.java] + + viewModel.vendors.observe(this, Observer { onResource(it) }) } diff --git a/hackertracker/src/main/java/com/shortstack/hackertracker/ui/maps/MapFragment.kt b/app/src/main/java/com/shortstack/hackertracker/ui/maps/MapFragment.kt similarity index 100% rename from hackertracker/src/main/java/com/shortstack/hackertracker/ui/maps/MapFragment.kt rename to app/src/main/java/com/shortstack/hackertracker/ui/maps/MapFragment.kt diff --git a/hackertracker/src/main/java/com/shortstack/hackertracker/ui/maps/MapsFragment.kt b/app/src/main/java/com/shortstack/hackertracker/ui/maps/MapsFragment.kt similarity index 58% rename from hackertracker/src/main/java/com/shortstack/hackertracker/ui/maps/MapsFragment.kt rename to app/src/main/java/com/shortstack/hackertracker/ui/maps/MapsFragment.kt index 5a45262d..f1fc4bc2 100644 --- a/hackertracker/src/main/java/com/shortstack/hackertracker/ui/maps/MapsFragment.kt +++ b/app/src/main/java/com/shortstack/hackertracker/ui/maps/MapsFragment.kt @@ -8,21 +8,38 @@ import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentStatePagerAdapter import androidx.lifecycle.Observer -import androidx.lifecycle.ViewModelProviders -import com.crashlytics.android.answers.CustomEvent +import androidx.lifecycle.ViewModelProvider import com.shortstack.hackertracker.R -import com.shortstack.hackertracker.utilities.Analytics import com.shortstack.hackertracker.models.firebase.FirebaseConferenceMap +import com.shortstack.hackertracker.models.local.Location +import com.shortstack.hackertracker.ui.HackerTrackerViewModel +import com.shortstack.hackertracker.ui.activities.MainActivity +import com.shortstack.hackertracker.utilities.Analytics import kotlinx.android.synthetic.main.fragment_maps.* import org.koin.android.ext.android.inject class MapsFragment : Fragment() { companion object { - fun newInstance() = MapsFragment() + + private const val EXTRA_LOCATION = "location" + + fun newInstance(location: Location? = null): MapsFragment { + val fragment = MapsFragment() + + if (location != null) { + val bundle = Bundle() + + bundle.putParcelable(EXTRA_LOCATION, location) + fragment.arguments = bundle + } + + return fragment + } } private val analytics: Analytics by inject() + private var isFirstLoad: Boolean = true override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { return inflater.inflate(R.layout.fragment_maps, container, false) @@ -36,10 +53,16 @@ class MapsFragment : Fragment() { setupWithViewPager(pager) } + toolbar.setNavigationOnClickListener { + (context as MainActivity).openNavDrawer() + } - val mapsViewModel = ViewModelProviders.of(this).get(MapsViewModel::class.java) + + val mapsViewModel = ViewModelProvider(context as MainActivity)[HackerTrackerViewModel::class.java] mapsViewModel.maps.observe(this, Observer { - when (it.size) { + val maps = it.data ?: emptyList() + + when (maps.size) { 0 -> { tab_layout.visibility = View.GONE empty_view.visibility = View.VISIBLE @@ -54,11 +77,27 @@ class MapsFragment : Fragment() { } } - val adapter = PagerAdapter(activity!!.supportFragmentManager, it) + val adapter = PagerAdapter(activity!!.supportFragmentManager, maps) pager.adapter = adapter + + if (isFirstLoad) { + isFirstLoad = false + + showSelectedMap(maps) + } + }) - analytics.logCustom(CustomEvent(Analytics.MAP_VIEW)) + analytics.logCustom(Analytics.CustomEvent(Analytics.MAP_VIEW)) + } + + private fun showSelectedMap(it: List) { + val location = arguments?.getParcelable(EXTRA_LOCATION) + if (location != null) { + val position = it.indexOfFirst { it.title == location.hotel } + if (position != -1) + pager.currentItem = position + } } class PagerAdapter(fm: FragmentManager, private val maps: List) : FragmentStatePagerAdapter(fm) { diff --git a/hackertracker/src/main/java/com/shortstack/hackertracker/ui/schedule/EventViewHolder.kt b/app/src/main/java/com/shortstack/hackertracker/ui/schedule/EventViewHolder.kt similarity index 64% rename from hackertracker/src/main/java/com/shortstack/hackertracker/ui/schedule/EventViewHolder.kt rename to app/src/main/java/com/shortstack/hackertracker/ui/schedule/EventViewHolder.kt index 796722d6..910b11f2 100644 --- a/hackertracker/src/main/java/com/shortstack/hackertracker/ui/schedule/EventViewHolder.kt +++ b/app/src/main/java/com/shortstack/hackertracker/ui/schedule/EventViewHolder.kt @@ -1,25 +1,25 @@ package com.shortstack.hackertracker.ui.schedule -import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView -import com.shortstack.hackertracker.R import com.shortstack.hackertracker.models.local.Event import com.shortstack.hackertracker.ui.activities.MainActivity -import kotlinx.android.synthetic.main.row.view.* +import com.shortstack.hackertracker.views.EventView class EventViewHolder(private val view: View) : RecyclerView.ViewHolder(view) { companion object { - fun inflate(parent: ViewGroup): EventViewHolder { - val view = LayoutInflater.from(parent.context).inflate(R.layout.row, parent, false) + + fun inflate(parent: ViewGroup, mode: Int): EventViewHolder { + val view = EventView(parent.context, display = mode) + view.layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) return EventViewHolder(view) } } fun render(event: Event) { - view.event.setContent(event) + (view as EventView).setContent(event) view.setOnClickListener { showEventFragment(event) diff --git a/app/src/main/java/com/shortstack/hackertracker/ui/schedule/ScheduleFragment.kt b/app/src/main/java/com/shortstack/hackertracker/ui/schedule/ScheduleFragment.kt new file mode 100644 index 00000000..45d175cc --- /dev/null +++ b/app/src/main/java/com/shortstack/hackertracker/ui/schedule/ScheduleFragment.kt @@ -0,0 +1,236 @@ +package com.shortstack.hackertracker.ui.schedule + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import androidx.core.view.ViewCompat +import androidx.fragment.app.Fragment +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProvider +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.LinearSmoothScroller +import androidx.recyclerview.widget.RecyclerView +import com.advice.timehop.StickyRecyclerHeadersDecoration +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.shortstack.hackertracker.R +import com.shortstack.hackertracker.Status +import com.shortstack.hackertracker.models.Day +import com.shortstack.hackertracker.models.local.Event +import com.shortstack.hackertracker.models.local.Type +import com.shortstack.hackertracker.ui.HackerTrackerViewModel +import com.shortstack.hackertracker.ui.activities.MainActivity +import com.shortstack.hackertracker.ui.schedule.list.ScheduleAdapter +import com.shortstack.hackertracker.views.DaySelectorView +import kotlinx.android.synthetic.main.fragment_schedule.* +import kotlinx.android.synthetic.main.fragment_schedule.list +import kotlinx.android.synthetic.main.view_empty.view.* +import kotlinx.android.synthetic.main.view_filter.* +import java.util.* + + +class ScheduleFragment : Fragment() { + + companion object { + private const val EXTRA_TYPE = "type" + + fun newInstance(type: Type? = null): ScheduleFragment { + val fragment = ScheduleFragment() + + if (type != null) { + val bundle = Bundle() + bundle.putParcelable(EXTRA_TYPE, type) + fragment.arguments = bundle + } + + return fragment + } + } + + private val adapter: ScheduleAdapter = ScheduleAdapter() + + private var shouldScroll = true + + private lateinit var bottomSheet: BottomSheetBehavior + + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + return inflater.inflate(R.layout.fragment_schedule, container, false) as ViewGroup + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + val type = arguments?.getParcelable(EXTRA_TYPE) + if (type != null) { + toolbar.title = type.name + filter.visibility = View.GONE + } + + toolbar.inflateMenu(R.menu.schedule) + toolbar.setOnMenuItemClickListener { + if (it?.itemId == R.id.search) { + (context as MainActivity).showSearch() + true + } + false + } + + shouldScroll = true + list.adapter = adapter + + toolbar.setNavigationOnClickListener { + (context as MainActivity).openNavDrawer() + } + + + val decoration = StickyRecyclerHeadersDecoration(adapter) + list.addItemDecoration(decoration) + + list.addOnScrollListener(object : RecyclerView.OnScrollListener() { + override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { + super.onScrolled(recyclerView, dx, dy) + val manager = list.layoutManager as? LinearLayoutManager + if (manager != null) { + val first = manager.findFirstVisibleItemPosition() + val last = manager.findLastVisibleItemPosition() + + if (first == -1 || last == -1) + return + + day_selector.onScroll(adapter.getDateOfPosition(first), adapter.getDateOfPosition(last)) + } + } + }) + + day_selector.addOnDaySelectedListener(object : DaySelectorView.OnDaySelectedListener { + override fun onDaySelected(day: Date) { + scrollToDate(day) + } + }) + + + val scheduleViewModel = ViewModelProvider(context as MainActivity)[HackerTrackerViewModel::class.java] + scheduleViewModel.schedule.observe(this, Observer { + hideViews() + + if (it != null) { + adapter.state = it.status + + when (it.status) { + Status.SUCCESS -> { + val list = adapter.setSchedule(it.data) + val days = list.filterIsInstance() + day_selector.setDays(days) + + if (adapter.isEmpty()) { + showEmptyView() + } + + scrollToCurrentPosition(list) + } + Status.ERROR -> { + showErrorView(it.message) + } + Status.LOADING -> { + adapter.clearAndNotify() + showProgress() + } + Status.NOT_INITIALIZED -> { + showEmptyView() + } + } + } + }) + + scheduleViewModel.types.observe(this, Observer { + filters.setTypes(it.data) + }) + + + bottomSheet = BottomSheetBehavior.from(filters) + bottomSheet.state = BottomSheetBehavior.STATE_HIDDEN + + filter.setOnClickListener { expandFilters() } + close.setOnClickListener { hideFilters() } + + ViewCompat.setTranslationZ(filters, 10f) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.search -> (context as MainActivity).showSearch() + } + return super.onOptionsItemSelected(item) + } + + private fun scrollToCurrentPosition(data: ArrayList) { + val manager = list.layoutManager ?: return + val first = data.filterIsInstance().firstOrNull { !it.hasStarted } ?: return + + if (shouldScroll) { + shouldScroll = false + val index = getScrollIndex(data, first) + manager.scrollToPosition(index) + } + } + + private fun getScrollIndex(data: ArrayList, first: Event): Int { + val event = data.indexOf(first) + + val element = data.subList(0, event).filterIsInstance().last() + val index = data.indexOf(element) + + val x = data.subList(index, event).filterIsInstance().firstOrNull { it.start.time != first.start.time } == null + if (!x) { + return event + } + + + if (index != -1) { + return index + } + return event + } + + private fun scrollToDate(date: Date) { + val index = adapter.getDatePosition(date) + if (index != -1) { + val scroller = object : LinearSmoothScroller(context) { + + override fun getVerticalSnapPreference(): Int { + return SNAP_TO_START + } + } + scroller.targetPosition = index + list.layoutManager?.startSmoothScroll(scroller) + } + } + + private fun showProgress() { + loading_progress.visibility = View.VISIBLE + } + + private fun hideViews() { + empty.visibility = View.GONE + loading_progress.visibility = View.GONE + } + + private fun showEmptyView() { + empty.visibility = View.VISIBLE + } + + private fun showErrorView(message: String?) { + empty.title.text = message + empty.visibility = View.VISIBLE + } + + private fun expandFilters() { + bottomSheet.state = BottomSheetBehavior.STATE_EXPANDED + } + + private fun hideFilters() { + bottomSheet.state = BottomSheetBehavior.STATE_HIDDEN + } +} \ No newline at end of file diff --git a/hackertracker/src/main/java/com/shortstack/hackertracker/ui/schedule/TimeViewHolder.kt b/app/src/main/java/com/shortstack/hackertracker/ui/schedule/TimeViewHolder.kt similarity index 88% rename from hackertracker/src/main/java/com/shortstack/hackertracker/ui/schedule/TimeViewHolder.kt rename to app/src/main/java/com/shortstack/hackertracker/ui/schedule/TimeViewHolder.kt index 8baa3085..c46dbef5 100644 --- a/hackertracker/src/main/java/com/shortstack/hackertracker/ui/schedule/TimeViewHolder.kt +++ b/app/src/main/java/com/shortstack/hackertracker/ui/schedule/TimeViewHolder.kt @@ -20,8 +20,8 @@ class TimeViewHolder(val view: View) : RecyclerView.ViewHolder(view) { } } - fun render(time: Time) { - view.time_item.setContent(time) + fun render(time: Time?) { + view.time_item.render(time) } } @@ -35,6 +35,6 @@ class DayViewHolder(val view: View) : RecyclerView.ViewHolder(view) { } fun render(day: Day) { - (view as TextView).text = TimeUtil.getRelativeDateStamp(view.context, day) + (view as TextView).text = TimeUtil.getDateStamp(day) } } diff --git a/hackertracker/src/main/java/com/shortstack/hackertracker/ui/schedule/list/ScheduleAdapter.kt b/app/src/main/java/com/shortstack/hackertracker/ui/schedule/list/ScheduleAdapter.kt similarity index 67% rename from hackertracker/src/main/java/com/shortstack/hackertracker/ui/schedule/list/ScheduleAdapter.kt rename to app/src/main/java/com/shortstack/hackertracker/ui/schedule/list/ScheduleAdapter.kt index 16e7c3fa..ff4d526b 100644 --- a/hackertracker/src/main/java/com/shortstack/hackertracker/ui/schedule/list/ScheduleAdapter.kt +++ b/app/src/main/java/com/shortstack/hackertracker/ui/schedule/list/ScheduleAdapter.kt @@ -3,6 +3,7 @@ package com.shortstack.hackertracker.ui.schedule.list import android.view.ViewGroup import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView +import com.advice.timehop.StickyRecyclerHeadersAdapter import com.shortstack.hackertracker.Status import com.shortstack.hackertracker.models.Day import com.shortstack.hackertracker.models.Time @@ -10,24 +11,58 @@ import com.shortstack.hackertracker.models.local.Event import com.shortstack.hackertracker.ui.schedule.DayViewHolder import com.shortstack.hackertracker.ui.schedule.EventViewHolder import com.shortstack.hackertracker.ui.schedule.TimeViewHolder +import com.shortstack.hackertracker.views.EventView +import java.util.* +import kotlin.collections.ArrayList -class ScheduleAdapter : RecyclerView.Adapter() { +class ScheduleAdapter : RecyclerView.Adapter(), + StickyRecyclerHeadersAdapter { companion object { private const val EVENT = 0 private const val DAY = 1 - private const val TIME = 2 } private val collection = ArrayList() var state: Status = Status.NOT_INITIALIZED + override fun getHeaderId(position: Int): Long { + return when (val obj = collection[position]) { + // (time - 1) to make it different than the first Event if they both start at 12:00:00 + is Day -> obj.time - 1 + is Event -> obj.key + else -> throw java.lang.IllegalStateException("Unhandled object type ${obj.javaClass}") + } + } + + override fun getItemId(position: Int): Long { + when (val obj = collection[position]) { + is Event -> return obj.key + } + return super.getItemId(position) + } + + override fun onCreateHeaderViewHolder(parent: ViewGroup): RecyclerView.ViewHolder { + return TimeViewHolder.inflate(parent) + } + + override fun onBindHeaderViewHolder(viewHolder: RecyclerView.ViewHolder, position: Int) { + val event = (collection[position] as? Event) + + if (viewHolder is TimeViewHolder) { + if (event != null) { + viewHolder.render(Time(Date(event.key))) + } else { + viewHolder.render(null) + } + } + } + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { return when (viewType) { - EVENT -> EventViewHolder.inflate(parent) + EVENT -> EventViewHolder.inflate(parent, EventView.DISPLAY_MODE_FULL) DAY -> DayViewHolder.inflate(parent) - TIME -> TimeViewHolder.inflate(parent) else -> throw IllegalStateException("Unknown viewType $viewType.") } } @@ -40,7 +75,6 @@ class ScheduleAdapter : RecyclerView.Adapter() { when (holder) { is EventViewHolder -> holder.render(item as Event) is DayViewHolder -> holder.render(item as Day) - is TimeViewHolder -> holder.render(item as Time) } } @@ -48,70 +82,28 @@ class ScheduleAdapter : RecyclerView.Adapter() { return when (collection[position]) { is Event -> EVENT is Day -> DAY - is Time -> TIME else -> throw IllegalStateException("Unknown viewType ${collection[position].javaClass}") } } private fun getFormattedElements(elements: List): ArrayList { val result = ArrayList() - + elements.groupBy { it.date }.toSortedMap().forEach { result.add(Day(it.key)) it.value.groupBy { it.start }.toSortedMap().forEach { - result.add(Time(it.key)) - if (it.value.isNotEmpty()) { val group = it.value.sortedWith(compareBy({ it.type.name }, { it.location.name })) - result.addAll(group) - } - } - } - - return result - } - - fun notifyTimeChanged() { - if (collection.isEmpty()) - return - val list = collection.toList() + group.forEach { event -> event.key = it.key.time } - list.forEach { - if (it is Event && it.hasFinished) { - removeAndNotify(it) - } - } - - - if (list.size != this.collection.size) { - - for (i in this.collection.size - 1 downTo 1) { - val any = this.collection[i] - val any1 = this.collection[i - 1] - if ((any is Day && any1 is Day) - || (any is Time && any1 is Time) - || (any is Day && any1 is Time)) { - removeAndNotify(any1) + result.addAll(group) } } - - // If no events and only headers remain. - if (collection.size == 2) { - val size = list.size - collection.clear() - notifyItemRangeRemoved(0, size) - } } - } - private fun removeAndNotify(item: Any) { - val index = collection.indexOf(item) - if (index != -1) { - collection.removeAt(index) - notifyItemRemoved(index) - } + return result } fun setSchedule(list: List?): ArrayList { @@ -133,8 +125,6 @@ class ScheduleAdapter : RecyclerView.Adapter() { return left.id == right.id } else if (left is Day && right is Day) { return left.time == right.time - } else if (left is Time && right is Time) { - return left.time == right.time } return false } @@ -152,8 +142,6 @@ class ScheduleAdapter : RecyclerView.Adapter() { && left.title == right.title && left.location.name == right.location.name } else if (left is Day && right is Day) { return left.time == right.time - } else if (left is Time && right is Time) { - return left.time == right.time } return false } @@ -169,8 +157,32 @@ class ScheduleAdapter : RecyclerView.Adapter() { } fun isEmpty() = state == Status.SUCCESS && collection.isEmpty() + fun clearAndNotify() { collection.clear() notifyDataSetChanged() } + + fun getDatePosition(date: Date): Int { + val calendar = Calendar.getInstance() + + calendar.time = date + calendar.set(Calendar.HOUR_OF_DAY, 0) + calendar.set(Calendar.MINUTE, 0) + calendar.set(Calendar.SECOND, 0) + calendar.set(Calendar.MILLISECOND, 0) + + + val otherDay = Day(calendar.time) + + return collection.indexOfFirst { it is Day && it.time == otherDay.time } + } + + fun getDateOfPosition(index: Int): Date { + return when (val obj = collection[index]) { + is Event -> obj.start + is Day -> Date(obj.time) + else -> TODO() + } + } } diff --git a/hackertracker/src/main/java/com/shortstack/hackertracker/ui/schedule/list/ScheduleInfiniteScrollListener.kt b/app/src/main/java/com/shortstack/hackertracker/ui/schedule/list/ScheduleInfiniteScrollListener.kt similarity index 100% rename from hackertracker/src/main/java/com/shortstack/hackertracker/ui/schedule/list/ScheduleInfiniteScrollListener.kt rename to app/src/main/java/com/shortstack/hackertracker/ui/schedule/list/ScheduleInfiniteScrollListener.kt diff --git a/hackertracker/src/main/java/com/shortstack/hackertracker/ui/search/HeaderViewHolder.kt b/app/src/main/java/com/shortstack/hackertracker/ui/search/HeaderViewHolder.kt similarity index 100% rename from hackertracker/src/main/java/com/shortstack/hackertracker/ui/search/HeaderViewHolder.kt rename to app/src/main/java/com/shortstack/hackertracker/ui/search/HeaderViewHolder.kt diff --git a/hackertracker/src/main/java/com/shortstack/hackertracker/ui/search/LocationViewHolder.kt b/app/src/main/java/com/shortstack/hackertracker/ui/search/LocationViewHolder.kt similarity index 100% rename from hackertracker/src/main/java/com/shortstack/hackertracker/ui/search/LocationViewHolder.kt rename to app/src/main/java/com/shortstack/hackertracker/ui/search/LocationViewHolder.kt diff --git a/hackertracker/src/main/java/com/shortstack/hackertracker/ui/search/SearchAdapter.kt b/app/src/main/java/com/shortstack/hackertracker/ui/search/SearchAdapter.kt similarity index 91% rename from hackertracker/src/main/java/com/shortstack/hackertracker/ui/search/SearchAdapter.kt rename to app/src/main/java/com/shortstack/hackertracker/ui/search/SearchAdapter.kt index 05aa8e22..0f82af2d 100644 --- a/hackertracker/src/main/java/com/shortstack/hackertracker/ui/search/SearchAdapter.kt +++ b/app/src/main/java/com/shortstack/hackertracker/ui/search/SearchAdapter.kt @@ -6,8 +6,9 @@ import androidx.recyclerview.widget.RecyclerView import com.shortstack.hackertracker.models.local.Event import com.shortstack.hackertracker.models.local.Location import com.shortstack.hackertracker.models.local.Speaker +import com.shortstack.hackertracker.ui.information.speakers.SpeakerViewHolder import com.shortstack.hackertracker.ui.schedule.EventViewHolder -import com.shortstack.hackertracker.ui.speakers.SpeakerViewHolder +import com.shortstack.hackertracker.views.EventView class SearchAdapter : RecyclerView.Adapter() { @@ -23,8 +24,8 @@ class SearchAdapter : RecyclerView.Adapter() { var query: String? = null override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { - return when(viewType) { - EVENT -> EventViewHolder.inflate(parent) + return when (viewType) { + EVENT -> EventViewHolder.inflate(parent, EventView.DISPLAY_MODE_MIN) SPEAKER -> SpeakerViewHolder.inflate(parent) LOCATION -> LocationViewHolder.inflate(parent) HEADER -> HeaderViewHolder.inflate(parent) @@ -37,7 +38,7 @@ class SearchAdapter : RecyclerView.Adapter() { override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { val item = collection[position] - when(holder) { + when (holder) { is EventViewHolder -> holder.render(item as Event) is SpeakerViewHolder -> holder.render(item as Speaker) is LocationViewHolder -> holder.render(item as Location) @@ -46,7 +47,7 @@ class SearchAdapter : RecyclerView.Adapter() { } override fun getItemViewType(position: Int): Int { - return when(collection[position]) { + return when (collection[position]) { is Speaker -> SPEAKER is Event -> EVENT is Location -> LOCATION diff --git a/hackertracker/src/main/java/com/shortstack/hackertracker/ui/SearchFragment.kt b/app/src/main/java/com/shortstack/hackertracker/ui/search/SearchFragment.kt similarity index 54% rename from hackertracker/src/main/java/com/shortstack/hackertracker/ui/SearchFragment.kt rename to app/src/main/java/com/shortstack/hackertracker/ui/search/SearchFragment.kt index af7f847e..593a83e5 100644 --- a/hackertracker/src/main/java/com/shortstack/hackertracker/ui/SearchFragment.kt +++ b/app/src/main/java/com/shortstack/hackertracker/ui/search/SearchFragment.kt @@ -1,27 +1,31 @@ -package com.shortstack.hackertracker.ui +package com.shortstack.hackertracker.ui.search +import android.app.Activity import android.os.Bundle -import android.view.* +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.inputmethod.InputMethodManager import androidx.appcompat.widget.SearchView import androidx.fragment.app.Fragment import androidx.lifecycle.Observer -import androidx.lifecycle.ViewModelProviders +import androidx.lifecycle.ViewModelProvider import androidx.recyclerview.widget.RecyclerView import com.shortstack.hackertracker.R +import com.shortstack.hackertracker.ui.HackerTrackerViewModel import com.shortstack.hackertracker.ui.activities.MainActivity -import com.shortstack.hackertracker.ui.search.SearchAdapter import com.shortstack.hackertracker.ui.search.SearchAdapter.State.* -import com.shortstack.hackertracker.ui.search.SearchViewModel -import kotlinx.android.synthetic.main.fragment_recyclerview.* +import kotlinx.android.synthetic.main.fragment_search.* -class SearchFragment : Fragment(), SearchView.OnQueryTextListener, MenuItem.OnActionExpandListener { + +class SearchFragment : Fragment(), SearchView.OnQueryTextListener { companion object { fun newInstance() = SearchFragment() } private val adapter = SearchAdapter() - private val viewModel by lazy { ViewModelProviders.of(this).get(SearchViewModel::class.java) } + private val viewModel by lazy { ViewModelProvider(context as MainActivity)[HackerTrackerViewModel::class.java] } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -29,18 +33,32 @@ class SearchFragment : Fragment(), SearchView.OnQueryTextListener, MenuItem.OnAc } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return inflater.inflate(R.layout.fragment_recyclerview, container, false) + return inflater.inflate(R.layout.fragment_search, container, false) } override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) - loading_progress.visibility = View.GONE empty_view.showDefault() list.adapter = adapter - viewModel.results.observe(this, Observer { + toolbar.setNavigationOnClickListener { + hideKeyboard(context as MainActivity) + (context as MainActivity).popBackStack() + } + + search.onActionViewExpanded() + search.setOnCloseListener { + hideKeyboard(context as MainActivity) + (context as MainActivity).popBackStack() + return@setOnCloseListener true + } + + search.setOnQueryTextListener(this) + + + viewModel.search.observe(this, Observer { adapter.setList(it) when (adapter.state) { @@ -54,33 +72,17 @@ class SearchFragment : Fragment(), SearchView.OnQueryTextListener, MenuItem.OnAc }) } - override fun onCreateOptionsMenu(menu: Menu?, inflater: MenuInflater?) { - menu?.findItem(R.id.search)?.apply { - expandActionView() - setOnActionExpandListener(this@SearchFragment) - - (actionView as SearchView).apply { - setOnQueryTextListener(this@SearchFragment) - } - } - - super.onCreateOptionsMenu(menu, inflater) - } - - override fun onQueryTextSubmit(query: String?) = true override fun onQueryTextChange(newText: String?): Boolean { adapter.query = newText - viewModel.search(newText) + viewModel.onQueryTextChange(newText) return false } - override fun onMenuItemActionExpand(item: MenuItem?) = false - - override fun onMenuItemActionCollapse(item: MenuItem?): Boolean { - val activity = context as? MainActivity - activity?.popBackStack() - return true + private fun hideKeyboard(activity: Activity) { + val imm = activity.getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager + val view = activity.currentFocus ?: View(activity) + imm.hideSoftInputFromWindow(view.windowToken, 0) } } \ No newline at end of file diff --git a/app/src/main/java/com/shortstack/hackertracker/ui/settings/SettingsFragment.kt b/app/src/main/java/com/shortstack/hackertracker/ui/settings/SettingsFragment.kt new file mode 100644 index 00000000..55d8e8c0 --- /dev/null +++ b/app/src/main/java/com/shortstack/hackertracker/ui/settings/SettingsFragment.kt @@ -0,0 +1,96 @@ +package com.shortstack.hackertracker.ui.settings + +import android.os.Build +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.app.AlertDialog +import androidx.fragment.app.Fragment +import androidx.lifecycle.Observer +import com.shortstack.hackertracker.BuildConfig +import com.shortstack.hackertracker.R +import com.shortstack.hackertracker.database.DatabaseManager +import com.shortstack.hackertracker.ui.activities.MainActivity +import com.shortstack.hackertracker.ui.themes.ThemesManager +import com.shortstack.hackertracker.utilities.Storage +import kotlinx.android.synthetic.main.fragment_settings.* +import org.koin.android.ext.android.inject + + +class SettingsFragment : Fragment() { + + companion object { + fun newInstance() = SettingsFragment() + } + + private val database: DatabaseManager by inject() + private val storage: Storage by inject() + private val themes: ThemesManager by inject() + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + return inflater.inflate(R.layout.fragment_settings, container, false) + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + + toolbar.setNavigationOnClickListener { + (context as MainActivity).openNavDrawer() + } + + // Disabling themes on old devices + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + change_theme.visibility = View.GONE + } + + change_theme.setOnClickListener { + showChangeThemeDialog() + } + + change_conference.setOnClickListener { + showChangeConferenceDialog() + } + + database.conference.observe(this, Observer { + if (it != null) { + force_time_zone.text = getString(R.string.setting_time_zone, it.timezone) + } + }) + + version.text = getString(R.string.version, BuildConfig.VERSION_NAME) + } + + private fun showChangeConferenceDialog() { + val context = context ?: return + + val conferences = database.conferences.value ?: emptyList() + val selected = conferences.indexOf(database.conference.value) + + val items = conferences.map { it.name }.toTypedArray() + + AlertDialog.Builder(context, R.style.MyAlertDialogStyle) + .setTitle(getString(R.string.choose_conference)) + .setSingleChoiceItems(items, selected) { dialog, which -> + database.changeConference(conferences[which].id) + dialog.dismiss() + }.show() + } + + private fun showChangeThemeDialog() { + val context = context ?: return + + val list = themes.getThemes() + val selected = list.indexOf(storage.theme) + + val items = list.map { it.label }.toTypedArray() + + AlertDialog.Builder(context, R.style.MyAlertDialogStyle) + .setTitle(getString(R.string.choose_theme)) + .setSingleChoiceItems(items, selected) { dialog, which -> + storage.theme = list[which] + dialog.dismiss() + (context as MainActivity).recreate() + }.show() + } +} diff --git a/app/src/main/java/com/shortstack/hackertracker/ui/themes/ThemeContainer.kt b/app/src/main/java/com/shortstack/hackertracker/ui/themes/ThemeContainer.kt new file mode 100644 index 00000000..44f91a11 --- /dev/null +++ b/app/src/main/java/com/shortstack/hackertracker/ui/themes/ThemeContainer.kt @@ -0,0 +1,3 @@ +package com.shortstack.hackertracker.ui.themes + +data class ThemeContainer(val label: String, val id: Int) \ No newline at end of file diff --git a/app/src/main/java/com/shortstack/hackertracker/ui/themes/ThemesManager.kt b/app/src/main/java/com/shortstack/hackertracker/ui/themes/ThemesManager.kt new file mode 100644 index 00000000..c0d8d826 --- /dev/null +++ b/app/src/main/java/com/shortstack/hackertracker/ui/themes/ThemesManager.kt @@ -0,0 +1,12 @@ +package com.shortstack.hackertracker.ui.themes + +class ThemesManager { + + enum class Theme(val label: String) { + Dark("Dark"), + Light("Light"), + Developer("Advice") + } + + fun getThemes() = Theme.values().toList() +} \ No newline at end of file diff --git a/hackertracker/src/main/java/com/shortstack/hackertracker/utilities/Analytics.kt b/app/src/main/java/com/shortstack/hackertracker/utilities/Analytics.kt similarity index 73% rename from hackertracker/src/main/java/com/shortstack/hackertracker/utilities/Analytics.kt rename to app/src/main/java/com/shortstack/hackertracker/utilities/Analytics.kt index 4e3a9532..936da478 100644 --- a/hackertracker/src/main/java/com/shortstack/hackertracker/utilities/Analytics.kt +++ b/app/src/main/java/com/shortstack/hackertracker/utilities/Analytics.kt @@ -1,12 +1,13 @@ package com.shortstack.hackertracker.utilities -import com.crashlytics.android.Crashlytics -import com.crashlytics.android.answers.Answers -import com.crashlytics.android.answers.CustomEvent + +import android.content.Context +import android.os.Bundle +import com.google.firebase.analytics.FirebaseAnalytics import com.shortstack.hackertracker.models.local.Event import com.shortstack.hackertracker.models.local.Speaker -class Analytics(private val storage: Storage) { +class Analytics(context: Context, private val storage: Storage) { companion object { const val EVENT_VIEW = "Event - View" @@ -27,6 +28,8 @@ class Analytics(private val storage: Storage) { const val SETTINGS_EXPIRED_EVENTS = "Settings - Expired Events" } + private val analytics: FirebaseAnalytics = FirebaseAnalytics.getInstance(context) + fun onEventAction(action: String, event: Event) { val event = CustomEvent(action).apply { putCustomAttribute("Title", event.title) @@ -57,11 +60,20 @@ class Analytics(private val storage: Storage) { return } - Answers.getInstance().logCustom(event) + analytics.logEvent(event.event, event.extras) } fun log(message: String) { - Crashlytics.getInstance().core.log(message) + //Crashlytics.getInstance().core.log(message) + } + + + @Deprecated("Replace with Firebase default solution.") + class CustomEvent(val event: String, val extras: Bundle = Bundle()) { + + fun putCustomAttribute(key: String, value: String) { + extras.putString(key, value) + } } } diff --git a/app/src/main/java/com/shortstack/hackertracker/utilities/MyClock.kt b/app/src/main/java/com/shortstack/hackertracker/utilities/MyClock.kt new file mode 100644 index 00000000..dc27970b --- /dev/null +++ b/app/src/main/java/com/shortstack/hackertracker/utilities/MyClock.kt @@ -0,0 +1,18 @@ +package com.shortstack.hackertracker.utilities + +import com.shortstack.hackertracker.BuildConfig +import java.text.SimpleDateFormat +import java.util.* + +data class MyClock(val value: Int = 0) + +fun MyClock.now(): Date { + if(BuildConfig.DEBUG) { + return parse("2019-06-01T12:00:00.000-0000") + } + return Date() +} + +private fun parse(date: String): Date { + return SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ").parse(date) +} diff --git a/hackertracker/src/main/java/com/shortstack/hackertracker/utilities/NotificationHelper.kt b/app/src/main/java/com/shortstack/hackertracker/utilities/NotificationHelper.kt similarity index 75% rename from hackertracker/src/main/java/com/shortstack/hackertracker/utilities/NotificationHelper.kt rename to app/src/main/java/com/shortstack/hackertracker/utilities/NotificationHelper.kt index ac9cb7ca..39b5c0c8 100644 --- a/hackertracker/src/main/java/com/shortstack/hackertracker/utilities/NotificationHelper.kt +++ b/app/src/main/java/com/shortstack/hackertracker/utilities/NotificationHelper.kt @@ -13,32 +13,30 @@ import android.os.Bundle import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import androidx.core.content.ContextCompat -import com.firebase.jobdispatcher.FirebaseJobDispatcher import com.shortstack.hackertracker.R -import com.shortstack.hackertracker.database.DatabaseManager import com.shortstack.hackertracker.models.local.Event import com.shortstack.hackertracker.ui.activities.MainActivity -import org.koin.standalone.KoinComponent -import org.koin.standalone.inject +import org.koin.core.KoinComponent class NotificationHelper(private val context: Context) : KoinComponent { - private val dispatcher: FirebaseJobDispatcher by inject() - - private val database: DatabaseManager by inject() - private val manager = NotificationManagerCompat.from(context) init { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - - val channel = NotificationChannel(CHANNEL_UPDATES, "Schedule Updates", NotificationManager.IMPORTANCE_DEFAULT) - .apply { - description = "Notifications about changes within the schedule" - enableLights(true) - lightColor = Color.MAGENTA - } + val manager = + context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + + val channel = NotificationChannel( + CHANNEL_UPDATES, + "Schedule Updates", + NotificationManager.IMPORTANCE_DEFAULT + ) + .apply { + description = "Notifications about changes within the events" + enableLights(true) + lightColor = Color.MAGENTA + } manager.createNotificationChannel(channel) } @@ -48,7 +46,12 @@ class NotificationHelper(private val context: Context) : KoinComponent { val builder = notificationBuilder builder.setContentTitle(item.title) - builder.setContentText(String.format(context.getString(R.string.notification_text), item.location.name)) + builder.setContentText( + String.format( + context.getString(R.string.notification_text), + item.location.name + ) + ) setItemPendingIntent(builder, item) @@ -95,7 +98,8 @@ class NotificationHelper(private val context: Context) : KoinComponent { intent.putExtras(bundle) } - val pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT) + val pendingIntent = + PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT) builder.setContentIntent(pendingIntent) } diff --git a/app/src/main/java/com/shortstack/hackertracker/utilities/StickyHeaderInterface.kt b/app/src/main/java/com/shortstack/hackertracker/utilities/StickyHeaderInterface.kt new file mode 100644 index 00000000..78330dad --- /dev/null +++ b/app/src/main/java/com/shortstack/hackertracker/utilities/StickyHeaderInterface.kt @@ -0,0 +1,36 @@ +package com.shortstack.hackertracker.utils + +import android.view.View + +interface StickyHeaderInterface { + + /** + * This method gets called by [StickHeaderItemDecoration] to fetch the position of the header item in the adapter + * that is used for (represents) item at specified position. + * @param itemPosition int. Adapter's position of the item for which to do the onQueryTextChange of the position of the header item. + * @return int. Position of the header item in the adapter. + */ + fun getHeaderPositionForItem(itemPosition: Int): Int + + /** + * This method gets called by [StickHeaderItemDecoration] to get layout resource id for the header item at specified adapter's position. + * @param headerPosition int. Position of the header item in the adapter. + * @return int. Layout resource id. + */ + fun getHeaderLayout(headerPosition: Int): Int + + /** + * This method gets called by [StickHeaderItemDecoration] to setup the header View. + * @param header View. Header to set the data on. + * @param headerPosition int. Position of the header item in the adapter. + */ + fun bindHeaderData(header: View, headerPosition: Int) + + /** + * This method gets called by [StickHeaderItemDecoration] to verify whether the item represents a header. + * @param itemPosition int. + * @return true, if item at the specified adapter's position represents a header. + */ + fun isHeader(itemPosition: Int): Boolean + +} \ No newline at end of file diff --git a/app/src/main/java/com/shortstack/hackertracker/utilities/Storage.kt b/app/src/main/java/com/shortstack/hackertracker/utilities/Storage.kt new file mode 100644 index 00000000..4c54a75a --- /dev/null +++ b/app/src/main/java/com/shortstack/hackertracker/utilities/Storage.kt @@ -0,0 +1,84 @@ +package com.shortstack.hackertracker.utilities + +import android.content.Context +import android.content.SharedPreferences +import android.preference.PreferenceManager +import com.google.gson.Gson +import com.shortstack.hackertracker.ui.themes.ThemesManager + +class Storage(context: Context, private val gson: Gson) { + + companion object { + private const val USER_THEME = "user_theme" + private const val USER_CONFERENCE = "user_conference" + + private const val EASTER_EGGS = "easter_eggs" + private const val NAV_DRAWER_ON_BACK = "nav_drawer_on_back" + private const val FORCE_TIME_ZONE = "force_time_zone" + + private const val USER_ALLOW_PUSH = "user_allow_push_notifications" + private const val USER_ANALYTICS = "user_analytics" + } + + private val preferences: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) + + val allowPushNotification: Boolean + get() = preferences.getBoolean(USER_ALLOW_PUSH, true) + + var allowAnalytics: Boolean + get() = preferences.getBoolean(USER_ANALYTICS, true) + set(value) { + preferences.edit().putBoolean(USER_ANALYTICS, value).apply() + } + + var navDrawerOnBack: Boolean + get() = preferences.getBoolean(NAV_DRAWER_ON_BACK, false) + set(value) { + preferences.edit().putBoolean(NAV_DRAWER_ON_BACK, value).apply() + } + + var forceTimeZone: Boolean + get() = preferences.getBoolean(FORCE_TIME_ZONE, true) + set(value) { + preferences.edit().putBoolean(FORCE_TIME_ZONE, value).apply() + } + + var easterEggs: Boolean + get() = preferences.getBoolean(EASTER_EGGS, false) + set(value) { + preferences.edit().putBoolean(EASTER_EGGS, value).apply() + } + + + var preferredConference: Int + get() = preferences.getInt(USER_CONFERENCE, -1) + set(value) { + preferences.edit().putInt(USER_CONFERENCE, value).apply() + } + + var theme: ThemesManager.Theme? + get() = gson.fromJson(preferences.getString(USER_THEME, ""), ThemesManager.Theme::class.java) + set(value) { + preferences.edit().putString(USER_THEME, gson.toJson(value)).apply() + } + + fun setPreference(key: String, isChecked: Boolean) { + when (key) { + USER_ANALYTICS -> allowAnalytics = isChecked + NAV_DRAWER_ON_BACK -> navDrawerOnBack = isChecked + FORCE_TIME_ZONE -> forceTimeZone = isChecked + EASTER_EGGS -> easterEggs = isChecked + else -> throw IllegalArgumentException("Unknown key: $key") + } + } + + fun getPreference(key: String, defaultValue: Boolean): Boolean { + return when (key) { + USER_ANALYTICS -> allowAnalytics + NAV_DRAWER_ON_BACK -> navDrawerOnBack + FORCE_TIME_ZONE -> forceTimeZone + EASTER_EGGS -> easterEggs + else -> throw IllegalArgumentException("Unknown key: $key") + } + } +} diff --git a/hackertracker/src/main/java/com/shortstack/hackertracker/utilities/TimeUtil.kt b/app/src/main/java/com/shortstack/hackertracker/utilities/TimeUtil.kt similarity index 51% rename from hackertracker/src/main/java/com/shortstack/hackertracker/utilities/TimeUtil.kt rename to app/src/main/java/com/shortstack/hackertracker/utilities/TimeUtil.kt index 745f35c5..15bb43ee 100644 --- a/hackertracker/src/main/java/com/shortstack/hackertracker/utilities/TimeUtil.kt +++ b/app/src/main/java/com/shortstack/hackertracker/utilities/TimeUtil.kt @@ -2,22 +2,15 @@ package com.shortstack.hackertracker.utilities import android.annotation.SuppressLint import android.content.Context -import com.shortstack.hackertracker.* +import com.shortstack.hackertracker.App +import com.shortstack.hackertracker.R import java.text.SimpleDateFormat import java.util.* object TimeUtil { - private const val SOON_DAYS_AMOUNT = 5 - @SuppressLint("SimpleDateFormat") - fun getRelativeDateStamp(context: Context, date: Date): String { - if (date.isToday()) - return context.getString(R.string.today) - - if (date.isTomorrow()) - return context.getString(R.string.tomorrow) - + fun getDateStamp(date: Date): String { val format = SimpleDateFormat("MMMM d") return format.format(date) @@ -30,10 +23,20 @@ object TimeUtil { if (date == null) return context.getString(R.string.tba) - return if (android.text.format.DateFormat.is24HourFormat(context)) { - SimpleDateFormat("HH:mm").format(date) + + val s = if (android.text.format.DateFormat.is24HourFormat(context)) { + "HH:mm" } else { - SimpleDateFormat("h:mm aa").format(date) + "h:mm\naa" } + + val formatter = SimpleDateFormat(s) + + if (App.instance.storage.forceTimeZone) { + val timezone = App.instance.database.conference.value?.timezone ?: "America/Los_Angeles" + formatter.timeZone = TimeZone.getTimeZone(timezone) + } + + return formatter.format(date) } } diff --git a/app/src/main/java/com/shortstack/hackertracker/views/DaySelectorView.kt b/app/src/main/java/com/shortstack/hackertracker/views/DaySelectorView.kt new file mode 100644 index 00000000..e14188a5 --- /dev/null +++ b/app/src/main/java/com/shortstack/hackertracker/views/DaySelectorView.kt @@ -0,0 +1,200 @@ +package com.shortstack.hackertracker.views + +import android.animation.ObjectAnimator +import android.app.Activity +import android.content.Context +import android.util.AttributeSet +import android.view.View +import android.view.animation.AnticipateOvershootInterpolator +import android.widget.FrameLayout +import android.widget.TextView +import androidx.constraintlayout.widget.ConstraintSet +import androidx.constraintlayout.widget.ConstraintSet.END +import androidx.constraintlayout.widget.ConstraintSet.START +import androidx.transition.ChangeBounds +import androidx.transition.TransitionManager +import com.shortstack.hackertracker.R +import kotlinx.android.synthetic.main.view_day_selector.view.* +import java.text.SimpleDateFormat +import java.util.* +import kotlin.collections.ArrayList + + +class DaySelectorView(context: Context, attrs: AttributeSet?) : FrameLayout(context, attrs) { + + private val children = ArrayList() + private var listener: OnDaySelectedListener? = null + + private var begin: Int = -1 + private var end: Int = -1 + + init { + View.inflate(context, R.layout.view_day_selector, this) + + for (i in 0..frame.childCount) { + val view = frame.getChildAt(i) + if (view is TextView) { + children.add(view) + view.setOnClickListener { + val tag = view.tag as? Long + if( tag != null) { + listener?.onDaySelected(Date(tag)) + } + } + } + } + } + + private fun onDaySelected(view: View, position: Int) { + val constraintSet = ConstraintSet() + constraintSet.clone(frame) + + constraintSet.apply { + connect(bubble.id, position, view.id, position) + } + + + val transition = ChangeBounds().apply { + interpolator = AnticipateOvershootInterpolator(1.0f) + duration = 500 + } + + TransitionManager.beginDelayedTransition(frame, transition) + constraintSet.applyTo(frame) + } + + fun onScroll(begin: Date, end: Date) { + val dates = children.map { Date(it.tag as Long) } + + val instance = Calendar.getInstance() + + instance.time = begin + instance.set(Calendar.HOUR_OF_DAY, 0) + instance.set(Calendar.MINUTE, 0) + instance.set(Calendar.SECOND, 0) + instance.set(Calendar.MILLISECOND, 0) + + val beginDay = instance.time + + instance.time = end + instance.set(Calendar.HOUR_OF_DAY, 0) + instance.set(Calendar.MINUTE, 0) + instance.set(Calendar.SECOND, 0) + instance.set(Calendar.MILLISECOND, 0) + + val endDay = instance.time + + + val beginIndex = getDayIndex(dates, beginDay) + if(beginIndex != -1) { + val view = getViewByIndex(beginIndex) + onBeginDaySelected(view) + setCenter(view) + } + + val endIndex = getDayIndex(dates, endDay) + if (endIndex != -1) { + onEndDaySelected(getViewByIndex(endIndex)) + } + + + } + + private fun setCenter(view: View) { + val screenWidth = (context as Activity).windowManager + .defaultDisplay.width + + val scrollX = view.left - screenWidth / 2 + view.width / 2 + + val animator = ObjectAnimator.ofInt(root, "scrollX", scrollX) + animator.duration = 300 + animator.start() + } + + private fun getViewByIndex(index: Int): View { + return when (index) { + 0 -> day_1 + 1 -> day_2 + 2 -> day_3 + 3 -> day_4 + 4 -> day_5 + 5 -> day_6 + 6 -> day_7 + 7 -> day_8 + 8 -> day_9 + 9 -> day_10 + 10 -> day_11 + 11 -> day_12 + else -> throw ArrayIndexOutOfBoundsException("Index out of bounds: $index.") + } + } + + private fun getDayIndex(dates: List, endDay: Date): Int { + return dates.indexOfFirst { + it.time == endDay.time + } + } + + private fun onBeginDaySelected(view: View) { + if (begin == view.id) + return + + begin = view.id + + onDaySelected(view, START) + } + + private fun onEndDaySelected(view: View) { + if (end == view.id) + return + + end = view.id + + onDaySelected(view, END) + } + + fun addOnDaySelectedListener(listener: OnDaySelectedListener) { + this.listener = listener + } + + fun setDays(days: List) { + val calendar = Calendar.getInstance() + + days.forEach { + calendar.time = it + calendar.set(Calendar.HOUR_OF_DAY, 0) + calendar.set(Calendar.MINUTE, 0) + calendar.set(Calendar.SECOND, 0) + calendar.set(Calendar.MILLISECOND, 0) + + it.time = calendar.time.time + } + + + children.clear() + + val format = SimpleDateFormat("MMM d") + + + for (index in 0..11) { + val view = getViewByIndex(index) as TextView + + if (index < days.size) { + val date = days[index] + + view.visibility = View.VISIBLE + view.text = format.format(date) + view.tag = date.time + + children.add(view) + } else { + view.visibility = View.GONE + } + } + } + + interface OnDaySelectedListener { + fun onDaySelected(day: Date) + } + +} \ No newline at end of file diff --git a/hackertracker/src/main/java/com/shortstack/hackertracker/views/EmptyView.kt b/app/src/main/java/com/shortstack/hackertracker/views/EmptyView.kt similarity index 96% rename from hackertracker/src/main/java/com/shortstack/hackertracker/views/EmptyView.kt rename to app/src/main/java/com/shortstack/hackertracker/views/EmptyView.kt index 2d6a0135..a0f60718 100644 --- a/hackertracker/src/main/java/com/shortstack/hackertracker/views/EmptyView.kt +++ b/app/src/main/java/com/shortstack/hackertracker/views/EmptyView.kt @@ -8,7 +8,7 @@ import android.widget.TextView import com.shortstack.hackertracker.R import kotlinx.android.synthetic.main.view_empty.view.* -class EmptyView(context: Context?, attrs: AttributeSet?) : FrameLayout(context, attrs) { +class EmptyView(context: Context, attrs: AttributeSet?) : FrameLayout(context, attrs) { init { View.inflate(context, R.layout.view_empty, this) diff --git a/app/src/main/java/com/shortstack/hackertracker/views/EventView.kt b/app/src/main/java/com/shortstack/hackertracker/views/EventView.kt new file mode 100644 index 00000000..792fc404 --- /dev/null +++ b/app/src/main/java/com/shortstack/hackertracker/views/EventView.kt @@ -0,0 +1,152 @@ +package com.shortstack.hackertracker.views + +import android.content.Context +import android.graphics.Color +import android.os.Build +import android.util.AttributeSet +import android.util.TypedValue +import android.view.View +import android.widget.FrameLayout +import androidx.core.content.ContextCompat +import com.shortstack.hackertracker.R +import com.shortstack.hackertracker.database.DatabaseManager +import com.shortstack.hackertracker.database.ReminderManager +import com.shortstack.hackertracker.models.local.Event +import com.shortstack.hackertracker.ui.activities.MainActivity +import com.shortstack.hackertracker.utilities.Analytics +import kotlinx.android.synthetic.main.row_event.view.* +import org.koin.core.KoinComponent +import org.koin.core.inject + +class EventView : FrameLayout, KoinComponent { + + companion object { + const val DISPLAY_MODE_MIN = 0 + const val DISPLAY_MODE_FULL = 1 + } + + private val analytics: Analytics by inject() + private val database: DatabaseManager by inject() + private val reminder: ReminderManager by inject() + + var displayMode: Int = DISPLAY_MODE_MIN + + constructor( + context: Context, + attrs: AttributeSet? = null, + display: Int = DISPLAY_MODE_MIN + ) : super(context, attrs) { + displayMode = display + init() + } + + constructor(context: Context, event: Event, display: Int = DISPLAY_MODE_FULL) : super(context) { + displayMode = display + init() + setContent(event) + } + + private fun init() { + inflate(context, R.layout.row_event, this) + setDisplayMode() + } + + fun setContent(event: Event) { + render(event) + } + + private fun setDisplayMode() { + when (displayMode) { + DISPLAY_MODE_MIN -> { + val width = context.resources.getDimension(R.dimen.event_view_min_guideline).toInt() + guideline.setGuidelineBegin(width) + category_dot.visibility = View.GONE + category_text.visibility = View.GONE + } + DISPLAY_MODE_FULL -> { + val width = context.resources.getDimension(R.dimen.time_width).toInt() + guideline.setGuidelineBegin(width) + category_dot.visibility = View.VISIBLE + category_text.visibility = View.VISIBLE + } + } + } + + private fun render(event: Event) { + title.text = event.title + + // Stage 2 + if (displayMode == DISPLAY_MODE_FULL) { + location.text = event.location.name + } else { + location.text = event.getFullTimeStamp(context) + " / " + event.location.name + } + + + renderCategoryColour(event) + updateBookmark(event) + + setOnClickListener { + (context as? MainActivity)?.navigate(event) + } + + star_bar.setOnClickListener { + onBookmarkClick(event) + } + } + + private fun renderCategoryColour(event: Event) { + val type = event.type + + category_text.text = type.name + + val value = TypedValue() + context.theme.resolveAttribute(R.attr.category_tint, value, true) + val id = value.resourceId + + val color = if (id > 0) { + ContextCompat.getColor(context, id) + } else { + Color.parseColor(type.color) + } + category.setBackgroundColor(color) + + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + val drawable = ContextCompat.getDrawable(context, R.drawable.chip_background)?.mutate() + + drawable?.setTint(color) + category_dot.background = drawable + } + } + + + private fun updateBookmark(event: Event) { + val isBookmarked = event.isBookmarked + + val drawable = if (isBookmarked) { + R.drawable.ic_star_accent_24dp + } else { + R.drawable.ic_star_border_white_24dp + } + + star_bar.setImageResource(drawable) + } + + private fun onBookmarkClick(event: Event) { + event.isBookmarked = !event.isBookmarked + database.updateBookmark(event) + + if (event.isBookmarked) { + reminder.setReminder(event) + } else { + reminder.cancel(event) + } + + val action = + if (event.isBookmarked) Analytics.EVENT_BOOKMARK else Analytics.EVENT_UNBOOKMARK + analytics.onEventAction(action, event) + + updateBookmark(event) + } +} diff --git a/hackertracker/src/main/java/com/shortstack/hackertracker/views/FilterAdapter.kt b/app/src/main/java/com/shortstack/hackertracker/views/FilterAdapter.kt similarity index 100% rename from hackertracker/src/main/java/com/shortstack/hackertracker/views/FilterAdapter.kt rename to app/src/main/java/com/shortstack/hackertracker/views/FilterAdapter.kt diff --git a/hackertracker/src/main/java/com/shortstack/hackertracker/views/FilterView.kt b/app/src/main/java/com/shortstack/hackertracker/views/FilterView.kt similarity index 64% rename from hackertracker/src/main/java/com/shortstack/hackertracker/views/FilterView.kt rename to app/src/main/java/com/shortstack/hackertracker/views/FilterView.kt index 37981a04..3ef0c513 100644 --- a/hackertracker/src/main/java/com/shortstack/hackertracker/views/FilterView.kt +++ b/app/src/main/java/com/shortstack/hackertracker/views/FilterView.kt @@ -9,9 +9,8 @@ import androidx.recyclerview.widget.RecyclerView import com.shortstack.hackertracker.R import com.shortstack.hackertracker.models.local.Type import kotlinx.android.synthetic.main.view_filter.view.* -import org.koin.standalone.KoinComponent -class FilterView(context: Context, attrs: AttributeSet) : LinearLayout(context, attrs), KoinComponent { +class FilterView(context: Context, attrs: AttributeSet) : LinearLayout(context, attrs) { companion object { private const val SPAN_COUNT = 2 @@ -32,21 +31,22 @@ class FilterView(context: Context, attrs: AttributeSet) : LinearLayout(context, collection.add(context.getString(R.string.types)) - val elements = types.filter { !it.isBookmark && !it.filtered } + val elements = types.filter { !it.isBookmark } collection.addAll(elements) val adapter = FilterAdapter(collection) - list.layoutManager = GridLayoutManager(context, SPAN_COUNT, RecyclerView.VERTICAL, false).apply { - spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() { - override fun getSpanSize(position: Int): Int { - return when (adapter.getItemViewType(position)) { - FilterAdapter.TYPE_HEADER -> SPAN_COUNT - else -> 1 + list.layoutManager = + GridLayoutManager(context, SPAN_COUNT, RecyclerView.VERTICAL, false).apply { + spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() { + override fun getSpanSize(position: Int): Int { + return when (adapter.getItemViewType(position)) { + FilterAdapter.TYPE_HEADER -> SPAN_COUNT + else -> 1 + } } } } - } list.adapter = adapter } diff --git a/hackertracker/src/main/java/com/shortstack/hackertracker/views/NonSwipeableViewPager.kt b/app/src/main/java/com/shortstack/hackertracker/views/NonSwipeableViewPager.kt similarity index 100% rename from hackertracker/src/main/java/com/shortstack/hackertracker/views/NonSwipeableViewPager.kt rename to app/src/main/java/com/shortstack/hackertracker/views/NonSwipeableViewPager.kt diff --git a/hackertracker/src/main/java/com/shortstack/hackertracker/views/ScrollingFABBehavior.kt b/app/src/main/java/com/shortstack/hackertracker/views/ScrollingFABBehavior.kt similarity index 100% rename from hackertracker/src/main/java/com/shortstack/hackertracker/views/ScrollingFABBehavior.kt rename to app/src/main/java/com/shortstack/hackertracker/views/ScrollingFABBehavior.kt diff --git a/hackertracker/src/main/java/com/shortstack/hackertracker/views/SettingsActionView.kt b/app/src/main/java/com/shortstack/hackertracker/views/SettingsActionView.kt similarity index 89% rename from hackertracker/src/main/java/com/shortstack/hackertracker/views/SettingsActionView.kt rename to app/src/main/java/com/shortstack/hackertracker/views/SettingsActionView.kt index 0f6fd408..230eb84c 100644 --- a/hackertracker/src/main/java/com/shortstack/hackertracker/views/SettingsActionView.kt +++ b/app/src/main/java/com/shortstack/hackertracker/views/SettingsActionView.kt @@ -23,10 +23,9 @@ class SettingsActionView(context: Context?, attrs: AttributeSet?) : LinearLayout } } - - override fun setOnClickListener(listener: OnClickListener) { + override fun setOnClickListener(listener: OnClickListener?) { control_overlay.setOnClickListener { - listener.onClick(it) + listener?.onClick(it) } } } \ No newline at end of file diff --git a/hackertracker/src/main/java/com/shortstack/hackertracker/views/SettingsSwitchView.kt b/app/src/main/java/com/shortstack/hackertracker/views/SettingsSwitchView.kt similarity index 57% rename from hackertracker/src/main/java/com/shortstack/hackertracker/views/SettingsSwitchView.kt rename to app/src/main/java/com/shortstack/hackertracker/views/SettingsSwitchView.kt index fb03e48b..ce044354 100644 --- a/hackertracker/src/main/java/com/shortstack/hackertracker/views/SettingsSwitchView.kt +++ b/app/src/main/java/com/shortstack/hackertracker/views/SettingsSwitchView.kt @@ -7,25 +7,46 @@ import com.shortstack.hackertracker.R import com.shortstack.hackertracker.utilities.Analytics import com.shortstack.hackertracker.utilities.Storage import kotlinx.android.synthetic.main.view_settings_switch.view.* -import org.koin.standalone.KoinComponent -import org.koin.standalone.inject +import org.koin.core.KoinComponent +import org.koin.core.inject -class SettingsSwitchView(context: Context?, attrs: AttributeSet?) : LinearLayout(context, attrs), KoinComponent { +class SettingsSwitchView(context: Context?, attrs: AttributeSet?) : LinearLayout(context, attrs), + KoinComponent { private val storage: Storage by inject() private val analytics: Analytics by inject() + var text: String + get() = label.text.toString() + set(value) { + label.text = value + } + init { inflate(context, R.layout.view_settings_switch, this) context?.theme?.obtainStyledAttributes( - attrs, - R.styleable.SettingsSwitchView, - 0, 0)?.apply { + attrs, + R.styleable.SettingsSwitchView, + 0, 0 + )?.apply { try { - label.text = getString(R.styleable.SettingsSwitchView_switchText) - control.tag = getString(R.styleable.SettingsSwitchView_switchKey) - control.isChecked = getBoolean(R.styleable.SettingsSwitchView_switchDefaultValue, true) + + val text = getString(R.styleable.SettingsSwitchView_switchText) + val defaultValue = + getBoolean(R.styleable.SettingsSwitchView_switchDefaultValue, true) + val key = getString(R.styleable.SettingsSwitchView_switchKey) ?: "" + + label.text = text + control.tag = key + control.id = key.hashCode() + + if (!isInEditMode) { + val isChecked = storage.getPreference(key, defaultValue) + control.isChecked = isChecked + } else { + control.isChecked = defaultValue + } } finally { recycle() } @@ -37,9 +58,9 @@ class SettingsSwitchView(context: Context?, attrs: AttributeSet?) : LinearLayout } - override fun setOnClickListener(listener: OnClickListener) { + override fun setOnClickListener(listener: OnClickListener?) { control_overlay.setOnClickListener { - listener.onClick(it) + listener?.onClick(it) onClick() } } diff --git a/hackertracker/src/main/java/com/shortstack/hackertracker/views/SpeakerView.kt b/app/src/main/java/com/shortstack/hackertracker/views/SpeakerView.kt similarity index 68% rename from hackertracker/src/main/java/com/shortstack/hackertracker/views/SpeakerView.kt rename to app/src/main/java/com/shortstack/hackertracker/views/SpeakerView.kt index 632b263d..3c884e1b 100644 --- a/hackertracker/src/main/java/com/shortstack/hackertracker/views/SpeakerView.kt +++ b/app/src/main/java/com/shortstack/hackertracker/views/SpeakerView.kt @@ -3,8 +3,10 @@ package com.shortstack.hackertracker.views import android.content.Context import android.graphics.Color import android.util.AttributeSet +import android.util.TypedValue import android.view.LayoutInflater import android.widget.LinearLayout +import androidx.core.content.ContextCompat import com.shortstack.hackertracker.R import com.shortstack.hackertracker.models.local.Speaker import com.shortstack.hackertracker.ui.activities.MainActivity @@ -22,9 +24,18 @@ class SpeakerView : LinearLayout { speaker.title } - val colours = context.resources.getStringArray(R.array.colors) - val color = Color.parseColor(colours[speaker.id % colours.size]) - card.setCardBackgroundColor(color) + val value = TypedValue() + context.theme.resolveAttribute(R.attr.category_tint, value, true) + val id = value.resourceId + + val color = if (id > 0) { + ContextCompat.getColor(context, id) + } else { + val colours = context.resources.getStringArray(R.array.colors) + Color.parseColor(colours[speaker.id % colours.size]) + } + + category.setBackgroundColor(color) setOnClickListener { (context as? MainActivity)?.navigate(speaker) diff --git a/hackertracker/src/main/java/com/shortstack/hackertracker/views/StatusBarSpacer.kt b/app/src/main/java/com/shortstack/hackertracker/views/StatusBarSpacer.kt similarity index 100% rename from hackertracker/src/main/java/com/shortstack/hackertracker/views/StatusBarSpacer.kt rename to app/src/main/java/com/shortstack/hackertracker/views/StatusBarSpacer.kt diff --git a/app/src/main/java/com/shortstack/hackertracker/views/TimeView.kt b/app/src/main/java/com/shortstack/hackertracker/views/TimeView.kt new file mode 100644 index 00000000..a93c84c1 --- /dev/null +++ b/app/src/main/java/com/shortstack/hackertracker/views/TimeView.kt @@ -0,0 +1,26 @@ +package com.shortstack.hackertracker.views + +import android.content.Context +import android.util.AttributeSet +import android.widget.LinearLayout +import com.shortstack.hackertracker.R +import com.shortstack.hackertracker.utilities.TimeUtil +import kotlinx.android.synthetic.main.row_header_time.view.* +import java.util.* + +class TimeView(context: Context, attrs: AttributeSet) : LinearLayout(context, attrs) { + + init { + inflate(context, R.layout.row_header_time, this) + orientation = VERTICAL + } + + fun render(date: Date?) { + if(date == null) { + header.text = null + } else { + header.text = TimeUtil.getTimeStamp(context, date) + } + } + +} diff --git a/hackertracker/src/main/java/com/shortstack/hackertracker/views/TypeViewHolder.kt b/app/src/main/java/com/shortstack/hackertracker/views/TypeViewHolder.kt similarity index 73% rename from hackertracker/src/main/java/com/shortstack/hackertracker/views/TypeViewHolder.kt rename to app/src/main/java/com/shortstack/hackertracker/views/TypeViewHolder.kt index 82890864..ca264771 100644 --- a/hackertracker/src/main/java/com/shortstack/hackertracker/views/TypeViewHolder.kt +++ b/app/src/main/java/com/shortstack/hackertracker/views/TypeViewHolder.kt @@ -9,18 +9,18 @@ import androidx.recyclerview.widget.RecyclerView import com.shortstack.hackertracker.R import com.shortstack.hackertracker.database.DatabaseManager import com.shortstack.hackertracker.models.local.Type -import io.reactivex.Single -import io.reactivex.android.schedulers.AndroidSchedulers -import io.reactivex.schedulers.Schedulers import kotlinx.android.synthetic.main.item_type.view.* -import org.koin.standalone.KoinComponent -import org.koin.standalone.inject +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import org.koin.core.KoinComponent +import org.koin.core.inject class TypeViewHolder(val view: View) : RecyclerView.ViewHolder(view), KoinComponent { companion object { fun inflate(parent: ViewGroup): TypeViewHolder { - val view = LayoutInflater.from(parent.context).inflate(R.layout.item_type, parent, false) + val view = + LayoutInflater.from(parent.context).inflate(R.layout.item_type, parent, false) return TypeViewHolder(view) } } @@ -44,12 +44,11 @@ class TypeViewHolder(val view: View) : RecyclerView.ViewHolder(view), KoinCompon chip.isCloseIconEnabled = isChecked - Single.fromCallable { + // todo: put this on the right scope + GlobalScope.launch { type.isSelected = isChecked database.updateTypeIsSelected(type) - }.subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe({}, {}) + } } } } diff --git a/app/src/main/java/com/shortstack/hackertracker/views/WiFiHelperView.kt b/app/src/main/java/com/shortstack/hackertracker/views/WiFiHelperView.kt new file mode 100644 index 00000000..7f70ed86 --- /dev/null +++ b/app/src/main/java/com/shortstack/hackertracker/views/WiFiHelperView.kt @@ -0,0 +1,93 @@ +package com.shortstack.hackertracker.views + +import android.annotation.TargetApi +import android.content.Context +import android.net.wifi.WifiConfiguration +import android.net.wifi.WifiConfiguration.* +import android.net.wifi.WifiEnterpriseConfig +import android.net.wifi.WifiManager +import android.os.Build +import android.util.AttributeSet +import android.view.View +import android.widget.FrameLayout +import android.widget.Toast +import com.shortstack.hackertracker.R +import kotlinx.android.synthetic.main.view_wifi_helper.view.* +import java.security.cert.CertificateFactory +import java.security.cert.X509Certificate + + +class WiFiHelperView(context: Context, attrs: AttributeSet?) : FrameLayout(context, attrs) { + + init { + inflate(context, R.layout.view_wifi_helper, this) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { + save.visibility = View.VISIBLE + save.setOnClickListener { connectWifi() } + } else { + content.text = context.getString(R.string.wifi_content_legacy) + save.visibility = View.GONE + } + } + + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2) + private fun connectWifi() { + val wifi = context.getSystemService(Context.WIFI_SERVICE) as WifiManager + + wifi.isWifiEnabled = true + + + val exists = wifi.configuredNetworks.find { it.SSID == "\"DefCon\"" } != null + + val config = WifiConfiguration().apply { + SSID = "\"DefCon\"" + hiddenSSID = false + priority = 40 + status = Status.ENABLED + + allowedKeyManagement.clear() + allowedKeyManagement.set(KeyMgmt.WPA_EAP) + + allowedGroupCiphers.clear() + allowedGroupCiphers.set(GroupCipher.CCMP) + allowedGroupCiphers.set(GroupCipher.TKIP) + allowedGroupCiphers.set(GroupCipher.WEP104) + + allowedPairwiseCiphers.clear() + allowedPairwiseCiphers.set(PairwiseCipher.CCMP) + + allowedAuthAlgorithms.clear() + allowedAuthAlgorithms.set(AuthAlgorithm.OPEN) + + allowedProtocols.clear() + allowedProtocols.set(Protocol.RSN) + + + val x509Certificate = CertificateFactory.getInstance("X.509").generateCertificate(resources.openRawResource(R.raw.digirootonly)) as X509Certificate + + enterpriseConfig = WifiEnterpriseConfig().apply { + identity = "defcon" + password = "defcon" + anonymousIdentity = "anonymous" + eapMethod = WifiEnterpriseConfig.Eap.PEAP + phase2Method = WifiEnterpriseConfig.Phase2.MSCHAPV2 + caCertificate = x509Certificate + subjectMatch = "/CN=wifireg.defcon.org" + } + } + + val network = if (exists) { + wifi.updateNetwork(config) + } else { + wifi.addNetwork(config) + } + + val result = wifi.enableNetwork(network, true) + if (result) { + Toast.makeText(context, "Wifi network saved", Toast.LENGTH_SHORT).show() + } else { + Toast.makeText(context, "Could not save network", Toast.LENGTH_SHORT).show() + } + } +} \ No newline at end of file diff --git a/hackertracker/src/main/res/drawable-hdpi/skull.png b/app/src/main/res/drawable-hdpi/skull.png similarity index 100% rename from hackertracker/src/main/res/drawable-hdpi/skull.png rename to app/src/main/res/drawable-hdpi/skull.png diff --git a/hackertracker/src/main/res/drawable-hdpi/skull_lg.png b/app/src/main/res/drawable-hdpi/skull_lg.png similarity index 100% rename from hackertracker/src/main/res/drawable-hdpi/skull_lg.png rename to app/src/main/res/drawable-hdpi/skull_lg.png diff --git a/hackertracker/src/main/res/drawable-ldpi/skull.png b/app/src/main/res/drawable-ldpi/skull.png similarity index 100% rename from hackertracker/src/main/res/drawable-ldpi/skull.png rename to app/src/main/res/drawable-ldpi/skull.png diff --git a/hackertracker/src/main/res/drawable-ldpi/skull_lg.png b/app/src/main/res/drawable-ldpi/skull_lg.png similarity index 100% rename from hackertracker/src/main/res/drawable-ldpi/skull_lg.png rename to app/src/main/res/drawable-ldpi/skull_lg.png diff --git a/hackertracker/src/main/res/drawable-mdpi/skull.png b/app/src/main/res/drawable-mdpi/skull.png similarity index 100% rename from hackertracker/src/main/res/drawable-mdpi/skull.png rename to app/src/main/res/drawable-mdpi/skull.png diff --git a/hackertracker/src/main/res/drawable-mdpi/skull_lg.png b/app/src/main/res/drawable-mdpi/skull_lg.png similarity index 100% rename from hackertracker/src/main/res/drawable-mdpi/skull_lg.png rename to app/src/main/res/drawable-mdpi/skull_lg.png diff --git a/hackertracker/src/main/res/drawable/article_background.xml b/app/src/main/res/drawable-v21/article_background.xml similarity index 80% rename from hackertracker/src/main/res/drawable/article_background.xml rename to app/src/main/res/drawable-v21/article_background.xml index e491eb7c..6edbf68c 100644 --- a/hackertracker/src/main/res/drawable/article_background.xml +++ b/app/src/main/res/drawable-v21/article_background.xml @@ -3,5 +3,5 @@ + android:color="?border_tint" /> \ No newline at end of file diff --git a/app/src/main/res/drawable-v21/bottom_sheet_background.xml b/app/src/main/res/drawable-v21/bottom_sheet_background.xml new file mode 100644 index 00000000..4ec85ae0 --- /dev/null +++ b/app/src/main/res/drawable-v21/bottom_sheet_background.xml @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-v21/day_selector_bubble.xml b/app/src/main/res/drawable-v21/day_selector_bubble.xml new file mode 100644 index 00000000..c4e3d17d --- /dev/null +++ b/app/src/main/res/drawable-v21/day_selector_bubble.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/hackertracker/src/main/res/drawable-xhdpi/skull.png b/app/src/main/res/drawable-xhdpi/skull.png similarity index 100% rename from hackertracker/src/main/res/drawable-xhdpi/skull.png rename to app/src/main/res/drawable-xhdpi/skull.png diff --git a/hackertracker/src/main/res/drawable-xhdpi/skull_lg.png b/app/src/main/res/drawable-xhdpi/skull_lg.png similarity index 100% rename from hackertracker/src/main/res/drawable-xhdpi/skull_lg.png rename to app/src/main/res/drawable-xhdpi/skull_lg.png diff --git a/app/src/main/res/drawable-xxhdpi/doggo.png b/app/src/main/res/drawable-xxhdpi/doggo.png new file mode 100644 index 00000000..2d51e13b Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/doggo.png differ diff --git a/hackertracker/src/main/res/drawable-xxhdpi/icon_shadow.png b/app/src/main/res/drawable-xxhdpi/icon_shadow.png similarity index 100% rename from hackertracker/src/main/res/drawable-xxhdpi/icon_shadow.png rename to app/src/main/res/drawable-xxhdpi/icon_shadow.png diff --git a/hackertracker/src/main/res/drawable-xxhdpi/skull.png b/app/src/main/res/drawable-xxhdpi/skull.png similarity index 100% rename from hackertracker/src/main/res/drawable-xxhdpi/skull.png rename to app/src/main/res/drawable-xxhdpi/skull.png diff --git a/hackertracker/src/main/res/drawable-xxhdpi/skull_lg.png b/app/src/main/res/drawable-xxhdpi/skull_lg.png similarity index 100% rename from hackertracker/src/main/res/drawable-xxhdpi/skull_lg.png rename to app/src/main/res/drawable-xxhdpi/skull_lg.png diff --git a/app/src/main/res/drawable/article_background.xml b/app/src/main/res/drawable/article_background.xml new file mode 100644 index 00000000..220c4988 --- /dev/null +++ b/app/src/main/res/drawable/article_background.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/hackertracker/src/main/res/drawable/bottom_sheet_background.xml b/app/src/main/res/drawable/bottom_sheet_background.xml similarity index 100% rename from hackertracker/src/main/res/drawable/bottom_sheet_background.xml rename to app/src/main/res/drawable/bottom_sheet_background.xml diff --git a/app/src/main/res/drawable/chip_background.xml b/app/src/main/res/drawable/chip_background.xml new file mode 100644 index 00000000..5619167b --- /dev/null +++ b/app/src/main/res/drawable/chip_background.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/day_selector_bubble.xml b/app/src/main/res/drawable/day_selector_bubble.xml new file mode 100644 index 00000000..e9de015c --- /dev/null +++ b/app/src/main/res/drawable/day_selector_bubble.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/hackertracker/src/main/res/drawable/ic_arrow_back_white_24dp.xml b/app/src/main/res/drawable/ic_arrow_back_white_24dp.xml similarity index 83% rename from hackertracker/src/main/res/drawable/ic_arrow_back_white_24dp.xml rename to app/src/main/res/drawable/ic_arrow_back_white_24dp.xml index 71d5bbd2..e1d335a4 100644 --- a/hackertracker/src/main/res/drawable/ic_arrow_back_white_24dp.xml +++ b/app/src/main/res/drawable/ic_arrow_back_white_24dp.xml @@ -1,4 +1,4 @@ - diff --git a/hackertracker/src/main/res/drawable/ic_cake_white_24dp.xml b/app/src/main/res/drawable/ic_cake_white_24dp.xml similarity index 100% rename from hackertracker/src/main/res/drawable/ic_cake_white_24dp.xml rename to app/src/main/res/drawable/ic_cake_white_24dp.xml diff --git a/hackertracker/src/main/res/drawable/ic_chevron_right_white_24dp.xml b/app/src/main/res/drawable/ic_chevron_right_white_24dp.xml similarity index 100% rename from hackertracker/src/main/res/drawable/ic_chevron_right_white_24dp.xml rename to app/src/main/res/drawable/ic_chevron_right_white_24dp.xml diff --git a/hackertracker/src/main/res/drawable/ic_close_white_24dp.xml b/app/src/main/res/drawable/ic_close_white_24dp.xml similarity index 100% rename from hackertracker/src/main/res/drawable/ic_close_white_24dp.xml rename to app/src/main/res/drawable/ic_close_white_24dp.xml diff --git a/hackertracker/src/main/res/drawable/ic_computer_white_24dp.xml b/app/src/main/res/drawable/ic_computer_white_24dp.xml similarity index 100% rename from hackertracker/src/main/res/drawable/ic_computer_white_24dp.xml rename to app/src/main/res/drawable/ic_computer_white_24dp.xml diff --git a/hackertracker/src/main/res/drawable/ic_expand_more_white_24dp.xml b/app/src/main/res/drawable/ic_expand_more_white_24dp.xml similarity index 100% rename from hackertracker/src/main/res/drawable/ic_expand_more_white_24dp.xml rename to app/src/main/res/drawable/ic_expand_more_white_24dp.xml diff --git a/hackertracker/src/main/res/drawable/ic_filter_list_white_24dp.xml b/app/src/main/res/drawable/ic_filter_list_white_24dp.xml similarity index 100% rename from hackertracker/src/main/res/drawable/ic_filter_list_white_24dp.xml rename to app/src/main/res/drawable/ic_filter_list_white_24dp.xml diff --git a/hackertracker/src/main/res/drawable/ic_home_white_24dp.xml b/app/src/main/res/drawable/ic_home_white_24dp.xml similarity index 100% rename from hackertracker/src/main/res/drawable/ic_home_white_24dp.xml rename to app/src/main/res/drawable/ic_home_white_24dp.xml diff --git a/hackertracker/src/main/res/drawable/ic_image_white_128dp.xml b/app/src/main/res/drawable/ic_image_white_128dp.xml similarity index 100% rename from hackertracker/src/main/res/drawable/ic_image_white_128dp.xml rename to app/src/main/res/drawable/ic_image_white_128dp.xml diff --git a/hackertracker/src/main/res/drawable/ic_info_outline_black_24dp.xml b/app/src/main/res/drawable/ic_info_outline_black_24dp.xml similarity index 100% rename from hackertracker/src/main/res/drawable/ic_info_outline_black_24dp.xml rename to app/src/main/res/drawable/ic_info_outline_black_24dp.xml diff --git a/hackertracker/src/main/res/drawable/ic_info_outline_white_24dp.xml b/app/src/main/res/drawable/ic_info_outline_white_24dp.xml similarity index 100% rename from hackertracker/src/main/res/drawable/ic_info_outline_white_24dp.xml rename to app/src/main/res/drawable/ic_info_outline_white_24dp.xml diff --git a/hackertracker/src/main/res/drawable/ic_keyboard_arrow_down_white_24dp.xml b/app/src/main/res/drawable/ic_keyboard_arrow_down_white_24dp.xml similarity index 52% rename from hackertracker/src/main/res/drawable/ic_keyboard_arrow_down_white_24dp.xml rename to app/src/main/res/drawable/ic_keyboard_arrow_down_white_24dp.xml index 4ba163b8..a261ce20 100644 --- a/hackertracker/src/main/res/drawable/ic_keyboard_arrow_down_white_24dp.xml +++ b/app/src/main/res/drawable/ic_keyboard_arrow_down_white_24dp.xml @@ -1,9 +1,10 @@ + android:width="24dp" + android:height="24dp" + android:tint="?menu_tint" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + android:pathData="M7.41,7.84L12,12.42l4.59,-4.58L18,9.25l-6,6 -6,-6z" /> diff --git a/hackertracker/src/main/res/drawable/ic_link_white_24dp.xml b/app/src/main/res/drawable/ic_link_white_24dp.xml similarity index 100% rename from hackertracker/src/main/res/drawable/ic_link_white_24dp.xml rename to app/src/main/res/drawable/ic_link_white_24dp.xml diff --git a/hackertracker/src/main/res/drawable/ic_map_white_24dp.xml b/app/src/main/res/drawable/ic_map_white_24dp.xml similarity index 100% rename from hackertracker/src/main/res/drawable/ic_map_white_24dp.xml rename to app/src/main/res/drawable/ic_map_white_24dp.xml diff --git a/app/src/main/res/drawable/ic_menu_black_24dp.xml b/app/src/main/res/drawable/ic_menu_black_24dp.xml new file mode 100644 index 00000000..8e95cbcb --- /dev/null +++ b/app/src/main/res/drawable/ic_menu_black_24dp.xml @@ -0,0 +1,10 @@ + + + diff --git a/hackertracker/src/main/res/drawable/ic_people_white_24dp.xml b/app/src/main/res/drawable/ic_people_white_24dp.xml similarity index 100% rename from hackertracker/src/main/res/drawable/ic_people_white_24dp.xml rename to app/src/main/res/drawable/ic_people_white_24dp.xml diff --git a/hackertracker/src/main/res/drawable/ic_schedule_white_24dp.xml b/app/src/main/res/drawable/ic_schedule_white_24dp.xml similarity index 100% rename from hackertracker/src/main/res/drawable/ic_schedule_white_24dp.xml rename to app/src/main/res/drawable/ic_schedule_white_24dp.xml diff --git a/app/src/main/res/drawable/ic_search_white_24dp.xml b/app/src/main/res/drawable/ic_search_white_24dp.xml new file mode 100644 index 00000000..febf4271 --- /dev/null +++ b/app/src/main/res/drawable/ic_search_white_24dp.xml @@ -0,0 +1,10 @@ + + + diff --git a/hackertracker/src/main/res/drawable/ic_settings_white_24dp.xml b/app/src/main/res/drawable/ic_settings_white_24dp.xml similarity index 100% rename from hackertracker/src/main/res/drawable/ic_settings_white_24dp.xml rename to app/src/main/res/drawable/ic_settings_white_24dp.xml diff --git a/hackertracker/src/main/res/drawable/ic_share_white_24dp.xml b/app/src/main/res/drawable/ic_share_white_24dp.xml similarity index 100% rename from hackertracker/src/main/res/drawable/ic_share_white_24dp.xml rename to app/src/main/res/drawable/ic_share_white_24dp.xml diff --git a/hackertracker/src/main/res/drawable/ic_star_accent_24dp.xml b/app/src/main/res/drawable/ic_star_accent_24dp.xml similarity index 82% rename from hackertracker/src/main/res/drawable/ic_star_accent_24dp.xml rename to app/src/main/res/drawable/ic_star_accent_24dp.xml index 6e6af653..04a07ce8 100644 --- a/hackertracker/src/main/res/drawable/ic_star_accent_24dp.xml +++ b/app/src/main/res/drawable/ic_star_accent_24dp.xml @@ -1,4 +1,4 @@ - diff --git a/hackertracker/src/main/res/drawable/ic_star_border_white_24dp.xml b/app/src/main/res/drawable/ic_star_border_white_24dp.xml similarity index 100% rename from hackertracker/src/main/res/drawable/ic_star_border_white_24dp.xml rename to app/src/main/res/drawable/ic_star_border_white_24dp.xml diff --git a/hackertracker/src/main/res/drawable/ic_twitter.xml b/app/src/main/res/drawable/ic_twitter.xml similarity index 100% rename from hackertracker/src/main/res/drawable/ic_twitter.xml rename to app/src/main/res/drawable/ic_twitter.xml diff --git a/hackertracker/src/main/res/drawable/progress_bar.xml b/app/src/main/res/drawable/progress_bar.xml similarity index 100% rename from hackertracker/src/main/res/drawable/progress_bar.xml rename to app/src/main/res/drawable/progress_bar.xml diff --git a/hackertracker/src/main/res/drawable/splash_background.xml b/app/src/main/res/drawable/splash_background.xml similarity index 100% rename from hackertracker/src/main/res/drawable/splash_background.xml rename to app/src/main/res/drawable/splash_background.xml diff --git a/hackertracker/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml similarity index 100% rename from hackertracker/src/main/res/layout/activity_main.xml rename to app/src/main/res/layout/activity_main.xml diff --git a/app/src/main/res/layout/app_bar_main.xml b/app/src/main/res/layout/app_bar_main.xml new file mode 100644 index 00000000..fc43323f --- /dev/null +++ b/app/src/main/res/layout/app_bar_main.xml @@ -0,0 +1,22 @@ + + + + + + + + + diff --git a/hackertracker/src/main/res/layout/bottom_sheet_generic.xml b/app/src/main/res/layout/bottom_sheet_generic.xml similarity index 100% rename from hackertracker/src/main/res/layout/bottom_sheet_generic.xml rename to app/src/main/res/layout/bottom_sheet_generic.xml diff --git a/hackertracker/src/main/res/layout/bottom_sheet_review.xml b/app/src/main/res/layout/bottom_sheet_review.xml similarity index 100% rename from hackertracker/src/main/res/layout/bottom_sheet_review.xml rename to app/src/main/res/layout/bottom_sheet_review.xml diff --git a/hackertracker/src/main/res/layout/empty_feed.xml b/app/src/main/res/layout/empty_feed.xml similarity index 100% rename from hackertracker/src/main/res/layout/empty_feed.xml rename to app/src/main/res/layout/empty_feed.xml diff --git a/hackertracker/src/main/res/layout/empty_text.xml b/app/src/main/res/layout/empty_text.xml similarity index 100% rename from hackertracker/src/main/res/layout/empty_text.xml rename to app/src/main/res/layout/empty_text.xml diff --git a/hackertracker/src/main/res/layout/fragment_event.xml b/app/src/main/res/layout/fragment_event.xml similarity index 76% rename from hackertracker/src/main/res/layout/fragment_event.xml rename to app/src/main/res/layout/fragment_event.xml index 2766915b..f3f94473 100644 --- a/hackertracker/src/main/res/layout/fragment_event.xml +++ b/app/src/main/res/layout/fragment_event.xml @@ -40,9 +40,10 @@ android:id="@+id/toolbar" android:layout_width="match_parent" android:layout_height="?android:attr/actionBarSize" + android:layout_marginTop="0dp" android:background="@android:color/transparent" app:layout_collapseMode="pin" - app:popupTheme="@style/ThemeOverlay.AppCompat.Light" /> + app:navigationIcon="@drawable/ic_arrow_back_white_24dp" /> @@ -76,13 +77,21 @@ + + - - - - - - - - - - - + + + - - - - - - + android:padding="0dp" /> diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml new file mode 100644 index 00000000..f4ad6c62 --- /dev/null +++ b/app/src/main/res/layout/fragment_home.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_info.xml b/app/src/main/res/layout/fragment_info.xml new file mode 100644 index 00000000..dd70ea5a --- /dev/null +++ b/app/src/main/res/layout/fragment_info.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_information.xml b/app/src/main/res/layout/fragment_information.xml new file mode 100644 index 00000000..bda2a5c3 --- /dev/null +++ b/app/src/main/res/layout/fragment_information.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/hackertracker/src/main/res/layout/fragment_list.xml b/app/src/main/res/layout/fragment_list.xml similarity index 91% rename from hackertracker/src/main/res/layout/fragment_list.xml rename to app/src/main/res/layout/fragment_list.xml index fae67921..dc8919c7 100644 --- a/hackertracker/src/main/res/layout/fragment_list.xml +++ b/app/src/main/res/layout/fragment_list.xml @@ -8,4 +8,4 @@ android:paddingLeft="16dp" android:paddingRight="16dp" android:paddingBottom="30dp" - tools:listitem="@layout/row" /> \ No newline at end of file + tools:listitem="@layout/row_event" /> \ No newline at end of file diff --git a/hackertracker/src/main/res/layout/fragment_map.xml b/app/src/main/res/layout/fragment_map.xml similarity index 75% rename from hackertracker/src/main/res/layout/fragment_map.xml rename to app/src/main/res/layout/fragment_map.xml index 9c43f075..de97a3d7 100644 --- a/hackertracker/src/main/res/layout/fragment_map.xml +++ b/app/src/main/res/layout/fragment_map.xml @@ -1,19 +1,20 @@ + android:layout_height="match_parent" + > - + android:background="?android:colorBackground" /> + android:background="?android:colorBackground"> + android:layout_height="match_parent"> + + + + + + app:layout_constraintTop_toBottomOf="@+id/appBarLayout" /> + app:layout_constraintStart_toStartOf="parent" /> + app:layout_constraintTop_toBottomOf="@+id/appBarLayout" /> \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_recyclerview.xml b/app/src/main/res/layout/fragment_recyclerview.xml new file mode 100644 index 00000000..8de4a295 --- /dev/null +++ b/app/src/main/res/layout/fragment_recyclerview.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + diff --git a/hackertracker/src/main/res/layout/app_bar_main.xml b/app/src/main/res/layout/fragment_schedule.xml similarity index 51% rename from hackertracker/src/main/res/layout/app_bar_main.xml rename to app/src/main/res/layout/fragment_schedule.xml index 7b36b3c5..7fb791f1 100644 --- a/hackertracker/src/main/res/layout/app_bar_main.xml +++ b/app/src/main/res/layout/fragment_schedule.xml @@ -4,35 +4,59 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" - android:background="@color/background" - android:fitsSystemWindows="true"> + android:background="?android:colorBackground"> + android:background="?android:colorBackground"> + android:layout_height="wrap_content" + app:navigationIcon="@drawable/ic_menu_black_24dp" + app:title="@string/schedule" /> + + app:layout_behavior="@string/appbar_scrolling_view_behavior"> + + + + + + + + + + app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior" /> - diff --git a/app/src/main/res/layout/fragment_search.xml b/app/src/main/res/layout/fragment_search.xml new file mode 100644 index 00000000..faa66dcb --- /dev/null +++ b/app/src/main/res/layout/fragment_search.xml @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_settings.xml b/app/src/main/res/layout/fragment_settings.xml new file mode 100644 index 00000000..f9fe9dac --- /dev/null +++ b/app/src/main/res/layout/fragment_settings.xml @@ -0,0 +1,112 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/hackertracker/src/main/res/layout/fragment_speakers.xml b/app/src/main/res/layout/fragment_speakers.xml similarity index 85% rename from hackertracker/src/main/res/layout/fragment_speakers.xml rename to app/src/main/res/layout/fragment_speakers.xml index a8e357d0..f7d8c4b8 100644 --- a/hackertracker/src/main/res/layout/fragment_speakers.xml +++ b/app/src/main/res/layout/fragment_speakers.xml @@ -67,13 +67,25 @@ android:layout_marginTop="8dp" android:layout_marginEnd="16dp" android:layout_marginRight="16dp" + android:tint="?android:colorForeground" app:srcCompat="@drawable/ic_twitter" /> - + android:layout_height="match_parent"> + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_article.xml b/app/src/main/res/layout/item_article.xml new file mode 100644 index 00000000..c6a12943 --- /dev/null +++ b/app/src/main/res/layout/item_article.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/hackertracker/src/main/res/layout/item_header.xml b/app/src/main/res/layout/item_header.xml similarity index 100% rename from hackertracker/src/main/res/layout/item_header.xml rename to app/src/main/res/layout/item_header.xml diff --git a/hackertracker/src/main/res/layout/item_type.xml b/app/src/main/res/layout/item_type.xml similarity index 100% rename from hackertracker/src/main/res/layout/item_type.xml rename to app/src/main/res/layout/item_type.xml diff --git a/hackertracker/src/main/res/layout/item_type_header.xml b/app/src/main/res/layout/item_type_header.xml similarity index 100% rename from hackertracker/src/main/res/layout/item_type_header.xml rename to app/src/main/res/layout/item_type_header.xml diff --git a/hackertracker/src/main/res/layout/item_upcoming.xml b/app/src/main/res/layout/item_upcoming.xml similarity index 100% rename from hackertracker/src/main/res/layout/item_upcoming.xml rename to app/src/main/res/layout/item_upcoming.xml diff --git a/hackertracker/src/main/res/layout/nav_header_main.xml b/app/src/main/res/layout/nav_header_main.xml similarity index 77% rename from hackertracker/src/main/res/layout/nav_header_main.xml rename to app/src/main/res/layout/nav_header_main.xml index 537bb956..5acef673 100644 --- a/hackertracker/src/main/res/layout/nav_header_main.xml +++ b/app/src/main/res/layout/nav_header_main.xml @@ -19,7 +19,8 @@ android:layout_height="0dp" android:layout_weight="1" android:scaleType="fitCenter" - android:src="@drawable/skull" /> + android:src="?footer_icon" + android:tint="?footer_tint" /> + + diff --git a/hackertracker/src/main/res/layout/row_event.xml b/app/src/main/res/layout/row_event.xml similarity index 50% rename from hackertracker/src/main/res/layout/row_event.xml rename to app/src/main/res/layout/row_event.xml index ba7b1e1d..e5589e6c 100644 --- a/hackertracker/src/main/res/layout/row_event.xml +++ b/app/src/main/res/layout/row_event.xml @@ -3,48 +3,16 @@ xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" - android:layout_height="wrap_content" - android:background="@color/background" - android:orientation="horizontal"> - - - - - - - - + android:layout_height="wrap_content"> @@ -54,44 +22,38 @@ style="@style/TextAppearance.AppCompat.Body2" android:layout_width="0dp" android:layout_height="wrap_content" - android:layout_marginStart="8dp" - android:layout_marginLeft="8dp" android:layout_marginTop="8dp" + android:layout_marginEnd="8dp" + android:layout_marginRight="8dp" app:layout_constraintEnd_toStartOf="@+id/star_bar" - app:layout_constraintStart_toEndOf="@+id/category" + app:layout_constraintStart_toEndOf="@+id/guideline" app:layout_constraintTop_toTopOf="parent" - tools:text="Compelled Decryption - State of the Art in Doctrinal Perversions" /> - + tools:text="Compelled Decryption" /> - - + tools:text="Track 1" /> + + + app:layout_constraintStart_toEndOf="@+id/category_dot" + app:layout_constraintTop_toBottomOf="@+id/location" + tools:text="Event" /> + tools:srcCompat="@drawable/ic_star_accent_24dp" /> + \ No newline at end of file diff --git a/hackertracker/src/main/res/layout/row_faq.xml b/app/src/main/res/layout/row_faq.xml similarity index 100% rename from hackertracker/src/main/res/layout/row_faq.xml rename to app/src/main/res/layout/row_faq.xml diff --git a/hackertracker/src/main/res/layout/row_footer.xml b/app/src/main/res/layout/row_footer.xml similarity index 71% rename from hackertracker/src/main/res/layout/row_footer.xml rename to app/src/main/res/layout/row_footer.xml index aab9815e..af36459d 100644 --- a/hackertracker/src/main/res/layout/row_footer.xml +++ b/app/src/main/res/layout/row_footer.xml @@ -1,10 +1,9 @@ + android:src="?footer_icon" + android:tint="?footer_tint" /> diff --git a/hackertracker/src/main/res/layout/row_header.xml b/app/src/main/res/layout/row_header.xml similarity index 65% rename from hackertracker/src/main/res/layout/row_header.xml rename to app/src/main/res/layout/row_header.xml index c554bccc..8e370ffb 100644 --- a/hackertracker/src/main/res/layout/row_header.xml +++ b/app/src/main/res/layout/row_header.xml @@ -1,11 +1,9 @@ diff --git a/app/src/main/res/layout/row_header_time.xml b/app/src/main/res/layout/row_header_time.xml new file mode 100644 index 00000000..5a9218dd --- /dev/null +++ b/app/src/main/res/layout/row_header_time.xml @@ -0,0 +1,17 @@ + + + + + + diff --git a/hackertracker/src/main/res/layout/row_info.xml b/app/src/main/res/layout/row_info.xml similarity index 100% rename from hackertracker/src/main/res/layout/row_info.xml rename to app/src/main/res/layout/row_info.xml diff --git a/hackertracker/src/main/res/layout/row_nav.xml b/app/src/main/res/layout/row_nav.xml similarity index 100% rename from hackertracker/src/main/res/layout/row_nav.xml rename to app/src/main/res/layout/row_nav.xml diff --git a/hackertracker/src/main/res/layout/row_nav_view.xml b/app/src/main/res/layout/row_nav_view.xml similarity index 80% rename from hackertracker/src/main/res/layout/row_nav_view.xml rename to app/src/main/res/layout/row_nav_view.xml index c7785567..16bd340e 100644 --- a/hackertracker/src/main/res/layout/row_nav_view.xml +++ b/app/src/main/res/layout/row_nav_view.xml @@ -6,10 +6,9 @@ android:layout_width="wrap_content" android:layout_height="match_parent" android:layout_gravity="start" - android:background="@color/background" - android:fitsSystemWindows="true" + app:itemTextColor="?android:colorForeground" + app:itemIconTint="?android:colorForeground" app:headerLayout="@layout/nav_header_main" - app:itemTextColor="@drawable/nav_text_color" app:menu="@menu/activity_main_drawer" tools:showIn="@layout/activity_main" /> diff --git a/app/src/main/res/layout/row_speaker.xml b/app/src/main/res/layout/row_speaker.xml new file mode 100644 index 00000000..3ea39078 --- /dev/null +++ b/app/src/main/res/layout/row_speaker.xml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/hackertracker/src/main/res/layout/row_sub_header.xml b/app/src/main/res/layout/row_sub_header.xml similarity index 100% rename from hackertracker/src/main/res/layout/row_sub_header.xml rename to app/src/main/res/layout/row_sub_header.xml diff --git a/hackertracker/src/main/res/layout/row_time_container.xml b/app/src/main/res/layout/row_time_container.xml similarity index 99% rename from hackertracker/src/main/res/layout/row_time_container.xml rename to app/src/main/res/layout/row_time_container.xml index c3bbd200..f3f47393 100644 --- a/hackertracker/src/main/res/layout/row_time_container.xml +++ b/app/src/main/res/layout/row_time_container.xml @@ -1,5 +1,4 @@ - + + + + + + + + + + + + + + \ No newline at end of file diff --git a/hackertracker/src/main/res/layout/select_dialog_item.xml b/app/src/main/res/layout/select_dialog_item.xml similarity index 94% rename from hackertracker/src/main/res/layout/select_dialog_item.xml rename to app/src/main/res/layout/select_dialog_item.xml index ed1374f4..a9f0ca77 100644 --- a/hackertracker/src/main/res/layout/select_dialog_item.xml +++ b/app/src/main/res/layout/select_dialog_item.xml @@ -7,9 +7,8 @@ android:ellipsize="marquee" android:gravity="center_vertical" android:minHeight="?android:attr/listPreferredItemHeight" - android:paddingEnd="16dp" android:paddingStart="16dp" + android:paddingEnd="16dp" android:textAppearance="?android:attr/textAppearanceLarge" - android:textColor="@color/white" android:textSize="16sp" tools:text="@tools:sample/lorem" /> diff --git a/app/src/main/res/layout/view_code_of_conduct.xml b/app/src/main/res/layout/view_code_of_conduct.xml new file mode 100644 index 00000000..b9624f98 --- /dev/null +++ b/app/src/main/res/layout/view_code_of_conduct.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/view_day.xml b/app/src/main/res/layout/view_day.xml new file mode 100644 index 00000000..4a75267d --- /dev/null +++ b/app/src/main/res/layout/view_day.xml @@ -0,0 +1,22 @@ + + diff --git a/app/src/main/res/layout/view_day_selector.xml b/app/src/main/res/layout/view_day_selector.xml new file mode 100644 index 00000000..1001671e --- /dev/null +++ b/app/src/main/res/layout/view_day_selector.xml @@ -0,0 +1,132 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/hackertracker/src/main/res/layout/view_empty.xml b/app/src/main/res/layout/view_empty.xml similarity index 94% rename from hackertracker/src/main/res/layout/view_empty.xml rename to app/src/main/res/layout/view_empty.xml index 84cefb69..1b82ec41 100644 --- a/hackertracker/src/main/res/layout/view_empty.xml +++ b/app/src/main/res/layout/view_empty.xml @@ -23,7 +23,8 @@ android:id="@+id/icon" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:src="@drawable/splash_background" + android:src="@drawable/skull_lg" + android:tint="?android:colorForeground" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="@id/right_guide" app:layout_constraintStart_toStartOf="@id/left_guide" @@ -37,7 +38,6 @@ android:layout_marginTop="24dp" android:gravity="center" android:text="@string/no_results_for" - android:textColor="@color/white" android:textSize="20sp" app:layout_constraintEnd_toEndOf="@id/right_guide" app:layout_constraintStart_toStartOf="@id/left_guide" @@ -51,7 +51,6 @@ android:fontFamily="sans-serif-light" android:gravity="center" android:text="@string/no_results_message" - android:textColor="@color/white" android:textSize="14sp" app:layout_constraintEnd_toEndOf="@id/right_guide" app:layout_constraintStart_toStartOf="@id/left_guide" diff --git a/hackertracker/src/main/res/layout/view_filter.xml b/app/src/main/res/layout/view_filter.xml similarity index 100% rename from hackertracker/src/main/res/layout/view_filter.xml rename to app/src/main/res/layout/view_filter.xml diff --git a/app/src/main/res/layout/view_help_line.xml b/app/src/main/res/layout/view_help_line.xml new file mode 100644 index 00000000..ba0f92a5 --- /dev/null +++ b/app/src/main/res/layout/view_help_line.xml @@ -0,0 +1,23 @@ + + + + + + + + + + +