aboutsummaryrefslogtreecommitdiff
path: root/app/src/main
diff options
context:
space:
mode:
Diffstat (limited to 'app/src/main')
-rw-r--r--app/src/main/AndroidManifest.xml5
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/activities/BaseMainActivity.kt4
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/activities/ImageActivity.kt4
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/activities/LoginActivity.kt5
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/activities/WebOverlayActivity.kt17
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/contracts/ActivityContract.kt1
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/enums/OverlayContext.kt11
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/facebook/FbItem.kt17
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/facebook/FbRegex.kt6
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/facebook/FbRequest.kt85
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/fragments/FragmentBase.kt119
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/fragments/FragmentContract.kt7
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/fragments/RecyclerFragments.kt27
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/fragments/WebFragments.kt26
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/glide/GlideUtils.kt27
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/glide/RoundCornerTransformation.kt41
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/iitems/NotificationIItem.kt83
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/parsers/FrostParser.kt18
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/parsers/MessageParser.kt23
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/parsers/NotifParser.kt23
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/parsers/SearchParser.kt7
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/services/FrostNotifications.kt60
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/services/FrostRequestService.kt182
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/services/NotificationService.kt2
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/utils/EnumUtils.kt57
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/utils/Prefs.kt2
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/utils/Utils.kt14
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/views/AccountItem.kt6
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/views/FrostRecyclerView.kt37
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/views/FrostWebView.kt8
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/web/LoginWebView.kt1
-rw-r--r--app/src/main/res/layout/iitem_notification.xml60
-rw-r--r--app/src/main/res/layout/view_content_base_recycler.xml1
-rw-r--r--app/src/main/res/values/dimens.xml1
-rw-r--r--app/src/main/res/values/strings.xml1
35 files changed, 808 insertions, 180 deletions
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 1839a122..c6414c51 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -148,6 +148,11 @@
android:enabled="true"
android:label="@string/frost_notifications"
android:permission="android.permission.BIND_JOB_SERVICE" />
+ <service
+ android:name=".services.FrostRequestService"
+ android:enabled="true"
+ android:label="@string/frost_requests"
+ android:permission="android.permission.BIND_JOB_SERVICE" />
<receiver
android:name=".services.UpdateReceiver"
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/activities/BaseMainActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/BaseMainActivity.kt
index 8f932a94..0d4ea46c 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/activities/BaseMainActivity.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/BaseMainActivity.kt
@@ -1,6 +1,7 @@
package com.pitchedapps.frost.activities
import android.annotation.SuppressLint
+import android.app.ActivityOptions
import android.app.AlarmManager
import android.app.PendingIntent
import android.content.Context
@@ -14,7 +15,6 @@ import android.support.design.widget.AppBarLayout
import android.support.design.widget.CoordinatorLayout
import android.support.design.widget.FloatingActionButton
import android.support.design.widget.TabLayout
-import android.support.v4.app.ActivityOptionsCompat
import android.support.v4.app.Fragment
import android.support.v4.app.FragmentManager
import android.support.v4.app.FragmentPagerAdapter
@@ -293,7 +293,7 @@ abstract class BaseMainActivity : BaseActivity(), MainActivityContract,
R.id.action_settings -> {
val intent = Intent(this, SettingsActivity::class.java)
intent.putParcelableArrayListExtra(EXTRA_COOKIES, cookies())
- val bundle = ActivityOptionsCompat.makeCustomAnimation(this, R.anim.kau_slide_in_right, R.anim.kau_fade_out).toBundle()
+ val bundle = ActivityOptions.makeCustomAnimation(this, R.anim.kau_slide_in_right, R.anim.kau_fade_out).toBundle()
startActivityForResult(intent, ACTIVITY_SETTINGS, bundle)
}
else -> return super.onOptionsItemSelected(item)
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/activities/ImageActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/ImageActivity.kt
index 2fe6b8d8..cd01a718 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/activities/ImageActivity.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/ImageActivity.kt
@@ -70,9 +70,9 @@ class ImageActivity : KauBaseActivity() {
value.update(fab)
}
- val imageUrl: String by lazy { intent.extras.getString(ARG_IMAGE_URL).trim('"') }
+ val imageUrl: String by lazy { intent.getStringExtra(ARG_IMAGE_URL).trim('"') }
- val text: String? by lazy { intent.extras.getString(ARG_TEXT) }
+ val text: String? by lazy { intent.getStringExtra(ARG_TEXT) }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/activities/LoginActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/LoginActivity.kt
index e2f7a3d2..f98f9eaf 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/activities/LoginActivity.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/LoginActivity.kt
@@ -23,6 +23,8 @@ import com.pitchedapps.frost.dbflow.fetchUsername
import com.pitchedapps.frost.dbflow.loadFbCookiesAsync
import com.pitchedapps.frost.facebook.FbCookie
import com.pitchedapps.frost.facebook.PROFILE_PICTURE_URL
+import com.pitchedapps.frost.glide.FrostGlide
+import com.pitchedapps.frost.glide.transform
import com.pitchedapps.frost.utils.*
import com.pitchedapps.frost.web.LoginWebView
import io.reactivex.Single
@@ -108,7 +110,8 @@ class LoginActivity : BaseActivity() {
private fun loadProfile(id: Long) {
- profileLoader.load(PROFILE_PICTURE_URL(id)).withRoundIcon().listener(object : RequestListener<Drawable> {
+ profileLoader.load(PROFILE_PICTURE_URL(id))
+ .transform(FrostGlide.roundCorner).listener(object : RequestListener<Drawable> {
override fun onResourceReady(resource: Drawable?, model: Any?, target: Target<Drawable>?, dataSource: DataSource?, isFirstResource: Boolean): Boolean {
profileSubject.onSuccess(true)
return false
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/activities/WebOverlayActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/WebOverlayActivity.kt
index 5b565d96..c750c88b 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/activities/WebOverlayActivity.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/WebOverlayActivity.kt
@@ -22,6 +22,7 @@ import com.pitchedapps.frost.R
import com.pitchedapps.frost.contracts.*
import com.pitchedapps.frost.enums.OverlayContext
import com.pitchedapps.frost.facebook.*
+import com.pitchedapps.frost.services.FrostRunnable
import com.pitchedapps.frost.utils.*
import com.pitchedapps.frost.views.FrostContentWeb
import com.pitchedapps.frost.views.FrostVideoViewer
@@ -111,18 +112,18 @@ open class WebOverlayActivityBase(private val forceBasicAgent: Boolean) : BaseAc
val coordinator: CoordinatorLayout by bindView(R.id.overlay_main_content)
private inline val urlTest: String?
- get() = intent.extras?.getString(ARG_URL) ?: intent.dataString
+ get() = intent.getStringExtra(ARG_URL) ?: intent.dataString
override val baseUrl: String
- get() = (intent.extras?.getString(ARG_URL) ?: intent.dataString).formattedFbUrl
+ get() = (intent.getStringExtra(ARG_URL) ?: intent.dataString).formattedFbUrl
override val baseEnum: FbItem? = null
private inline val userId: Long
- get() = intent.extras?.getLong(ARG_USER_ID, Prefs.userId) ?: Prefs.userId
+ get() = intent.getLongExtra(ARG_USER_ID, Prefs.userId)
- private inline val overlayContext: OverlayContext?
- get() = intent.extras?.getSerializable(ARG_OVERLAY_CONTEXT) as OverlayContext?
+ private val overlayContext: OverlayContext?
+ get() = OverlayContext[intent.extras]
override fun setTitle(title: String) {
toolbar.title = title
@@ -136,6 +137,7 @@ open class WebOverlayActivityBase(private val forceBasicAgent: Boolean) : BaseAc
finish()
return
}
+
setFrameContentView(R.layout.activity_web_overlay)
setSupportActionBar(toolbar)
supportActionBar?.setDisplayShowHomeEnabled(true)
@@ -167,6 +169,9 @@ open class WebOverlayActivityBase(private val forceBasicAgent: Boolean) : BaseAc
}
}
+ FrostRunnable.propagate(this, intent)
+ L.e("Done propagation")
+
kauSwipeOnCreate {
if (!Prefs.overlayFullScreenSwipe) edgeSize = 20.dpToPx
transitionSystemBars = false
@@ -180,7 +185,7 @@ open class WebOverlayActivityBase(private val forceBasicAgent: Boolean) : BaseAc
*/
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
- val newUrl = (intent.extras?.getString(ARG_URL) ?: intent.dataString ?: return).formattedFbUrl
+ val newUrl = (intent.getStringExtra(ARG_URL) ?: intent.dataString ?: return).formattedFbUrl
L.d("New intent")
if (baseUrl != newUrl) {
this.intent = intent
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/contracts/ActivityContract.kt b/app/src/main/kotlin/com/pitchedapps/frost/contracts/ActivityContract.kt
index 14d7ae09..e46a4bfb 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/contracts/ActivityContract.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/contracts/ActivityContract.kt
@@ -1,6 +1,5 @@
package com.pitchedapps.frost.contracts
-import com.pitchedapps.frost.dbflow.CookieModel
import io.reactivex.subjects.PublishSubject
/**
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/enums/OverlayContext.kt b/app/src/main/kotlin/com/pitchedapps/frost/enums/OverlayContext.kt
index 8f26e152..4f37c6c7 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/enums/OverlayContext.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/enums/OverlayContext.kt
@@ -6,6 +6,9 @@ import android.view.MenuItem
import ca.allanwang.kau.utils.toDrawable
import com.pitchedapps.frost.R
import com.pitchedapps.frost.facebook.FbItem
+import com.pitchedapps.frost.utils.EnumBundle
+import com.pitchedapps.frost.utils.EnumBundleCompanion
+import com.pitchedapps.frost.utils.EnumCompanion
import com.pitchedapps.frost.views.FrostWebView
/**
@@ -16,7 +19,7 @@ import com.pitchedapps.frost.views.FrostWebView
*
* For now, this is able to add new menu options upon first load
*/
-enum class OverlayContext(private val menuItem: FrostMenuItem?) {
+enum class OverlayContext(private val menuItem: FrostMenuItem?) : EnumBundle<OverlayContext> {
NOTIFICATION(FrostMenuItem(R.id.action_notification, FbItem.NOTIFICATIONS)),
MESSAGE(FrostMenuItem(R.id.action_messages, FbItem.MESSAGES));
@@ -28,9 +31,11 @@ enum class OverlayContext(private val menuItem: FrostMenuItem?) {
menuItem?.addToMenu(context, menu, 0)
}
- companion object {
+ override val bundleContract: EnumBundleCompanion<OverlayContext>
+ get() = Companion
+
+ companion object : EnumCompanion<OverlayContext>("frost_arg_overlay_context", values()) {
- val values = OverlayContext.values() //save one instance
/**
* Execute selection call for an item by id
* Returns [true] if selection was consumed, [false] otherwise
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbItem.kt b/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbItem.kt
index cc2ca556..e2e9d9e5 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbItem.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbItem.kt
@@ -7,15 +7,19 @@ import com.mikepenz.iconics.typeface.IIcon
import com.mikepenz.material_design_iconic_typeface_library.MaterialDesignIconic
import com.pitchedapps.frost.R
import com.pitchedapps.frost.fragments.BaseFragment
+import com.pitchedapps.frost.fragments.NotificationFragment
import com.pitchedapps.frost.fragments.WebFragment
import com.pitchedapps.frost.fragments.WebFragmentMenu
+import com.pitchedapps.frost.utils.EnumBundle
+import com.pitchedapps.frost.utils.EnumBundleCompanion
+import com.pitchedapps.frost.utils.EnumCompanion
enum class FbItem(
@StringRes val titleId: Int,
val icon: IIcon,
relativeUrl: String,
val fragmentCreator: () -> BaseFragment = ::WebFragment
-) {
+) : EnumBundle<FbItem> {
ACTIVITY_LOG(R.string.activity_log, GoogleMaterial.Icon.gmd_list, "me/allactivity"),
BIRTHDAYS(R.string.birthdays, GoogleMaterial.Icon.gmd_cake, "events/birthdays"),
CHAT(R.string.chat, GoogleMaterial.Icon.gmd_chat, "buddylist"),
@@ -28,7 +32,7 @@ enum class FbItem(
MENU(R.string.menu, GoogleMaterial.Icon.gmd_menu, "settings", ::WebFragmentMenu),
MESSAGES(R.string.messages, MaterialDesignIconic.Icon.gmi_comments, "messages"),
NOTES(R.string.notes, CommunityMaterial.Icon.cmd_note, "notes"),
- NOTIFICATIONS(R.string.notifications, MaterialDesignIconic.Icon.gmi_globe, "notifications"),
+ NOTIFICATIONS(R.string.notifications, MaterialDesignIconic.Icon.gmi_globe, "notifications", ::NotificationFragment),
ON_THIS_DAY(R.string.on_this_day, GoogleMaterial.Icon.gmd_today, "onthisday"),
PAGES(R.string.pages, GoogleMaterial.Icon.gmd_flag, "pages"),
PHOTOS(R.string.photos, GoogleMaterial.Icon.gmd_photo, "me/photos"),
@@ -39,12 +43,11 @@ enum class FbItem(
;
val url = "$FB_URL_BASE$relativeUrl"
-}
-inline val fbSearch
- get() = fbSearch()
+ override val bundleContract: EnumBundleCompanion<FbItem>
+ get() = Companion
-fun fbSearch(query: String = "a") = "$FB_SEARCH$query"
+ companion object : EnumCompanion<FbItem>("frost_arg_fb_item", values())
+}
-private const val FB_SEARCH = "${FB_URL_BASE}search/top/?q="
fun defaultTabs(): List<FbItem> = listOf(FbItem.FEED, FbItem.MESSAGES, FbItem.NOTIFICATIONS, FbItem.MENU)
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbRegex.kt b/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbRegex.kt
index 8d625582..24f685be 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbRegex.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbRegex.kt
@@ -13,7 +13,7 @@ package com.pitchedapps.frost.facebook
* Matches the fb_dtsg component of a page containing it as a hidden value
*/
val FB_DTSG_MATCHER: Regex by lazy { Regex("name=\"fb_dtsg\" value=\"(.*?)\"") }
-val FB_REV_MATCHER: Regex by lazy{Regex("\"app_version\":\"(.*?)\"")}
+val FB_REV_MATCHER: Regex by lazy { Regex("\"app_version\":\"(.*?)\"") }
/**
* Matches user id from cookie
@@ -21,9 +21,9 @@ val FB_REV_MATCHER: Regex by lazy{Regex("\"app_version\":\"(.*?)\"")}
val FB_USER_MATCHER: Regex by lazy { Regex("c_user=([0-9]*);") }
val FB_EPOCH_MATCHER: Regex by lazy { Regex(":([0-9]+)") }
-val FB_NOTIF_ID_MATCHER: Regex by lazy { Regex("notif_id\":([0-9]+)") }
+val FB_NOTIF_ID_MATCHER: Regex by lazy { Regex("notif_([0-9]+)") }
val FB_MESSAGE_NOTIF_ID_MATCHER: Regex by lazy { Regex("[thread|user]_fbid_([0-9]+)") }
-val FB_CSS_URL_MATCHER: Regex by lazy { Regex("url\\([\"|'](.*?)[\"|']\\)") }
+val FB_CSS_URL_MATCHER: Regex by lazy { Regex("url\\([\"|']?(.*?)[\"|']?\\)") }
operator fun MatchResult?.get(groupIndex: Int) = this?.groupValues?.get(groupIndex)
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbRequest.kt b/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbRequest.kt
index 2fa20917..51e14097 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbRequest.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbRequest.kt
@@ -11,6 +11,24 @@ import org.apache.commons.text.StringEscapeUtils
/**
* Created by Allan Wang on 21/12/17.
*/
+private val authMap: MutableMap<String, RequestAuth> = mutableMapOf()
+
+fun String.fbRequest(action: RequestAuth.() -> Unit) {
+ val savedAuth = authMap[this]
+ if (savedAuth != null) {
+ savedAuth.action()
+ } else {
+ val auth = getAuth()
+ if (!auth.isValid) {
+ L.e("Attempted fbrequest with invalid auth")
+ return
+ }
+ authMap.put(this, auth)
+ L.i(null, "Found auth $auth")
+ auth.action()
+ }
+}
+
data class RequestAuth(val userId: Long = -1,
val cookie: String = "",
val fb_dtsg: String = "",
@@ -19,6 +37,22 @@ data class RequestAuth(val userId: Long = -1,
get() = userId > 0 && cookie.isNotEmpty() && fb_dtsg.isNotEmpty() && rev.isNotEmpty()
}
+/**
+ * Request container with the execution call
+ */
+class FrostRequest<out T : Any>(val call: Call, private val invoke: (Call) -> T) {
+ fun invoke() = invoke(call)
+}
+
+private inline fun <T : Any> RequestAuth.frostRequest(
+ noinline invoke: (Call) -> T,
+ builder: Request.Builder.() -> Request.Builder // to ensure we don't do anything extra at the end
+): FrostRequest<T> {
+ val request = cookie.requestBuilder()
+ request.builder()
+ return FrostRequest(request.call(), invoke)
+}
+
private val client: OkHttpClient by lazy {
val builder = OkHttpClient.Builder()
if (BuildConfig.DEBUG)
@@ -49,12 +83,12 @@ private fun String.requestBuilder() = Request.Builder()
private fun Request.Builder.call() = client.newCall(build())
-
-fun Pair<Long, String>.getAuth(): RequestAuth {
- val (userId, cookie) = this
- var auth = RequestAuth(userId, cookie)
- val call = cookie.requestBuilder()
- .url("https://touch.facebook.com")
+fun String.getAuth(): RequestAuth {
+ var auth = RequestAuth(cookie = this)
+ val id = FB_USER_MATCHER.find(this)[1]?.toLong() ?: return auth
+ auth = auth.copy(userId = id)
+ val call = this.requestBuilder()
+ .url(FB_URL_BASE)
.get()
.call()
call.execute().body()?.charStream()?.useLines {
@@ -62,14 +96,14 @@ fun Pair<Long, String>.getAuth(): RequestAuth {
val text = StringEscapeUtils.unescapeEcmaScript(it)
val fb_dtsg = FB_DTSG_MATCHER.find(text)[1]
if (fb_dtsg != null) {
- L.d(null, "fb_dtsg for $userId: $fb_dtsg")
+ L.d(null, "fb_dtsg for ${auth.userId}: $fb_dtsg")
auth = auth.copy(fb_dtsg = fb_dtsg)
if (auth.isValid) return auth
}
val rev = FB_REV_MATCHER.find(text)[1]
if (rev != null) {
- L.d(null, "rev for $userId: $rev")
+ L.d(null, "rev for ${auth.userId}: $rev")
auth = auth.copy(rev = rev)
if (auth.isValid) return auth
}
@@ -79,7 +113,7 @@ fun Pair<Long, String>.getAuth(): RequestAuth {
return auth
}
-fun RequestAuth.markNotificationRead(notifId: Long): Call {
+fun RequestAuth.markNotificationRead(notifId: Long): FrostRequest<Boolean> {
val body = listOf(
"click_type" to "notification_click",
@@ -89,40 +123,37 @@ fun RequestAuth.markNotificationRead(notifId: Long): Call {
"__user" to userId
).withEmptyData("m_sess", "__dyn", "__req", "__ajax__")
- return cookie.requestBuilder()
- .url("${FB_URL_BASE}a/jewel_notifications_log.php")
- .post(body.toForm())
- .call()
+ return frostRequest(::executeForNoError) {
+ url("${FB_URL_BASE}a/jewel_notifications_log.php")
+ post(body.toForm())
+ }
}
-private inline fun <T, reified R : Any, O> zip(data: Array<T>,
- crossinline mapper: (List<R>) -> O,
- crossinline caller: (T) -> R): Single<O> {
- val singles = data.map { Single.fromCallable { caller(it) }.subscribeOn(Schedulers.io()) }
+inline fun <T, reified R : Any, O> Array<T>.zip(crossinline mapper: (List<R>) -> O,
+ crossinline caller: (T) -> R): Single<O> {
+ val singles = map { Single.fromCallable { caller(it) }.subscribeOn(Schedulers.io()) }
return Single.zip(singles) {
val results = it.mapNotNull { it as? R }
mapper(results)
}
}
-fun RequestAuth.markNotificationsRead(vararg notifId: Long) = zip<Long, Boolean, Int>(notifId.toTypedArray(),
- { it.count { it } }) {
- val response = markNotificationRead(it).execute()
- val buffer = CharArray(20)
- response.body()?.charStream()?.read(buffer) ?: return@zip false
- !buffer.toString().contains("error")
-}
+fun RequestAuth.markNotificationsRead(vararg notifId: Long) =
+ notifId.toTypedArray().zip<Long, Boolean, Boolean>(
+ { it.all { it } },
+ { markNotificationRead(it).invoke() })
/**
* Execute the call and attempt to check validity
+ * Valid = not blank & no "error" instance
*/
-fun Call.executeAndCheck(): Boolean {
- val body = execute().body() ?: return false
+fun executeForNoError(call: Call): Boolean {
+ val body = call.execute().body() ?: return false
var empty = true
body.charStream().useLines {
it.forEach {
+ if (it.contains("error")) return false
if (empty && it.isNotEmpty()) empty = false
- if (it.contains("error")) return true
}
}
return !empty
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/fragments/FragmentBase.kt b/app/src/main/kotlin/com/pitchedapps/frost/fragments/FragmentBase.kt
index 58d9ebd4..963d00bb 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/fragments/FragmentBase.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/fragments/FragmentBase.kt
@@ -6,27 +6,28 @@ import android.support.v4.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
+import ca.allanwang.kau.adapters.fastAdapter
import ca.allanwang.kau.utils.withArguments
+import com.mikepenz.fastadapter.FastAdapter
import com.mikepenz.fastadapter.IItem
-import com.mikepenz.fastadapter.commons.adapters.FastItemAdapter
+import com.mikepenz.fastadapter.adapters.ItemAdapter
+import com.mikepenz.fastadapter_extensions.items.ProgressItem
import com.pitchedapps.frost.R
import com.pitchedapps.frost.contracts.DynamicUiContract
import com.pitchedapps.frost.contracts.FrostContentParent
import com.pitchedapps.frost.contracts.MainActivityContract
-import com.pitchedapps.frost.dbflow.CookieModel
import com.pitchedapps.frost.enums.FeedSort
import com.pitchedapps.frost.facebook.FbCookie
import com.pitchedapps.frost.facebook.FbItem
import com.pitchedapps.frost.parsers.FrostParser
+import com.pitchedapps.frost.parsers.ParseResponse
import com.pitchedapps.frost.utils.*
import com.pitchedapps.frost.views.FrostRecyclerView
-import com.pitchedapps.frost.views.FrostWebView
-import com.pitchedapps.frost.web.FrostWebViewClient
-import com.pitchedapps.frost.web.FrostWebViewClientMenu
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
import org.jetbrains.anko.doAsync
import org.jetbrains.anko.toast
+import org.jetbrains.anko.uiThread
/**
* Created by Allan Wang on 2017-11-07.
@@ -37,7 +38,6 @@ import org.jetbrains.anko.toast
abstract class BaseFragment : Fragment(), FragmentContract, DynamicUiContract {
companion object {
- private const val ARG_URL_ENUM = "arg_url_enum"
private const val ARG_POSITION = "arg_position"
internal operator fun invoke(base: () -> BaseFragment, data: FbItem, position: Int): BaseFragment {
@@ -45,15 +45,15 @@ abstract class BaseFragment : Fragment(), FragmentContract, DynamicUiContract {
val d = if (data == FbItem.FEED) FeedSort(Prefs.feedSort).item else data
fragment.withArguments(
ARG_URL to d.url,
- ARG_POSITION to position,
- ARG_URL_ENUM to d
+ ARG_POSITION to position
)
+ d.put(fragment.arguments!!)
return fragment
}
}
override val baseUrl: String by lazy { arguments!!.getString(ARG_URL) }
- override val baseEnum: FbItem by lazy { arguments!!.getSerializable(ARG_URL_ENUM) as FbItem }
+ override val baseEnum: FbItem by lazy { FbItem[arguments]!! }
override val position: Int by lazy { arguments!!.getInt(ARG_POSITION) }
override var firstLoad: Boolean = true
@@ -66,6 +66,7 @@ abstract class BaseFragment : Fragment(), FragmentContract, DynamicUiContract {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
+ firstLoad = true
if (context !is MainActivityContract)
throw IllegalArgumentException("${this::class.java.simpleName} is not attached to a context implementing MainActivityContract")
}
@@ -92,8 +93,9 @@ abstract class BaseFragment : Fragment(), FragmentContract, DynamicUiContract {
}
override fun firstLoadRequest() {
+ val core = core ?: return
if (userVisibleHint && isVisible && firstLoad) {
- core?.reloadBase(true)
+ core.reloadBase(true)
firstLoad = false
}
}
@@ -182,64 +184,83 @@ abstract class RecyclerFragment<T : Any, Item : IItem<*, *>> : BaseFragment(), R
*/
abstract val parser: FrostParser<T>
- abstract val adapter: FastItemAdapter<Item>
+ open fun getDoc(cookie: String?) = frostJsoup(cookie, parser.url)
- abstract fun toItems(data: T): List<Item>
+ val adapter: ItemAdapter<Item> = ItemAdapter()
- override fun bind(recyclerView: FrostRecyclerView) {
- recyclerView.adapter = this.adapter
+ abstract fun toItems(response: ParseResponse<T>): List<Item>
+
+ override final fun bind(recyclerView: FrostRecyclerView) {
+ recyclerView.adapter = getAdapter()
+ recyclerView.onReloadClear = { adapter.clear() }
+ bindImpl(recyclerView)
}
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- val tail = tailMapper(baseEnum)
- if (tail.isNotEmpty()) {
- val baseUrl = baseEnum.url
- L.d("Adding $tail to $baseUrl for RecyclerFragment")
- arguments!!.putString(ARG_URL, "$baseUrl$tail")
+ override fun firstLoadRequest() {
+ val core = core ?: return
+ if (firstLoad) {
+ core.reloadBase(true)
+ firstLoad = false
}
}
- private fun tailMapper(item: FbItem) = when (item) {
- FbItem.NOTIFICATIONS, FbItem.MESSAGES -> "/?more"
- else -> ""
- }
+ /**
+ * Anything to call for one time bindings
+ * At this stage, all adapters will have FastAdapter references
+ */
+ open fun bindImpl(recyclerView: FrostRecyclerView) = Unit
+
+ /**
+ * Create the fast adapter to bind to the recyclerview
+ */
+ open fun getAdapter(): FastAdapter<IItem<*, *>> = fastAdapter(this.adapter)
override fun reload(progress: (Int) -> Unit, callback: (Boolean) -> Unit) {
doAsync {
progress(10)
- val doc = frostJsoup(baseUrl)
+ val cookie = FbCookie.webCookie
+ val doc = getDoc(cookie)
progress(60)
- val data = parser.parse(FbCookie.webCookie, doc)
- if (data == null) {
- context?.toast(R.string.error_generic)
+ val response = parser.parse(cookie, doc)
+ if (response == null) {
+ uiThread { context?.toast(R.string.error_generic) }
L.eThrow("RecyclerFragment failed for ${baseEnum.name}")
Prefs.nativeViews = false
return@doAsync callback(false)
}
progress(80)
- val items = toItems(data.data)
+ val items = toItems(response)
progress(97)
- adapter.setNewList(items)
+ uiThread { adapter.setNewList(items) }
+ callback(true)
}
}
}
-open class WebFragment : BaseFragment(), FragmentContract {
-
- override val layoutRes: Int = R.layout.view_content_web
-
- /**
- * Given a webview, output a client
- */
- open fun client(web: FrostWebView) = FrostWebViewClient(web)
-
- override fun innerView(context: Context) = FrostWebView(context)
-
-}
-
-class WebFragmentMenu : WebFragment() {
-
- override fun client(web: FrostWebView) = FrostWebViewClientMenu(web)
-
-} \ No newline at end of file
+//abstract class PagedRecyclerFragment<T : Any, Item : IItem<*, *>> : RecyclerFragment<T, Item>() {
+//
+// var allowPagedLoading = true
+//
+// val footerAdapter = ItemAdapter<FrostProgress>()
+//
+// val footerScrollListener = object : EndlessRecyclerOnScrollListener(footerAdapter) {
+// override fun onLoadMore(currentPage: Int) {
+// TODO("not implemented")
+//
+// }
+//
+// }
+//
+// override fun getAdapter() = fastAdapter(adapter, footerAdapter)
+//
+// override fun bindImpl(recyclerView: FrostRecyclerView) {
+// recyclerView.addOnScrollListener(footerScrollListener)
+// }
+//
+// override fun reload(progress: (Int) -> Unit, callback: (Boolean) -> Unit) {
+// footerScrollListener.
+// super.reload(progress, callback)
+// }
+//}
+
+class FrostProgress : ProgressItem() \ No newline at end of file
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/fragments/FragmentContract.kt b/app/src/main/kotlin/com/pitchedapps/frost/fragments/FragmentContract.kt
index 00429730..a78eb0d0 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/fragments/FragmentContract.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/fragments/FragmentContract.kt
@@ -1,11 +1,9 @@
package com.pitchedapps.frost.fragments
-import android.content.Context
import com.pitchedapps.frost.contracts.FrostContentContainer
import com.pitchedapps.frost.contracts.FrostContentCore
import com.pitchedapps.frost.contracts.FrostContentParent
import com.pitchedapps.frost.contracts.MainActivityContract
-import com.pitchedapps.frost.dbflow.CookieModel
import com.pitchedapps.frost.views.FrostRecyclerView
import io.reactivex.disposables.Disposable
@@ -56,11 +54,6 @@ interface FragmentContract : FrostContentContainer {
fun attachMainObservable(contract: MainActivityContract): Disposable
/**
- * Load custom layout to container
- */
- fun innerView(context: Context): FrostContentCore
-
- /**
* Call when fragment is detached so that any existing
* observable is disposed
*/
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/fragments/RecyclerFragments.kt b/app/src/main/kotlin/com/pitchedapps/frost/fragments/RecyclerFragments.kt
new file mode 100644
index 00000000..4d4a6f8b
--- /dev/null
+++ b/app/src/main/kotlin/com/pitchedapps/frost/fragments/RecyclerFragments.kt
@@ -0,0 +1,27 @@
+package com.pitchedapps.frost.fragments
+
+import com.pitchedapps.frost.facebook.FbItem
+import com.pitchedapps.frost.iitems.NotificationIItem
+import com.pitchedapps.frost.parsers.FrostNotifs
+import com.pitchedapps.frost.parsers.NotifParser
+import com.pitchedapps.frost.parsers.ParseResponse
+import com.pitchedapps.frost.utils.frostJsoup
+import com.pitchedapps.frost.views.FrostRecyclerView
+
+/**
+ * Created by Allan Wang on 27/12/17.
+ */
+class NotificationFragment : RecyclerFragment<FrostNotifs, NotificationIItem>() {
+
+ override val parser = NotifParser
+
+ override fun getDoc(cookie: String?) = frostJsoup(cookie, "${FbItem.NOTIFICATIONS.url}?more")
+
+ override fun toItems(response: ParseResponse<FrostNotifs>): List<NotificationIItem> =
+ response.data.notifs.map { NotificationIItem(it, response.cookie) }
+
+ override fun bindImpl(recyclerView: FrostRecyclerView) {
+ NotificationIItem.bindEvents(adapter)
+ }
+
+} \ No newline at end of file
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/fragments/WebFragments.kt b/app/src/main/kotlin/com/pitchedapps/frost/fragments/WebFragments.kt
new file mode 100644
index 00000000..2740a36f
--- /dev/null
+++ b/app/src/main/kotlin/com/pitchedapps/frost/fragments/WebFragments.kt
@@ -0,0 +1,26 @@
+package com.pitchedapps.frost.fragments
+
+import com.pitchedapps.frost.R
+import com.pitchedapps.frost.views.FrostWebView
+import com.pitchedapps.frost.web.FrostWebViewClient
+import com.pitchedapps.frost.web.FrostWebViewClientMenu
+
+/**
+ * Created by Allan Wang on 27/12/17.
+ */
+open class WebFragment : BaseFragment() {
+
+ override val layoutRes: Int = R.layout.view_content_web
+
+ /**
+ * Given a webview, output a client
+ */
+ open fun client(web: FrostWebView) = FrostWebViewClient(web)
+
+}
+
+class WebFragmentMenu : WebFragment() {
+
+ override fun client(web: FrostWebView) = FrostWebViewClientMenu(web)
+
+} \ No newline at end of file
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/glide/GlideUtils.kt b/app/src/main/kotlin/com/pitchedapps/frost/glide/GlideUtils.kt
new file mode 100644
index 00000000..6d2b3cda
--- /dev/null
+++ b/app/src/main/kotlin/com/pitchedapps/frost/glide/GlideUtils.kt
@@ -0,0 +1,27 @@
+package com.pitchedapps.frost.glide
+
+import com.bumptech.glide.RequestBuilder
+import com.bumptech.glide.load.MultiTransformation
+import com.bumptech.glide.load.resource.bitmap.BitmapTransformation
+import com.bumptech.glide.load.resource.bitmap.CircleCrop
+import com.bumptech.glide.request.RequestOptions
+
+/**
+ * Created by Allan Wang on 28/12/17.
+ *
+ * Collection of transformations
+ * Each caller will generate a new one upon request
+ */
+object FrostGlide {
+ val roundCorner
+ get() = RoundCornerTransformation()
+ val circleCrop
+ get() = CircleCrop()
+}
+
+fun <T> RequestBuilder<T>.transform(vararg transformation: BitmapTransformation): RequestBuilder<T> =
+ when (transformation.size) {
+ 0 -> this
+ 1 -> apply(RequestOptions.bitmapTransform(transformation[0]))
+ else -> apply(RequestOptions.bitmapTransform(MultiTransformation(*transformation)))
+ }
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/glide/RoundCornerTransformation.kt b/app/src/main/kotlin/com/pitchedapps/frost/glide/RoundCornerTransformation.kt
new file mode 100644
index 00000000..5eded159
--- /dev/null
+++ b/app/src/main/kotlin/com/pitchedapps/frost/glide/RoundCornerTransformation.kt
@@ -0,0 +1,41 @@
+package com.pitchedapps.frost.glide
+
+import android.graphics.*
+import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool
+import com.bumptech.glide.load.resource.bitmap.BitmapTransformation
+import com.pitchedapps.frost.utils.Prefs
+import java.security.MessageDigest
+
+
+/**
+ * Created by Allan Wang on 27/12/17.
+ */
+class RoundCornerTransformation : BitmapTransformation() {
+
+ override fun updateDiskCacheKey(messageDigest: MessageDigest) {
+ messageDigest.update("FrostRoundCornerTransform-${Prefs.showRoundedIcons}".toByteArray())
+ }
+
+ override fun transform(pool: BitmapPool, toTransform: Bitmap, outWidth: Int, outHeight: Int): Bitmap {
+
+ val width = toTransform.width
+ val height = toTransform.height
+
+ val bitmap = pool.get(width, height, Bitmap.Config.ARGB_8888)
+ bitmap.setHasAlpha(true)
+
+ val radius = Math.min(width, height).toFloat() /
+ (if (Prefs.showRoundedIcons) 2f else 10f)
+
+ val canvas = Canvas(bitmap)
+ val paint = Paint()
+ paint.isAntiAlias = true
+ paint.shader = BitmapShader(toTransform, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP)
+ canvas.drawRoundRect(RectF(0f, 0f, width.toFloat(), height.toFloat()),
+ radius, radius, paint)
+
+ return bitmap
+ }
+
+
+} \ No newline at end of file
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/iitems/NotificationIItem.kt b/app/src/main/kotlin/com/pitchedapps/frost/iitems/NotificationIItem.kt
new file mode 100644
index 00000000..c7f61351
--- /dev/null
+++ b/app/src/main/kotlin/com/pitchedapps/frost/iitems/NotificationIItem.kt
@@ -0,0 +1,83 @@
+package com.pitchedapps.frost.iitems
+
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageView
+import android.widget.TextView
+import ca.allanwang.kau.iitems.KauIItem
+import ca.allanwang.kau.ui.createSimpleRippleDrawable
+import ca.allanwang.kau.utils.*
+import com.bumptech.glide.Glide
+import com.mikepenz.fastadapter.FastAdapter
+import com.mikepenz.fastadapter.adapters.ItemAdapter
+import com.pitchedapps.frost.R
+import com.pitchedapps.frost.glide.FrostGlide
+import com.pitchedapps.frost.glide.transform
+import com.pitchedapps.frost.parsers.FrostNotif
+import com.pitchedapps.frost.services.FrostRunnable
+import com.pitchedapps.frost.utils.Prefs
+import com.pitchedapps.frost.utils.launchWebOverlay
+
+/**
+ * Created by Allan Wang on 27/12/17.
+ */
+class NotificationIItem(val notification: FrostNotif, val cookie: String) : KauIItem<NotificationIItem, NotificationIItem.ViewHolder>(
+ R.layout.iitem_notification, ::ViewHolder
+) {
+
+ companion object {
+ fun bindEvents(adapter: ItemAdapter<NotificationIItem>) {
+ adapter.fastAdapter.withSelectable(false)
+ .withOnClickListener { v, _, item, position ->
+ val notif = item.notification
+ if (notif.unread) {
+ FrostRunnable.markNotificationRead(v.context, notif.id, item.cookie)
+ adapter.set(position, NotificationIItem(notif.copy(unread = false), item.cookie))
+ }
+ v.context.launchWebOverlay(notif.url)
+ true
+ }
+ }
+ }
+
+ class ViewHolder(itemView: View) : FastAdapter.ViewHolder<NotificationIItem>(itemView) {
+
+ val frame: ViewGroup by bindView(R.id.item_frame)
+ val avatar: ImageView by bindView(R.id.item_avatar)
+ val content: TextView by bindView(R.id.item_content)
+ val date: TextView by bindView(R.id.item_date)
+ val thumbnail: ImageView by bindView(R.id.item_thumbnail)
+
+ private val glide
+ get() = Glide.with(itemView)
+
+ override fun bindView(item: NotificationIItem, payloads: MutableList<Any>) {
+ val notif = item.notification
+ frame.background = createSimpleRippleDrawable(Prefs.textColor,
+ Prefs.bgColor.colorToForeground(if (notif.unread) 0.7f else 0.0f)
+ .withAlpha(30))
+ content.setTextColor(Prefs.textColor)
+ date.setTextColor(Prefs.textColor.withAlpha(150))
+
+ val glide = glide
+ glide.load(notif.img)
+ .transform(FrostGlide.roundCorner)
+ .into(avatar)
+ if (notif.thumbnailUrl != null)
+ glide.load(notif.thumbnailUrl).into(thumbnail.visible())
+
+ content.text = notif.content
+ date.text = notif.timeString
+ }
+
+ override fun unbindView(item: NotificationIItem) {
+ frame.background = null
+ val glide = glide
+ glide.clear(avatar)
+ glide.clear(thumbnail)
+ thumbnail.gone()
+ content.text = null
+ date.text = null
+ }
+ }
+} \ No newline at end of file
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/parsers/FrostParser.kt b/app/src/main/kotlin/com/pitchedapps/frost/parsers/FrostParser.kt
index 016f33e8..f0938eca 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/parsers/FrostParser.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/parsers/FrostParser.kt
@@ -9,6 +9,7 @@ import com.pitchedapps.frost.utils.frostJsoup
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
+import org.jsoup.select.Elements
/**
* Created by Allan Wang on 2017-10-06.
@@ -39,12 +40,19 @@ interface FrostParser<out T : Any> {
fun parse(cookie: String?, document: Document): ParseResponse<T>?
/**
+ * Call parsing using jsoup to fetch from given url
+ */
+ fun parseFromUrl(cookie: String?, url: String): ParseResponse<T>?
+
+ /**
* Call parsing with given data
*/
fun parseFromData(cookie: String?, text: String): ParseResponse<T>?
}
+const val FALLBACK_TIME_MOD = 1000000
+
data class FrostLink(val text: String, val href: String)
data class ParseResponse<out T>(val cookie: String, val data: T) {
@@ -68,7 +76,7 @@ internal fun <T> List<T>.toJsonString(tag: String, indent: Int) = StringBuilder(
*/
internal abstract class FrostParserBase<out T : Any>(private val redirectToText: Boolean) : FrostParser<T> {
- override final fun parse(cookie: String?) = parse(cookie, frostJsoup(cookie, url))
+ override final fun parse(cookie: String?) = parseFromUrl(cookie, url)
override final fun parseFromData(cookie: String?, text: String): ParseResponse<T>? {
cookie ?: return null
@@ -77,6 +85,9 @@ internal abstract class FrostParserBase<out T : Any>(private val redirectToText:
return ParseResponse(cookie, data)
}
+ override final fun parseFromUrl(cookie: String?, url: String): ParseResponse<T>? =
+ parse(cookie, frostJsoup(cookie, url))
+
override fun parse(cookie: String?, document: Document): ParseResponse<T>? {
cookie ?: return null
if (redirectToText)
@@ -94,7 +105,10 @@ internal abstract class FrostParserBase<out T : Any>(private val redirectToText:
* Returns the formatted url, or an empty string if nothing was found
*/
protected fun Element.getInnerImgStyle() =
- FB_CSS_URL_MATCHER.find(select("i.img[style*=url]").attr("style"))[1]?.formattedFbUrl ?: ""
+ select("i.img[style*=url]").getStyleUrl()
+
+ protected fun Elements.getStyleUrl() =
+ FB_CSS_URL_MATCHER.find(attr("style"))[1]?.formattedFbUrl
protected open fun textToDoc(text: String) = if (!redirectToText)
Jsoup.parse(text)
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/parsers/MessageParser.kt b/app/src/main/kotlin/com/pitchedapps/frost/parsers/MessageParser.kt
index 9d4a2193..02c6f189 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/parsers/MessageParser.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/parsers/MessageParser.kt
@@ -16,7 +16,11 @@ import org.jsoup.nodes.Element
* We can parse out the content we want directly and load it ourselves
*
*/
-object MessageParser : FrostParser<FrostMessages> by MessageParserImpl()
+object MessageParser : FrostParser<FrostMessages> by MessageParserImpl() {
+
+ fun queryUser(cookie: String?, name: String) = parseFromUrl(cookie, "${FbItem.MESSAGES.url}/?q=$name")
+
+}
data class FrostMessages(val threads: List<FrostThread>,
val seeMore: FrostLink?,
@@ -35,7 +39,7 @@ data class FrostMessages(val threads: List<FrostThread>,
with(it) {
NotificationContent(
data = data,
- notifId = Math.abs(id.toInt()),
+ id = id,
href = url,
title = title,
text = content ?: "",
@@ -55,12 +59,13 @@ data class FrostMessages(val threads: List<FrostThread>,
* [content] optional string for thread
*/
data class FrostThread(val id: Long,
- val img: String,
+ val img: String?,
val title: String,
val time: Long,
val url: String,
val unread: Boolean,
- val content: String?)
+ val content: String?,
+ val contentImgUrl: String?)
private class MessageParserImpl : FrostParserBase<FrostMessages>(true) {
@@ -99,10 +104,11 @@ private class MessageParserImpl : FrostParserBase<FrostMessages>(true) {
val epoch = FB_EPOCH_MATCHER.find(abbr.attr("data-store"))[1]?.toLongOrNull() ?: -1L
//fetch id
val id = FB_MESSAGE_NOTIF_ID_MATCHER.find(element.id())[1]?.toLongOrNull()
- ?: System.currentTimeMillis()
- val content = element.select("span.snippet").firstOrNull()?.text()?.trim()
+ ?: System.currentTimeMillis() % FALLBACK_TIME_MOD
+ val snippet = element.select("span.snippet").firstOrNull()
+ val content = snippet?.text()?.trim()
+ val contentImg = snippet?.select("i[style*=url]")?.getStyleUrl()
val img = element.getInnerImgStyle()
- L.v("url", a.attr("href"))
return FrostThread(
id = id,
img = img,
@@ -110,7 +116,8 @@ private class MessageParserImpl : FrostParserBase<FrostMessages>(true) {
time = epoch,
url = a.attr("href").formattedFbUrl,
unread = !element.hasClass("acw"),
- content = content
+ content = content,
+ contentImgUrl = contentImg
)
}
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/parsers/NotifParser.kt b/app/src/main/kotlin/com/pitchedapps/frost/parsers/NotifParser.kt
index f743a43a..451eb774 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/parsers/NotifParser.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/parsers/NotifParser.kt
@@ -3,7 +3,6 @@ package com.pitchedapps.frost.parsers
import com.pitchedapps.frost.dbflow.CookieModel
import com.pitchedapps.frost.facebook.*
import com.pitchedapps.frost.services.NotificationContent
-import com.pitchedapps.frost.utils.L
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
@@ -29,10 +28,10 @@ data class FrostNotifs(
with(it) {
NotificationContent(
data = data,
- notifId = Math.abs(id.toInt()),
+ id = id,
href = url,
title = null,
- text = content ?: "",
+ text = content,
timestamp = time,
profileUrl = img
)
@@ -47,13 +46,17 @@ data class FrostNotifs(
* [url] link to thread
* [unread] true if image is unread, false otherwise
* [content] optional string for thread
+ * [timeString] text version of time from Facebook
+ * [thumbnailUrl] optional thumbnail url if existent
*/
data class FrostNotif(val id: Long,
- val img: String,
+ val img: String?,
val time: Long,
val url: String,
val unread: Boolean,
- val content: String?)
+ val content: String,
+ val timeString: String,
+ val thumbnailUrl: String?)
private class NotifParserImpl : FrostParserBase<FrostNotifs>(false) {
@@ -62,7 +65,7 @@ private class NotifParserImpl : FrostParserBase<FrostNotifs>(false) {
override fun parseImpl(doc: Document): FrostNotifs? {
val notificationList = doc.getElementById("notifications_list") ?: return null
val notifications = notificationList.getElementsByAttributeValueContaining("id", "list_notif_")
- .mapNotNull { parseNotif(it) }
+ .mapNotNull(this::parseNotif)
val seeMore = parseLink(doc.getElementsByAttributeValue("href", "/notifications.php?more").first())
return FrostNotifs(notifications, seeMore)
}
@@ -73,18 +76,20 @@ private class NotifParserImpl : FrostParserBase<FrostNotifs>(false) {
val epoch = FB_EPOCH_MATCHER.find(abbr.attr("data-store"))[1]?.toLongOrNull() ?: -1L
//fetch id
val id = FB_NOTIF_ID_MATCHER.find(element.id())[1]?.toLongOrNull()
- ?: System.currentTimeMillis()
+ ?: System.currentTimeMillis() % FALLBACK_TIME_MOD
val img = element.getInnerImgStyle()
val timeString = abbr.text()
val content = a.text().replace("\u00a0", " ").removeSuffix(timeString).trim() //remove &nbsp;
- L.v("url", a.attr("href"))
+ val thumbnail = element.selectFirst("img.thumbnail")?.attr("src")
return FrostNotif(
id = id,
img = img,
time = epoch,
url = a.attr("href").formattedFbUrl,
unread = !element.hasClass("acw"),
- content = content
+ content = content,
+ timeString = timeString,
+ thumbnailUrl = if (thumbnail?.isNotEmpty() == true) thumbnail else null
)
}
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/parsers/SearchParser.kt b/app/src/main/kotlin/com/pitchedapps/frost/parsers/SearchParser.kt
index bc09d4db..557e80b8 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/parsers/SearchParser.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/parsers/SearchParser.kt
@@ -1,13 +1,10 @@
package com.pitchedapps.frost.parsers
import ca.allanwang.kau.searchview.SearchItem
-import com.pitchedapps.frost.dbflow.CookieModel
import com.pitchedapps.frost.facebook.FbItem
import com.pitchedapps.frost.facebook.formattedFbUrl
import com.pitchedapps.frost.parsers.FrostSearch.Companion.create
import com.pitchedapps.frost.utils.L
-import com.pitchedapps.frost.utils.frostJsoup
-import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
@@ -18,7 +15,7 @@ object SearchParser : FrostParser<FrostSearches> by SearchParserImpl() {
fun query(cookie: String?, input: String): ParseResponse<FrostSearches>? {
val url = "${FbItem._SEARCH.url}?q=${if (input.isNotBlank()) input else "a"}"
L.i(null, "Search Query $url")
- return parse(cookie, frostJsoup(url))
+ return parseFromUrl(cookie, url)
}
}
@@ -27,7 +24,7 @@ enum class SearchKeys(val key: String) {
EVENTS("keywords_events")
}
-data class FrostSearches(val results: List<FrostSearch>) {
+data class FrostSearches(val results: List<FrostSearch>) {
override fun toString() = StringBuilder().apply {
append("FrostSearches {\n")
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/services/FrostNotifications.kt b/app/src/main/kotlin/com/pitchedapps/frost/services/FrostNotifications.kt
index afa30a91..2d9e0803 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/services/FrostNotifications.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/services/FrostNotifications.kt
@@ -11,7 +11,9 @@ import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.net.Uri
+import android.os.BaseBundle
import android.os.Build
+import android.os.Bundle
import android.support.v4.app.NotificationCompat
import android.support.v4.app.NotificationManagerCompat
import ca.allanwang.kau.utils.color
@@ -28,11 +30,16 @@ import com.pitchedapps.frost.dbflow.NotificationModel
import com.pitchedapps.frost.dbflow.lastNotificationTime
import com.pitchedapps.frost.enums.OverlayContext
import com.pitchedapps.frost.facebook.FbItem
+import com.pitchedapps.frost.glide.FrostGlide
+import com.pitchedapps.frost.glide.transform
import com.pitchedapps.frost.parsers.FrostParser
import com.pitchedapps.frost.parsers.MessageParser
import com.pitchedapps.frost.parsers.NotifParser
import com.pitchedapps.frost.parsers.ParseNotification
-import com.pitchedapps.frost.utils.*
+import com.pitchedapps.frost.utils.ARG_USER_ID
+import com.pitchedapps.frost.utils.L
+import com.pitchedapps.frost.utils.Prefs
+import com.pitchedapps.frost.utils.frostAnswersCustom
import org.jetbrains.anko.runOnUiThread
import java.util.*
@@ -54,6 +61,7 @@ inline val Context.frostNotification: NotificationCompat.Builder
get() = NotificationCompat.Builder(this, BuildConfig.APPLICATION_ID).apply {
setSmallIcon(R.drawable.frost_f_24)
setAutoCancel(true)
+ setStyle(NotificationCompat.BigTextStyle())
color = color(R.color.frost_notification_accent)
}
@@ -68,9 +76,6 @@ fun NotificationCompat.Builder.withDefaults(ringtone: String = Prefs.notificatio
setDefaults(defaults)
}
-inline val NotificationCompat.Builder.withBigText: NotificationCompat.BigTextStyle
- get() = NotificationCompat.BigTextStyle(this)
-
/**
* Created by Allan Wang on 2017-07-08.
*
@@ -85,7 +90,7 @@ class FrostNotificationTarget(val context: Context,
override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>) {
builder.setLargeIcon(resource)
- NotificationManagerCompat.from(context).notify(notifTag, notifId, builder.withBigText.build())
+ NotificationManagerCompat.from(context).notify(notifTag, notifId, builder.build())
}
}
@@ -99,12 +104,18 @@ enum class NotificationType(
private val getTime: (notif: NotificationModel) -> Long,
private val putTime: (notif: NotificationModel, time: Long) -> NotificationModel,
private val ringtone: () -> String) {
+
GENERAL(OverlayContext.NOTIFICATION,
FbItem.NOTIFICATIONS,
NotifParser,
NotificationModel::epoch,
{ notif, time -> notif.copy(epoch = time) },
- Prefs::notificationRingtone),
+ Prefs::notificationRingtone) {
+
+ override fun bindRequest(content: NotificationContent, cookie: String) =
+ FrostRunnable.prepareMarkNotificationRead(content.id, cookie)
+ },
+
MESSAGE(OverlayContext.MESSAGE,
FbItem.MESSAGES,
MessageParser,
@@ -115,6 +126,19 @@ enum class NotificationType(
private val groupPrefix = "frost_${name.toLowerCase(Locale.CANADA)}"
/**
+ * Optional binder to return the request bundle builder
+ */
+ internal open fun bindRequest(content: NotificationContent, cookie: String): (BaseBundle.() -> Unit)? = null
+
+ private fun bindRequest(intent: Intent, content: NotificationContent, cookie: String?) {
+ cookie ?: return
+ val binder = bindRequest(content, cookie) ?: return
+ val bundle = Bundle()
+ bundle.binder()
+ intent.putExtras(bundle)
+ }
+
+ /**
* Get unread data from designated parser
* Display notifications for those after old epoch
* Save new epoch
@@ -138,7 +162,7 @@ enum class NotificationType(
newLatestEpoch = notif.timestamp
notifCount++
}
- if (newLatestEpoch != prevLatestEpoch)
+ if (newLatestEpoch > prevLatestEpoch)
putTime(prevNotifTime, newLatestEpoch).save()
L.d("Notif $name new epoch ${getTime(lastNotificationTime(userId))}")
summaryNotification(context, userId, notifCount)
@@ -154,9 +178,11 @@ enum class NotificationType(
val intent = Intent(context, FrostWebActivity::class.java)
intent.data = Uri.parse(href)
intent.putExtra(ARG_USER_ID, data.id)
- intent.putExtra(ARG_OVERLAY_CONTEXT, overlayContext)
+ overlayContext.put(intent)
+ bindRequest(intent, content, data.cookie)
+
val group = "${groupPrefix}_${data.id}"
- val pendingIntent = PendingIntent.getActivity(context, 0, intent, 0)
+ val pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
val notifBuilder = context.frostNotification
.setContentTitle(title ?: context.string(R.string.frost_name))
.setContentText(text)
@@ -170,15 +196,15 @@ enum class NotificationType(
if (timestamp != -1L) notifBuilder.setWhen(timestamp * 1000)
L.v("Notif load", context.toString())
- NotificationManagerCompat.from(context).notify(group, notifId, notifBuilder.withBigText.build())
+ NotificationManagerCompat.from(context).notify(group, notifId, notifBuilder.build())
- if (profileUrl.isNotBlank()) {
+ if (profileUrl != null) {
context.runOnUiThread {
//todo verify if context is valid?
Glide.with(context)
.asBitmap()
.load(profileUrl)
- .withRoundIcon()
+ .transform(FrostGlide.circleCrop)
.into(FrostNotificationTarget(context, notifId, group, notifBuilder))
}
}
@@ -196,7 +222,7 @@ enum class NotificationType(
val intent = Intent(context, FrostWebActivity::class.java)
intent.data = Uri.parse(fbItem.url)
intent.putExtra(ARG_USER_ID, userId)
- val pendingIntent = PendingIntent.getActivity(context, 0, intent, 0)
+ val pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
val notifBuilder = context.frostNotification.withDefaults(ringtone())
.setContentTitle(context.string(R.string.frost_name))
.setContentText("$count ${context.string(fbItem.titleId)}")
@@ -213,12 +239,16 @@ enum class NotificationType(
* Notification data holder
*/
data class NotificationContent(val data: CookieModel,
- val notifId: Int,
+ val id: Long,
val href: String,
val title: String? = null, // defaults to frost title
val text: String,
val timestamp: Long,
- val profileUrl: String)
+ val profileUrl: String?) {
+
+ val notifId = Math.abs(id.toInt())
+
+}
const val NOTIFICATION_PERIODIC_JOB = 7
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/services/FrostRequestService.kt b/app/src/main/kotlin/com/pitchedapps/frost/services/FrostRequestService.kt
new file mode 100644
index 00000000..74a8b98d
--- /dev/null
+++ b/app/src/main/kotlin/com/pitchedapps/frost/services/FrostRequestService.kt
@@ -0,0 +1,182 @@
+package com.pitchedapps.frost.services
+
+import android.app.job.JobInfo
+import android.app.job.JobParameters
+import android.app.job.JobScheduler
+import android.app.job.JobService
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.os.BaseBundle
+import android.os.PersistableBundle
+import com.pitchedapps.frost.facebook.RequestAuth
+import com.pitchedapps.frost.facebook.fbRequest
+import com.pitchedapps.frost.facebook.markNotificationRead
+import com.pitchedapps.frost.utils.EnumBundle
+import com.pitchedapps.frost.utils.EnumBundleCompanion
+import com.pitchedapps.frost.utils.EnumCompanion
+import com.pitchedapps.frost.utils.L
+import org.jetbrains.anko.doAsync
+import java.util.concurrent.Future
+
+/**
+ * Created by Allan Wang on 28/12/17.
+ */
+
+/**
+ * Private helper data
+ */
+private enum class FrostRequestCommands : EnumBundle<FrostRequestCommands> {
+
+ NOTIF_READ {
+
+ override fun invoke(auth: RequestAuth, bundle: PersistableBundle) {
+ val id = bundle.getLong(ARG_0, -1L)
+ val success = auth.markNotificationRead(id).invoke()
+ L.d("Marked notif $id as read: $success")
+ }
+
+ override fun propagate(bundle: BaseBundle) =
+ FrostRunnable.prepareMarkNotificationRead(
+ bundle.getLong(ARG_0),
+ bundle.getCookie())
+
+ };
+
+ override val bundleContract: EnumBundleCompanion<FrostRequestCommands>
+ get() = Companion
+
+ /**
+ * Call request with arguments inside bundle
+ */
+ abstract fun invoke(auth: RequestAuth, bundle: PersistableBundle)
+
+ /**
+ * Return bundle builder given arguments in the old bundle
+ * Must not write to old bundle!
+ */
+ abstract fun propagate(bundle: BaseBundle): BaseBundle.() -> Unit
+
+ companion object : EnumCompanion<FrostRequestCommands>("frost_arg_commands", values())
+
+}
+
+private const val ARG_COMMAND = "frost_request_command"
+private const val ARG_COOKIE = "frost_request_cookie"
+private const val ARG_0 = "frost_request_arg_0"
+private const val ARG_1 = "frost_request_arg_1"
+private const val ARG_2 = "frost_request_arg_2"
+private const val ARG_3 = "frost_request_arg_3"
+private const val JOB_REQUEST_BASE = 928
+
+private fun BaseBundle.getCookie() = getString(ARG_COOKIE)
+private fun BaseBundle.putCookie(cookie: String) = putString(ARG_COOKIE, cookie)
+
+/**
+ * Singleton handler for running requests in [FrostRequestService]
+ * Requests are typically completely decoupled from the UI,
+ * and are optional enhancers.
+ *
+ * Nothing guarantees the completion time, or whether it even executes at all
+ *
+ * Design:
+ * prepare function - creates a bundle binder
+ * actor function - calls the service with the given arguments
+ *
+ * Global:
+ * propagator - given a bundle with a command, extracts and executes the requests
+ */
+object FrostRunnable {
+
+ fun prepareMarkNotificationRead(id: Long, cookie: String): BaseBundle.() -> Unit = {
+ FrostRequestCommands.NOTIF_READ.put(this)
+ putLong(ARG_0, id)
+ putCookie(cookie)
+ }
+
+ fun markNotificationRead(context: Context, id: Long, cookie: String): Boolean {
+ if (id <= 0) {
+ L.d("Invalid notification id $id for marking as read")
+ return false
+ }
+ return schedule(context, FrostRequestCommands.NOTIF_READ,
+ prepareMarkNotificationRead(id, cookie))
+ }
+
+ fun propagate(context: Context, intent: Intent?) {
+ intent?.extras ?: return
+ val command = FrostRequestCommands[intent] ?: return
+ intent.removeExtra(ARG_COMMAND) // reset
+ L.d("Propagating command ${command.name}")
+ val builder = command.propagate(intent.extras)
+ schedule(context, command, builder)
+ }
+
+ private fun schedule(context: Context,
+ command: FrostRequestCommands,
+ bundleBuilder: PersistableBundle.() -> Unit): Boolean {
+ val scheduler = context.getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler
+ val serviceComponent = ComponentName(context, FrostRequestService::class.java)
+ val bundle = PersistableBundle()
+ bundle.bundleBuilder()
+ bundle.putString(ARG_COMMAND, command.name)
+
+ if (bundle.getCookie().isNullOrBlank()) {
+ L.e("Scheduled frost request with empty cookie)")
+ return false
+ }
+
+ val builder = JobInfo.Builder(JOB_REQUEST_BASE + command.ordinal, serviceComponent)
+ .setMinimumLatency(0L)
+ .setExtras(bundle)
+ .setOverrideDeadline(2000L)
+ .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
+ val result = scheduler.schedule(builder.build())
+ if (result <= 0) {
+ L.eThrow("FrostRequestService scheduler failed for ${command.name}")
+ return false
+ }
+ L.d("Scheduled ${command.name}")
+ return true
+ }
+
+}
+
+class FrostRequestService : JobService() {
+
+ var future: Future<Unit>? = null
+
+ override fun onStopJob(params: JobParameters?): Boolean {
+ future?.cancel(true)
+ future = null
+ return false
+ }
+
+ override fun onStartJob(params: JobParameters?): Boolean {
+ val bundle = params?.extras
+ if (bundle == null) {
+ L.eThrow("Launched ${this::class.java.simpleName} without param data")
+ return false
+ }
+ val cookie = bundle.getCookie()
+ if (cookie.isNullOrBlank()) {
+ L.eThrow("Launched ${this::class.java.simpleName} without cookie")
+ return false
+ }
+ val command = FrostRequestCommands[bundle]
+ if (command == null) {
+ L.eThrow("Launched ${this::class.java.simpleName} without command")
+ return false
+ }
+ val now = System.currentTimeMillis()
+ future = doAsync {
+ cookie.fbRequest {
+ L.d("Requesting frost service for ${command.name}")
+ command.invoke(this, bundle)
+ }
+ L.d("Finished frost service for ${command.name} in ${System.currentTimeMillis() - now} ms")
+ jobFinished(params, false)
+ }
+ return true
+ }
+} \ No newline at end of file
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/services/NotificationService.kt b/app/src/main/kotlin/com/pitchedapps/frost/services/NotificationService.kt
index adeefec6..c1a4ace1 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/services/NotificationService.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/services/NotificationService.kt
@@ -77,7 +77,7 @@ class NotificationService : JobService() {
return null
}
- private fun Context.debugNotification(text: String) {
+ private fun Context.debugNotification(text: String = string(R.string.kau_lorem_ipsum)) {
if (!BuildConfig.DEBUG) return
val notifBuilder = frostNotification.withDefaults()
.setContentTitle(string(R.string.frost_name))
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/utils/EnumUtils.kt b/app/src/main/kotlin/com/pitchedapps/frost/utils/EnumUtils.kt
new file mode 100644
index 00000000..d20d1573
--- /dev/null
+++ b/app/src/main/kotlin/com/pitchedapps/frost/utils/EnumUtils.kt
@@ -0,0 +1,57 @@
+package com.pitchedapps.frost.utils
+
+import android.content.Intent
+import android.os.BaseBundle
+
+/**
+ * Created by Allan Wang on 29/12/17.
+ *
+ * Helper to set enum using its name rather than the serialized version
+ * Name is used in case the enum is involved in persistent data, where updates may shift indices
+ */
+interface EnumBundle<E : Enum<E>> {
+
+ val bundleContract: EnumBundleCompanion<E>
+
+ val name: String
+
+ val ordinal: Int
+
+ fun put(intent: Intent) {
+ intent.putExtra(bundleContract.argTag, name)
+ }
+
+ fun put(bundle: BaseBundle?) {
+ bundle?.putString(bundleContract.argTag, name)
+ }
+}
+
+interface EnumBundleCompanion<E : Enum<E>> {
+
+ val argTag: String
+
+ val values: Array<E>
+
+ val valueMap: Map<String, E>
+
+ operator fun get(name: String?) = if (name == null) null else valueMap[name]
+
+ operator fun get(bundle: BaseBundle?) = get(bundle?.getString(argTag))
+
+ operator fun get(intent: Intent?) = get(intent?.getStringExtra(argTag))
+
+}
+
+open class EnumCompanion<E : Enum<E>>(
+ override final val argTag: String,
+ override final val values: Array<E>) : EnumBundleCompanion<E> {
+
+ override final val valueMap: Map<String, E> = values.map { it.name to it }.toMap()
+
+ override final fun get(name: String?) = super.get(name)
+
+ override final fun get(bundle: BaseBundle?) = super.get(bundle)
+
+ override final fun get(intent: Intent?) = super.get(intent)
+
+} \ No newline at end of file
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/utils/Prefs.kt b/app/src/main/kotlin/com/pitchedapps/frost/utils/Prefs.kt
index cc5ee733..f14039b7 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/utils/Prefs.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/utils/Prefs.kt
@@ -102,7 +102,7 @@ object Prefs : KPref() {
var animate: Boolean by kpref("fancy_animations", true)
- var notificationKeywords: StringSet by kpref("notification_keywords", mutableSetOf<String>())
+ var notificationKeywords: StringSet by kpref("notification_keywords", mutableSetOf())
var notificationAllAccounts: Boolean by kpref("notification_all_accounts", true)
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/utils/Utils.kt b/app/src/main/kotlin/com/pitchedapps/frost/utils/Utils.kt
index 0ca068b5..592dd4fc 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/utils/Utils.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/utils/Utils.kt
@@ -2,6 +2,7 @@ package com.pitchedapps.frost.utils
import android.annotation.SuppressLint
import android.app.Activity
+import android.app.ActivityOptions
import android.content.Context
import android.content.Intent
import android.graphics.Color
@@ -10,7 +11,6 @@ import android.net.Uri
import android.support.annotation.StringRes
import android.support.design.internal.SnackbarContentLayout
import android.support.design.widget.Snackbar
-import android.support.v4.app.ActivityOptionsCompat
import android.support.v7.widget.Toolbar
import android.view.View
import android.widget.FrameLayout
@@ -22,9 +22,6 @@ import ca.allanwang.kau.mediapicker.createPrivateMediaFile
import ca.allanwang.kau.utils.*
import ca.allanwang.kau.xml.showChangelog
import com.afollestad.materialdialogs.MaterialDialog
-import com.bumptech.glide.RequestBuilder
-import com.bumptech.glide.load.resource.bitmap.CircleCrop
-import com.bumptech.glide.request.RequestOptions
import com.crashlytics.android.answers.Answers
import com.crashlytics.android.answers.CustomEvent
import com.pitchedapps.frost.BuildConfig
@@ -47,7 +44,6 @@ const val ARG_URL = "arg_url"
const val ARG_USER_ID = "arg_user_id"
const val ARG_IMAGE_URL = "arg_image_url"
const val ARG_TEXT = "arg_text"
-const val ARG_OVERLAY_CONTEXT = "arg_overlay_context"
fun Context.launchNewTask(clazz: Class<out Activity>, cookieList: ArrayList<CookieModel> = arrayListOf(), clearStack: Boolean = false) {
startActivity(clazz, clearStack, intentBuilder = {
@@ -61,7 +57,7 @@ fun Context.launchLogin(cookieList: ArrayList<CookieModel>, clearStack: Boolean
}
fun Activity.cookies(): ArrayList<CookieModel> {
- return intent?.extras?.getParcelableArrayList<CookieModel>(EXTRA_COOKIES) ?: arrayListOf()
+ return intent?.getParcelableArrayListExtra<CookieModel>(EXTRA_COOKIES) ?: arrayListOf()
}
/**
@@ -81,7 +77,7 @@ fun Context.launchWebOverlay(url: String, clazz: Class<out WebOverlayActivityBas
})
}
-private fun Context.fadeBundle() = ActivityOptionsCompat.makeCustomAnimation(this,
+private fun Context.fadeBundle() = ActivityOptions.makeCustomAnimation(this,
android.R.anim.fade_in, android.R.anim.fade_out).toBundle()
fun Context.launchImageActivity(imageUrl: String, text: String?) {
@@ -103,7 +99,7 @@ fun Activity.launchIntroActivity(cookieList: ArrayList<CookieModel>)
= launchNewTask(IntroActivity::class.java, cookieList, true)
fun WebOverlayActivity.url(): String {
- return intent.extras?.getString(ARG_URL) ?: FbItem.FEED.url
+ return intent.getStringExtra(ARG_URL) ?: FbItem.FEED.url
}
fun Context.materialDialogThemed(action: MaterialDialog.Builder.() -> Unit): MaterialDialog {
@@ -192,8 +188,6 @@ fun Activity.frostNavigationBar() {
navigationBarColor = if (Prefs.tintNavBar) Prefs.headerColor else Color.BLACK
}
-fun <T> RequestBuilder<T>.withRoundIcon() = apply(RequestOptions().transform(CircleCrop()))!!
-
@Throws(IOException::class)
fun createMediaFile(extension: String) = createMediaFile("Frost", extension)
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/views/AccountItem.kt b/app/src/main/kotlin/com/pitchedapps/frost/views/AccountItem.kt
index 2ab1d572..64cf34a1 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/views/AccountItem.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/views/AccountItem.kt
@@ -16,8 +16,9 @@ import com.mikepenz.google_material_typeface_library.GoogleMaterial
import com.pitchedapps.frost.R
import com.pitchedapps.frost.dbflow.CookieModel
import com.pitchedapps.frost.facebook.PROFILE_PICTURE_URL
+import com.pitchedapps.frost.glide.FrostGlide
+import com.pitchedapps.frost.glide.transform
import com.pitchedapps.frost.utils.Prefs
-import com.pitchedapps.frost.utils.withRoundIcon
/**
* Created by Allan Wang on 2017-06-05.
@@ -32,7 +33,8 @@ class AccountItem(val cookie: CookieModel?) : KauIItem<AccountItem, AccountItem.
text.setTextColor(Prefs.textColor)
if (cookie != null) {
text.text = cookie.name
- Glide.with(itemView).load(PROFILE_PICTURE_URL(cookie.id)).withRoundIcon().listener(object : RequestListener<Drawable> {
+ Glide.with(itemView).load(PROFILE_PICTURE_URL(cookie.id))
+ .transform(FrostGlide.roundCorner).listener(object : RequestListener<Drawable> {
override fun onResourceReady(resource: Drawable?, model: Any?, target: Target<Drawable>?, dataSource: DataSource?, isFirstResource: Boolean): Boolean {
text.fadeIn()
return false
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/views/FrostRecyclerView.kt b/app/src/main/kotlin/com/pitchedapps/frost/views/FrostRecyclerView.kt
index 436f8b00..38b09657 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/views/FrostRecyclerView.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/views/FrostRecyclerView.kt
@@ -1,15 +1,17 @@
package com.pitchedapps.frost.views
import android.content.Context
+import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView
import android.util.AttributeSet
import android.view.View
+import ca.allanwang.kau.utils.circularReveal
+import ca.allanwang.kau.utils.fadeOut
import com.pitchedapps.frost.contracts.FrostContentContainer
import com.pitchedapps.frost.contracts.FrostContentCore
import com.pitchedapps.frost.contracts.FrostContentParent
import com.pitchedapps.frost.fragments.RecyclerContentContract
-import com.pitchedapps.frost.utils.L
-import java.lang.ref.WeakReference
+import com.pitchedapps.frost.utils.Prefs
/**
* Created by Allan Wang on 2017-05-29.
@@ -27,12 +29,16 @@ class FrostRecyclerView @JvmOverloads constructor(
override val currentUrl: String
get() = parent.baseUrl
- lateinit var recyclerContract: WeakReference<RecyclerContentContract>
+ lateinit var recyclerContract: RecyclerContentContract
+
+ init {
+ layoutManager = LinearLayoutManager(context)
+ }
override fun bind(container: FrostContentContainer): View {
if (container !is RecyclerContentContract)
throw IllegalStateException("FrostRecyclerView must bind to a container that is a RecyclerContentContract")
- this.recyclerContract = WeakReference(container)
+ this.recyclerContract = container
container.bind(this)
return this
}
@@ -41,15 +47,15 @@ class FrostRecyclerView @JvmOverloads constructor(
isNestedScrollingEnabled = true
}
+ var onReloadClear: () -> Unit = {}
+
override fun reloadBase(animate: Boolean) {
- val contract = recyclerContract.get()
- if (contract == null) {
- L.eThrow("Attempted to reload with invalid contract")
- return
- }
- contract.reload({ parent.progressObservable.onNext(it) }) {
+ if (Prefs.animate) fadeOut(onFinish = onReloadClear)
+ parent.refreshObservable.onNext(true)
+ recyclerContract.reload({ parent.progressObservable.onNext(it) }) {
parent.progressObservable.onNext(100)
parent.refreshObservable.onNext(false)
+ if (Prefs.animate) post { circularReveal() }
}
}
@@ -64,11 +70,12 @@ class FrostRecyclerView @JvmOverloads constructor(
override fun onBackPressed() = false
/**
- * If webview is already at the top, refresh
+ * If recycler is already at the top, refresh
* Otherwise scroll to top
*/
override fun onTabClicked() {
- if (scrollY < 5) reloadBase(true)
+ val firstPosition = (layoutManager as LinearLayoutManager).findFirstCompletelyVisibleItemPosition()
+ if (firstPosition == 0) reloadBase(true)
else scrollToTop()
}
@@ -77,12 +84,8 @@ class FrostRecyclerView @JvmOverloads constructor(
smoothScrollToPosition(0)
}
+ // nothing running in background; no need to listen
override var active: Boolean = true
- set(value) {
- if (field == value) return
- field = value
- // todo
- }
override fun reloadTheme() {
reloadThemeSelf()
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/views/FrostWebView.kt b/app/src/main/kotlin/com/pitchedapps/frost/views/FrostWebView.kt
index 0d04fcd9..c35f1bb8 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/views/FrostWebView.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/views/FrostWebView.kt
@@ -6,6 +6,7 @@ import android.content.Context
import android.graphics.Color
import android.util.AttributeSet
import android.view.View
+import android.view.ViewGroup
import ca.allanwang.kau.utils.AnimHolder
import com.pitchedapps.frost.contracts.FrostContentContainer
import com.pitchedapps.frost.contracts.FrostContentCore
@@ -147,4 +148,11 @@ class FrostWebView @JvmOverloads constructor(
settings.textZoom = Prefs.webTextScaling
}
+ override fun destroy() {
+ val parent = getParent() as? ViewGroup
+ if (parent != null) {
+ parent.removeView(this)
+ super.destroy()
+ }
+ }
} \ No newline at end of file
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/web/LoginWebView.kt b/app/src/main/kotlin/com/pitchedapps/frost/web/LoginWebView.kt
index 9251e607..9855040d 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/web/LoginWebView.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/web/LoginWebView.kt
@@ -2,7 +2,6 @@ package com.pitchedapps.frost.web
import android.annotation.SuppressLint
import android.content.Context
-import android.graphics.Bitmap
import android.graphics.Color
import android.util.AttributeSet
import android.view.View
diff --git a/app/src/main/res/layout/iitem_notification.xml b/app/src/main/res/layout/iitem_notification.xml
new file mode 100644
index 00000000..266b9dc4
--- /dev/null
+++ b/app/src/main/res/layout/iitem_notification.xml
@@ -0,0 +1,60 @@
+<?xml version="1.0" encoding="utf-8"?>
+<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/item_frame"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:clickable="true"
+ android:focusable="true"
+ android:paddingBottom="@dimen/kau_activity_vertical_margin"
+ android:paddingStart="@dimen/kau_activity_horizontal_margin"
+ android:paddingTop="@dimen/kau_activity_vertical_margin">
+
+ <ImageView
+ android:id="@+id/item_avatar"
+ android:layout_width="@dimen/avatar_image_size"
+ android:layout_height="@dimen/avatar_image_size"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent" />
+
+
+ <ImageView
+ android:id="@+id/item_thumbnail"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginEnd="@dimen/kau_padding_normal"
+ android:layout_marginStart="@dimen/kau_padding_normal"
+ android:scaleType="fitCenter"
+ android:visibility="gone"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintWidth_default="percent"
+ app:layout_constraintWidth_max="120dp"
+ app:layout_constraintWidth_percent="0.3" />
+
+ <TextView
+ android:id="@+id/item_content"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginEnd="@dimen/kau_padding_normal"
+ android:layout_marginStart="@dimen/kau_padding_normal"
+ app:layout_constraintEnd_toStartOf="@id/item_thumbnail"
+ app:layout_constraintStart_toEndOf="@id/item_avatar"
+ app:layout_constraintTop_toTopOf="parent" />
+
+ <TextView
+ android:id="@+id/item_date"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginEnd="@dimen/kau_padding_normal"
+ android:layout_marginStart="@dimen/kau_padding_normal"
+ android:textSize="12sp"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toStartOf="@id/item_thumbnail"
+ app:layout_constraintStart_toEndOf="@id/item_avatar"
+ app:layout_constraintTop_toBottomOf="@id/item_content"
+ app:layout_constraintVertical_bias="1.0" />
+
+
+</android.support.constraint.ConstraintLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/view_content_base_recycler.xml b/app/src/main/res/layout/view_content_base_recycler.xml
index efe69913..e1591efe 100644
--- a/app/src/main/res/layout/view_content_base_recycler.xml
+++ b/app/src/main/res/layout/view_content_base_recycler.xml
@@ -17,7 +17,6 @@
android:layout_height="match_parent"
android:focusable="true"
android:focusableInTouchMode="true"
- app:layoutManager="android.support.v7.widget.LinearLayoutManager"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
</android.support.v4.widget.SwipeRefreshLayout>
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
index 3d03888e..713bd1b4 100644
--- a/app/src/main/res/values/dimens.xml
+++ b/app/src/main/res/values/dimens.xml
@@ -2,6 +2,7 @@
<dimen name="splash_logo">16dp</dimen>
<dimen name="progress_bar_height">1dip</dimen>
<dimen name="account_image_size">100dp</dimen>
+ <dimen name="avatar_image_size">48dp</dimen>
<dimen name="context_menu_height">60dp</dimen>
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 540ee0da..e39e91ed 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -25,6 +25,7 @@
<string name="select_facebook_account">Select Facebook Account</string>
<string name="account_not_found">Current account is not in the database</string>
+ <string name="frost_requests">Frost Requests</string>
<string name="frost_notifications">Frost Notifications</string>
<string name="requires_custom_theme">Requires custom theme</string>