From d90cb9b61cd2e033b46f4780ad1340c5f35b7751 Mon Sep 17 00:00:00 2001 From: Allan Wang Date: Sun, 16 Jul 2017 17:26:58 -0700 Subject: Add image viewing and downloading (#63) * Commence aggressive image caching * Add glide toggle and css url parsing * Add image hook and refractor activities * Update version analytics * Implemented imageactivity but glide will not load * Create working image loader * Finalize image view * Finalize image view logic * Remove custom cache experiment --- .../kotlin/com/pitchedapps/frost/AboutActivity.kt | 143 ------- .../kotlin/com/pitchedapps/frost/BaseActivity.kt | 31 -- .../main/kotlin/com/pitchedapps/frost/FrostApp.kt | 1 + .../com/pitchedapps/frost/FrostWebActivity.kt | 20 - .../kotlin/com/pitchedapps/frost/LoginActivity.kt | 125 ------ .../kotlin/com/pitchedapps/frost/MainActivity.kt | 462 -------------------- .../com/pitchedapps/frost/SelectorActivity.kt | 46 -- .../com/pitchedapps/frost/SettingsActivity.kt | 143 ------- .../kotlin/com/pitchedapps/frost/StartActivity.kt | 3 + .../com/pitchedapps/frost/WebOverlayActivity.kt | 157 ------- .../pitchedapps/frost/activities/AboutActivity.kt | 145 +++++++ .../pitchedapps/frost/activities/BaseActivity.kt | 32 ++ .../frost/activities/FrostWebActivity.kt | 20 + .../pitchedapps/frost/activities/ImageActivity.kt | 297 +++++++++++++ .../pitchedapps/frost/activities/LoginActivity.kt | 126 ++++++ .../pitchedapps/frost/activities/MainActivity.kt | 472 +++++++++++++++++++++ .../frost/activities/SelectorActivity.kt | 47 ++ .../frost/activities/SettingsActivity.kt | 145 +++++++ .../frost/activities/WebOverlayActivity.kt | 158 +++++++ .../com/pitchedapps/frost/facebook/FbConst.kt | 2 +- .../pitchedapps/frost/facebook/FbUrlFormatter.kt | 16 +- .../com/pitchedapps/frost/fragments/WebFragment.kt | 2 +- .../frost/services/FrostNotifications.kt | 2 +- .../com/pitchedapps/frost/settings/Appearance.kt | 4 +- .../com/pitchedapps/frost/settings/Behaviour.kt | 4 +- .../com/pitchedapps/frost/settings/Experimental.kt | 8 +- .../kotlin/com/pitchedapps/frost/settings/Feed.kt | 4 +- .../pitchedapps/frost/settings/Notifications.kt | 2 +- .../kotlin/com/pitchedapps/frost/utils/Prefs.kt | 16 + .../kotlin/com/pitchedapps/frost/utils/Utils.kt | 21 +- .../com/pitchedapps/frost/utils/WebContextMenu.kt | 12 +- .../kotlin/com/pitchedapps/frost/utils/iab/IAB.kt | 2 +- .../com/pitchedapps/frost/utils/iab/IABDialogs.kt | 4 +- .../com/pitchedapps/frost/web/BaseWebViewClient.kt | 16 + .../com/pitchedapps/frost/web/FrostChromeClient.kt | 16 +- .../kotlin/com/pitchedapps/frost/web/FrostJSI.kt | 31 +- .../frost/web/FrostRequestInterceptor.kt | 67 +++ .../com/pitchedapps/frost/web/FrostWebView.kt | 2 +- .../pitchedapps/frost/web/FrostWebViewClient.kt | 21 +- .../com/pitchedapps/frost/web/FrostWebViewCore.kt | 13 +- .../pitchedapps/frost/web/FrostWebViewSearch.kt | 18 +- .../com/pitchedapps/frost/web/LoginWebView.kt | 4 +- 42 files changed, 1668 insertions(+), 1192 deletions(-) delete mode 100644 app/src/main/kotlin/com/pitchedapps/frost/AboutActivity.kt delete mode 100644 app/src/main/kotlin/com/pitchedapps/frost/BaseActivity.kt delete mode 100644 app/src/main/kotlin/com/pitchedapps/frost/FrostWebActivity.kt delete mode 100644 app/src/main/kotlin/com/pitchedapps/frost/LoginActivity.kt delete mode 100644 app/src/main/kotlin/com/pitchedapps/frost/MainActivity.kt delete mode 100644 app/src/main/kotlin/com/pitchedapps/frost/SelectorActivity.kt delete mode 100644 app/src/main/kotlin/com/pitchedapps/frost/SettingsActivity.kt delete mode 100644 app/src/main/kotlin/com/pitchedapps/frost/WebOverlayActivity.kt create mode 100644 app/src/main/kotlin/com/pitchedapps/frost/activities/AboutActivity.kt create mode 100644 app/src/main/kotlin/com/pitchedapps/frost/activities/BaseActivity.kt create mode 100644 app/src/main/kotlin/com/pitchedapps/frost/activities/FrostWebActivity.kt create mode 100644 app/src/main/kotlin/com/pitchedapps/frost/activities/ImageActivity.kt create mode 100644 app/src/main/kotlin/com/pitchedapps/frost/activities/LoginActivity.kt create mode 100644 app/src/main/kotlin/com/pitchedapps/frost/activities/MainActivity.kt create mode 100644 app/src/main/kotlin/com/pitchedapps/frost/activities/SelectorActivity.kt create mode 100644 app/src/main/kotlin/com/pitchedapps/frost/activities/SettingsActivity.kt create mode 100644 app/src/main/kotlin/com/pitchedapps/frost/activities/WebOverlayActivity.kt create mode 100644 app/src/main/kotlin/com/pitchedapps/frost/web/BaseWebViewClient.kt create mode 100644 app/src/main/kotlin/com/pitchedapps/frost/web/FrostRequestInterceptor.kt (limited to 'app/src/main/kotlin/com/pitchedapps') diff --git a/app/src/main/kotlin/com/pitchedapps/frost/AboutActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/AboutActivity.kt deleted file mode 100644 index 6cab2a59..00000000 --- a/app/src/main/kotlin/com/pitchedapps/frost/AboutActivity.kt +++ /dev/null @@ -1,143 +0,0 @@ -package com.pitchedapps.frost - -import android.support.constraint.ConstraintLayout -import android.support.constraint.ConstraintSet -import android.support.v7.widget.RecyclerView -import android.view.View -import android.view.ViewGroup -import android.widget.ImageView -import ca.allanwang.kau.about.AboutActivityBase -import ca.allanwang.kau.about.LibraryIItem -import ca.allanwang.kau.adapters.FastItemThemedAdapter -import ca.allanwang.kau.adapters.ThemableIItem -import ca.allanwang.kau.adapters.ThemableIItemDelegate -import ca.allanwang.kau.utils.* -import com.mikepenz.aboutlibraries.Libs -import com.mikepenz.aboutlibraries.entity.Library -import com.mikepenz.aboutlibraries.entity.License -import com.mikepenz.community_material_typeface_library.CommunityMaterial -import com.mikepenz.fastadapter.IItem -import com.mikepenz.fastadapter.items.AbstractItem -import com.mikepenz.google_material_typeface_library.GoogleMaterial -import com.mikepenz.iconics.typeface.IIcon -import com.pitchedapps.frost.utils.Prefs - - -/** - * Created by Allan Wang on 2017-06-26. - */ -class AboutActivity : AboutActivityBase(null, { - textColor = Prefs.textColor - accentColor = Prefs.accentColor - backgroundColor = Prefs.bgColor.withMinAlpha(200) - cutoutForeground = if (0xff3b5998.toInt().isColorVisibleOn(Prefs.bgColor)) 0xff3b5998.toInt() else Prefs.accentColor - cutoutDrawableRes = R.drawable.frost_f_256 -}) { - - override fun getLibraries(libs: Libs): List { - val include = arrayOf( - "AboutLibraries", - "AndroidIconics", - "dbflow", - "fastadapter", - "glide", - "Jsoup", - "kau", - "kotterknife", - "materialdialogs", - "materialdrawer" - ) - - /* - * These are great libraries, but either aren't used directly or are too common to be listed - * Give more emphasis on the unique libs! - */ - val exclude = arrayOf( - "GoogleMaterialDesignIcons", - "intellijannotations", - "MaterialDesignIconicIcons", - "MaterialDesignIcons", - "materialize", - "appcompat_v7", - "design", - "recyclerview_v7", - "support_v4" - ) - val l = libs.prepareLibraries(this, include, null, false, true) -// l.forEach { KL.d("Lib ${it.definedName}") } - return l - } - - override fun postInflateMainPage(adapter: FastItemThemedAdapter>) { - /** - * Frost may not be a library but we're conveying the same info - */ - val frost = Library().apply { - libraryName = string(R.string.app_name) - author = "Pitched Apps" - libraryWebsite = "https://github.com/AllanWang/Frost-for-Facebook" - isOpenSource = true - libraryDescription = string(R.string.frost_description) - libraryVersion = BuildConfig.VERSION_NAME - license = License().apply { - licenseName = "GNU GPL v3" - licenseWebsite = "https://www.gnu.org/licenses/gpl-3.0.en.html" - } - } - adapter.add(LibraryIItem(frost)).add(AboutLinks()) - - } - - class AboutLinks : AbstractItem(), ThemableIItem by ThemableIItemDelegate() { - override fun getViewHolder(v: View): ViewHolder = ViewHolder(v) - - override fun getType(): Int = R.id.item_about_links - - override fun getLayoutRes(): Int = R.layout.item_about_links - - override fun bindView(holder: ViewHolder, payloads: MutableList?) { - super.bindView(holder, payloads) - with(holder) { - bindIconColor(*images.toTypedArray()) - bindBackgroundColor(container) - } - } - - class ViewHolder(v: View) : RecyclerView.ViewHolder(v) { - - val container: ConstraintLayout by bindView(R.id.about_icons_container) - val images: List - - /** - * There are a lot of constraints to be added to each item just to have them chained properly - * My as well do it programmatically - * Initializing the viewholder will setup the icons, scale type and background of all icons, - * link their click listeners and chain them together via a horizontal spread - */ - init { - val c = itemView.context - val size = c.dimenPixelSize(R.dimen.kau_avatar_bounds) - images = arrayOf Unit>>( - GoogleMaterial.Icon.gmd_star to { c.startPlayStoreLink(R.string.play_store_package_id) }, - CommunityMaterial.Icon.cmd_reddit to { c.startLink("https://www.reddit.com/r/FrostForFacebook/") }, - CommunityMaterial.Icon.cmd_github_circle to { c.startLink("https://github.com/AllanWang/Frost-for-Facebook") } - ).mapIndexed { i, (icon, onClick) -> - ImageView(c).apply { - layoutParams = ViewGroup.LayoutParams(size, size) - id = 109389 + i - setImageDrawable(icon.toDrawable(context, 32)) - scaleType = ImageView.ScaleType.CENTER - background = context.resolveDrawable(android.R.attr.selectableItemBackgroundBorderless) - setOnClickListener({ onClick() }) - container.addView(this) - } - } - val set = ConstraintSet() - set.clone(container) - set.createHorizontalChain(ConstraintSet.PARENT_ID, ConstraintSet.LEFT, ConstraintSet.PARENT_ID, ConstraintSet.RIGHT, - images.map { it.id }.toIntArray(), null, ConstraintSet.CHAIN_SPREAD_INSIDE) - set.applyTo(container) - } - } - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/pitchedapps/frost/BaseActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/BaseActivity.kt deleted file mode 100644 index c2551125..00000000 --- a/app/src/main/kotlin/com/pitchedapps/frost/BaseActivity.kt +++ /dev/null @@ -1,31 +0,0 @@ -package com.pitchedapps.frost - -import android.os.Bundle -import android.support.v7.app.AppCompatActivity -import com.pitchedapps.frost.utils.Prefs -import com.pitchedapps.frost.utils.materialDialogThemed -import com.pitchedapps.frost.utils.setFrostTheme - -/** - * Created by Allan Wang on 2017-06-12. - */ -open class BaseActivity : AppCompatActivity() { - override fun onBackPressed() { - if (isTaskRoot && Prefs.exitConfirmation) { - materialDialogThemed { - title(R.string.kau_exit) - content(R.string.kau_exit_confirmation) - positiveText(R.string.kau_yes) - negativeText(R.string.kau_no) - onPositive { _, _ -> super.onBackPressed() } - checkBoxPromptRes(R.string.kau_do_not_show_again, false, { _, b -> Prefs.exitConfirmation = !b }) - } - } else super.onBackPressed() - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setFrostTheme() - } - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/pitchedapps/frost/FrostApp.kt b/app/src/main/kotlin/com/pitchedapps/frost/FrostApp.kt index 4b62d244..3f5bdeda 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/FrostApp.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/FrostApp.kt @@ -10,6 +10,7 @@ import com.crashlytics.android.Crashlytics import com.crashlytics.android.answers.Answers import com.mikepenz.materialdrawer.util.AbstractDrawerImageLoader import com.mikepenz.materialdrawer.util.DrawerImageLoader +import com.pitchedapps.frost.BuildConfig import com.pitchedapps.frost.facebook.FbCookie import com.pitchedapps.frost.utils.CrashReportingTree import com.pitchedapps.frost.utils.GlideApp diff --git a/app/src/main/kotlin/com/pitchedapps/frost/FrostWebActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/FrostWebActivity.kt deleted file mode 100644 index 3e337813..00000000 --- a/app/src/main/kotlin/com/pitchedapps/frost/FrostWebActivity.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.pitchedapps.frost - -import android.os.Bundle -import com.pitchedapps.frost.utils.Prefs - - -/** - * Created by Allan Wang on 2017-06-19. - * - * Replica of [WebOverlayActivity] with a different base url - * Didn't use activity-alias because it causes issues when only one activity has the singleInstance mode - */ -class FrostWebActivity : WebOverlayActivity() { - - override fun onCreate(savedInstanceState: Bundle?) { - Prefs.prevId = Prefs.userId - super.onCreate(savedInstanceState) - } - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/pitchedapps/frost/LoginActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/LoginActivity.kt deleted file mode 100644 index a27a1ee2..00000000 --- a/app/src/main/kotlin/com/pitchedapps/frost/LoginActivity.kt +++ /dev/null @@ -1,125 +0,0 @@ -package com.pitchedapps.frost - -import android.graphics.drawable.Drawable -import android.os.Bundle -import android.os.Handler -import android.support.v4.widget.SwipeRefreshLayout -import android.support.v7.widget.AppCompatTextView -import android.support.v7.widget.Toolbar -import android.widget.ImageView -import ca.allanwang.kau.utils.bindView -import ca.allanwang.kau.utils.fadeIn -import ca.allanwang.kau.utils.fadeOut -import com.bumptech.glide.Glide -import com.bumptech.glide.load.DataSource -import com.bumptech.glide.load.engine.GlideException -import com.bumptech.glide.request.RequestListener -import com.bumptech.glide.request.target.Target -import com.crashlytics.android.answers.LoginEvent -import com.pitchedapps.frost.dbflow.CookieModel -import com.pitchedapps.frost.dbflow.fetchUsername -import com.pitchedapps.frost.dbflow.loadFbCookiesAsync -import com.pitchedapps.frost.facebook.PROFILE_PICTURE_URL -import com.pitchedapps.frost.utils.* -import com.pitchedapps.frost.web.LoginWebView -import io.reactivex.Observable -import io.reactivex.android.schedulers.AndroidSchedulers -import io.reactivex.functions.BiFunction -import io.reactivex.internal.operators.single.SingleToObservable -import io.reactivex.subjects.BehaviorSubject -import io.reactivex.subjects.SingleSubject - - -/** - * Created by Allan Wang on 2017-06-01. - */ -class LoginActivity : BaseActivity() { - - val toolbar: Toolbar by bindView(R.id.toolbar) - val web: LoginWebView by bindView(R.id.login_webview) - val swipeRefresh: SwipeRefreshLayout by bindView(R.id.swipe_refresh) - val textview: AppCompatTextView by bindView(R.id.textview) - val profile: ImageView by bindView(R.id.profile) - - val loginObservable = SingleSubject.create() - val progressObservable = BehaviorSubject.create()!! - val profileObservable = SingleSubject.create() - val usernameObservable = SingleSubject.create() - - // Helper to set and enable swipeRefresh - var refresh: Boolean - get() = swipeRefresh.isRefreshing - set(value) { - if (value) swipeRefresh.isEnabled = true - swipeRefresh.isRefreshing = value - if (!value) swipeRefresh.isEnabled = false - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_login) - setSupportActionBar(toolbar) - setTitle(R.string.kau_login) - setFrostColors(toolbar) - web.loginObservable = loginObservable - web.progressObservable = progressObservable - loginObservable.observeOn(AndroidSchedulers.mainThread()).subscribe { - cookie -> - web.fadeOut(onFinish = { - profile.fadeIn() - loadInfo(cookie) - }) - } - progressObservable.observeOn(AndroidSchedulers.mainThread()).subscribe { refresh = it != 100 } - web.loadLogin() - } - - fun loadInfo(cookie: CookieModel) { - refresh = true - Observable.zip(SingleToObservable(profileObservable), SingleToObservable(usernameObservable), - BiFunction> { foundImage, name -> Pair(foundImage, name) }) - .observeOn(AndroidSchedulers.mainThread()).subscribe { - (foundImage, name) -> - refresh = false - if (!foundImage) { - L.eThrow("Could not get profile photo; Invalid userId?") - L.i("-\t$cookie") - } - textview.text = String.format(getString(R.string.welcome), name) - textview.fadeIn() - frostAnswers { logLogin(LoginEvent().putMethod("frost_browser").putSuccess(true)) } - /* - * The user may have logged into an account that is already in the database - * We will let the db handle duplicates and load it now after the new account has been saved - */ - loadFbCookiesAsync { - cookies -> - Handler().postDelayed({ - launchNewTask(MainActivity::class.java, ArrayList(cookies), clearStack = true) - }, 1000) - } - } - loadProfile(cookie.id) - loadUsername(cookie) - } - - - fun loadProfile(id: Long) { - Glide.with(this@LoginActivity).load(PROFILE_PICTURE_URL(id)).withRoundIcon().listener(object : RequestListener { - override fun onResourceReady(resource: Drawable?, model: Any?, target: Target?, dataSource: DataSource?, isFirstResource: Boolean): Boolean { - profileObservable.onSuccess(true) - return false - } - - override fun onLoadFailed(e: GlideException?, model: Any?, target: Target?, isFirstResource: Boolean): Boolean { - if (e != null) L.e(e, "Profile loading exception") - profileObservable.onSuccess(false) - return false - } - }).into(profile) - } - - fun loadUsername(cookie: CookieModel) { - cookie.fetchUsername { usernameObservable.onSuccess(it) } - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/pitchedapps/frost/MainActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/MainActivity.kt deleted file mode 100644 index f9a597db..00000000 --- a/app/src/main/kotlin/com/pitchedapps/frost/MainActivity.kt +++ /dev/null @@ -1,462 +0,0 @@ -package com.pitchedapps.frost - -import android.app.AlarmManager -import android.app.PendingIntent -import android.content.Context -import android.content.Intent -import android.graphics.drawable.ColorDrawable -import android.net.Uri -import android.os.Build -import android.os.Bundle -import android.support.annotation.StringRes -import android.support.design.widget.* -import android.support.v4.app.ActivityOptionsCompat -import android.support.v4.app.Fragment -import android.support.v4.app.FragmentManager -import android.support.v4.app.FragmentPagerAdapter -import android.support.v4.view.ViewPager -import android.support.v7.widget.Toolbar -import android.view.Menu -import android.view.MenuItem -import android.webkit.ValueCallback -import android.webkit.WebChromeClient -import ca.allanwang.kau.changelog.showChangelog -import ca.allanwang.kau.permissions.kauOnRequestPermissionsResult -import ca.allanwang.kau.searchview.SearchItem -import ca.allanwang.kau.searchview.SearchView -import ca.allanwang.kau.searchview.bindSearchView -import ca.allanwang.kau.utils.* -import co.zsmb.materialdrawerkt.builders.Builder -import co.zsmb.materialdrawerkt.builders.accountHeader -import co.zsmb.materialdrawerkt.builders.drawer -import co.zsmb.materialdrawerkt.draweritems.badgeable.primaryItem -import co.zsmb.materialdrawerkt.draweritems.badgeable.secondaryItem -import co.zsmb.materialdrawerkt.draweritems.divider -import co.zsmb.materialdrawerkt.draweritems.profile.profile -import co.zsmb.materialdrawerkt.draweritems.profile.profileSetting -import com.crashlytics.android.answers.ContentViewEvent -import com.mikepenz.google_material_typeface_library.GoogleMaterial -import com.mikepenz.iconics.IconicsDrawable -import com.mikepenz.materialdrawer.AccountHeader -import com.mikepenz.materialdrawer.Drawer -import com.pitchedapps.frost.contracts.ActivityWebContract -import com.pitchedapps.frost.contracts.FileChooserContract -import com.pitchedapps.frost.contracts.FileChooserDelegate -import com.pitchedapps.frost.dbflow.loadFbCookie -import com.pitchedapps.frost.dbflow.loadFbTabs -import com.pitchedapps.frost.facebook.FbCookie -import com.pitchedapps.frost.facebook.FbCookie.switchUser -import com.pitchedapps.frost.facebook.FbTab -import com.pitchedapps.frost.facebook.PROFILE_PICTURE_URL -import com.pitchedapps.frost.fragments.WebFragment -import com.pitchedapps.frost.utils.* -import com.pitchedapps.frost.utils.iab.validatePro -import com.pitchedapps.frost.views.BadgedIcon -import com.pitchedapps.frost.views.FrostViewPager -import com.pitchedapps.frost.web.FrostWebViewSearch -import io.reactivex.android.schedulers.AndroidSchedulers -import io.reactivex.disposables.Disposable -import io.reactivex.schedulers.Schedulers -import io.reactivex.subjects.PublishSubject -import org.jsoup.Jsoup -import java.util.concurrent.TimeUnit - -class MainActivity : BaseActivity(), FrostWebViewSearch.SearchContract, - ActivityWebContract, FileChooserContract by FileChooserDelegate() { - - lateinit var adapter: SectionsPagerAdapter - val toolbar: Toolbar by bindView(R.id.toolbar) - val viewPager: FrostViewPager by bindView(R.id.container) - val fab: FloatingActionButton by bindView(R.id.fab) - val tabs: TabLayout by bindView(R.id.tabs) - val appBar: AppBarLayout by bindView(R.id.appbar) - val coordinator: CoordinatorLayout by bindView(R.id.main_content) - lateinit var drawer: Drawer - lateinit var drawerHeader: AccountHeader - var webFragmentObservable = PublishSubject.create()!! - var lastPosition = -1 - val headerBadgeObservable = PublishSubject.create() - var hiddenSearchView: FrostWebViewSearch? = null - var firstLoadFinished = false - set(value) { - L.d("First fragment load has finished") - field = value - if (value && hiddenSearchView == null) { - hiddenSearchView = FrostWebViewSearch(this, this) - } - } - var searchView: SearchView? = null - override val isSearchOpened: Boolean - get() = searchView?.isOpen ?: false - - companion object { - const val ACTIVITY_SETTINGS = 97 - /* - * Possible responses from the SettingsActivity - * after the configurations have changed - */ - const val REQUEST_RESTART = 90909 - const val REQUEST_REFRESH = 80808 - const val REQUEST_WEB_ZOOM = 50505 - const val REQUEST_NAV = 10101 - const val REQUEST_SEARCH = 70707 - const val REQUEST_RESTART_APPLICATION = 60606 - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - if (BuildConfig.VERSION_CODE > Prefs.versionCode) { - Prefs.versionCode = BuildConfig.VERSION_CODE - if (!BuildConfig.DEBUG) showChangelog(R.xml.changelog, Prefs.textColor) { theme() } - } - setContentView(R.layout.activity_main) - setSupportActionBar(toolbar) - adapter = SectionsPagerAdapter(supportFragmentManager, loadFbTabs()) - viewPager.adapter = adapter - viewPager.offscreenPageLimit = 5 - viewPager.addOnPageChangeListener(object : ViewPager.SimpleOnPageChangeListener() { - override fun onPageSelected(position: Int) { - super.onPageSelected(position) - if (lastPosition == position) return - if (lastPosition != -1) webFragmentObservable.onNext(-(lastPosition + 1)) - webFragmentObservable.onNext(position) - lastPosition = position - } - - override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) { - super.onPageScrolled(position, positionOffset, positionOffsetPixels) - val delta: Float by lazy { positionOffset * (255 - 128).toFloat() } - tabsForEachView { - tabPosition, view -> - view.setAllAlpha(when (tabPosition) { - position -> 255.0f - delta - position + 1 -> 128.0f + delta - else -> 128f - }) - } - } - }) - viewPager.post { webFragmentObservable.onNext(0); lastPosition = 0 } //trigger hook so title is set - setupDrawer(savedInstanceState) - setupTabs() - fab.setOnClickListener { view -> - Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG) - .setAction("Action", null).show() - } - setFrostColors(toolbar, themeWindow = false, headers = arrayOf(tabs, appBar), backgrounds = arrayOf(viewPager)) - validatePro() - } - - fun tabsForEachView(action: (position: Int, view: BadgedIcon) -> Unit) { - (0 until tabs.tabCount).asSequence().forEach { - i -> - action(i, tabs.getTabAt(i)!!.customView as BadgedIcon) - } - } - - fun setupTabs() { - viewPager.addOnPageChangeListener(TabLayout.TabLayoutOnPageChangeListener(tabs)) - tabs.addOnTabSelectedListener(object : TabLayout.ViewPagerOnTabSelectedListener(viewPager) { - override fun onTabReselected(tab: TabLayout.Tab) { - super.onTabReselected(tab) - currentFragment.web.scrollOrRefresh() - } - - override fun onTabSelected(tab: TabLayout.Tab) { - super.onTabSelected(tab) - (tab.customView as BadgedIcon).badgeText = null - } - }) - headerBadgeObservable.throttleFirst(15, TimeUnit.SECONDS).subscribeOn(Schedulers.newThread()) - .map { Jsoup.parse(it) } - .filter { it.select("[data-sigil=count]").size >= 0 } //ensure headers exist - .map { - val feed = it.select("[data-sigil*=feed] [data-sigil=count]") - val requests = it.select("[data-sigil*=requests] [data-sigil=count]") - val messages = it.select("[data-sigil*=messages] [data-sigil=count]") - val notifications = it.select("[data-sigil*=notifications] [data-sigil=count]") - return@map arrayOf(feed, requests, messages, notifications).map { it?.getOrNull(0)?.ownText() } - } - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { - (feed, requests, messages, notifications) -> - tabsForEachView { - _, view -> - when (view.iicon) { - FbTab.FEED.icon -> view.badgeText = feed - FbTab.FRIENDS.icon -> view.badgeText = requests - FbTab.MESSAGES.icon -> view.badgeText = messages - FbTab.NOTIFICATIONS.icon -> view.badgeText = notifications - } - } - } - adapter.pages.forEach { - tabs.addTab(tabs.newTab() - .setCustomView(BadgedIcon(this).apply { - iicon = it.icon - })) - } - } - - fun setupDrawer(savedInstanceState: Bundle?) { - val navBg = Prefs.bgColor.withMinAlpha(200).toLong() - val navHeader = Prefs.headerColor.withMinAlpha(200) - drawer = drawer { - toolbar = this@MainActivity.toolbar - savedInstance = savedInstanceState - translucentStatusBar = false - sliderBackgroundColor = navBg - drawerHeader = accountHeader { - textColor = Prefs.iconColor.toLong() - backgroundDrawable = ColorDrawable(navHeader) - selectionSecondLineShown = false - paddingBelow = false - cookies().forEach { (id, name) -> - profile(name = name ?: "") { - iconUrl = PROFILE_PICTURE_URL(id) - textColor = Prefs.textColor.toLong() - selectedTextColor = Prefs.textColor.toLong() - selectedColor = 0x00000001.toLong() - identifier = id - } - } - profileSetting(nameRes = R.string.kau_logout) { - iicon = GoogleMaterial.Icon.gmd_exit_to_app - iconColor = Prefs.textColor.toLong() - textColor = Prefs.textColor.toLong() - identifier = -2L - } - profileSetting(nameRes = R.string.kau_add_account) { - iconDrawable = IconicsDrawable(this@MainActivity, GoogleMaterial.Icon.gmd_add).actionBar().paddingDp(5).color(Prefs.textColor) - textColor = Prefs.textColor.toLong() - identifier = -3L - } - profileSetting(nameRes = R.string.kau_manage_account) { - iicon = GoogleMaterial.Icon.gmd_settings - iconColor = Prefs.textColor.toLong() - textColor = Prefs.textColor.toLong() - identifier = -4L - } - onProfileChanged { _, profile, current -> - if (current) launchWebOverlay(FbTab.PROFILE.url) - else when (profile.identifier) { - -2L -> { - val currentCookie = loadFbCookie(Prefs.userId) - if (currentCookie == null) { - toast(R.string.account_not_found) - FbCookie.reset { launchLogin(cookies(), true) } - } else { - materialDialogThemed { - title(R.string.kau_logout) - content(String.format(string(R.string.kau_logout_confirm_as_x), currentCookie.name ?: Prefs.userId.toString())) - positiveText(R.string.kau_yes) - negativeText(R.string.kau_no) - onPositive { _, _ -> - FbCookie.logout(Prefs.userId) { - val allCookies = cookies() - allCookies.remove(currentCookie) - launchLogin(allCookies, true) - } - } - } - } - } - -3L -> launchNewTask(LoginActivity::class.java, clearStack = false) - -4L -> launchNewTask(SelectorActivity::class.java, cookies(), false) - else -> { - switchUser(profile.identifier, { refreshAll() }) - tabsForEachView { _, view -> view.badgeText = null } - } - } - false - } - } - drawerHeader.setActiveProfile(Prefs.userId) - primaryFrostItem(FbTab.FEED_MOST_RECENT) - primaryFrostItem(FbTab.FEED_TOP_STORIES) - primaryFrostItem(FbTab.ACTIVITY_LOG) - divider() - primaryFrostItem(FbTab.PHOTOS) - primaryFrostItem(FbTab.GROUPS) - primaryFrostItem(FbTab.PAGES) - divider() - primaryFrostItem(FbTab.EVENTS) - primaryFrostItem(FbTab.BIRTHDAYS) - primaryFrostItem(FbTab.ON_THIS_DAY) - divider() - primaryFrostItem(FbTab.NOTES) - primaryFrostItem(FbTab.SAVED) - } - } - - fun Builder.primaryFrostItem(item: FbTab) = this.primaryItem(item.titleId) { - iicon = item.icon - iconColor = Prefs.textColor.toLong() - textColor = Prefs.textColor.toLong() - selectedIconColor = Prefs.textColor.toLong() - selectedTextColor = Prefs.textColor.toLong() - selectedColor = 0x00000001.toLong() - identifier = item.titleId.toLong() - onClick { _ -> - frostAnswers { - logContentView(ContentViewEvent() - .putContentName(item.name) - .putContentType("drawer_item")) - } - launchWebOverlay(item.url) - false - } - } - - fun Builder.secondaryFrostItem(@StringRes title: Int, onClick: () -> Unit) = this.secondaryItem(title) { - textColor = Prefs.textColor.toLong() - selectedIconColor = Prefs.textColor.toLong() - selectedTextColor = Prefs.textColor.toLong() - selectedColor = 0x00000001.toLong() - identifier = title.toLong() - onClick { _ -> onClick(); false } - } - - - /** - * Something happened where the normal search function won't work - * Fallback to overlay style - */ - override fun searchOverlayDispose() { - hiddenSearchView?.dispose() - hiddenSearchView = null - searchView = null - //todo remove true searchview and add contract - } - - override fun emitSearchResponse(items: List) { - searchView?.results = items - } - - fun refreshAll() { - webFragmentObservable.onNext(WebFragment.REQUEST_REFRESH) - } - - override fun onCreateOptionsMenu(menu: Menu): Boolean { - menuInflater.inflate(R.menu.menu_main, menu) - toolbar.tint(Prefs.iconColor) - setMenuIcons(menu, Prefs.iconColor, - R.id.action_settings to GoogleMaterial.Icon.gmd_settings, - R.id.action_search to GoogleMaterial.Icon.gmd_search) - if (Prefs.searchBar) { - if (firstLoadFinished && hiddenSearchView == null) hiddenSearchView = FrostWebViewSearch(this, this) - if (searchView == null) searchView = bindSearchView(menu, R.id.action_search, Prefs.iconColor) { - textObserver = { - observable, _ -> - observable.observeOn(AndroidSchedulers.mainThread()).subscribe { hiddenSearchView?.query(it) } - } - foregroundColor = Prefs.textColor - backgroundColor = Prefs.bgColor.withMinAlpha(200) - openListener = { hiddenSearchView?.pauseLoad = false } - closeListener = { hiddenSearchView?.pauseLoad = true } - onItemClick = { _, key, _, _ -> launchWebOverlay(key) } - } - } else { - searchOverlayDispose() - menu.findItem(R.id.action_search).setOnMenuItemClickListener { _ -> launchWebOverlay(FbTab.SEARCH.url); true } - } - return true - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - when (item.itemId) { - R.id.action_settings -> { - val intent = Intent(this, SettingsActivity::class.java) - val bundle = ActivityOptionsCompat.makeCustomAnimation(this, R.anim.kau_slide_in_right, R.anim.kau_fade_out).toBundle() - startActivityForResult(intent, ACTIVITY_SETTINGS, bundle) - } - else -> return super.onOptionsItemSelected(item) - } - return true - } - - override fun openFileChooser(filePathCallback: ValueCallback>, fileChooserParams: WebChromeClient.FileChooserParams) { - openFileChooser(this, filePathCallback, fileChooserParams) - } - - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - if (onActivityResultWeb(requestCode, resultCode, data)) return - super.onActivityResult(requestCode, resultCode, data) - if (requestCode == ACTIVITY_SETTINGS) { - when (resultCode) { - REQUEST_RESTART -> restart() - REQUEST_REFRESH -> webFragmentObservable.onNext(WebFragment.REQUEST_REFRESH) - REQUEST_NAV -> frostNavigationBar() - REQUEST_WEB_ZOOM -> webFragmentObservable.onNext(WebFragment.REQUEST_TEXT_ZOOM) - REQUEST_SEARCH -> invalidateOptionsMenu() - REQUEST_RESTART_APPLICATION -> { //completely restart application - L.d("Restart Application Requested") - val intent = packageManager.getLaunchIntentForPackage(packageName) - intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK) - val pending = PendingIntent.getActivity(this, 666, intent, PendingIntent.FLAG_CANCEL_CURRENT) - val alarm = getSystemService(Context.ALARM_SERVICE) as AlarmManager - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) - alarm.setExactAndAllowWhileIdle(AlarmManager.RTC, System.currentTimeMillis() + 100, pending) - else - alarm.setExact(AlarmManager.RTC, System.currentTimeMillis() + 100, pending) - finish() - System.exit(0) - } - } - } - } - - override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { - super.onRequestPermissionsResult(requestCode, permissions, grantResults) - kauOnRequestPermissionsResult(permissions, grantResults) - } - - override fun onResume() { - super.onResume() - FbCookie.switchBackUser { } - } - - override fun onStart() { - //validate some pro features - if (!Prefs.pro) { - if (Prefs.theme == Theme.CUSTOM.ordinal) Prefs.theme = Theme.DEFAULT.ordinal - } - super.onStart() - } - - override fun onBackPressed() { - if (searchView?.onBackPressed() ?: false) return - if (currentFragment.onBackPressed()) return - super.onBackPressed() - } - - val currentFragment - get() = supportFragmentManager.findFragmentByTag("android:switcher:${R.id.container}:${viewPager.currentItem}") as WebFragment - - inner class SectionsPagerAdapter(fm: FragmentManager, val pages: List) : FragmentPagerAdapter(fm) { - - override fun getItem(position: Int): Fragment { - val fragment = WebFragment(pages[position], position) - //If first load hasn't occurred, add a listener - if (!firstLoadFinished) { - var disposable: Disposable? = null - fragment.post { - disposable = it.web.refreshObservable.subscribe { - if (!it) { - //Ensure first load finisher only happens once - if (!firstLoadFinished) firstLoadFinished = true - disposable?.dispose() - disposable = null - } - } - } - } - return fragment - } - - override fun getCount() = pages.size - - override fun getPageTitle(position: Int): CharSequence = getString(pages[position].titleId) - } - -} diff --git a/app/src/main/kotlin/com/pitchedapps/frost/SelectorActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/SelectorActivity.kt deleted file mode 100644 index 103ae227..00000000 --- a/app/src/main/kotlin/com/pitchedapps/frost/SelectorActivity.kt +++ /dev/null @@ -1,46 +0,0 @@ -package com.pitchedapps.frost - -import android.os.Bundle -import android.support.constraint.ConstraintLayout -import android.support.v7.widget.AppCompatTextView -import android.support.v7.widget.GridLayoutManager -import android.support.v7.widget.RecyclerView -import android.view.View -import ca.allanwang.kau.utils.bindView -import com.mikepenz.fastadapter.FastAdapter -import com.mikepenz.fastadapter.commons.adapters.FastItemAdapter -import com.mikepenz.fastadapter.listeners.ClickEventHook -import com.pitchedapps.frost.facebook.FbCookie -import com.pitchedapps.frost.utils.cookies -import com.pitchedapps.frost.utils.launchNewTask -import com.pitchedapps.frost.utils.setFrostColors -import com.pitchedapps.frost.views.AccountItem - -/** - * Created by Allan Wang on 2017-06-04. - */ -class SelectorActivity : BaseActivity() { - - val recycler: RecyclerView by bindView(R.id.selector_recycler) - val adapter = FastItemAdapter() - val text: AppCompatTextView by bindView(R.id.text_select_account) - val container: ConstraintLayout by bindView(R.id.container) - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_selector) - recycler.layoutManager = GridLayoutManager(this, 2) - recycler.adapter = adapter - adapter.add(cookies().map { AccountItem(it) }) - adapter.add(AccountItem(null)) // add account - adapter.withEventHook(object : ClickEventHook() { - override fun onBind(viewHolder: RecyclerView.ViewHolder): View? = (viewHolder as? AccountItem.ViewHolder)?.v - - override fun onClick(v: View, position: Int, fastAdapter: FastAdapter, item: AccountItem) { - if (item.cookie == null) this@SelectorActivity.launchNewTask(LoginActivity::class.java) - else FbCookie.switchUser(item.cookie, { launchNewTask(MainActivity::class.java, cookies()) }) - } - }) - setFrostColors(texts = arrayOf(text), backgrounds = arrayOf(container)) - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/pitchedapps/frost/SettingsActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/SettingsActivity.kt deleted file mode 100644 index e0be330d..00000000 --- a/app/src/main/kotlin/com/pitchedapps/frost/SettingsActivity.kt +++ /dev/null @@ -1,143 +0,0 @@ -package com.pitchedapps.frost - -import android.content.Intent -import android.os.Bundle -import android.view.Menu -import android.view.MenuItem -import ca.allanwang.kau.changelog.showChangelog -import ca.allanwang.kau.kpref.activity.CoreAttributeContract -import ca.allanwang.kau.kpref.activity.KPrefActivity -import ca.allanwang.kau.kpref.activity.KPrefAdapterBuilder -import ca.allanwang.kau.kpref.activity.items.KPrefItemBase -import ca.allanwang.kau.ui.views.RippleCanvas -import ca.allanwang.kau.utils.* -import com.mikepenz.community_material_typeface_library.CommunityMaterial -import com.mikepenz.google_material_typeface_library.GoogleMaterial -import com.pitchedapps.frost.settings.* -import com.pitchedapps.frost.utils.* -import com.pitchedapps.frost.utils.iab.* - - -/** - * Created by Allan Wang on 2017-06-06. - */ -class SettingsActivity : KPrefActivity(), IabBroadcastReceiver.IabBroadcastListener { - - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - if (!IAB.handleActivityResult(requestCode, resultCode, data)) { - super.onActivityResult(requestCode, resultCode, data) - adapter.notifyDataSetChanged() - } - } - - - override fun receivedBroadcast() { - L.d("IAB broadcast") - adapter.notifyDataSetChanged() - } - - override fun kPrefCoreAttributes(): CoreAttributeContract.() -> Unit = { - textColor = { Prefs.textColor } - accentColor = { Prefs.accentColor } - } - - override fun onCreateKPrefs(savedInstanceState: android.os.Bundle?): KPrefAdapterBuilder.() -> Unit = { - subItems(R.string.appearance, getAppearancePrefs()) { - descRes = R.string.appearance_desc - iicon = GoogleMaterial.Icon.gmd_palette - } - - subItems(R.string.behaviour, getBehaviourPrefs()) { - descRes = R.string.behaviour_desc - iicon = GoogleMaterial.Icon.gmd_trending_up - } - - subItems(R.string.newsfeed, getFeedPrefs()) { - descRes = R.string.newsfeed_desc - iicon = CommunityMaterial.Icon.cmd_newspaper - } - - subItems(R.string.notifications, getNotificationPrefs()) { - descRes = R.string.notifications_desc - iicon = GoogleMaterial.Icon.gmd_notifications - } - - subItems(R.string.experimental, getExperimentalPrefs()) { - descRes = R.string.experimental_desc - iicon = CommunityMaterial.Icon.cmd_flask_outline - } - - plainText(R.string.restore_purchases) { - descRes = R.string.restore_purchases_desc - iicon = GoogleMaterial.Icon.gmd_refresh - onClick = { _, _, _ -> this@SettingsActivity.restorePurchases(); true } - } - - plainText(R.string.about_frost) { - iicon = GoogleMaterial.Icon.gmd_info - onClick = { _, _, _ -> startActivity(AboutActivity::class.java, transition = true); true } - } - - if (BuildConfig.DEBUG) { - checkbox(R.string.custom_pro, { Prefs.debugPro }, { Prefs.debugPro = it }) - } - } - - fun KPrefItemBase.BaseContract<*>.dependsOnPro() { - onDisabledClick = { _, _, _ -> openPlayProPurchase(0); true } - enabler = { IS_FROST_PRO } - } - - fun shouldRestartMain() { - setResult(MainActivity.REQUEST_RESTART) - } - - override fun onCreate(savedInstanceState: Bundle?) { - setFrostTheme(true) - super.onCreate(savedInstanceState) - animate = Prefs.animate - themeExterior(false) - } - - fun themeExterior(animate: Boolean = true) { - if (animate) bgCanvas.fade(Prefs.bgColor) - else bgCanvas.set(Prefs.bgColor) - if (animate) toolbarCanvas.ripple(Prefs.headerColor, RippleCanvas.MIDDLE, RippleCanvas.END) - else toolbarCanvas.set(Prefs.headerColor) - frostNavigationBar() - } - - override fun onBackPressed() { - if (!super.backPress()) - finishSlideOut() - } - - override fun onCreateOptionsMenu(menu: Menu): Boolean { - menuInflater.inflate(R.menu.menu_settings, menu) - toolbar.tint(Prefs.iconColor) - toolbarTitle.textColor = Prefs.iconColor - toolbarTitle.invalidate() - setMenuIcons(menu, Prefs.iconColor, - R.id.action_email to GoogleMaterial.Icon.gmd_email, - R.id.action_changelog to GoogleMaterial.Icon.gmd_info) - return true - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - when (item.itemId) { - R.id.action_email -> materialDialogThemed { - title(R.string.subject) - items(Support.values().map { string(it.title) }) - itemsCallback { _, _, which, _ -> Support.values()[which].sendEmail(this@SettingsActivity) } - } - R.id.action_changelog -> showChangelog(R.xml.changelog, Prefs.textColor) { theme() } - else -> return super.onOptionsItemSelected(item) - } - return true - } - - override fun onDestroy() { - IAB.dispose() - super.onDestroy() - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/pitchedapps/frost/StartActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/StartActivity.kt index 122c571e..12cb955d 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/StartActivity.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/StartActivity.kt @@ -2,6 +2,9 @@ package com.pitchedapps.frost import android.os.Bundle import android.support.v7.app.AppCompatActivity +import com.pitchedapps.frost.activities.LoginActivity +import com.pitchedapps.frost.activities.MainActivity +import com.pitchedapps.frost.activities.SelectorActivity import com.pitchedapps.frost.dbflow.loadFbCookiesAsync import com.pitchedapps.frost.facebook.FbCookie import com.pitchedapps.frost.utils.L diff --git a/app/src/main/kotlin/com/pitchedapps/frost/WebOverlayActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/WebOverlayActivity.kt deleted file mode 100644 index 6c1fb5bd..00000000 --- a/app/src/main/kotlin/com/pitchedapps/frost/WebOverlayActivity.kt +++ /dev/null @@ -1,157 +0,0 @@ -package com.pitchedapps.frost - -import android.content.Intent -import android.net.Uri -import android.os.Bundle -import android.support.design.widget.CoordinatorLayout -import android.support.design.widget.Snackbar -import android.support.v7.app.AppCompatActivity -import android.support.v7.widget.Toolbar -import android.view.Menu -import android.view.MenuItem -import android.webkit.ValueCallback -import android.webkit.WebChromeClient -import ca.allanwang.kau.permissions.kauOnRequestPermissionsResult -import ca.allanwang.kau.swipe.kauSwipeOnCreate -import ca.allanwang.kau.swipe.kauSwipeOnDestroy -import ca.allanwang.kau.swipe.kauSwipeOnPostCreate -import ca.allanwang.kau.utils.* -import com.mikepenz.community_material_typeface_library.CommunityMaterial -import com.mikepenz.google_material_typeface_library.GoogleMaterial -import com.pitchedapps.frost.contracts.ActivityWebContract -import com.pitchedapps.frost.contracts.FileChooserContract -import com.pitchedapps.frost.contracts.FileChooserDelegate -import com.pitchedapps.frost.facebook.FbCookie -import com.pitchedapps.frost.facebook.formattedFbUrl -import com.pitchedapps.frost.utils.* -import com.pitchedapps.frost.web.FrostWebView - - -/** - * Created by Allan Wang on 2017-06-01. - */ -open class WebOverlayActivity : AppCompatActivity(), - ActivityWebContract, FileChooserContract by FileChooserDelegate() { - - val toolbar: Toolbar by bindView(R.id.overlay_toolbar) - val frostWeb: FrostWebView by bindView(R.id.overlay_frost_webview) - val coordinator: CoordinatorLayout by bindView(R.id.overlay_main_content) - - val urlTest: String? - get() = intent.extras?.getString(ARG_URL) ?: intent.dataString - - open val url: String - get() = (intent.extras?.getString(ARG_URL) ?: intent.dataString).formattedFbUrl - - val userId: Long - get() = intent.extras?.getLong(ARG_USER_ID, Prefs.userId) ?: Prefs.userId - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - if (urlTest == null) { - L.eThrow("Empty link on web overlay") - toast(R.string.null_url_overlay) - finish() - return - } - setContentView(R.layout.activity_web_overlay) - setSupportActionBar(toolbar) - supportActionBar?.setDisplayShowHomeEnabled(true) - supportActionBar?.setDisplayHomeAsUpEnabled(true) - toolbar.navigationIcon = GoogleMaterial.Icon.gmd_close.toDrawable(this, 16, Prefs.iconColor) - toolbar.setNavigationOnClickListener { finishSlideOut() } - kauSwipeOnCreate { - if (!Prefs.overlayFullScreenSwipe) edgeSize = 20.dpToPx - transitionSystemBars = false - } - setFrostColors(toolbar, themeWindow = false) - coordinator.setBackgroundColor(Prefs.bgColor.withAlpha(255)) - - frostWeb.setupWebview(url) - frostWeb.web.addTitleListener({ toolbar.title = it }) - if (userId != Prefs.userId) FbCookie.switchUser(userId) { frostWeb.web.loadBaseUrl() } - else frostWeb.web.loadBaseUrl() - if (Showcase.firstWebOverlay) { - coordinator.frostSnackbar(R.string.web_overlay_swipe_hint) { - duration = Snackbar.LENGTH_INDEFINITE - setAction(R.string.kau_got_it) { _ -> this.dismiss() } - } - } - } - - /** - * Manage url loadings - * This is usually only called when multiple listeners are added and inject the same url - * We will avoid reloading if the url is the same - */ - override fun onNewIntent(intent: Intent) { - super.onNewIntent(intent) - val newUrl = (intent.extras?.getString(ARG_URL) ?: intent.dataString ?: return).formattedFbUrl - L.d("New intent") - if (url != newUrl) { - this.intent = intent - frostWeb.web.baseUrl = newUrl - frostWeb.web.loadBaseUrl() - } - } - - /** - * Our theme for the overlay should be fully opaque - */ - fun theme() { - val opaqueAccent = Prefs.headerColor.withAlpha(255) - statusBarColor = opaqueAccent.darken() - navigationBarColor = opaqueAccent - toolbar.setBackgroundColor(opaqueAccent) - toolbar.setTitleTextColor(Prefs.iconColor) - coordinator.setBackgroundColor(Prefs.bgColor.withAlpha(255)) - toolbar.overflowIcon?.setTint(Prefs.iconColor) - } - - override fun onPostCreate(savedInstanceState: Bundle?) { - super.onPostCreate(savedInstanceState) - kauSwipeOnPostCreate() - } - - override fun onDestroy() { - super.onDestroy() - kauSwipeOnDestroy() - } - - override fun onBackPressed() { - if (!frostWeb.onBackPressed()) { - finishSlideOut() - } - } - - override fun openFileChooser(filePathCallback: ValueCallback>, fileChooserParams: WebChromeClient.FileChooserParams) { - openFileChooser(this, filePathCallback, fileChooserParams) - } - - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - if (onActivityResultWeb(requestCode, resultCode, data)) return - } - - override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { - super.onRequestPermissionsResult(requestCode, permissions, grantResults) - kauOnRequestPermissionsResult(permissions, grantResults) - } - - override fun onCreateOptionsMenu(menu: Menu): Boolean { - menuInflater.inflate(R.menu.menu_web, menu) - toolbar.tint(Prefs.iconColor) - setMenuIcons(menu, Prefs.iconColor, - R.id.action_share to CommunityMaterial.Icon.cmd_share, - R.id.action_copy_link to GoogleMaterial.Icon.gmd_content_copy) - return true - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - when (item.itemId) { - R.id.action_copy_link -> copyToClipboard(frostWeb.web.url) - R.id.action_share -> shareText(frostWeb.web.url) - else -> return super.onOptionsItemSelected(item) - } - return true - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/pitchedapps/frost/activities/AboutActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/AboutActivity.kt new file mode 100644 index 00000000..63ad8bae --- /dev/null +++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/AboutActivity.kt @@ -0,0 +1,145 @@ +package com.pitchedapps.frost.activities + +import android.support.constraint.ConstraintLayout +import android.support.constraint.ConstraintSet +import android.support.v7.widget.RecyclerView +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import ca.allanwang.kau.about.AboutActivityBase +import ca.allanwang.kau.about.LibraryIItem +import ca.allanwang.kau.adapters.FastItemThemedAdapter +import ca.allanwang.kau.adapters.ThemableIItem +import ca.allanwang.kau.adapters.ThemableIItemDelegate +import ca.allanwang.kau.utils.* +import com.mikepenz.aboutlibraries.Libs +import com.mikepenz.aboutlibraries.entity.Library +import com.mikepenz.aboutlibraries.entity.License +import com.mikepenz.community_material_typeface_library.CommunityMaterial +import com.mikepenz.fastadapter.IItem +import com.mikepenz.fastadapter.items.AbstractItem +import com.mikepenz.google_material_typeface_library.GoogleMaterial +import com.mikepenz.iconics.typeface.IIcon +import com.pitchedapps.frost.BuildConfig +import com.pitchedapps.frost.R +import com.pitchedapps.frost.utils.Prefs + + +/** + * Created by Allan Wang on 2017-06-26. + */ +class AboutActivity : AboutActivityBase(null, { + textColor = Prefs.textColor + accentColor = Prefs.accentColor + backgroundColor = Prefs.bgColor.withMinAlpha(200) + cutoutForeground = if (0xff3b5998.toInt().isColorVisibleOn(Prefs.bgColor)) 0xff3b5998.toInt() else Prefs.accentColor + cutoutDrawableRes = R.drawable.frost_f_256 +}) { + + override fun getLibraries(libs: Libs): List { + val include = arrayOf( + "AboutLibraries", + "AndroidIconics", + "dbflow", + "fastadapter", + "glide", + "Jsoup", + "kau", + "kotterknife", + "materialdialogs", + "materialdrawer" + ) + + /* + * These are great libraries, but either aren't used directly or are too common to be listed + * Give more emphasis on the unique libs! + */ + val exclude = arrayOf( + "GoogleMaterialDesignIcons", + "intellijannotations", + "MaterialDesignIconicIcons", + "MaterialDesignIcons", + "materialize", + "appcompat_v7", + "design", + "recyclerview_v7", + "support_v4" + ) + val l = libs.prepareLibraries(this, include, null, false, true) +// l.forEach { KL.d("Lib ${it.definedName}") } + return l + } + + override fun postInflateMainPage(adapter: FastItemThemedAdapter>) { + /** + * Frost may not be a library but we're conveying the same info + */ + val frost = Library().apply { + libraryName = string(R.string.app_name) + author = "Pitched Apps" + libraryWebsite = "https://github.com/AllanWang/Frost-for-Facebook" + isOpenSource = true + libraryDescription = string(R.string.frost_description) + libraryVersion = BuildConfig.VERSION_NAME + license = License().apply { + licenseName = "GNU GPL v3" + licenseWebsite = "https://www.gnu.org/licenses/gpl-3.0.en.html" + } + } + adapter.add(LibraryIItem(frost)).add(AboutLinks()) + + } + + class AboutLinks : AbstractItem(), ThemableIItem by ThemableIItemDelegate() { + override fun getViewHolder(v: View): ViewHolder = ViewHolder(v) + + override fun getType(): Int = R.id.item_about_links + + override fun getLayoutRes(): Int = R.layout.item_about_links + + override fun bindView(holder: ViewHolder, payloads: MutableList?) { + super.bindView(holder, payloads) + with(holder) { + bindIconColor(*images.toTypedArray()) + bindBackgroundColor(container) + } + } + + class ViewHolder(v: View) : RecyclerView.ViewHolder(v) { + + val container: ConstraintLayout by bindView(R.id.about_icons_container) + val images: List + + /** + * There are a lot of constraints to be added to each item just to have them chained properly + * My as well do it programmatically + * Initializing the viewholder will setup the icons, scale type and background of all icons, + * link their click listeners and chain them together via a horizontal spread + */ + init { + val c = itemView.context + val size = c.dimenPixelSize(R.dimen.kau_avatar_bounds) + images = arrayOf Unit>>( + GoogleMaterial.Icon.gmd_star to { c.startPlayStoreLink(R.string.play_store_package_id) }, + CommunityMaterial.Icon.cmd_reddit to { c.startLink("https://www.reddit.com/r/FrostForFacebook/") }, + CommunityMaterial.Icon.cmd_github_circle to { c.startLink("https://github.com/AllanWang/Frost-for-Facebook") } + ).mapIndexed { i, (icon, onClick) -> + ImageView(c).apply { + layoutParams = ViewGroup.LayoutParams(size, size) + id = 109389 + i + setImageDrawable(icon.toDrawable(context, 32)) + scaleType = ImageView.ScaleType.CENTER + background = context.resolveDrawable(android.R.attr.selectableItemBackgroundBorderless) + setOnClickListener({ onClick() }) + container.addView(this) + } + } + val set = ConstraintSet() + set.clone(container) + set.createHorizontalChain(ConstraintSet.PARENT_ID, ConstraintSet.LEFT, ConstraintSet.PARENT_ID, ConstraintSet.RIGHT, + images.map { it.id }.toIntArray(), null, ConstraintSet.CHAIN_SPREAD_INSIDE) + set.applyTo(container) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/pitchedapps/frost/activities/BaseActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/BaseActivity.kt new file mode 100644 index 00000000..fd020af1 --- /dev/null +++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/BaseActivity.kt @@ -0,0 +1,32 @@ +package com.pitchedapps.frost.activities + +import android.os.Bundle +import android.support.v7.app.AppCompatActivity +import com.pitchedapps.frost.R +import com.pitchedapps.frost.utils.Prefs +import com.pitchedapps.frost.utils.materialDialogThemed +import com.pitchedapps.frost.utils.setFrostTheme + +/** + * Created by Allan Wang on 2017-06-12. + */ +open class BaseActivity : AppCompatActivity() { + override fun onBackPressed() { + if (isTaskRoot && Prefs.exitConfirmation) { + materialDialogThemed { + title(R.string.kau_exit) + content(R.string.kau_exit_confirmation) + positiveText(R.string.kau_yes) + negativeText(R.string.kau_no) + onPositive { _, _ -> super.onBackPressed() } + checkBoxPromptRes(R.string.kau_do_not_show_again, false, { _, b -> Prefs.exitConfirmation = !b }) + } + } else super.onBackPressed() + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setFrostTheme() + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/pitchedapps/frost/activities/FrostWebActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/FrostWebActivity.kt new file mode 100644 index 00000000..1773471f --- /dev/null +++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/FrostWebActivity.kt @@ -0,0 +1,20 @@ +package com.pitchedapps.frost.activities + +import android.os.Bundle +import com.pitchedapps.frost.utils.Prefs + + +/** + * Created by Allan Wang on 2017-06-19. + * + * Replica of [WebOverlayActivity] with a different base url + * Didn't use activity-alias because it causes issues when only one activity has the singleInstance mode + */ +class FrostWebActivity : WebOverlayActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + Prefs.prevId = Prefs.userId + super.onCreate(savedInstanceState) + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/pitchedapps/frost/activities/ImageActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/ImageActivity.kt new file mode 100644 index 00000000..509ac2cb --- /dev/null +++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/ImageActivity.kt @@ -0,0 +1,297 @@ +package com.pitchedapps.frost.activities + +import android.animation.ValueAnimator +import android.annotation.SuppressLint +import android.content.Intent +import android.content.res.ColorStateList +import android.graphics.Bitmap +import android.graphics.Color +import android.graphics.drawable.Drawable +import android.net.Uri +import android.os.Bundle +import android.os.Environment +import android.support.design.widget.FloatingActionButton +import android.support.v4.content.FileProvider +import android.support.v7.app.AppCompatActivity +import android.view.View +import android.view.ViewGroup +import android.widget.ProgressBar +import android.widget.TextView +import ca.allanwang.kau.permissions.PERMISSION_WRITE_EXTERNAL_STORAGE +import ca.allanwang.kau.permissions.kauOnRequestPermissionsResult +import ca.allanwang.kau.permissions.kauRequestPermissions +import ca.allanwang.kau.utils.* +import com.bumptech.glide.request.target.BaseTarget +import com.bumptech.glide.request.target.SizeReadyCallback +import com.bumptech.glide.request.target.Target +import com.bumptech.glide.request.transition.Transition +import com.davemorrissey.labs.subscaleview.ImageSource +import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView +import com.mikepenz.google_material_typeface_library.GoogleMaterial +import com.mikepenz.iconics.typeface.IIcon +import com.pitchedapps.frost.BuildConfig +import com.pitchedapps.frost.R +import com.pitchedapps.frost.utils.* +import com.sothree.slidinguppanel.SlidingUpPanelLayout +import org.jetbrains.anko.doAsync +import org.jetbrains.anko.uiThread +import timber.log.Timber +import java.io.File +import java.io.IOException +import java.text.SimpleDateFormat +import java.util.* + +/** + * Created by Allan Wang on 2017-07-15. + */ +class ImageActivity : AppCompatActivity() { + + val progress: ProgressBar by bindView(R.id.image_progress) + val container: ViewGroup by bindView(R.id.image_container) + val panel: SlidingUpPanelLayout? by bindOptionalView(R.id.image_panel) + val photo: SubsamplingScaleImageView by bindView(R.id.image_photo) + val caption: TextView? by bindOptionalView(R.id.image_text) + val fab: FloatingActionButton by bindView(R.id.image_fab) + + /** + * Reference to the temporary file path + * Should be nonnull if the image is successfully loaded + * As this is temporary, the image is deleted upon exit + */ + internal var tempFilePath: String? = null + /** + * Reference to path for downloaded image + * Nonnull once the image is downloaded by the user + */ + internal var downloadPath: String? = null + /** + * Indicator for fab's click result + */ + internal var fabAction: FabStates = FabStates.NOTHING + set(value) { + if (field == value) return + field = value + value.update(fab) + } + + val imageUrl: String + get() = intent.extras.getString(ARG_IMAGE_URL) + + val text: String? + get() = intent.extras.getString(ARG_TEXT) + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(if (!text.isNullOrBlank()) R.layout.activity_image else R.layout.activity_image_textless) + container.setBackgroundColor(Prefs.bgColor.withMinAlpha(222)) + caption?.setTextColor(Prefs.textColor) + caption?.setBackgroundColor(Prefs.bgColor.colorToForeground(0.1f).withAlpha(255)) + caption?.text = text + progress.tint(Prefs.accentColor) + panel?.addPanelSlideListener(object : SlidingUpPanelLayout.SimplePanelSlideListener() { + + override fun onPanelSlide(panel: View, slideOffset: Float) { + if (slideOffset == 0f && !fab.isShown) fab.show() + else if (slideOffset != 0f && fab.isShown) fab.hide() + caption?.alpha = slideOffset / 2 + 0.5f + } + + }) + fab.setOnClickListener { fabAction.onClick(this) } + photo.setOnImageEventListener(object : SubsamplingScaleImageView.DefaultOnImageEventListener() { + override fun onImageLoadError(e: Exception) { + L.e(e, "Image load error") + imageCallback(null, false) + } + }) + GlideApp.with(this).asBitmap().load(imageUrl).into(PhotoTarget(this::imageCallback)) + } + + /** + * Callback to add image to view + * [resource] is guaranteed to be nonnull when [success] is true + * and null when it is false + */ + private fun imageCallback(resource: Bitmap?, success: Boolean) { + if (progress.isVisible) progress.fadeOut() + if (success) { + saveTempImage(resource!!, { + if (it == null) { + imageCallback(null, false) + } else { + photo.setImage(ImageSource.uri(it)) + fabAction = FabStates.DOWNLOAD + photo.animate().alpha(1f).scaleX(1f).scaleY(1f).withEndAction { fab.show() }.start() + } + }) + } else { + fabAction = FabStates.ERROR + fab.show() + } + } + + /** + * Bitmap load handler + */ + class PhotoTarget(val callback: (resource: Bitmap?, success: Boolean) -> Unit) : BaseTarget() { + + override fun removeCallback(cb: SizeReadyCallback?) {} + + override fun onResourceReady(resource: Bitmap, transition: Transition?) = callback(resource, true) + + override fun onLoadFailed(errorDrawable: Drawable?) = callback(null, false) + + override fun getSize(cb: SizeReadyCallback) = cb.onSizeReady(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL) + + } + + private fun saveTempImage(resource: Bitmap, callback: (uri: Uri?) -> Unit) { + var photoFile: File? = null + try { + photoFile = createImageFile() + } catch (ignored: IOException) { + } finally { + if (photoFile == null) { + callback(null) + } else { + tempFilePath = photoFile.absolutePath + Timber.d("Temp image path $tempFilePath") + // File created; proceed with request + val photoURI = FileProvider.getUriForFile(this, + BuildConfig.APPLICATION_ID + ".provider", + photoFile) + photoFile.outputStream().use { resource.compress(Bitmap.CompressFormat.PNG, 100, it) } + callback(photoURI) + } + } + } + + @Suppress("SIMPLE_DATE_FORMAT") + @Throws(IOException::class) + private fun createImageFile(): File { + // Create an image file name + @SuppressLint("SimpleDateFormat") + val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss").format(Date()) + val imageFileName = "Frost_" + timeStamp + "_" + val storageDir = getExternalFilesDir(Environment.DIRECTORY_PICTURES) + return File.createTempFile( + imageFileName, /* prefix */ + ".png", /* suffix */ + storageDir /* directory */ + ) + } + + internal fun downloadImage() { + kauRequestPermissions(PERMISSION_WRITE_EXTERNAL_STORAGE) { + granted, _ -> + if (granted) { + doAsync { + val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss").format(Date()) + val imageFileName = "Frost_" + timeStamp + "_" + val storageDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES) + val frostDir = File(storageDir, "Frost") + if (!frostDir.exists()) frostDir.mkdirs() + val destination = File.createTempFile(imageFileName, ".png", frostDir) + downloadPath = destination.absolutePath + var success = true + try { + File(tempFilePath).copyTo(destination, true) + } catch (e: Exception) { + success = false + } finally { + uiThread { + snackbar(if (success) R.string.image_download_success else R.string.image_download_fail) + if (success) { + deleteTempFile() + fabAction = FabStates.SHARE + } + } + } + } + } + } + } + + internal fun deleteTempFile() { + if (tempFilePath != null) { + File(tempFilePath!!).delete() + tempFilePath = null + } + } + + override fun onDestroy() { + deleteTempFile() + super.onDestroy() + } + + override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + kauOnRequestPermissionsResult(permissions, grantResults) + } +} + +internal enum class FabStates(val iicon: IIcon, val iconColor: Int = Prefs.textColor, val backgroundTint: Int = Prefs.accentBackgroundColor.withAlpha(255)) { + ERROR(GoogleMaterial.Icon.gmd_error, Color.WHITE, Color.RED) { + override fun onClick(activity: ImageActivity) { + //todo add something + } + }, + NOTHING(GoogleMaterial.Icon.gmd_adjust) { + override fun onClick(activity: ImageActivity) {} + }, + DOWNLOAD(GoogleMaterial.Icon.gmd_file_download) { + override fun onClick(activity: ImageActivity) { + activity.downloadImage() + } + }, + SHARE(GoogleMaterial.Icon.gmd_share) { + override fun onClick(activity: ImageActivity) { + try { + val photoURI = FileProvider.getUriForFile(activity, + BuildConfig.APPLICATION_ID + ".provider", + File(activity.downloadPath)) + val intent = Intent(Intent.ACTION_SEND).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK + putExtra(Intent.EXTRA_STREAM, photoURI) + type = "image/png" + } + activity.startActivity(intent) + } catch (e: Exception) { + L.e(e, "Image share failed"); + activity.snackbar(R.string.image_share_failed) + } + } + }; + + /** + * Change the fab look + * If it's in view, give it some animations + */ + fun update(fab: FloatingActionButton) { + if (!fab.isShown) { + fab.setIcon(iicon, color = iconColor) + fab.backgroundTintList = ColorStateList.valueOf(backgroundTint) + } else { + var switched = false + ValueAnimator.ofFloat(1.0f, 0.0f, 1.0f).apply { + duration = 500L + addUpdateListener { + val x = it.animatedValue as Float + val scale = x * 0.3f + 0.7f + fab.scaleX = scale + fab.scaleY = scale + fab.imageAlpha = (x * 255).toInt() + if (it.animatedFraction > 0.5f && !switched) { + switched = true + fab.setIcon(iicon, color = iconColor) + fab.backgroundTintList = ColorStateList.valueOf(backgroundTint) + } + } + start() + } + } + } + + abstract fun onClick(activity: ImageActivity) + +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/pitchedapps/frost/activities/LoginActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/LoginActivity.kt new file mode 100644 index 00000000..e4897be5 --- /dev/null +++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/LoginActivity.kt @@ -0,0 +1,126 @@ +package com.pitchedapps.frost.activities + +import android.graphics.drawable.Drawable +import android.os.Bundle +import android.os.Handler +import android.support.v4.widget.SwipeRefreshLayout +import android.support.v7.widget.AppCompatTextView +import android.support.v7.widget.Toolbar +import android.widget.ImageView +import ca.allanwang.kau.utils.bindView +import ca.allanwang.kau.utils.fadeIn +import ca.allanwang.kau.utils.fadeOut +import com.bumptech.glide.Glide +import com.bumptech.glide.load.DataSource +import com.bumptech.glide.load.engine.GlideException +import com.bumptech.glide.request.RequestListener +import com.bumptech.glide.request.target.Target +import com.crashlytics.android.answers.LoginEvent +import com.pitchedapps.frost.R +import com.pitchedapps.frost.dbflow.CookieModel +import com.pitchedapps.frost.dbflow.fetchUsername +import com.pitchedapps.frost.dbflow.loadFbCookiesAsync +import com.pitchedapps.frost.facebook.PROFILE_PICTURE_URL +import com.pitchedapps.frost.utils.* +import com.pitchedapps.frost.web.LoginWebView +import io.reactivex.Observable +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.functions.BiFunction +import io.reactivex.internal.operators.single.SingleToObservable +import io.reactivex.subjects.BehaviorSubject +import io.reactivex.subjects.SingleSubject + + +/** + * Created by Allan Wang on 2017-06-01. + */ +class LoginActivity : BaseActivity() { + + val toolbar: Toolbar by bindView(R.id.toolbar) + val web: LoginWebView by bindView(R.id.login_webview) + val swipeRefresh: SwipeRefreshLayout by bindView(R.id.swipe_refresh) + val textview: AppCompatTextView by bindView(R.id.textview) + val profile: ImageView by bindView(R.id.profile) + + val loginObservable = SingleSubject.create() + val progressObservable = BehaviorSubject.create()!! + val profileObservable = SingleSubject.create() + val usernameObservable = SingleSubject.create() + + // Helper to set and enable swipeRefresh + var refresh: Boolean + get() = swipeRefresh.isRefreshing + set(value) { + if (value) swipeRefresh.isEnabled = true + swipeRefresh.isRefreshing = value + if (!value) swipeRefresh.isEnabled = false + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_login) + setSupportActionBar(toolbar) + setTitle(R.string.kau_login) + setFrostColors(toolbar) + web.loginObservable = loginObservable + web.progressObservable = progressObservable + loginObservable.observeOn(AndroidSchedulers.mainThread()).subscribe { + cookie -> + web.fadeOut(onFinish = { + profile.fadeIn() + loadInfo(cookie) + }) + } + progressObservable.observeOn(AndroidSchedulers.mainThread()).subscribe { refresh = it != 100 } + web.loadLogin() + } + + fun loadInfo(cookie: CookieModel) { + refresh = true + Observable.zip(SingleToObservable(profileObservable), SingleToObservable(usernameObservable), + BiFunction> { foundImage, name -> Pair(foundImage, name) }) + .observeOn(AndroidSchedulers.mainThread()).subscribe { + (foundImage, name) -> + refresh = false + if (!foundImage) { + L.eThrow("Could not get profile photo; Invalid userId?") + L.i("-\t$cookie") + } + textview.text = String.format(getString(R.string.welcome), name) + textview.fadeIn() + frostAnswers { logLogin(LoginEvent().putMethod("frost_browser").putSuccess(true)) } + /* + * The user may have logged into an account that is already in the database + * We will let the db handle duplicates and load it now after the new account has been saved + */ + loadFbCookiesAsync { + cookies -> + Handler().postDelayed({ + launchNewTask(MainActivity::class.java, ArrayList(cookies), clearStack = true) + }, 1000) + } + } + loadProfile(cookie.id) + loadUsername(cookie) + } + + + fun loadProfile(id: Long) { + Glide.with(this@LoginActivity).load(PROFILE_PICTURE_URL(id)).withRoundIcon().listener(object : RequestListener { + override fun onResourceReady(resource: Drawable?, model: Any?, target: Target?, dataSource: DataSource?, isFirstResource: Boolean): Boolean { + profileObservable.onSuccess(true) + return false + } + + override fun onLoadFailed(e: GlideException?, model: Any?, target: Target?, isFirstResource: Boolean): Boolean { + if (e != null) L.e(e, "Profile loading exception") + profileObservable.onSuccess(false) + return false + } + }).into(profile) + } + + fun loadUsername(cookie: CookieModel) { + cookie.fetchUsername { usernameObservable.onSuccess(it) } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/pitchedapps/frost/activities/MainActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/MainActivity.kt new file mode 100644 index 00000000..ba76e594 --- /dev/null +++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/MainActivity.kt @@ -0,0 +1,472 @@ +package com.pitchedapps.frost.activities + +import android.app.AlarmManager +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.graphics.drawable.ColorDrawable +import android.net.Uri +import android.os.Build +import android.os.Bundle +import android.support.annotation.StringRes +import android.support.design.widget.* +import android.support.v4.app.ActivityOptionsCompat +import android.support.v4.app.Fragment +import android.support.v4.app.FragmentManager +import android.support.v4.app.FragmentPagerAdapter +import android.support.v4.view.ViewPager +import android.support.v7.widget.Toolbar +import android.view.Menu +import android.view.MenuItem +import android.webkit.ValueCallback +import android.webkit.WebChromeClient +import ca.allanwang.kau.changelog.showChangelog +import ca.allanwang.kau.permissions.kauOnRequestPermissionsResult +import ca.allanwang.kau.searchview.SearchItem +import ca.allanwang.kau.searchview.SearchView +import ca.allanwang.kau.searchview.bindSearchView +import ca.allanwang.kau.utils.* +import co.zsmb.materialdrawerkt.builders.Builder +import co.zsmb.materialdrawerkt.builders.accountHeader +import co.zsmb.materialdrawerkt.builders.drawer +import co.zsmb.materialdrawerkt.draweritems.badgeable.primaryItem +import co.zsmb.materialdrawerkt.draweritems.badgeable.secondaryItem +import co.zsmb.materialdrawerkt.draweritems.divider +import co.zsmb.materialdrawerkt.draweritems.profile.profile +import co.zsmb.materialdrawerkt.draweritems.profile.profileSetting +import com.crashlytics.android.answers.ContentViewEvent +import com.mikepenz.google_material_typeface_library.GoogleMaterial +import com.mikepenz.iconics.IconicsDrawable +import com.mikepenz.materialdrawer.AccountHeader +import com.mikepenz.materialdrawer.Drawer +import com.pitchedapps.frost.BuildConfig +import com.pitchedapps.frost.R +import com.pitchedapps.frost.contracts.ActivityWebContract +import com.pitchedapps.frost.contracts.FileChooserContract +import com.pitchedapps.frost.contracts.FileChooserDelegate +import com.pitchedapps.frost.dbflow.loadFbCookie +import com.pitchedapps.frost.dbflow.loadFbTabs +import com.pitchedapps.frost.facebook.FbCookie +import com.pitchedapps.frost.facebook.FbCookie.switchUser +import com.pitchedapps.frost.facebook.FbTab +import com.pitchedapps.frost.facebook.PROFILE_PICTURE_URL +import com.pitchedapps.frost.fragments.WebFragment +import com.pitchedapps.frost.utils.* +import com.pitchedapps.frost.utils.iab.validatePro +import com.pitchedapps.frost.views.BadgedIcon +import com.pitchedapps.frost.views.FrostViewPager +import com.pitchedapps.frost.web.FrostWebViewSearch +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.Disposable +import io.reactivex.schedulers.Schedulers +import io.reactivex.subjects.PublishSubject +import org.jsoup.Jsoup +import java.util.concurrent.TimeUnit + +class MainActivity : BaseActivity(), FrostWebViewSearch.SearchContract, + ActivityWebContract, FileChooserContract by FileChooserDelegate() { + + lateinit var adapter: SectionsPagerAdapter + val toolbar: Toolbar by bindView(R.id.toolbar) + val viewPager: FrostViewPager by bindView(R.id.container) + val fab: FloatingActionButton by bindView(R.id.fab) + val tabs: TabLayout by bindView(R.id.tabs) + val appBar: AppBarLayout by bindView(R.id.appbar) + val coordinator: CoordinatorLayout by bindView(R.id.main_content) + lateinit var drawer: Drawer + lateinit var drawerHeader: AccountHeader + var webFragmentObservable = PublishSubject.create()!! + var lastPosition = -1 + val headerBadgeObservable = PublishSubject.create() + var hiddenSearchView: FrostWebViewSearch? = null + var firstLoadFinished = false + set(value) { + L.d("First fragment load has finished") + field = value + if (value && hiddenSearchView == null) { + hiddenSearchView = FrostWebViewSearch(this, this) + } + } + var searchView: SearchView? = null + override val isSearchOpened: Boolean + get() = searchView?.isOpen ?: false + + companion object { + const val ACTIVITY_SETTINGS = 97 + /* + * Possible responses from the SettingsActivity + * after the configurations have changed + */ + const val REQUEST_RESTART = 90909 + const val REQUEST_REFRESH = 80808 + const val REQUEST_WEB_ZOOM = 50505 + const val REQUEST_NAV = 10101 + const val REQUEST_SEARCH = 70707 + const val REQUEST_RESTART_APPLICATION = 60606 + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + if (BuildConfig.VERSION_CODE > Prefs.versionCode) { + Prefs.versionCode = BuildConfig.VERSION_CODE + if (!BuildConfig.DEBUG) { + showChangelog(R.xml.changelog, Prefs.textColor) { theme() } + frostAnswersCustom("Version") { + putCustomAttribute("Version code", BuildConfig.VERSION_CODE) + putCustomAttribute("Version name", BuildConfig.VERSION_NAME) + putCustomAttribute("Build type", BuildConfig.BUILD_TYPE) + putCustomAttribute("Frost id", Prefs.frostId) + } + } + } + setContentView(R.layout.activity_main) + setSupportActionBar(toolbar) + adapter = SectionsPagerAdapter(supportFragmentManager, loadFbTabs()) + viewPager.adapter = adapter + viewPager.offscreenPageLimit = 5 + viewPager.addOnPageChangeListener(object : ViewPager.SimpleOnPageChangeListener() { + override fun onPageSelected(position: Int) { + super.onPageSelected(position) + if (lastPosition == position) return + if (lastPosition != -1) webFragmentObservable.onNext(-(lastPosition + 1)) + webFragmentObservable.onNext(position) + lastPosition = position + } + + override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) { + super.onPageScrolled(position, positionOffset, positionOffsetPixels) + val delta: Float by lazy { positionOffset * (255 - 128).toFloat() } + tabsForEachView { + tabPosition, view -> + view.setAllAlpha(when (tabPosition) { + position -> 255.0f - delta + position + 1 -> 128.0f + delta + else -> 128f + }) + } + } + }) + viewPager.post { webFragmentObservable.onNext(0); lastPosition = 0 } //trigger hook so title is set + setupDrawer(savedInstanceState) + setupTabs() + fab.setOnClickListener { view -> + Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG) + .setAction("Action", null).show() + } + setFrostColors(toolbar, themeWindow = false, headers = arrayOf(tabs, appBar), backgrounds = arrayOf(viewPager)) + validatePro() + } + + fun tabsForEachView(action: (position: Int, view: BadgedIcon) -> Unit) { + (0 until tabs.tabCount).asSequence().forEach { + i -> + action(i, tabs.getTabAt(i)!!.customView as BadgedIcon) + } + } + + fun setupTabs() { + viewPager.addOnPageChangeListener(TabLayout.TabLayoutOnPageChangeListener(tabs)) + tabs.addOnTabSelectedListener(object : TabLayout.ViewPagerOnTabSelectedListener(viewPager) { + override fun onTabReselected(tab: TabLayout.Tab) { + super.onTabReselected(tab) + currentFragment.web.scrollOrRefresh() + } + + override fun onTabSelected(tab: TabLayout.Tab) { + super.onTabSelected(tab) + (tab.customView as BadgedIcon).badgeText = null + } + }) + headerBadgeObservable.throttleFirst(15, TimeUnit.SECONDS).subscribeOn(Schedulers.newThread()) + .map { Jsoup.parse(it) } + .filter { it.select("[data-sigil=count]").size >= 0 } //ensure headers exist + .map { + val feed = it.select("[data-sigil*=feed] [data-sigil=count]") + val requests = it.select("[data-sigil*=requests] [data-sigil=count]") + val messages = it.select("[data-sigil*=messages] [data-sigil=count]") + val notifications = it.select("[data-sigil*=notifications] [data-sigil=count]") + return@map arrayOf(feed, requests, messages, notifications).map { it?.getOrNull(0)?.ownText() } + } + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { + (feed, requests, messages, notifications) -> + tabsForEachView { + _, view -> + when (view.iicon) { + FbTab.FEED.icon -> view.badgeText = feed + FbTab.FRIENDS.icon -> view.badgeText = requests + FbTab.MESSAGES.icon -> view.badgeText = messages + FbTab.NOTIFICATIONS.icon -> view.badgeText = notifications + } + } + } + adapter.pages.forEach { + tabs.addTab(tabs.newTab() + .setCustomView(BadgedIcon(this).apply { + iicon = it.icon + })) + } + } + + fun setupDrawer(savedInstanceState: Bundle?) { + val navBg = Prefs.bgColor.withMinAlpha(200).toLong() + val navHeader = Prefs.headerColor.withMinAlpha(200) + drawer = drawer { + toolbar = this@MainActivity.toolbar + savedInstance = savedInstanceState + translucentStatusBar = false + sliderBackgroundColor = navBg + drawerHeader = accountHeader { + textColor = Prefs.iconColor.toLong() + backgroundDrawable = ColorDrawable(navHeader) + selectionSecondLineShown = false + paddingBelow = false + cookies().forEach { (id, name) -> + profile(name = name ?: "") { + iconUrl = PROFILE_PICTURE_URL(id) + textColor = Prefs.textColor.toLong() + selectedTextColor = Prefs.textColor.toLong() + selectedColor = 0x00000001.toLong() + identifier = id + } + } + profileSetting(nameRes = R.string.kau_logout) { + iicon = GoogleMaterial.Icon.gmd_exit_to_app + iconColor = Prefs.textColor.toLong() + textColor = Prefs.textColor.toLong() + identifier = -2L + } + profileSetting(nameRes = R.string.kau_add_account) { + iconDrawable = IconicsDrawable(this@MainActivity, GoogleMaterial.Icon.gmd_add).actionBar().paddingDp(5).color(Prefs.textColor) + textColor = Prefs.textColor.toLong() + identifier = -3L + } + profileSetting(nameRes = R.string.kau_manage_account) { + iicon = GoogleMaterial.Icon.gmd_settings + iconColor = Prefs.textColor.toLong() + textColor = Prefs.textColor.toLong() + identifier = -4L + } + onProfileChanged { _, profile, current -> + if (current) launchWebOverlay(FbTab.PROFILE.url) + else when (profile.identifier) { + -2L -> { + val currentCookie = loadFbCookie(Prefs.userId) + if (currentCookie == null) { + toast(R.string.account_not_found) + FbCookie.reset { launchLogin(cookies(), true) } + } else { + materialDialogThemed { + title(R.string.kau_logout) + content(String.format(string(R.string.kau_logout_confirm_as_x), currentCookie.name ?: Prefs.userId.toString())) + positiveText(R.string.kau_yes) + negativeText(R.string.kau_no) + onPositive { _, _ -> + FbCookie.logout(Prefs.userId) { + val allCookies = cookies() + allCookies.remove(currentCookie) + launchLogin(allCookies, true) + } + } + } + } + } + -3L -> launchNewTask(LoginActivity::class.java, clearStack = false) + -4L -> launchNewTask(SelectorActivity::class.java, cookies(), false) + else -> { + switchUser(profile.identifier, { refreshAll() }) + tabsForEachView { _, view -> view.badgeText = null } + } + } + false + } + } + drawerHeader.setActiveProfile(Prefs.userId) + primaryFrostItem(FbTab.FEED_MOST_RECENT) + primaryFrostItem(FbTab.FEED_TOP_STORIES) + primaryFrostItem(FbTab.ACTIVITY_LOG) + divider() + primaryFrostItem(FbTab.PHOTOS) + primaryFrostItem(FbTab.GROUPS) + primaryFrostItem(FbTab.PAGES) + divider() + primaryFrostItem(FbTab.EVENTS) + primaryFrostItem(FbTab.BIRTHDAYS) + primaryFrostItem(FbTab.ON_THIS_DAY) + divider() + primaryFrostItem(FbTab.NOTES) + primaryFrostItem(FbTab.SAVED) + } + } + + fun Builder.primaryFrostItem(item: FbTab) = this.primaryItem(item.titleId) { + iicon = item.icon + iconColor = Prefs.textColor.toLong() + textColor = Prefs.textColor.toLong() + selectedIconColor = Prefs.textColor.toLong() + selectedTextColor = Prefs.textColor.toLong() + selectedColor = 0x00000001.toLong() + identifier = item.titleId.toLong() + onClick { _ -> + frostAnswers { + logContentView(ContentViewEvent() + .putContentName(item.name) + .putContentType("drawer_item")) + } + launchWebOverlay(item.url) + false + } + } + + fun Builder.secondaryFrostItem(@StringRes title: Int, onClick: () -> Unit) = this.secondaryItem(title) { + textColor = Prefs.textColor.toLong() + selectedIconColor = Prefs.textColor.toLong() + selectedTextColor = Prefs.textColor.toLong() + selectedColor = 0x00000001.toLong() + identifier = title.toLong() + onClick { _ -> onClick(); false } + } + + + /** + * Something happened where the normal search function won't work + * Fallback to overlay style + */ + override fun searchOverlayDispose() { + hiddenSearchView?.dispose() + hiddenSearchView = null + searchView = null + //todo remove true searchview and add contract + } + + override fun emitSearchResponse(items: List) { + searchView?.results = items + } + + fun refreshAll() { + webFragmentObservable.onNext(WebFragment.REQUEST_REFRESH) + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.menu_main, menu) + toolbar.tint(Prefs.iconColor) + setMenuIcons(menu, Prefs.iconColor, + R.id.action_settings to GoogleMaterial.Icon.gmd_settings, + R.id.action_search to GoogleMaterial.Icon.gmd_search) + if (Prefs.searchBar) { + if (firstLoadFinished && hiddenSearchView == null) hiddenSearchView = FrostWebViewSearch(this, this) + if (searchView == null) searchView = bindSearchView(menu, R.id.action_search, Prefs.iconColor) { + textObserver = { + observable, _ -> + observable.observeOn(AndroidSchedulers.mainThread()).subscribe { hiddenSearchView?.query(it) } + } + foregroundColor = Prefs.textColor + backgroundColor = Prefs.bgColor.withMinAlpha(200) + openListener = { hiddenSearchView?.pauseLoad = false } + closeListener = { hiddenSearchView?.pauseLoad = true } + onItemClick = { _, key, _, _ -> launchWebOverlay(key) } + } + } else { + searchOverlayDispose() + menu.findItem(R.id.action_search).setOnMenuItemClickListener { _ -> launchWebOverlay(FbTab.SEARCH.url); true } + } + return true + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.action_settings -> { + val intent = Intent(this, SettingsActivity::class.java) + val bundle = ActivityOptionsCompat.makeCustomAnimation(this, R.anim.kau_slide_in_right, R.anim.kau_fade_out).toBundle() + startActivityForResult(intent, ACTIVITY_SETTINGS, bundle) + } + else -> return super.onOptionsItemSelected(item) + } + return true + } + + override fun openFileChooser(filePathCallback: ValueCallback>, fileChooserParams: WebChromeClient.FileChooserParams) { + openFileChooser(this, filePathCallback, fileChooserParams) + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + if (onActivityResultWeb(requestCode, resultCode, data)) return + super.onActivityResult(requestCode, resultCode, data) + if (requestCode == ACTIVITY_SETTINGS) { + when (resultCode) { + REQUEST_RESTART -> restart() + REQUEST_REFRESH -> webFragmentObservable.onNext(WebFragment.REQUEST_REFRESH) + REQUEST_NAV -> frostNavigationBar() + REQUEST_WEB_ZOOM -> webFragmentObservable.onNext(WebFragment.REQUEST_TEXT_ZOOM) + REQUEST_SEARCH -> invalidateOptionsMenu() + REQUEST_RESTART_APPLICATION -> { //completely restart application + L.d("Restart Application Requested") + val intent = packageManager.getLaunchIntentForPackage(packageName) + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK) + val pending = PendingIntent.getActivity(this, 666, intent, PendingIntent.FLAG_CANCEL_CURRENT) + val alarm = getSystemService(Context.ALARM_SERVICE) as AlarmManager + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) + alarm.setExactAndAllowWhileIdle(AlarmManager.RTC, System.currentTimeMillis() + 100, pending) + else + alarm.setExact(AlarmManager.RTC, System.currentTimeMillis() + 100, pending) + finish() + System.exit(0) + } + } + } + } + + override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + kauOnRequestPermissionsResult(permissions, grantResults) + } + + override fun onResume() { + super.onResume() + FbCookie.switchBackUser { } + } + + override fun onStart() { + //validate some pro features + if (!Prefs.pro) { + if (Prefs.theme == Theme.CUSTOM.ordinal) Prefs.theme = Theme.DEFAULT.ordinal + } + super.onStart() + } + + override fun onBackPressed() { + if (searchView?.onBackPressed() ?: false) return + if (currentFragment.onBackPressed()) return + super.onBackPressed() + } + + val currentFragment + get() = supportFragmentManager.findFragmentByTag("android:switcher:${R.id.container}:${viewPager.currentItem}") as WebFragment + + inner class SectionsPagerAdapter(fm: FragmentManager, val pages: List) : FragmentPagerAdapter(fm) { + + override fun getItem(position: Int): Fragment { + val fragment = WebFragment(pages[position], position) + //If first load hasn't occurred, add a listener + if (!firstLoadFinished) { + var disposable: Disposable? = null + fragment.post { + disposable = it.web.refreshObservable.subscribe { + if (!it) { + //Ensure first load finisher only happens once + if (!firstLoadFinished) firstLoadFinished = true + disposable?.dispose() + disposable = null + } + } + } + } + return fragment + } + + override fun getCount() = pages.size + + override fun getPageTitle(position: Int): CharSequence = getString(pages[position].titleId) + } + +} diff --git a/app/src/main/kotlin/com/pitchedapps/frost/activities/SelectorActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/SelectorActivity.kt new file mode 100644 index 00000000..ff87f448 --- /dev/null +++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/SelectorActivity.kt @@ -0,0 +1,47 @@ +package com.pitchedapps.frost.activities + +import android.os.Bundle +import android.support.constraint.ConstraintLayout +import android.support.v7.widget.AppCompatTextView +import android.support.v7.widget.GridLayoutManager +import android.support.v7.widget.RecyclerView +import android.view.View +import ca.allanwang.kau.utils.bindView +import com.mikepenz.fastadapter.FastAdapter +import com.mikepenz.fastadapter.commons.adapters.FastItemAdapter +import com.mikepenz.fastadapter.listeners.ClickEventHook +import com.pitchedapps.frost.R +import com.pitchedapps.frost.facebook.FbCookie +import com.pitchedapps.frost.utils.cookies +import com.pitchedapps.frost.utils.launchNewTask +import com.pitchedapps.frost.utils.setFrostColors +import com.pitchedapps.frost.views.AccountItem + +/** + * Created by Allan Wang on 2017-06-04. + */ +class SelectorActivity : BaseActivity() { + + val recycler: RecyclerView by bindView(R.id.selector_recycler) + val adapter = FastItemAdapter() + val text: AppCompatTextView by bindView(R.id.text_select_account) + val container: ConstraintLayout by bindView(R.id.container) + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_selector) + recycler.layoutManager = GridLayoutManager(this, 2) + recycler.adapter = adapter + adapter.add(cookies().map { AccountItem(it) }) + adapter.add(AccountItem(null)) // add account + adapter.withEventHook(object : ClickEventHook() { + override fun onBind(viewHolder: RecyclerView.ViewHolder): View? = (viewHolder as? AccountItem.ViewHolder)?.v + + override fun onClick(v: View, position: Int, fastAdapter: FastAdapter, item: AccountItem) { + if (item.cookie == null) this@SelectorActivity.launchNewTask(LoginActivity::class.java) + else FbCookie.switchUser(item.cookie, { launchNewTask(MainActivity::class.java, cookies()) }) + } + }) + setFrostColors(texts = arrayOf(text), backgrounds = arrayOf(container)) + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/pitchedapps/frost/activities/SettingsActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/SettingsActivity.kt new file mode 100644 index 00000000..b3b3bd7c --- /dev/null +++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/SettingsActivity.kt @@ -0,0 +1,145 @@ +package com.pitchedapps.frost.activities + +import android.content.Intent +import android.os.Bundle +import android.view.Menu +import android.view.MenuItem +import ca.allanwang.kau.changelog.showChangelog +import ca.allanwang.kau.kpref.activity.CoreAttributeContract +import ca.allanwang.kau.kpref.activity.KPrefActivity +import ca.allanwang.kau.kpref.activity.KPrefAdapterBuilder +import ca.allanwang.kau.kpref.activity.items.KPrefItemBase +import ca.allanwang.kau.ui.views.RippleCanvas +import ca.allanwang.kau.utils.* +import com.mikepenz.community_material_typeface_library.CommunityMaterial +import com.mikepenz.google_material_typeface_library.GoogleMaterial +import com.pitchedapps.frost.BuildConfig +import com.pitchedapps.frost.R +import com.pitchedapps.frost.settings.* +import com.pitchedapps.frost.utils.* +import com.pitchedapps.frost.utils.iab.* + + +/** + * Created by Allan Wang on 2017-06-06. + */ +class SettingsActivity : KPrefActivity(), IabBroadcastReceiver.IabBroadcastListener { + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + if (!IAB.handleActivityResult(requestCode, resultCode, data)) { + super.onActivityResult(requestCode, resultCode, data) + adapter.notifyDataSetChanged() + } + } + + + override fun receivedBroadcast() { + L.d("IAB broadcast") + adapter.notifyDataSetChanged() + } + + override fun kPrefCoreAttributes(): CoreAttributeContract.() -> Unit = { + textColor = { Prefs.textColor } + accentColor = { Prefs.accentColor } + } + + override fun onCreateKPrefs(savedInstanceState: android.os.Bundle?): KPrefAdapterBuilder.() -> Unit = { + subItems(R.string.appearance, getAppearancePrefs()) { + descRes = R.string.appearance_desc + iicon = GoogleMaterial.Icon.gmd_palette + } + + subItems(R.string.behaviour, getBehaviourPrefs()) { + descRes = R.string.behaviour_desc + iicon = GoogleMaterial.Icon.gmd_trending_up + } + + subItems(R.string.newsfeed, getFeedPrefs()) { + descRes = R.string.newsfeed_desc + iicon = CommunityMaterial.Icon.cmd_newspaper + } + + subItems(R.string.notifications, getNotificationPrefs()) { + descRes = R.string.notifications_desc + iicon = GoogleMaterial.Icon.gmd_notifications + } + + subItems(R.string.experimental, getExperimentalPrefs()) { + descRes = R.string.experimental_desc + iicon = CommunityMaterial.Icon.cmd_flask_outline + } + + plainText(R.string.restore_purchases) { + descRes = R.string.restore_purchases_desc + iicon = GoogleMaterial.Icon.gmd_refresh + onClick = { _, _, _ -> this@SettingsActivity.restorePurchases(); true } + } + + plainText(R.string.about_frost) { + iicon = GoogleMaterial.Icon.gmd_info + onClick = { _, _, _ -> startActivity(AboutActivity::class.java, transition = true); true } + } + + if (BuildConfig.DEBUG) { + checkbox(R.string.custom_pro, { Prefs.debugPro }, { Prefs.debugPro = it }) + } + } + + fun KPrefItemBase.BaseContract<*>.dependsOnPro() { + onDisabledClick = { _, _, _ -> openPlayProPurchase(0); true } + enabler = { IS_FROST_PRO } + } + + fun shouldRestartMain() { + setResult(MainActivity.REQUEST_RESTART) + } + + override fun onCreate(savedInstanceState: Bundle?) { + setFrostTheme(true) + super.onCreate(savedInstanceState) + animate = Prefs.animate + themeExterior(false) + } + + fun themeExterior(animate: Boolean = true) { + if (animate) bgCanvas.fade(Prefs.bgColor) + else bgCanvas.set(Prefs.bgColor) + if (animate) toolbarCanvas.ripple(Prefs.headerColor, RippleCanvas.MIDDLE, RippleCanvas.END) + else toolbarCanvas.set(Prefs.headerColor) + frostNavigationBar() + } + + override fun onBackPressed() { + if (!super.backPress()) + finishSlideOut() + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.menu_settings, menu) + toolbar.tint(Prefs.iconColor) + toolbarTitle.textColor = Prefs.iconColor + toolbarTitle.invalidate() + setMenuIcons(menu, Prefs.iconColor, + R.id.action_email to GoogleMaterial.Icon.gmd_email, + R.id.action_changelog to GoogleMaterial.Icon.gmd_info) + return true + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.action_email -> materialDialogThemed { + title(R.string.subject) + items(Support.values().map { string(it.title) }) + itemsCallback { _, _, which, _ -> Support.values()[which].sendEmail(this@SettingsActivity) } + } + R.id.action_changelog -> showChangelog(R.xml.changelog, Prefs.textColor) { theme() } + else -> return super.onOptionsItemSelected(item) + } + return true + } + + override fun onDestroy() { + IAB.dispose() + super.onDestroy() + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/pitchedapps/frost/activities/WebOverlayActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/WebOverlayActivity.kt new file mode 100644 index 00000000..f03c653c --- /dev/null +++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/WebOverlayActivity.kt @@ -0,0 +1,158 @@ +package com.pitchedapps.frost.activities + +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.support.design.widget.CoordinatorLayout +import android.support.design.widget.Snackbar +import android.support.v7.app.AppCompatActivity +import android.support.v7.widget.Toolbar +import android.view.Menu +import android.view.MenuItem +import android.webkit.ValueCallback +import android.webkit.WebChromeClient +import ca.allanwang.kau.permissions.kauOnRequestPermissionsResult +import ca.allanwang.kau.swipe.kauSwipeOnCreate +import ca.allanwang.kau.swipe.kauSwipeOnDestroy +import ca.allanwang.kau.swipe.kauSwipeOnPostCreate +import ca.allanwang.kau.utils.* +import com.mikepenz.community_material_typeface_library.CommunityMaterial +import com.mikepenz.google_material_typeface_library.GoogleMaterial +import com.pitchedapps.frost.R +import com.pitchedapps.frost.contracts.ActivityWebContract +import com.pitchedapps.frost.contracts.FileChooserContract +import com.pitchedapps.frost.contracts.FileChooserDelegate +import com.pitchedapps.frost.facebook.FbCookie +import com.pitchedapps.frost.facebook.formattedFbUrl +import com.pitchedapps.frost.utils.* +import com.pitchedapps.frost.web.FrostWebView + + +/** + * Created by Allan Wang on 2017-06-01. + */ +open class WebOverlayActivity : AppCompatActivity(), + ActivityWebContract, FileChooserContract by FileChooserDelegate() { + + val toolbar: Toolbar by bindView(R.id.overlay_toolbar) + val frostWeb: FrostWebView by bindView(R.id.overlay_frost_webview) + val coordinator: CoordinatorLayout by bindView(R.id.overlay_main_content) + + val urlTest: String? + get() = intent.extras?.getString(ARG_URL) ?: intent.dataString + + open val url: String + get() = (intent.extras?.getString(ARG_URL) ?: intent.dataString).formattedFbUrl + + val userId: Long + get() = intent.extras?.getLong(ARG_USER_ID, Prefs.userId) ?: Prefs.userId + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + if (urlTest == null) { + L.eThrow("Empty link on web overlay") + toast(R.string.null_url_overlay) + finish() + return + } + setContentView(R.layout.activity_web_overlay) + setSupportActionBar(toolbar) + supportActionBar?.setDisplayShowHomeEnabled(true) + supportActionBar?.setDisplayHomeAsUpEnabled(true) + toolbar.navigationIcon = GoogleMaterial.Icon.gmd_close.toDrawable(this, 16, Prefs.iconColor) + toolbar.setNavigationOnClickListener { finishSlideOut() } + kauSwipeOnCreate { + if (!Prefs.overlayFullScreenSwipe) edgeSize = 20.dpToPx + transitionSystemBars = false + } + setFrostColors(toolbar, themeWindow = false) + coordinator.setBackgroundColor(Prefs.bgColor.withAlpha(255)) + + frostWeb.setupWebview(url) + frostWeb.web.addTitleListener({ toolbar.title = it }) + if (userId != Prefs.userId) FbCookie.switchUser(userId) { frostWeb.web.loadBaseUrl() } + else frostWeb.web.loadBaseUrl() + if (Showcase.firstWebOverlay) { + coordinator.frostSnackbar(R.string.web_overlay_swipe_hint) { + duration = Snackbar.LENGTH_INDEFINITE + setAction(R.string.kau_got_it) { _ -> this.dismiss() } + } + } + } + + /** + * Manage url loadings + * This is usually only called when multiple listeners are added and inject the same url + * We will avoid reloading if the url is the same + */ + override fun onNewIntent(intent: Intent) { + super.onNewIntent(intent) + val newUrl = (intent.extras?.getString(ARG_URL) ?: intent.dataString ?: return).formattedFbUrl + L.d("New intent") + if (url != newUrl) { + this.intent = intent + frostWeb.web.baseUrl = newUrl + frostWeb.web.loadBaseUrl() + } + } + + /** + * Our theme for the overlay should be fully opaque + */ + fun theme() { + val opaqueAccent = Prefs.headerColor.withAlpha(255) + statusBarColor = opaqueAccent.darken() + navigationBarColor = opaqueAccent + toolbar.setBackgroundColor(opaqueAccent) + toolbar.setTitleTextColor(Prefs.iconColor) + coordinator.setBackgroundColor(Prefs.bgColor.withAlpha(255)) + toolbar.overflowIcon?.setTint(Prefs.iconColor) + } + + override fun onPostCreate(savedInstanceState: Bundle?) { + super.onPostCreate(savedInstanceState) + kauSwipeOnPostCreate() + } + + override fun onDestroy() { + super.onDestroy() + kauSwipeOnDestroy() + } + + override fun onBackPressed() { + if (!frostWeb.onBackPressed()) { + finishSlideOut() + } + } + + override fun openFileChooser(filePathCallback: ValueCallback>, fileChooserParams: WebChromeClient.FileChooserParams) { + openFileChooser(this, filePathCallback, fileChooserParams) + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + if (onActivityResultWeb(requestCode, resultCode, data)) return + } + + override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + kauOnRequestPermissionsResult(permissions, grantResults) + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.menu_web, menu) + toolbar.tint(Prefs.iconColor) + setMenuIcons(menu, Prefs.iconColor, + R.id.action_share to CommunityMaterial.Icon.cmd_share, + R.id.action_copy_link to GoogleMaterial.Icon.gmd_content_copy) + return true + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.action_copy_link -> copyToClipboard(frostWeb.web.url) + R.id.action_share -> shareText(frostWeb.web.url) + else -> return super.onOptionsItemSelected(item) + } + return true + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbConst.kt b/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbConst.kt index e5216881..4d5127c5 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbConst.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbConst.kt @@ -8,6 +8,6 @@ const val FACEBOOK_COM = "facebook.com" const val FB_URL_BASE = "https://m.facebook.com/" fun PROFILE_PICTURE_URL(id: Long) = "https://graph.facebook.com/$id/picture?type=large" -const val USER_AGENT_BASIC_FULL = "Mozilla/5.0 (Linux; Android 4.4.2; en-us; SAMSUNG SM-G900T Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Version/1.6 Chrome/28.0.1500.94 Mobile Safari/537.36" +const val USER_AGENT_FULL = "Mozilla/5.0 (Linux; Android 4.4.2; en-us; SAMSUNG SM-G900T Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Version/1.6 Chrome/28.0.1500.94 Mobile Safari/537.36" const val USER_AGENT_BASIC = "Mozilla/5.0 (Linux; U; Android 2.3.3; en-gb; Nexus S Build/GRI20) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1" const val USER_AGENT_MESSENGER = "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36" \ No newline at end of file diff --git a/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbUrlFormatter.kt b/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbUrlFormatter.kt index e53bb202..622be067 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbUrlFormatter.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbUrlFormatter.kt @@ -14,10 +14,10 @@ class FbUrlFormatter(url: String) { init { var cleanedUrl = url - discardable.forEach { cleanedUrl = cleanedUrl.replace(it, "") } + discardable.forEach { cleanedUrl = cleanedUrl.replace(it, "", true) } val changed = cleanedUrl != url //note that discardables strip away the first ? - decoder.forEach { (k, v) -> cleanedUrl = cleanedUrl.replace(k, v) } - val qm = cleanedUrl.indexOf(if (changed)"&" else "?") + decoder.forEach { (k, v) -> cleanedUrl = cleanedUrl.replace(k, v, true) } + val qm = cleanedUrl.indexOf(if (changed) "&" else "?") if (qm > -1) { cleanedUrl.substring(qm + 1).split("&").forEach { val p = it.split("=") @@ -72,7 +72,15 @@ class FbUrlFormatter(url: String) { "%60" to "`", "%3B" to ";", "%2F" to "/", "%3F" to "?", "%3A" to ":", "%40" to "@", "%3D" to "=", "%26" to "&", "%24" to "$", "%2B" to "+", "%22" to "\"", "%2C" to ",", - "%20" to " " + "%20" to " ", + //css + "\\3C " to "<", "\\3E " to ">", "\\23 " to "#", "\\25 " to "%", + "\\7B " to "{", "\\7D " to "}", "\\7C " to "|", "\\5C " to "\\", + "\\5E " to "^", "\\7E " to "~", "\\5B " to "[", "\\5D " to "]", + "\\60 " to "`", "\\3B " to ";", "\\2F " to "/", "\\3F " to "?", + "\\3A " to ":", "\\40 " to "@", "\\3D " to "=", "\\26 " to "&", + "\\24 " to "$", "\\2B " to "+", "\\22 " to "\"", "\\2C " to ",", + "\\20 " to " " ) } } \ No newline at end of file diff --git a/app/src/main/kotlin/com/pitchedapps/frost/fragments/WebFragment.kt b/app/src/main/kotlin/com/pitchedapps/frost/fragments/WebFragment.kt index 87afc434..239f5842 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/fragments/WebFragment.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/fragments/WebFragment.kt @@ -7,7 +7,7 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import ca.allanwang.kau.utils.withArguments -import com.pitchedapps.frost.MainActivity +import com.pitchedapps.frost.activities.MainActivity import com.pitchedapps.frost.facebook.FbTab import com.pitchedapps.frost.facebook.FeedSort import com.pitchedapps.frost.utils.Prefs 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 7b60a718..737b86ab 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/services/FrostNotifications.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/services/FrostNotifications.kt @@ -17,7 +17,7 @@ import ca.allanwang.kau.utils.string import com.bumptech.glide.request.target.SimpleTarget import com.bumptech.glide.request.transition.Transition import com.pitchedapps.frost.BuildConfig -import com.pitchedapps.frost.FrostWebActivity +import com.pitchedapps.frost.activities.FrostWebActivity import com.pitchedapps.frost.R import com.pitchedapps.frost.dbflow.CookieModel import com.pitchedapps.frost.dbflow.fetchUsername diff --git a/app/src/main/kotlin/com/pitchedapps/frost/settings/Appearance.kt b/app/src/main/kotlin/com/pitchedapps/frost/settings/Appearance.kt index e05d0dd3..e814e1ac 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/settings/Appearance.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/settings/Appearance.kt @@ -5,9 +5,9 @@ import ca.allanwang.kau.kpref.activity.items.KPrefColorPicker import ca.allanwang.kau.kpref.activity.items.KPrefSeekbar import ca.allanwang.kau.ui.views.RippleCanvas import ca.allanwang.kau.utils.string -import com.pitchedapps.frost.MainActivity +import com.pitchedapps.frost.activities.MainActivity import com.pitchedapps.frost.R -import com.pitchedapps.frost.SettingsActivity +import com.pitchedapps.frost.activities.SettingsActivity import com.pitchedapps.frost.injectors.CssAssets import com.pitchedapps.frost.utils.* import com.pitchedapps.frost.utils.iab.IS_FROST_PRO diff --git a/app/src/main/kotlin/com/pitchedapps/frost/settings/Behaviour.kt b/app/src/main/kotlin/com/pitchedapps/frost/settings/Behaviour.kt index 7ce546b3..e7cf3598 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/settings/Behaviour.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/settings/Behaviour.kt @@ -1,9 +1,9 @@ package com.pitchedapps.frost.settings import ca.allanwang.kau.kpref.activity.KPrefAdapterBuilder -import com.pitchedapps.frost.MainActivity +import com.pitchedapps.frost.activities.MainActivity import com.pitchedapps.frost.R -import com.pitchedapps.frost.SettingsActivity +import com.pitchedapps.frost.activities.SettingsActivity import com.pitchedapps.frost.utils.Prefs /** diff --git a/app/src/main/kotlin/com/pitchedapps/frost/settings/Experimental.kt b/app/src/main/kotlin/com/pitchedapps/frost/settings/Experimental.kt index 86bd356b..236d19f9 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/settings/Experimental.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/settings/Experimental.kt @@ -1,9 +1,9 @@ package com.pitchedapps.frost.settings import ca.allanwang.kau.kpref.activity.KPrefAdapterBuilder -import com.pitchedapps.frost.MainActivity +import com.pitchedapps.frost.activities.MainActivity import com.pitchedapps.frost.R -import com.pitchedapps.frost.SettingsActivity +import com.pitchedapps.frost.activities.SettingsActivity import com.pitchedapps.frost.utils.Prefs import com.pitchedapps.frost.utils.Showcase @@ -20,10 +20,14 @@ fun SettingsActivity.getExperimentalPrefs(): KPrefAdapterBuilder.() -> Unit = { descRes = R.string.experimental_by_default_desc } + // Experimental content starts here ------------------ + checkbox(R.string.search_bar, { Prefs.searchBar }, { Prefs.searchBar = it; setResult(MainActivity.REQUEST_SEARCH) }) { descRes = R.string.search_bar_desc } + // Experimental content ends here -------------------- + checkbox(R.string.verbose_logging, { Prefs.verboseLogging }, { Prefs.verboseLogging = it }) { descRes = R.string.verbose_logging_desc } diff --git a/app/src/main/kotlin/com/pitchedapps/frost/settings/Feed.kt b/app/src/main/kotlin/com/pitchedapps/frost/settings/Feed.kt index 29256950..689e6bdb 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/settings/Feed.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/settings/Feed.kt @@ -2,9 +2,9 @@ package com.pitchedapps.frost.settings import ca.allanwang.kau.kpref.activity.KPrefAdapterBuilder import ca.allanwang.kau.utils.string -import com.pitchedapps.frost.MainActivity +import com.pitchedapps.frost.activities.MainActivity import com.pitchedapps.frost.R -import com.pitchedapps.frost.SettingsActivity +import com.pitchedapps.frost.activities.SettingsActivity import com.pitchedapps.frost.facebook.FeedSort import com.pitchedapps.frost.utils.Prefs import com.pitchedapps.frost.utils.materialDialogThemed diff --git a/app/src/main/kotlin/com/pitchedapps/frost/settings/Notifications.kt b/app/src/main/kotlin/com/pitchedapps/frost/settings/Notifications.kt index b6105054..59ebf700 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/settings/Notifications.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/settings/Notifications.kt @@ -4,7 +4,7 @@ import ca.allanwang.kau.kpref.activity.KPrefAdapterBuilder import ca.allanwang.kau.utils.minuteToText import ca.allanwang.kau.utils.snackbar import com.pitchedapps.frost.R -import com.pitchedapps.frost.SettingsActivity +import com.pitchedapps.frost.activities.SettingsActivity import com.pitchedapps.frost.services.fetchNotifications import com.pitchedapps.frost.services.scheduleNotifications import com.pitchedapps.frost.utils.Prefs 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 d2e04898..f8c7af56 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/utils/Prefs.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/utils/Prefs.kt @@ -4,6 +4,7 @@ import ca.allanwang.kau.kotlin.lazyResettable import ca.allanwang.kau.kpref.KPref import ca.allanwang.kau.kpref.StringSet import ca.allanwang.kau.kpref.kpref +import ca.allanwang.kau.utils.colorToForeground import ca.allanwang.kau.utils.isColorVisibleOn import com.pitchedapps.frost.facebook.FeedSort import com.pitchedapps.frost.injectors.InjectorContract @@ -57,9 +58,24 @@ object Prefs : KPref() { val iconColor: Int get() = t.iconColor + /** + * Ensures that the color is visible against the background + */ val accentColor: Int get() = if (headerColor.isColorVisibleOn(bgColor, 100)) headerColor else textColor + /** + * Ensures that the color is visible against both the foreground and background + */ + val accentBackgroundColor: Int + get() { + if (headerColor.isColorVisibleOn(textColor, 100)) { + if (headerColor.isColorVisibleOn(bgColor, 100)) return headerColor + else return headerColor.colorToForeground(0.2f) + } + return bgColor.colorToForeground(0.2f) + } + val themeInjector: InjectorContract get() = t.injector 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 9a599945..c0db8308 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/utils/Utils.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/utils/Utils.kt @@ -8,19 +8,27 @@ import android.support.annotation.StringRes import android.support.design.internal.SnackbarContentLayout import android.support.design.widget.Snackbar import android.support.v7.widget.Toolbar +import android.util.Log import android.view.View import android.widget.FrameLayout import android.widget.TextView import ca.allanwang.kau.utils.* import com.afollestad.materialdialogs.MaterialDialog +import com.bumptech.glide.GlideBuilder import com.bumptech.glide.RequestBuilder +import com.bumptech.glide.annotation.GlideExtension import com.bumptech.glide.annotation.GlideModule import com.bumptech.glide.load.resource.bitmap.CircleCrop import com.bumptech.glide.module.AppGlideModule import com.bumptech.glide.request.RequestOptions import com.crashlytics.android.answers.Answers import com.crashlytics.android.answers.CustomEvent -import com.pitchedapps.frost.* +import com.pitchedapps.frost.BuildConfig +import com.pitchedapps.frost.R +import com.pitchedapps.frost.activities.ImageActivity +import com.pitchedapps.frost.activities.LoginActivity +import com.pitchedapps.frost.activities.SelectorActivity +import com.pitchedapps.frost.activities.WebOverlayActivity import com.pitchedapps.frost.dbflow.CookieModel import com.pitchedapps.frost.facebook.FbTab import com.pitchedapps.frost.facebook.formattedFbUrl @@ -31,6 +39,8 @@ import com.pitchedapps.frost.facebook.formattedFbUrl const val EXTRA_COOKIES = "extra_cookies" 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" @GlideModule class FrostGlideModule : AppGlideModule() @@ -59,6 +69,13 @@ fun Context.launchWebOverlay(url: String) { }) } +fun Context.launchImageActivity(imageUrl: String, text: String?) { + startActivity(ImageActivity::class.java, intentBuilder = { + putExtra(ARG_IMAGE_URL, imageUrl) + putExtra(ARG_TEXT, text) + }) +} + fun WebOverlayActivity.url(): String { return intent.extras?.getString(ARG_URL) ?: FbTab.FEED.url } @@ -131,4 +148,4 @@ fun Activity.frostNavigationBar() { navigationBarColor = if (Prefs.tintNavBar) Prefs.headerColor else Color.BLACK } -fun RequestBuilder.withRoundIcon() = apply(RequestOptions().transform(CircleCrop())) +fun RequestBuilder.withRoundIcon() = apply(RequestOptions().transform(CircleCrop())) \ No newline at end of file diff --git a/app/src/main/kotlin/com/pitchedapps/frost/utils/WebContextMenu.kt b/app/src/main/kotlin/com/pitchedapps/frost/utils/WebContextMenu.kt index 67c20a5a..17ea46a3 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/utils/WebContextMenu.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/utils/WebContextMenu.kt @@ -4,7 +4,8 @@ import android.content.Context import ca.allanwang.kau.utils.copyToClipboard import ca.allanwang.kau.utils.shareText import ca.allanwang.kau.utils.string -import com.pitchedapps.frost.MainActivity +import ca.allanwang.kau.utils.toast +import com.pitchedapps.frost.activities.MainActivity import com.pitchedapps.frost.R /** @@ -18,7 +19,10 @@ fun Context.showWebContextMenu(wc: WebContext) { materialDialogThemed { title(title) - items(WebContextType.values.map { this@showWebContextMenu.string(it.textId) }) + items(WebContextType.values.map { + if (it == WebContextType.COPY_TEXT && wc.text == null) return@map null + this@showWebContextMenu.string(it.textId) + }.filterNotNull()) itemsCallback { _, _, position, _ -> WebContextType[position].onClick(this@showWebContextMenu, wc) @@ -30,11 +34,11 @@ fun Context.showWebContextMenu(wc: WebContext) { } } -class WebContext(val url: String, val text: String) +class WebContext(val url: String, val text: String?) enum class WebContextType(val textId: Int, val onClick: (c: Context, wc: WebContext) -> Unit) { COPY_LINK(R.string.copy_link, { c, wc -> c.copyToClipboard(wc.url) }), - COPY_TEXT(R.string.copy_text, { c, wc -> c.copyToClipboard(wc.text) }), + COPY_TEXT(R.string.copy_text, { c, wc -> if (wc.text != null) c.copyToClipboard(wc.text) else c.toast(R.string.no_text) }), SHARE_LINK(R.string.share_link, { c, wc -> c.shareText(wc.url) }) ; diff --git a/app/src/main/kotlin/com/pitchedapps/frost/utils/iab/IAB.kt b/app/src/main/kotlin/com/pitchedapps/frost/utils/iab/IAB.kt index 58c748c2..964e771c 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/utils/iab/IAB.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/utils/iab/IAB.kt @@ -9,7 +9,7 @@ import ca.allanwang.kau.utils.snackbar import com.crashlytics.android.answers.PurchaseEvent import com.pitchedapps.frost.BuildConfig import com.pitchedapps.frost.R -import com.pitchedapps.frost.SettingsActivity +import com.pitchedapps.frost.activities.SettingsActivity import com.pitchedapps.frost.utils.* /** diff --git a/app/src/main/kotlin/com/pitchedapps/frost/utils/iab/IABDialogs.kt b/app/src/main/kotlin/com/pitchedapps/frost/utils/iab/IABDialogs.kt index 4f65b7f8..fae2f6bb 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/utils/iab/IABDialogs.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/utils/iab/IABDialogs.kt @@ -4,9 +4,9 @@ import android.app.Activity import ca.allanwang.kau.utils.restart import ca.allanwang.kau.utils.startPlayStoreLink import ca.allanwang.kau.utils.string -import com.pitchedapps.frost.MainActivity +import com.pitchedapps.frost.activities.MainActivity import com.pitchedapps.frost.R -import com.pitchedapps.frost.SettingsActivity +import com.pitchedapps.frost.activities.SettingsActivity import com.pitchedapps.frost.utils.L import com.pitchedapps.frost.utils.Prefs import com.pitchedapps.frost.utils.materialDialogThemed diff --git a/app/src/main/kotlin/com/pitchedapps/frost/web/BaseWebViewClient.kt b/app/src/main/kotlin/com/pitchedapps/frost/web/BaseWebViewClient.kt new file mode 100644 index 00000000..09241254 --- /dev/null +++ b/app/src/main/kotlin/com/pitchedapps/frost/web/BaseWebViewClient.kt @@ -0,0 +1,16 @@ +package com.pitchedapps.frost.web + +import android.webkit.WebResourceRequest +import android.webkit.WebResourceResponse +import android.webkit.WebView +import android.webkit.WebViewClient + +/** + * Created by Allan Wang on 2017-07-13. + */ +open class BaseWebViewClient : WebViewClient() { + + override fun shouldInterceptRequest(view: WebView, request: WebResourceRequest): WebResourceResponse? + = shouldFrostInterceptRequest(view, request) + +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostChromeClient.kt b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostChromeClient.kt index aab3a165..4df6d6a7 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostChromeClient.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostChromeClient.kt @@ -1,9 +1,10 @@ package com.pitchedapps.frost.web import android.net.Uri -import android.os.Message -import android.view.View -import android.webkit.* +import android.webkit.ConsoleMessage +import android.webkit.ValueCallback +import android.webkit.WebChromeClient +import android.webkit.WebView import ca.allanwang.kau.utils.snackbar import com.pitchedapps.frost.contracts.ActivityWebContract import com.pitchedapps.frost.utils.L @@ -20,9 +21,16 @@ class FrostChromeClient(webCore: FrostWebViewCore) : WebChromeClient() { val titleObservable: BehaviorSubject = webCore.titleObservable val activityContract = (webCore.context as? ActivityWebContract) + companion object { + val consoleBlacklist = setOf( + "edge-chat" + ) + } + override fun onConsoleMessage(consoleMessage: ConsoleMessage): Boolean { + if (consoleBlacklist.any { consoleMessage.message().contains(it) }) return true L.i("Chrome Console ${consoleMessage.lineNumber()}: ${consoleMessage.message()}") - return super.onConsoleMessage(consoleMessage) + return true } override fun onReceivedTitle(view: WebView, title: String) { diff --git a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostJSI.kt b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostJSI.kt index 3340e7d2..3f976fb8 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostJSI.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostJSI.kt @@ -2,8 +2,9 @@ package com.pitchedapps.frost.web import android.content.Context import android.webkit.JavascriptInterface -import ca.allanwang.kau.logging.KL -import com.pitchedapps.frost.MainActivity +import ca.allanwang.kau.utils.startActivity +import com.pitchedapps.frost.activities.ImageActivity +import com.pitchedapps.frost.activities.MainActivity import com.pitchedapps.frost.dbflow.CookieModel import com.pitchedapps.frost.facebook.formattedFbUrl import com.pitchedapps.frost.utils.* @@ -13,12 +14,18 @@ import io.reactivex.subjects.Subject /** * Created by Allan Wang on 2017-06-01. */ -class FrostJSI(val context: Context, val webView: FrostWebViewCore) { +class FrostJSI(val webView: FrostWebViewCore) { - val headerObservable: Subject? = (context as? MainActivity)?.headerBadgeObservable + val context: Context + get() = webView.context + + val activity: MainActivity? + get() = (context as? MainActivity) + + val headerObservable: Subject? = activity?.headerBadgeObservable val cookies: ArrayList - get() = (context as? MainActivity)?.cookies() ?: arrayListOf() + get() = activity?.cookies() ?: arrayListOf() @JavascriptInterface fun loadUrl(url: String) { @@ -35,8 +42,8 @@ class FrostJSI(val context: Context, val webView: FrostWebViewCore) { } @JavascriptInterface - fun contextMenu(url: String, text: String) { - webView.post { webView.context.showWebContextMenu(WebContext(url.formattedFbUrl, text)) } + fun contextMenu(url: String, text: String?) { + webView.post { context.showWebContextMenu(WebContext(url.formattedFbUrl, text)) } } /** @@ -45,7 +52,7 @@ class FrostJSI(val context: Context, val webView: FrostWebViewCore) { */ @JavascriptInterface fun longClick(start: Boolean) { - (webView.context as? MainActivity)?.viewPager?.enableSwipe = !start + activity?.viewPager?.enableSwipe = !start } @JavascriptInterface @@ -53,6 +60,14 @@ class FrostJSI(val context: Context, val webView: FrostWebViewCore) { context.launchLogin(cookies, true) } + /** + * Launch image overlay + */ + @JavascriptInterface + fun loadImage(imageUrl: String, text: String?) { + context.launchImageActivity(imageUrl, text) + } + @JavascriptInterface fun emit(flag: Int) { webView.post { webView.frostWebClient.emit(flag) } diff --git a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostRequestInterceptor.kt b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostRequestInterceptor.kt new file mode 100644 index 00000000..45dc83aa --- /dev/null +++ b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostRequestInterceptor.kt @@ -0,0 +1,67 @@ +package com.pitchedapps.frost.web + +import android.graphics.Bitmap.CompressFormat +import android.webkit.WebResourceRequest +import android.webkit.WebResourceResponse +import android.webkit.WebView +import ca.allanwang.kau.utils.use +import com.pitchedapps.frost.utils.GlideApp +import com.pitchedapps.frost.utils.L +import com.pitchedapps.frost.utils.Prefs +import okhttp3.HttpUrl +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.io.InputStream + + +/** + * Created by Allan Wang on 2017-07-13. + * + * Handler to decide when a request should be done by us + * This is the crux of Frost's optimizations for the web browser + */ +val blankResource: WebResourceResponse by lazy { WebResourceResponse("text/plain", "utf-8", ByteArrayInputStream("".toByteArray())) } + +//these hosts will redirect to a blank resource +val blacklistHost: Set by lazy { + setOf( + "edge-chat.facebook.com" + ) +} + +//these hosts will return null and skip logging +val whitelistHost: Set by lazy { + setOf( + "static.xx.fbcdn.net", + "m.facebook.com", + "touch.facebook.com" + ) +} + +//these hosts will skip ad inspection +//this list does not have to include anything from the two above +val adWhitelistHost: Set by lazy { + setOf( + "scontent-sea1-1.xx.fbcdn.net" + ) +} + +var adblock: Set? = null + +fun shouldFrostInterceptRequest(view: WebView, request: WebResourceRequest): WebResourceResponse? { + val httpUrl = HttpUrl.parse(request.url?.toString() ?: return null) ?: return null + val host = httpUrl.host() + val url = httpUrl.toString() + if (blacklistHost.contains(host)) return blankResource + if (whitelistHost.contains(host)) return null + if (!adWhitelistHost.contains(host)) { + if (adblock == null) adblock = view.context.assets.open("adblock.txt").bufferedReader().use { it.readLines().toSet() } + if (adblock?.any { url.contains(it) } ?: false) return blankResource + } + L.v("Intercept Request ${host} ${url}") + return null +} + +fun WebResourceResponse?.filterCss(request: WebResourceRequest): WebResourceResponse? + = this ?: if (request.url.path.endsWith(".css")) blankResource else null + diff --git a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebView.kt b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebView.kt index 7c0a6597..e7dae22a 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebView.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebView.kt @@ -69,7 +69,7 @@ class FrostWebView @JvmOverloads constructor( frostWebClient = baseEnum?.webClient?.invoke(this) ?: FrostWebViewClient(this) webViewClient = frostWebClient webChromeClient = FrostChromeClient(this) - addJavascriptInterface(FrostJSI(context, this), "Frost") + addJavascriptInterface(FrostJSI(this), "Frost") setBackgroundColor(Color.TRANSPARENT) } } diff --git a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebViewClient.kt b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebViewClient.kt index 5e5dc597..5b2b4bfd 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebViewClient.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebViewClient.kt @@ -4,10 +4,9 @@ import android.content.Context import android.graphics.Bitmap import android.webkit.WebResourceRequest import android.webkit.WebView -import android.webkit.WebViewClient -import com.pitchedapps.frost.LoginActivity -import com.pitchedapps.frost.MainActivity -import com.pitchedapps.frost.SelectorActivity +import com.pitchedapps.frost.activities.LoginActivity +import com.pitchedapps.frost.activities.MainActivity +import com.pitchedapps.frost.activities.SelectorActivity import com.pitchedapps.frost.facebook.FACEBOOK_COM import com.pitchedapps.frost.facebook.FbCookie import com.pitchedapps.frost.injectors.* @@ -17,7 +16,7 @@ import io.reactivex.subjects.Subject /** * Created by Allan Wang on 2017-05-31. */ -open class FrostWebViewClient(val webCore: FrostWebViewCore) : WebViewClient() { +open class FrostWebViewClient(val webCore: FrostWebViewCore) : BaseWebViewClient() { val refreshObservable: Subject = webCore.refreshObservable @@ -97,15 +96,9 @@ open class FrostWebViewClient(val webCore: FrostWebViewCore) : WebViewClient() { return super.shouldOverrideUrlLoading(view, request) } - override fun onPageCommitVisible(view: WebView?, url: String?) { - L.d("ASDF PCV") - super.onPageCommitVisible(view, url) - } - -// override fun onLoadResource(view: WebView, url: String) { -// L.v("Load resource $url") -// super.onLoadResource(view, url) +// override fun onPageCommitVisible(view: WebView?, url: String?) { +// L.d("ASDF PCV") +// super.onPageCommitVisible(view, url) // } - } \ No newline at end of file diff --git a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebViewCore.kt b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebViewCore.kt index 1e023dca..d96fba55 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebViewCore.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebViewCore.kt @@ -72,7 +72,7 @@ class FrostWebViewCore @JvmOverloads constructor( dispose = refreshObservable.subscribeOn(AndroidSchedulers.mainThread()).subscribe { if (it) { loading = true - if (isVisible()) fadeOut(duration = 200L) + if (isVisible) fadeOut(duration = 200L) } else if (loading) { dispose?.dispose() if (animate && Prefs.animate) circularReveal(offset = 150L) @@ -148,11 +148,12 @@ class FrostWebViewCore @JvmOverloads constructor( if (scrollY > 10000) { scrollTo(0, 0) } else { - val animator = ValueAnimator.ofInt(scrollY, 0) - animator.duration = Math.min(scrollY, 500).toLong() - animator.interpolator = DecelerateInterpolator() - animator.addUpdateListener { scrollY = it.animatedValue as Int } - animator.start() + ValueAnimator.ofInt(scrollY, 0).apply { + duration = Math.min(scrollY, 500).toLong() + interpolator = DecelerateInterpolator() + addUpdateListener { scrollY = it.animatedValue as Int } + start() + } } } diff --git a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebViewSearch.kt b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebViewSearch.kt index b3b1cfe5..bcadf32a 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebViewSearch.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebViewSearch.kt @@ -3,9 +3,7 @@ package com.pitchedapps.frost.web import android.annotation.SuppressLint import android.content.Context import android.view.View -import android.webkit.JavascriptInterface -import android.webkit.WebView -import android.webkit.WebViewClient +import android.webkit.* import ca.allanwang.kau.searchview.SearchItem import ca.allanwang.kau.utils.gone import com.pitchedapps.frost.facebook.FbTab @@ -56,7 +54,8 @@ class FrostWebViewSearch(context: Context, val contract: SearchContract) : WebVi settings.javaScriptEnabled = true settings.userAgentString = USER_AGENT_BASIC setLayerType(View.LAYER_TYPE_HARDWARE, null) - webViewClient = FrostWebViewClientSearch() + webViewClient = SearchWebViewClient() + webChromeClient = SearchChromeClient() addJavascriptInterface(SearchJSI(), "Frost") searchSubject.debounce(300, TimeUnit.MILLISECONDS).subscribeOn(Schedulers.newThread()) .map { @@ -111,13 +110,22 @@ class FrostWebViewSearch(context: Context, val contract: SearchContract) : WebVi * * Barebones client that does what [FrostWebViewSearch] needs */ - inner class FrostWebViewClientSearch : WebViewClient() { + inner class SearchWebViewClient : BaseWebViewClient() { override fun onPageFinished(view: WebView, url: String) { super.onPageFinished(view, url) L.i("Search Page finished $url") view.jsInject(JsAssets.SEARCH) } + + override fun shouldInterceptRequest(view: WebView, request: WebResourceRequest): WebResourceResponse? + = super.shouldInterceptRequest(view, request).filterCss(request) + } + + class SearchChromeClient : WebChromeClient() { + + //mute console + override fun onConsoleMessage(consoleMessage: ConsoleMessage) = true } inner class SearchJSI { 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 38d4ad8c..d7a2db0a 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/web/LoginWebView.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/web/LoginWebView.kt @@ -66,7 +66,7 @@ class LoginWebView @JvmOverloads constructor( } - inner class LoginClient : WebViewClient() { + inner class LoginClient : BaseWebViewClient() { override fun onPageFinished(view: WebView, url: String) { super.onPageFinished(view, url) @@ -88,7 +88,7 @@ class LoginWebView @JvmOverloads constructor( inner class LoginChromeClient : WebChromeClient() { override fun onConsoleMessage(consoleMessage: ConsoleMessage): Boolean { L.d("Login Console ${consoleMessage.lineNumber()}: ${consoleMessage.message()}") - return super.onConsoleMessage(consoleMessage) + return true } override fun onProgressChanged(view: WebView, newProgress: Int) { -- cgit v1.2.3