Drawables and strings and everything else. Some unconverted java classes.

This commit is contained in:
Amine 2017-05-28 02:29:42 +02:00
parent 1d767ab99d
commit 97cee06ebe
125 changed files with 4619 additions and 35 deletions

View File

@ -17,7 +17,9 @@ You'll have to:
- Define the following in `res/values/strings.xml` or create `res/values/secrets.xml`
- mercury: A [Mercury](https://mercury.postlight.com/web-parser/) web parser api key for the internal browser
- feedback_email: An email to receive users feedback.
- source_url: an url to the source code, used in the settings
- tracker_url: an url to the tracker, used in the settings
## Useful links

View File

@ -22,7 +22,7 @@ android {
compileSdkVersion 25
buildToolsVersion "25.0.3"
defaultConfig {
applicationId "bou.amine.apps.readerforselfoss"
applicationId "apps.amine.bou.readerforselfoss"
minSdkVersion 16
targetSdkVersion 25
versionCode 1500
@ -64,21 +64,62 @@ dependencies {
// Android Support
compile 'com.android.support:appcompat-v7:25.3.1'
compile 'com.android.support:design:25.3.1'
compile 'com.android.support:recyclerview-v7:25.3.1'
compile 'com.android.support:support-v4:25.3.1'
compile 'com.android.support:support-vector-drawable:25.3.1'
compile 'com.android.support:customtabs:25.3.1'
compile 'com.android.support:cardview-v7:25.3.1'
compile 'com.android.support.constraint:constraint-layout:1.0.2'
// Firebase + crashlytics
compile 'com.google.firebase:firebase-core:10.2.4'
compile 'com.google.firebase:firebase-config:10.2.4'
compile 'com.google.firebase:firebase-invites:10.2.4'
compile 'com.google.android.gms:play-services:10.2.6'
compile 'com.google.firebase:firebase-core:10.2.6'
compile 'com.google.firebase:firebase-config:10.2.6'
compile 'com.google.firebase:firebase-invites:10.2.6'
compile('com.crashlytics.sdk.android:crashlytics:2.6.8@aar') {
transitive = true;
transitive = true
}
//multidex
compile 'com.android.support:multidex:1.0.1'
// Intro
compile 'agency.tango.android:material-intro-screen:0.0.5'
// About
compile('com.mikepenz:aboutlibraries:5.9.6@aar') {
transitive = true
}
// Retrofit + http logging + okhttp
compile 'com.squareup.retrofit2:retrofit:2.1.0'
compile 'com.squareup.okhttp3:logging-interceptor:3.4.1'
compile 'com.squareup.retrofit2:converter-gson:2.1.0'
compile 'com.burgstaller:okhttp-digest:1.12'
// Material-ish things
compile 'com.roughike:bottom-bar:2.2.0'
compile 'com.melnykov:floatingactionbutton:1.3.0'
compile 'com.github.jd-alexander:LikeButton:0.2.1'
compile 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1'
compile 'org.sufficientlysecure:html-textview:3.3'
// glide
compile 'com.github.bumptech.glide:glide:3.7.0'
// Asking politely users to rate the app
compile 'com.github.stkent:amplify:1.5.0'
// For the article reader
compile 'com.klinkerapps:drag-dismiss-activity:1.4.0'
}
apply plugin: 'com.google.gms.google-services'

View File

@ -23,3 +23,9 @@
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
#About libraries
-keep class .R
-keep class **.R$* {
<fields>;
}

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="bou.amine.apps.readerforselfoss">
package="apps.amine.bou.readerforselfoss">
<uses-permission android:name="android.permission.INTERNET" />
@ -20,7 +20,46 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".IntroActivity"
android:theme="@style/Theme.Intro">
</activity>
<activity android:name=".LoginActivity"
android:label="@string/title_activity_login">
</activity>
<activity android:name=".HomeActivity">
</activity>
<activity
android:name=".settings.SettingsActivity"
android:label="@string/title_activity_settings"
android:parentActivityName=".HomeActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="apps.amine.bou.readerforselfoss.HomeActivity" />
</activity>
<activity android:name=".SourcesActivity"
android:parentActivityName=".HomeActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".HomeActivity" />
</activity>
<activity android:name=".AddSourceActivity"
android:parentActivityName=".SourcesActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".SourcesActivity" />
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plain" />
</intent-filter>
</activity>
<activity android:name=".ReaderActivity"
android:theme="@style/DragDismissTheme">
</activity>
</application>
</manifest>

View File

@ -0,0 +1,122 @@
package apps.amine.bou.readerforselfoss
import android.content.Intent
import android.os.Bundle
import android.support.constraint.ConstraintLayout
import android.support.v7.app.AppCompatActivity
import android.view.View
import android.widget.*
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
import apps.amine.bou.readerforselfoss.api.selfoss.Spout
import apps.amine.bou.readerforselfoss.api.selfoss.SuccessResponse
import apps.amine.bou.readerforselfoss.utils.Config
import apps.amine.bou.readerforselfoss.utils.isUrlValid
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
class AddSourceActivity : AppCompatActivity() {
private var mSpoutsValue: String? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_add_source)
val mProgress = findViewById(R.id.progress) as ProgressBar
val mForm = findViewById(R.id.formContainer) as ConstraintLayout
val mNameInput = findViewById(R.id.nameInput) as EditText
val mSourceUri = findViewById(R.id.sourceUri) as EditText
val mTags = findViewById(R.id.tags) as EditText
val mSpoutsSpinner = findViewById(R.id.spoutsSpinner) as Spinner
val mSaveBtn = findViewById(R.id.saveBtn) as Button
val api = SelfossApi(this)
val intent = intent
if (Intent.ACTION_SEND == intent.action && "text/plain" == intent.type) {
mSourceUri.setText(intent.getStringExtra(Intent.EXTRA_TEXT))
mNameInput.setText(intent.getStringExtra(Intent.EXTRA_TITLE))
}
mSaveBtn.setOnClickListener { handleSaveSource(mTags, mNameInput.text.toString(), mSourceUri.text.toString(), api) }
val spoutsKV = HashMap<String, String>()
mSpoutsSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(adapterView: AdapterView<*>, view: View, i: Int, l: Long) {
val spoutName = (view as TextView).text.toString()
mSpoutsValue = spoutsKV[spoutName]
}
override fun onNothingSelected(adapterView: AdapterView<*>) {
mSpoutsValue = null
}
}
val config = Config(this)
if (config.baseUrl.isEmpty() || !isUrlValid(config.baseUrl)) {
Toast.makeText(this, getString(R.string.addStringNoUrl), Toast.LENGTH_SHORT).show()
val i = Intent(this, LoginActivity::class.java)
startActivity(i)
finish()
} else {
var items: Map<String, Spout>
api.spouts().enqueue(object : Callback<Map<String, Spout>> {
override fun onResponse(call: Call<Map<String, Spout>>, response: Response<Map<String, Spout>>) {
if (response.body() != null) {
items = response.body()
val itemsStrings = items.map { it.value.name }
for ((key, value) in items) {
spoutsKV.put(value.name, key)
}
mProgress.visibility = View.GONE
mForm.visibility = View.VISIBLE
val spinnerArrayAdapter = ArrayAdapter(this@AddSourceActivity, android.R.layout.simple_spinner_item, itemsStrings)
spinnerArrayAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
mSpoutsSpinner.adapter = spinnerArrayAdapter
} else {
handleProblemWithSpouts()
}
}
override fun onFailure(call: Call<Map<String, Spout>>, t: Throwable) {
handleProblemWithSpouts()
}
private fun handleProblemWithSpouts() {
Toast.makeText(this@AddSourceActivity, R.string.cant_get_spouts, Toast.LENGTH_SHORT).show()
mProgress.visibility = View.GONE
}
})
}
}
private fun handleSaveSource(mTags: EditText, title: String, url: String, api: SelfossApi) {
if (title.isEmpty() || url.isEmpty() || mSpoutsValue == null || mSpoutsValue!!.isEmpty()) {
Toast.makeText(this, R.string.form_not_complete, Toast.LENGTH_SHORT).show()
} else {
api.createSource(title, url, mSpoutsValue, mTags.text.toString(), "").enqueue(object : Callback<SuccessResponse> {
override fun onResponse(call: Call<SuccessResponse>, response: Response<SuccessResponse>) {
if (response.body() != null && response.body().isSuccess) {
finish()
} else {
Toast.makeText(this@AddSourceActivity, R.string.cant_create_source, Toast.LENGTH_SHORT).show()
}
}
override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
Toast.makeText(this@AddSourceActivity, R.string.cant_create_source, Toast.LENGTH_SHORT).show()
}
})
}
}
}

View File

@ -0,0 +1,477 @@
package apps.amine.bou.readerforselfoss
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.net.Uri
import android.os.Bundle
import android.preference.PreferenceManager
import android.support.design.widget.CoordinatorLayout
import android.support.v4.widget.SwipeRefreshLayout
import android.support.v7.app.AppCompatActivity
import android.support.v7.widget.GridLayoutManager
import android.support.v7.widget.RecyclerView
import android.support.v7.widget.StaggeredGridLayoutManager
import android.support.v7.widget.helper.ItemTouchHelper
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.widget.Toast
import apps.amine.bou.readerforselfoss.adapters.ItemCardAdapter
import apps.amine.bou.readerforselfoss.adapters.ItemListAdapter
import apps.amine.bou.readerforselfoss.api.selfoss.Item
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
import apps.amine.bou.readerforselfoss.api.selfoss.Stats
import apps.amine.bou.readerforselfoss.api.selfoss.SuccessResponse
import apps.amine.bou.readerforselfoss.settings.SettingsActivity
import apps.amine.bou.readerforselfoss.utils.Config
import apps.amine.bou.readerforselfoss.utils.checkAndDisplayStoreApk
import apps.amine.bou.readerforselfoss.utils.checkApkVersion
import apps.amine.bou.readerforselfoss.utils.customtabs.CustomTabActivityHelper
import com.crashlytics.android.answers.Answers
import com.crashlytics.android.answers.InviteEvent
import com.github.stkent.amplify.prompt.DefaultLayoutPromptView
import com.github.stkent.amplify.tracking.Amplify
import com.google.android.gms.appinvite.AppInviteInvitation
import com.google.android.gms.common.ConnectionResult
import com.google.android.gms.common.GoogleApiAvailability
import com.google.firebase.crash.FirebaseCrash
import com.google.firebase.remoteconfig.FirebaseRemoteConfig
import com.mikepenz.aboutlibraries.Libs
import com.mikepenz.aboutlibraries.LibsBuilder
import com.roughike.bottombar.BottomBar
import com.roughike.bottombar.BottomBarTab
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
class HomeActivity : AppCompatActivity() {
private val MENU_PREFERENCES = 12302
private val REQUEST_INVITE = 13231
private val REQUEST_INVITE_BYMAIL = 13232
private var mRecyclerView: RecyclerView? = null
private var api: SelfossApi? = null
private var items: List<Item>? = null
private var mCustomTabActivityHelper: CustomTabActivityHelper? = null
private var clickBehavior = false
private var internalBrowser = false
private var articleViewer = false
private var shouldBeCardView = false
private var displayUnreadCount = false
private var displayAllCount = false
private var editor: SharedPreferences.Editor? = null
private val UNREAD_SHOWN = 1
private val READ_SHOWN = 2
private val FAV_SHOWN = 3
private var elementsShown: Int = 0
private var mBottomBar: BottomBar? = null
private var mCoordinatorLayout: CoordinatorLayout? = null
private var mSwipeRefreshLayout: SwipeRefreshLayout? = null
private var sharedPref: SharedPreferences? = null
private var tabNew: BottomBarTab? = null
private var tabArchive: BottomBarTab? = null
private var tabStarred: BottomBarTab? = null
private var mFirebaseRemoteConfig: FirebaseRemoteConfig? = null
private var fullHeightCards: Boolean = false
private fun handleSharedPrefs() {
clickBehavior = this.sharedPref!!.getBoolean("tab_on_tap", false)
internalBrowser = this.sharedPref!!.getBoolean("prefer_internal_browser", true)
articleViewer = this.sharedPref!!.getBoolean("prefer_article_viewer", true)
shouldBeCardView = this.sharedPref!!.getBoolean("card_view_active", false)
displayUnreadCount = this.sharedPref!!.getBoolean("display_unread_count", true)
displayAllCount = this.sharedPref!!.getBoolean("display_other_count", false)
fullHeightCards = this.sharedPref!!.getBoolean("full_height_cards", false)
}
override fun onResume() {
super.onResume()
sharedPref = PreferenceManager.getDefaultSharedPreferences(this)
val settings = getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE)
editor = settings.edit()
if (BuildConfig.GITHUB_VERSION) {
checkApkVersion(settings, editor!!, this@HomeActivity, mFirebaseRemoteConfig!!)
}
handleSharedPrefs()
tabNew = mBottomBar!!.getTabWithId(R.id.tab_new)
tabArchive = mBottomBar!!.getTabWithId(R.id.tab_archive)
tabStarred = mBottomBar!!.getTabWithId(R.id.tab_fav)
getElementsAccordingToTab()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_home)
if (savedInstanceState == null) {
val promptView = findViewById(R.id.prompt_view) as DefaultLayoutPromptView
Amplify.getSharedInstance().promptIfReady(promptView)
}
mFirebaseRemoteConfig = FirebaseRemoteConfig.getInstance()
mFirebaseRemoteConfig!!.setDefaults(R.xml.default_remote_config)
mCustomTabActivityHelper = CustomTabActivityHelper()
api = SelfossApi(this)
items = ArrayList()
mBottomBar = findViewById(R.id.bottomBar) as BottomBar
// TODO: clean this hack
val listenerAlreadySet = booleanArrayOf(false)
mBottomBar!!.setOnTabSelectListener { tabId ->
if (listenerAlreadySet[0]) {
if (tabId == R.id.tab_new) {
getUnRead()
} else if (tabId == R.id.tab_archive) {
getRead()
} else if (tabId == R.id.tab_fav) {
getStarred()
}
getElementsAccordingToTab()
} else {
listenerAlreadySet[0] = true
}
}
mCoordinatorLayout = findViewById(R.id.coordLayout) as CoordinatorLayout
mSwipeRefreshLayout = findViewById(R.id.swipeRefreshLayout) as SwipeRefreshLayout
mRecyclerView = findViewById(R.id.my_recycler_view) as RecyclerView
reloadLayoutManager()
mSwipeRefreshLayout!!.setColorSchemeResources(
R.color.refresh_progress_1,
R.color.refresh_progress_2,
R.color.refresh_progress_3)
mSwipeRefreshLayout!!.setOnRefreshListener { getElementsAccordingToTab() }
val simpleItemTouchCallback = object : ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT) {
override fun getSwipeDirs(recyclerView: RecyclerView?, viewHolder: RecyclerView.ViewHolder?): Int {
if (elementsShown != UNREAD_SHOWN) {
return 0
} else {
return super.getSwipeDirs(recyclerView, viewHolder)
}
}
override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean {
return false
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, swipeDir: Int) {
try {
val i = items!![viewHolder.adapterPosition]
val position = items!!.indexOf(i)
if (shouldBeCardView) {
(mRecyclerView!!.adapter as ItemCardAdapter).removeItemAtIndex(position)
} else {
(mRecyclerView!!.adapter as ItemListAdapter).removeItemAtIndex(position)
}
tabNew!!.setBadgeCount(items!!.size - 1)
} catch (e: IndexOutOfBoundsException) {
FirebaseCrash.logcat(Log.ERROR, "SWIPE ERROR", "Swipe index out of bound")
FirebaseCrash.report(e)
}
}
}
val itemTouchHelper = ItemTouchHelper(simpleItemTouchCallback)
itemTouchHelper.attachToRecyclerView(mRecyclerView)
checkAndDisplayStoreApk(this@HomeActivity)
}
private fun reloadLayoutManager() {
val mLayoutManager: RecyclerView.LayoutManager
if (shouldBeCardView) {
mLayoutManager = StaggeredGridLayoutManager(calculateNoOfColumns(), StaggeredGridLayoutManager.VERTICAL)
mLayoutManager.gapStrategy = StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS
} else {
mLayoutManager = GridLayoutManager(this, calculateNoOfColumns())
}
mRecyclerView!!.layoutManager = mLayoutManager
mRecyclerView!!.setHasFixedSize(true)
mBottomBar!!.setOnTabReselectListener {
if (shouldBeCardView) {
if ((mLayoutManager as StaggeredGridLayoutManager).findFirstCompletelyVisibleItemPositions(null)[0] == 0) {
getElementsAccordingToTab()
} else {
mLayoutManager.scrollToPositionWithOffset(0, 0)
}
} else {
if ((mLayoutManager as GridLayoutManager).findFirstCompletelyVisibleItemPosition() == 0) {
getElementsAccordingToTab()
} else {
mLayoutManager.scrollToPositionWithOffset(0, 0)
}
}
}
}
private fun getElementsAccordingToTab() {
items = ArrayList()
when (elementsShown) {
UNREAD_SHOWN -> getUnRead()
READ_SHOWN -> getRead()
FAV_SHOWN -> getStarred()
else -> getUnRead()
}
}
private fun getUnRead() {
elementsShown = UNREAD_SHOWN
api!!.unreadItems.enqueue(object : Callback<List<Item>> {
override fun onResponse(call: Call<List<Item>>, response: Response<List<Item>>) {
if (response.body() != null && response.body().isNotEmpty()) {
items = response.body()
} else {
items = ArrayList()
}
handleListResult()
mSwipeRefreshLayout!!.isRefreshing = false
}
override fun onFailure(call: Call<List<Item>>, t: Throwable) {
mSwipeRefreshLayout!!.isRefreshing = false
Toast.makeText(this@HomeActivity, R.string.cant_get_new_elements, Toast.LENGTH_SHORT).show()
}
})
}
private fun getRead() {
elementsShown = READ_SHOWN
api!!.readItems.enqueue(object : Callback<List<Item>> {
override fun onResponse(call: Call<List<Item>>, response: Response<List<Item>>) {
if (response.body() != null && response.body().isNotEmpty()) {
items = response.body()
} else {
items = ArrayList()
}
handleListResult()
mSwipeRefreshLayout!!.isRefreshing = false
}
override fun onFailure(call: Call<List<Item>>, t: Throwable) {
Toast.makeText(this@HomeActivity, R.string.cant_get_read, Toast.LENGTH_SHORT).show()
mSwipeRefreshLayout!!.isRefreshing = false
}
})
}
private fun getStarred() {
elementsShown = FAV_SHOWN
api!!.starredItems.enqueue(object : Callback<List<Item>> {
override fun onResponse(call: Call<List<Item>>, response: Response<List<Item>>) {
if (response.body() != null && response.body().isNotEmpty()) {
items = response.body()
} else {
items = ArrayList()
}
handleListResult()
mSwipeRefreshLayout!!.isRefreshing = false
}
override fun onFailure(call: Call<List<Item>>, t: Throwable) {
Toast.makeText(this@HomeActivity, R.string.cant_get_favs, Toast.LENGTH_SHORT).show()
mSwipeRefreshLayout!!.isRefreshing = false
}
})
}
private fun handleListResult() {
reloadLayoutManager()
val mAdapter: RecyclerView.Adapter<*>
if (shouldBeCardView) {
mAdapter = ItemCardAdapter(this, items, api, mCustomTabActivityHelper, internalBrowser, articleViewer, fullHeightCards)
} else {
mAdapter = ItemListAdapter(this, items, api, mCustomTabActivityHelper, clickBehavior, internalBrowser, articleViewer)
}
mRecyclerView!!.adapter = mAdapter
mAdapter.notifyDataSetChanged()
if (items!!.isEmpty()) Toast.makeText(this@HomeActivity, R.string.nothing_here, Toast.LENGTH_SHORT).show()
reloadBadges()
}
override fun onStart() {
super.onStart()
mCustomTabActivityHelper!!.bindCustomTabsService(this)
}
override fun onStop() {
super.onStop()
mCustomTabActivityHelper!!.unbindCustomTabsService(this)
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
val inflater = menuInflater
inflater.inflate(R.menu.home_menu, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.refresh -> {
api!!.update().enqueue(object : Callback<String> {
override fun onResponse(call: Call<String>, response: Response<String>) {
Toast.makeText(this@HomeActivity,
R.string.refresh_success_response, Toast.LENGTH_LONG)
.show()
}
override fun onFailure(call: Call<String>, t: Throwable) {
Toast.makeText(this@HomeActivity, R.string.refresh_failer_message, Toast.LENGTH_SHORT).show()
}
})
Toast.makeText(this, R.string.refresh_in_progress, Toast.LENGTH_SHORT).show()
return true
}
R.id.readAll -> {
if (elementsShown == UNREAD_SHOWN) {
mSwipeRefreshLayout!!.isRefreshing = false
val ids = items!!.map { it.id }
api!!.readAll(ids).enqueue(object : Callback<SuccessResponse> {
override fun onResponse(call: Call<SuccessResponse>, response: Response<SuccessResponse>) {
if (response.body() != null && response.body().isSuccess) {
Toast.makeText(this@HomeActivity, R.string.all_posts_read, Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(this@HomeActivity, R.string.all_posts_not_read, Toast.LENGTH_SHORT).show()
}
mSwipeRefreshLayout!!.isRefreshing = false
}
override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
Toast.makeText(this@HomeActivity, R.string.all_posts_not_read, Toast.LENGTH_SHORT).show()
mSwipeRefreshLayout!!.isRefreshing = false
}
})
items = ArrayList()
handleListResult()
}
return true
}
R.id.action_disconnect -> {
editor!!.remove("url")
editor!!.remove("login")
editor!!.remove("password")
editor!!.apply()
val intent = Intent(this, LoginActivity::class.java)
startActivity(intent)
finish()
return true
}
R.id.action_sources -> {
val intent2 = Intent(this, SourcesActivity::class.java)
startActivity(intent2)
return true
}
R.id.action_settings -> {
val intent3 = Intent(this, SettingsActivity::class.java)
startActivityForResult(intent3, MENU_PREFERENCES)
return true
}
R.id.about -> {
LibsBuilder()
.withActivityStyle(Libs.ActivityStyle.LIGHT_DARK_TOOLBAR)
.withAboutIconShown(true)
.withAboutVersionShown(true)
.start(this)
return true
}
R.id.action_share_the_app -> {
if (GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(this) == ConnectionResult.SUCCESS) {
val share = AppInviteInvitation.IntentBuilder(getString(R.string.invitation_title))
.setMessage(getString(R.string.invitation_message))
.setDeepLink(Uri.parse("https://ymbh5.app.goo.gl/qbvQ"))
.setCallToActionText(getString(R.string.invitation_cta))
.build()
startActivityForResult(share, REQUEST_INVITE)
} else {
val sendIntent = Intent()
sendIntent.action = Intent.ACTION_SEND
sendIntent.putExtra(Intent.EXTRA_TEXT, getString(R.string.invitation_message) + " https://ymbh5.app.goo.gl/qbvQ")
sendIntent.type = "text/plain"
startActivityForResult(sendIntent, REQUEST_INVITE_BYMAIL)
}
return super.onOptionsItemSelected(item)
}
else -> return super.onOptionsItemSelected(item)
}
}
fun reloadBadges() {
if (displayUnreadCount || displayAllCount) {
api!!.stats.enqueue(object : Callback<Stats> {
override fun onResponse(call: Call<Stats>, response: Response<Stats>) {
if (response.body() != null) {
tabNew!!.setBadgeCount(response.body().unread)
if (displayAllCount) {
tabArchive!!.setBadgeCount(response.body().total)
tabStarred!!.setBadgeCount(response.body().starred)
} else {
tabArchive!!.removeBadge()
tabStarred!!.removeBadge()
}
}
}
override fun onFailure(call: Call<Stats>, t: Throwable) {
}
})
} else {
tabNew!!.removeBadge()
tabArchive!!.removeBadge()
tabStarred!!.removeBadge()
}
}
override fun onActivityResult(req: Int, result: Int, data: Intent?) {
when (req) {
MENU_PREFERENCES -> recreate()
REQUEST_INVITE -> if (result == Activity.RESULT_OK) {
Answers.getInstance().logInvite(InviteEvent())
}
REQUEST_INVITE_BYMAIL -> {
Answers.getInstance().logInvite(InviteEvent())
super.onActivityResult(req, result, data)
}
else -> super.onActivityResult(req, result, data)
}
}
fun calculateNoOfColumns(): Int {
val displayMetrics = resources.displayMetrics
val dpWidth = displayMetrics.widthPixels / displayMetrics.density
return (dpWidth / 300).toInt()
}
}

View File

@ -0,0 +1,57 @@
package apps.amine.bou.readerforselfoss
import agency.tango.materialintroscreen.MaterialIntroActivity
import agency.tango.materialintroscreen.MessageButtonBehaviour
import agency.tango.materialintroscreen.SlideFragmentBuilder
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.preference.PreferenceManager
import android.view.View
class IntroActivity : MaterialIntroActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
addSlide(SlideFragmentBuilder()
.backgroundColor(R.color.colorPrimary)
.buttonsColor(R.color.colorAccent)
.image(R.mipmap.ic_launcher)
.title(getString(R.string.intro_hello_title))
.description(getString(R.string.intro_hello_message))
.build())
addSlide(SlideFragmentBuilder()
.backgroundColor(R.color.colorAccent)
.buttonsColor(R.color.colorPrimary)
.image(R.drawable.ic_info_outline_white_48dp)
.title(getString(R.string.intro_needs_selfoss_title))
.description(getString(R.string.intro_needs_selfoss_message))
.build(),
MessageButtonBehaviour(View.OnClickListener {
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse("https://selfoss.aditu.de"))
startActivity(browserIntent)
}, getString(R.string.intro_needs_selfoss_link)))
addSlide(SlideFragmentBuilder()
.backgroundColor(R.color.colorPrimaryDark)
.buttonsColor(R.color.colorAccentDark)
.image(R.drawable.ic_thumb_up_white_48dp)
.title(getString(R.string.intro_all_set_title))
.description(getString(R.string.intro_all_set_message))
.build())
}
override fun onFinish() {
super.onFinish()
val getPrefs = PreferenceManager.getDefaultSharedPreferences(baseContext)
val e = getPrefs.edit()
e.putBoolean("firstStart", false)
e.apply()
val intent = Intent(this, LoginActivity::class.java)
startActivity(intent)
finish()
}
}

View File

@ -0,0 +1,258 @@
package apps.amine.bou.readerforselfoss
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.os.Bundle
import android.support.design.widget.TextInputLayout
import android.support.v7.app.AlertDialog
import android.support.v7.app.AppCompatActivity
import android.text.TextUtils
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.inputmethod.EditorInfo
import android.widget.Button
import android.widget.EditText
import android.widget.Switch
import android.widget.TextView
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
import apps.amine.bou.readerforselfoss.api.selfoss.SuccessResponse
import apps.amine.bou.readerforselfoss.utils.Config
import apps.amine.bou.readerforselfoss.utils.checkAndDisplayStoreApk
import apps.amine.bou.readerforselfoss.utils.isUrlValid
import com.mikepenz.aboutlibraries.Libs
import com.mikepenz.aboutlibraries.LibsBuilder
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
class LoginActivity : AppCompatActivity() {
private var settings: SharedPreferences? = null
private var mProgressView: View? = null
private var mUrlView: EditText? = null
private var mLoginView: TextView? = null
private var mHTTPLoginView: TextView? = null
private var mPasswordView: EditText? = null
private var mHTTPPasswordView: EditText? = null
private var inValidCount: Int = 0
private var isWithLogin = false
private var isWithHTTPLogin = false
private var mLoginFormView: View? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_login)
settings = getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE)
if (settings!!.getString("url", "").isNotEmpty()) {
goToMain()
} else {
checkAndDisplayStoreApk(this@LoginActivity)
}
isWithLogin = false
isWithHTTPLogin = false
inValidCount = 0
mUrlView = findViewById(R.id.url) as EditText
mLoginView = findViewById(R.id.login) as TextView
mHTTPLoginView = findViewById(R.id.httpLogin) as TextView
mPasswordView = findViewById(R.id.password) as EditText
mHTTPPasswordView = findViewById(R.id.httpPassword) as EditText
mLoginFormView = findViewById(R.id.login_form)
mProgressView = findViewById(R.id.login_progress)
val mSwitch = findViewById(R.id.withLogin) as Switch
val mHTTPSwitch = findViewById(R.id.withHttpLogin) as Switch
val mLoginLayout = findViewById(R.id.loginLayout) as TextInputLayout
val mHTTPLoginLayout = findViewById(R.id.httpLoginInput) as TextInputLayout
val mPasswordLayout = findViewById(R.id.passwordLayout) as TextInputLayout
val mHTTPPasswordLayout = findViewById(R.id.httpPasswordInput) as TextInputLayout
val mEmailSignInButton = findViewById(R.id.email_sign_in_button) as Button
mPasswordView!!.setOnEditorActionListener(TextView.OnEditorActionListener { _, id, _ ->
if (id == R.id.login || id == EditorInfo.IME_NULL) {
attemptLogin()
return@OnEditorActionListener true
}
false
})
mEmailSignInButton.setOnClickListener { attemptLogin() }
mSwitch.setOnCheckedChangeListener { _, b ->
isWithLogin = !isWithLogin
val visi: Int
if (b) {
visi = View.VISIBLE
} else {
visi = View.GONE
}
mLoginLayout.visibility = visi
mPasswordLayout.visibility = visi
}
mHTTPSwitch.setOnCheckedChangeListener { _, b ->
isWithHTTPLogin = !isWithHTTPLogin
val visi: Int
if (b) {
visi = View.VISIBLE
} else {
visi = View.GONE
}
mHTTPLoginLayout.visibility = visi
mHTTPPasswordLayout.visibility = visi
}
}
private fun goToMain() {
val intent = Intent(this, HomeActivity::class.java)
startActivity(intent)
finish()
}
private fun attemptLogin() {
// Reset errors.
mUrlView!!.error = null
mLoginView!!.error = null
mHTTPLoginView!!.error = null
mPasswordView!!.error = null
mHTTPPasswordView!!.error = null
// Store values at the time of the login attempt.
val url = mUrlView!!.text.toString()
val login = mLoginView!!.text.toString()
val httpLogin = mHTTPLoginView!!.text.toString()
val password = mPasswordView!!.text.toString()
val httpPassword = mHTTPPasswordView!!.text.toString()
var cancel = false
var focusView: View? = null
if (!isUrlValid(url)) {
mUrlView!!.error = getString(R.string.login_url_problem)
focusView = mUrlView
cancel = true
inValidCount++
if (inValidCount == 3) {
val alertDialog = AlertDialog.Builder(this).create()
alertDialog.setTitle(getString(R.string.warning_wrong_url))
alertDialog.setMessage(getString(R.string.text_wrong_url))
alertDialog.setButton(AlertDialog.BUTTON_NEUTRAL, "OK",
{ dialog, _ -> dialog.dismiss() })
alertDialog.show()
inValidCount = 0
}
}
if (isWithLogin || isWithHTTPLogin) {
if (TextUtils.isEmpty(password)) {
mPasswordView!!.error = getString(R.string.error_invalid_password)
focusView = mPasswordView
cancel = true
}
if (TextUtils.isEmpty(login)) {
mLoginView!!.error = getString(R.string.error_field_required)
focusView = mLoginView
cancel = true
}
}
if (cancel) {
focusView!!.requestFocus()
} else {
showProgress(true)
val editor = settings!!.edit()
editor.putString("url", url)
editor.putString("login", login)
editor.putString("httpUserName", httpLogin)
editor.putString("password", password)
editor.putString("httpPassword", httpPassword)
editor.apply()
val api = SelfossApi(this@LoginActivity)
api.login().enqueue(object : Callback<SuccessResponse> {
private fun preferenceError() {
editor.remove("url")
editor.remove("login")
editor.remove("httpUserName")
editor.remove("password")
editor.remove("httpPassword")
editor.apply()
mUrlView!!.error = getString(R.string.wrong_infos)
mLoginView!!.error = getString(R.string.wrong_infos)
mPasswordView!!.error = getString(R.string.wrong_infos)
mHTTPLoginView!!.error = getString(R.string.wrong_infos)
mHTTPPasswordView!!.error = getString(R.string.wrong_infos)
showProgress(false)
}
override fun onResponse(call: Call<SuccessResponse>, response: Response<SuccessResponse>) {
if (response.body() != null && response.body().isSuccess) {
goToMain()
} else {
preferenceError()
}
}
override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
preferenceError()
}
})
}
}
/**
* Shows the progress UI and hides the login form.
*/
private fun showProgress(show: Boolean) {
val shortAnimTime = resources.getInteger(android.R.integer.config_shortAnimTime)
mLoginFormView!!.visibility = if (show) View.GONE else View.VISIBLE
mLoginFormView!!.animate().setDuration(shortAnimTime.toLong()).alpha(
if (show) 0F else 1F).setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
mLoginFormView!!.visibility = if (show) View.GONE else View.VISIBLE
}
})
mProgressView!!.visibility = if (show) View.VISIBLE else View.GONE
mProgressView!!.animate().setDuration(shortAnimTime.toLong()).alpha(
if (show) 1F else 0F).setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
mProgressView!!.visibility = if (show) View.VISIBLE else View.GONE
}
})
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
val inflater = menuInflater
inflater.inflate(R.menu.login_menu, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.about -> {
LibsBuilder()
.withActivityStyle(Libs.ActivityStyle.LIGHT_DARK_TOOLBAR)
.withAboutIconShown(true)
.withAboutVersionShown(true)
.start(this)
return true
}
else -> return super.onOptionsItemSelected(item)
}
}
}

View File

@ -0,0 +1,26 @@
package apps.amine.bou.readerforselfoss
import android.content.Intent
import android.os.Bundle
import android.preference.PreferenceManager
import android.support.v7.app.AppCompatActivity
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
if (PreferenceManager.getDefaultSharedPreferences(baseContext).getBoolean("firstStart", true)) {
val i = Intent(this@MainActivity, IntroActivity::class.java)
startActivity(i)
} else {
val intent = Intent(this, LoginActivity::class.java)
startActivity(intent)
}
finish()
}
}

View File

@ -1,7 +1,8 @@
package bou.amine.apps.readerforselfoss
package apps.amine.bou.readerforselfoss
import android.support.multidex.MultiDexApplication
import com.crashlytics.android.Crashlytics
import com.github.stkent.amplify.tracking.Amplify
import io.fabric.sdk.android.Fabric
@ -10,5 +11,10 @@ class MyApp : MultiDexApplication() {
super.onCreate()
if (!BuildConfig.DEBUG)
Fabric.with(this, Crashlytics())
Amplify.initSharedInstance(this)
.setFeedbackEmailAddress(getString(R.string.feedback_email))
.setAlwaysShow(BuildConfig.DEBUG)
.applyAllDefaultRules()
}
}

View File

@ -0,0 +1,84 @@
package apps.amine.bou.readerforselfoss
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import apps.amine.bou.readerforselfoss.api.mercury.MercuryApi
import apps.amine.bou.readerforselfoss.api.mercury.ParsedContent
import apps.amine.bou.readerforselfoss.utils.buildCustomTabsIntent
import apps.amine.bou.readerforselfoss.utils.customtabs.CustomTabActivityHelper
import com.bumptech.glide.Glide
import org.sufficientlysecure.htmltextview.HtmlHttpImageGetter
import org.sufficientlysecure.htmltextview.HtmlTextView
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import xyz.klinker.android.drag_dismiss.activity.DragDismissActivity
class ReaderActivity : DragDismissActivity() {
private var mCustomTabActivityHelper: CustomTabActivityHelper? = null
override fun onStart() {
super.onStart()
mCustomTabActivityHelper!!.bindCustomTabsService(this)
}
override fun onStop() {
super.onStop()
mCustomTabActivityHelper!!.unbindCustomTabsService(this)
}
override fun onCreateContent(inflater: LayoutInflater, parent: ViewGroup, savedInstanceState: Bundle?): View {
val v = inflater.inflate(R.layout.activity_reader, parent, false)
showProgressBar()
val image = v.findViewById(R.id.imageView) as ImageView
val source = v.findViewById(R.id.source) as TextView
val title = v.findViewById(R.id.title) as TextView
val content = v.findViewById(R.id.content) as HtmlTextView
val url = intent.getStringExtra("url")
val parser = MercuryApi(getString(R.string.mercury))
val customTabsIntent = buildCustomTabsIntent(this@ReaderActivity)
mCustomTabActivityHelper = CustomTabActivityHelper()
mCustomTabActivityHelper!!.bindCustomTabsService(this)
parser.parseUrl(url).enqueue(object : Callback<ParsedContent> {
override fun onResponse(call: Call<ParsedContent>, response: Response<ParsedContent>) {
if (response.body() != null) {
source.text = response.body().domain
title.text = response.body().title
if (response.body().content != null && !response.body().content.isEmpty())
content.setHtml(response.body().content, HtmlHttpImageGetter(content, null, true))
if (response.body().lead_image_url != null && !response.body().lead_image_url.isEmpty())
Glide.with(applicationContext).load(response.body().lead_image_url).asBitmap().fitCenter().into(image)
hideProgressBar()
} else {
errorAfterMercuryCall()
}
}
override fun onFailure(call: Call<ParsedContent>, t: Throwable) {
errorAfterMercuryCall()
}
private fun errorAfterMercuryCall() {
CustomTabActivityHelper.openCustomTab(this@ReaderActivity, customTabsIntent, Uri.parse(url)
) { _, uri ->
val intent = Intent(Intent.ACTION_VIEW, uri)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
startActivity(intent)
}
finish()
}
})
return v
}
}

View File

@ -0,0 +1,57 @@
package apps.amine.bou.readerforselfoss
import android.content.Intent
import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView
import android.widget.Toast
import apps.amine.bou.readerforselfoss.adapters.SourcesListAdapter
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
import apps.amine.bou.readerforselfoss.api.selfoss.Sources
import com.melnykov.fab.FloatingActionButton
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
class SourcesActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_sources)
}
override fun onResume() {
super.onResume()
val mFab = findViewById(R.id.fab) as FloatingActionButton
val mRecyclerView = findViewById(R.id.activity_sources) as RecyclerView
val mLayoutManager = LinearLayoutManager(this)
val api = SelfossApi(this)
var items: List<Sources> = ArrayList()
mFab.attachToRecyclerView(mRecyclerView)
mRecyclerView.setHasFixedSize(true)
mRecyclerView.layoutManager = mLayoutManager
api.sources.enqueue(object : Callback<List<Sources>> {
override fun onResponse(call: Call<List<Sources>>, response: Response<List<Sources>>) {
if (response.body() != null && response.body().isNotEmpty()) {
items = response.body()
}
val mAdapter = SourcesListAdapter(this@SourcesActivity, items, api)
mRecyclerView.adapter = mAdapter
mAdapter.notifyDataSetChanged()
if (items.isEmpty()) Toast.makeText(this@SourcesActivity, R.string.nothing_here, Toast.LENGTH_SHORT).show()
}
override fun onFailure(call: Call<List<Sources>>, t: Throwable) {
Toast.makeText(this@SourcesActivity, R.string.cant_get_sources, Toast.LENGTH_SHORT).show()
}
})
mFab.setOnClickListener {
startActivity(Intent(this@SourcesActivity, AddSourceActivity::class.java))
}
}
}

View File

@ -0,0 +1,310 @@
package apps.amine.bou.readerforselfoss.adapters;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.net.Uri;
import android.support.constraint.ConstraintLayout;
import android.support.customtabs.CustomTabsIntent;
import android.support.design.widget.Snackbar;
import android.support.v4.graphics.drawable.RoundedBitmapDrawable;
import android.support.v4.graphics.drawable.RoundedBitmapDrawableFactory;
import android.support.v7.widget.RecyclerView;
import android.text.Html;
import android.text.format.DateUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.ImageView.ScaleType;
import android.widget.TextView;
import android.widget.Toast;
import apps.amine.bou.readerforselfoss.R;
import apps.amine.bou.readerforselfoss.api.selfoss.Item;
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi;
import apps.amine.bou.readerforselfoss.api.selfoss.SuccessResponse;
import apps.amine.bou.readerforselfoss.utils.customtabs.CustomTabActivityHelper;
import com.amulyakhare.textdrawable.TextDrawable;
import com.amulyakhare.textdrawable.TextDrawable.IBuilder;
import com.amulyakhare.textdrawable.util.ColorGenerator;
import com.bumptech.glide.Glide;
import com.bumptech.glide.request.target.BitmapImageViewTarget;
import com.like.LikeButton;
import com.like.OnLikeListener;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
import static apps.amine.bou.readerforselfoss.utils.LinksUtilsKt.buildCustomTabsIntent;
import static apps.amine.bou.readerforselfoss.utils.LinksUtilsKt.openItemUrl;
public class ItemCardAdapter extends RecyclerView.Adapter<ItemCardAdapter.ViewHolder> {
private final List<Item> items;
private final SelfossApi api;
private final CustomTabActivityHelper helper;
private final Context c;
private final boolean internalBrowser;
private final boolean articleViewer;
private final Activity app;
private final ColorGenerator generator;
private final boolean fullHeightCards;
public ItemCardAdapter(Activity a, List<Item> myObject, SelfossApi selfossApi,
CustomTabActivityHelper mCustomTabActivityHelper, boolean internalBrowser,
boolean articleViewer, boolean fullHeightCards) {
app = a;
items = myObject;
api = selfossApi;
helper = mCustomTabActivityHelper;
c = app.getApplicationContext();
this.internalBrowser = internalBrowser;
this.articleViewer = articleViewer;
generator = ColorGenerator.MATERIAL;
this.fullHeightCards = fullHeightCards;
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
ConstraintLayout v = (ConstraintLayout) LayoutInflater.from(c).inflate(R.layout.card_item, parent, false);
return new ViewHolder(v);
}
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
Item itm = items.get(position);
holder.saveBtn.setLiked((itm.getStarred()));
holder.title.setText(Html.fromHtml(itm.getTitle()));
String sourceAndDate = itm.getSourcetitle();
long d;
try {
d = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(itm.getDatetime()).getTime();
sourceAndDate += " " +
DateUtils.getRelativeTimeSpanString(
d,
new Date().getTime(),
DateUtils.MINUTE_IN_MILLIS,
DateUtils.FORMAT_ABBREV_RELATIVE
);
} catch (ParseException e) {
e.printStackTrace();
}
holder.sourceTitleAndDate.setText(sourceAndDate);
if (itm.getThumbnail(c).isEmpty()) {
Glide.clear(holder.itemImage);
holder.itemImage.setImageDrawable(null);
} else {
if (fullHeightCards) {
Glide.with(c).load(itm.getThumbnail(c)).asBitmap().fitCenter().into(holder.itemImage);
} else {
Glide.with(c).load(itm.getThumbnail(c)).asBitmap().centerCrop().into(holder.itemImage);
}
}
final ViewHolder fHolder = holder;
if (itm.getIcon(c).isEmpty()) {
int color = generator.getColor(itm.getSourcetitle());
StringBuilder textDrawable = new StringBuilder();
for(String s : itm.getSourcetitle().split(" "))
{
textDrawable.append(s.charAt(0));
}
IBuilder builder = TextDrawable.builder().round();
TextDrawable drawable = builder.build(textDrawable.toString(), color);
holder.sourceImage.setImageDrawable(drawable);
} else {
Glide.with(c).load(itm.getIcon(c)).asBitmap().centerCrop().into(new BitmapImageViewTarget(holder.sourceImage) {
@Override
protected void setResource(Bitmap resource) {
RoundedBitmapDrawable circularBitmapDrawable =
RoundedBitmapDrawableFactory.create(c.getResources(), resource);
circularBitmapDrawable.setCircular(true);
fHolder.sourceImage.setImageDrawable(circularBitmapDrawable);
}
});
}
holder.saveBtn.setLiked(itm.getStarred());
}
@Override
public int getItemCount() {
return items.size();
}
private void doUnmark(final Item i, final int position) {
Snackbar s = Snackbar
.make(app.findViewById(R.id.coordLayout), R.string.marked_as_read, Snackbar.LENGTH_LONG)
.setAction(R.string.undo_string, new View.OnClickListener() {
@Override
public void onClick(View view) {
items.add(position, i);
notifyItemInserted(position);
api.unmarkItem(i.getId()).enqueue(new Callback<SuccessResponse>() {
@Override
public void onResponse(Call<SuccessResponse> call, Response<SuccessResponse> response) {}
@Override
public void onFailure(Call<SuccessResponse> call, Throwable t) {
items.remove(i);
notifyItemRemoved(position);
doUnmark(i, position);
}
});
}
});
View view = s.getView();
TextView tv = (TextView) view.findViewById(android.support.design.R.id.snackbar_text);
tv.setTextColor(Color.WHITE);
s.show();
}
public void removeItemAtIndex(final int position) {
final Item i = items.get(position);
items.remove(i);
notifyItemRemoved(position);
api.markItem(i.getId()).enqueue(new Callback<SuccessResponse>() {
@Override
public void onResponse(Call<SuccessResponse> call, Response<SuccessResponse> response) {
doUnmark(i, position);
}
@Override
public void onFailure(Call<SuccessResponse> call, Throwable t) {
Toast.makeText(app, app.getString(R.string.cant_mark_read), Toast.LENGTH_SHORT).show();
items.add(i);
notifyItemInserted(position);
}
});
}
public class ViewHolder extends RecyclerView.ViewHolder {
LikeButton saveBtn;
ImageButton browserBtn;
ImageButton shareBtn;
ImageView itemImage;
ImageView sourceImage;
TextView title;
TextView sourceTitleAndDate;
public ConstraintLayout mView;
public ViewHolder(ConstraintLayout itemView) {
super(itemView);
mView = itemView;
handleClickListeners();
handleCustomTabActions();
}
private void handleClickListeners() {
sourceImage = (ImageView) mView.findViewById( R.id.sourceImage);
itemImage = (ImageView) mView.findViewById( R.id.itemImage);
title = (TextView) mView.findViewById( R.id.title);
sourceTitleAndDate = (TextView) mView.findViewById( R.id.sourceTitleAndDate);
saveBtn = (LikeButton) mView.findViewById( R.id.favButton);
shareBtn = (ImageButton) mView.findViewById( R.id.shareBtn);
browserBtn = (ImageButton) mView.findViewById( R.id.browserBtn);
if (!fullHeightCards) {
itemImage.setMaxHeight((int) c.getResources().getDimension(R.dimen.card_image_max_height));
itemImage.setScaleType(ScaleType.CENTER_CROP);
}
saveBtn.setOnLikeListener(new OnLikeListener() {
@Override
public void liked(LikeButton likeButton) {
Item i = items.get(getAdapterPosition());
api.starrItem(i.getId()).enqueue(new Callback<SuccessResponse>() {
@Override
public void onResponse(Call<SuccessResponse> call, Response<SuccessResponse> response) {}
@Override
public void onFailure(Call<SuccessResponse> call, Throwable t) {
saveBtn.setLiked(false);
Toast.makeText(c, R.string.cant_mark_favortie, Toast.LENGTH_SHORT).show();
}
});
}
@Override
public void unLiked(LikeButton likeButton) {
Item i = items.get(getAdapterPosition());
api.unstarrItem(i.getId()).enqueue(new Callback<SuccessResponse>() {
@Override
public void onResponse(Call<SuccessResponse> call, Response<SuccessResponse> response) {}
@Override
public void onFailure(Call<SuccessResponse> call, Throwable t) {
saveBtn.setLiked(true);
Toast.makeText(c, R.string.cant_unmark_favortie, Toast.LENGTH_SHORT).show();
}
});
}
});
shareBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Item i = items.get(getAdapterPosition());
Intent sendIntent = new Intent();
sendIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
sendIntent.setAction(Intent.ACTION_SEND);
sendIntent.putExtra(Intent.EXTRA_TEXT, i.getLinkDecoded());
sendIntent.setType("text/plain");
c.startActivity(Intent.createChooser(sendIntent, c.getString(R.string.share)).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
}
});
browserBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Item i = items.get(getAdapterPosition());
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setData(Uri.parse(i.getLinkDecoded()));
c.startActivity(intent);
}
});
}
private void handleCustomTabActions() {
final CustomTabsIntent customTabsIntent = buildCustomTabsIntent(c);
helper.bindCustomTabsService(app);
mView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
openItemUrl(items.get(getAdapterPosition()),
customTabsIntent,
internalBrowser,
articleViewer,
app,
c);
}
});
}
}
}

View File

@ -0,0 +1,363 @@
package apps.amine.bou.readerforselfoss.adapters;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.net.Uri;
import android.support.constraint.ConstraintLayout;
import android.support.customtabs.CustomTabsIntent;
import android.support.design.widget.Snackbar;
import android.support.v4.graphics.drawable.RoundedBitmapDrawable;
import android.support.v4.graphics.drawable.RoundedBitmapDrawableFactory;
import android.support.v7.widget.RecyclerView;
import android.text.Html;
import android.text.format.DateUtils;
import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.RelativeLayout;
import android.widget.TextView;
import android.widget.Toast;
import apps.amine.bou.readerforselfoss.R;
import apps.amine.bou.readerforselfoss.api.selfoss.Item;
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi;
import apps.amine.bou.readerforselfoss.api.selfoss.SuccessResponse;
import apps.amine.bou.readerforselfoss.utils.customtabs.CustomTabActivityHelper;
import com.amulyakhare.textdrawable.TextDrawable;
import com.amulyakhare.textdrawable.util.ColorGenerator;
import com.bumptech.glide.Glide;
import com.bumptech.glide.request.target.BitmapImageViewTarget;
import com.like.LikeButton;
import com.like.OnLikeListener;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
import static apps.amine.bou.readerforselfoss.utils.LinksUtilsKt.buildCustomTabsIntent;
import static apps.amine.bou.readerforselfoss.utils.LinksUtilsKt.openItemUrl;
public class ItemListAdapter extends RecyclerView.Adapter<ItemListAdapter.ViewHolder> {
private final boolean clickBehavior;
private final boolean articleViewer;
private final boolean internalBrowser;
private final ColorGenerator generator;
private SelfossApi api;
private Context c;
private List<Item> items;
private List<Boolean> bars;
private Activity app;
private CustomTabActivityHelper helper;
public ItemListAdapter(Activity a, List<Item> myObject, SelfossApi selfossApi,
CustomTabActivityHelper mCustomTabActivityHelper, boolean clickBehavior,
boolean internalBrowser, boolean articleViewer) {
app = a;
items = myObject;
api = selfossApi;
helper = mCustomTabActivityHelper;
c = app.getApplicationContext();
this.clickBehavior = clickBehavior;
this.internalBrowser = internalBrowser;
this.articleViewer = articleViewer;
bars = new ArrayList<>(Collections.nCopies(items.size() + 1, false));
generator = ColorGenerator.MATERIAL;
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
ConstraintLayout v = (ConstraintLayout) LayoutInflater.from(c).inflate(R.layout.list_item, parent, false);
return new ViewHolder(v);
}
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
Item itm = items.get(position);
holder.saveBtn.setLiked((itm.getStarred()));
holder.title.setText(Html.fromHtml(itm.getTitle()));
String sourceAndDate = itm.getSourcetitle();
long d;
try {
d = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(itm.getDatetime()).getTime();
sourceAndDate += " " +
DateUtils.getRelativeTimeSpanString(
d,
new Date().getTime(),
DateUtils.MINUTE_IN_MILLIS,
DateUtils.FORMAT_ABBREV_RELATIVE
);
} catch (ParseException e) {
e.printStackTrace();
}
holder.sourceTitleAndDate.setText(sourceAndDate);
if (itm.getThumbnail(c).isEmpty()) {
int sizeInInt = 46;
int sizeInDp = (int) TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, sizeInInt, c.getResources()
.getDisplayMetrics());
int marginInInt = 16;
int marginInDp = (int) TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, marginInInt, c.getResources()
.getDisplayMetrics());
ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) holder.sourceImage.getLayoutParams();
params.height = sizeInDp;
params.width = sizeInDp;
params.setMargins(marginInDp, 0, 0, 0);
holder.sourceImage.setLayoutParams(params);
if (itm.getIcon(c).isEmpty()) {
int color = generator.getColor(itm.getSourcetitle());
StringBuilder textDrawable = new StringBuilder();
for(String s : itm.getSourcetitle().split(" "))
{
textDrawable.append(s.charAt(0));
}
TextDrawable.IBuilder builder = TextDrawable.builder().round();
TextDrawable drawable = builder.build(textDrawable.toString(), color);
holder.sourceImage.setImageDrawable(drawable);
} else {
final ViewHolder fHolder = holder;
Glide.with(c).load(itm.getIcon(c)).asBitmap().centerCrop().into(new BitmapImageViewTarget(holder.sourceImage) {
@Override
protected void setResource(Bitmap resource) {
RoundedBitmapDrawable circularBitmapDrawable =
RoundedBitmapDrawableFactory.create(c.getResources(), resource);
circularBitmapDrawable.setCircular(true);
fHolder.sourceImage.setImageDrawable(circularBitmapDrawable);
}
});
}
} else {
Glide.with(c).load(itm.getThumbnail(c)).asBitmap().centerCrop().into(holder.sourceImage);
}
if (bars.get(position)) {
holder.actionBar.setVisibility(View.VISIBLE);
} else {
holder.actionBar.setVisibility(View.GONE);
}
holder.saveBtn.setLiked(itm.getStarred());
}
@Override
public int getItemCount() {
return items.size();
}
private void doUnmark(final Item i, final int position) {
Snackbar s = Snackbar
.make(app.findViewById(R.id.coordLayout), R.string.marked_as_read, Snackbar.LENGTH_LONG)
.setAction(R.string.undo_string, new View.OnClickListener() {
@Override
public void onClick(View view) {
items.add(position, i);
notifyItemInserted(position);
api.unmarkItem(i.getId()).enqueue(new Callback<SuccessResponse>() {
@Override
public void onResponse(Call<SuccessResponse> call, Response<SuccessResponse> response) {}
@Override
public void onFailure(Call<SuccessResponse> call, Throwable t) {
items.remove(i);
notifyItemRemoved(position);
doUnmark(i, position);
}
});
}
});
View view = s.getView();
TextView tv = (TextView) view.findViewById(android.support.design.R.id.snackbar_text);
tv.setTextColor(Color.WHITE);
s.show();
}
public void removeItemAtIndex(final int position) {
final Item i = items.get(position);
items.remove(i);
notifyItemRemoved(position);
api.markItem(i.getId()).enqueue(new Callback<SuccessResponse>() {
@Override
public void onResponse(Call<SuccessResponse> call, Response<SuccessResponse> response) {
doUnmark(i, position);
}
@Override
public void onFailure(Call<SuccessResponse> call, Throwable t) {
Toast.makeText(app, app.getString(R.string.cant_mark_read), Toast.LENGTH_SHORT).show();
items.add(i);
notifyItemInserted(position);
}
});
}
public class ViewHolder extends RecyclerView.ViewHolder {
LikeButton saveBtn;
ImageButton browserBtn;
ImageButton shareBtn;
public RelativeLayout actionBar;
ImageView sourceImage;
TextView title;
TextView sourceTitleAndDate;
public ConstraintLayout mView;
public ViewHolder(ConstraintLayout itemView) {
super(itemView);
mView = itemView;
handleClickListeners();
handleCustomTabActions();
}
private void handleClickListeners() {
actionBar = (RelativeLayout) mView.findViewById(R.id.actionBar);
sourceImage = (ImageView) mView.findViewById( R.id.itemImage);
title = (TextView) mView.findViewById( R.id.title);
sourceTitleAndDate = (TextView) mView.findViewById( R.id.sourceTitleAndDate);
saveBtn = (LikeButton) mView.findViewById( R.id.favButton);
shareBtn = (ImageButton) mView.findViewById( R.id.shareBtn);
browserBtn = (ImageButton) mView.findViewById( R.id.browserBtn);
saveBtn.setOnLikeListener(new OnLikeListener() {
@Override
public void liked(LikeButton likeButton) {
Item i = items.get(getAdapterPosition());
api.starrItem(i.getId()).enqueue(new Callback<SuccessResponse>() {
@Override
public void onResponse(Call<SuccessResponse> call, Response<SuccessResponse> response) {}
@Override
public void onFailure(Call<SuccessResponse> call, Throwable t) {
saveBtn.setLiked(false);
Toast.makeText(c, R.string.cant_mark_favortie, Toast.LENGTH_SHORT).show();
}
});
}
@Override
public void unLiked(LikeButton likeButton) {
Item i = items.get(getAdapterPosition());
api.unstarrItem(i.getId()).enqueue(new Callback<SuccessResponse>() {
@Override
public void onResponse(Call<SuccessResponse> call, Response<SuccessResponse> response) {}
@Override
public void onFailure(Call<SuccessResponse> call, Throwable t) {
saveBtn.setLiked(true);
Toast.makeText(c, R.string.cant_unmark_favortie, Toast.LENGTH_SHORT).show();
}
});
}
});
shareBtn.setOnClickListener(new View.OnClickListener() {
public void onClick(View view) {
Item i = items.get(getAdapterPosition());
Intent sendIntent = new Intent();
sendIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
sendIntent.setAction(Intent.ACTION_SEND);
sendIntent.putExtra(Intent.EXTRA_TEXT, i.getLinkDecoded());
sendIntent.setType("text/plain");
c.startActivity(Intent.createChooser(sendIntent, c.getString(R.string.share)).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
}
});
browserBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Item i = items.get(getAdapterPosition());
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setData(Uri.parse(i.getLinkDecoded()));
c.startActivity(intent);
}
});
}
private void handleCustomTabActions() {
final CustomTabsIntent customTabsIntent = buildCustomTabsIntent(c);
helper.bindCustomTabsService(app);
if (!clickBehavior) {
mView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
openItemUrl(items.get(getAdapterPosition()),
customTabsIntent,
internalBrowser,
articleViewer,
app,
c);
}
});
mView.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View view) {
actionBarShowHide();
return true;
}
});
} else {
mView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
actionBarShowHide();
}
});
mView.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View view) {
openItemUrl(items.get(getAdapterPosition()),
customTabsIntent,
internalBrowser,
articleViewer,
app,
c);
return true;
}
});
}
}
private void actionBarShowHide() {
bars.set(getAdapterPosition(), true);
if (actionBar.getVisibility() == View.GONE)
actionBar.setVisibility(View.VISIBLE);
else
actionBar.setVisibility(View.GONE);
}
}
}

View File

@ -0,0 +1,133 @@
package apps.amine.bou.readerforselfoss.adapters;
import android.app.Activity;
import android.content.Context;
import android.graphics.Bitmap;
import android.support.constraint.ConstraintLayout;
import android.support.v4.graphics.drawable.RoundedBitmapDrawable;
import android.support.v4.graphics.drawable.RoundedBitmapDrawableFactory;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.*;
import apps.amine.bou.readerforselfoss.R;
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi;
import apps.amine.bou.readerforselfoss.api.selfoss.Sources;
import apps.amine.bou.readerforselfoss.api.selfoss.SuccessResponse;
import com.amulyakhare.textdrawable.TextDrawable;
import com.amulyakhare.textdrawable.util.ColorGenerator;
import com.bumptech.glide.Glide;
import com.bumptech.glide.request.target.BitmapImageViewTarget;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
import java.util.List;
public class SourcesListAdapter extends RecyclerView.Adapter<SourcesListAdapter.ViewHolder> {
private final List<Sources> items;
private final Activity app;
private final SelfossApi api;
private final Context c;
private final ColorGenerator generator;
public SourcesListAdapter(Activity activity, List<Sources> items, SelfossApi api) {
this.app = activity;
this.items = items;
this.api = api;
this.c = app.getBaseContext();
generator = ColorGenerator.MATERIAL;
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
ConstraintLayout v = (ConstraintLayout) LayoutInflater.from(c).inflate(R.layout.source_list_item, parent, false);
return new ViewHolder(v);
}
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
Sources itm = items.get(position);
final ViewHolder fHolder = holder;
if (itm.getIcon(c).isEmpty()) {
int color = generator.getColor(itm.getTitle());
StringBuilder textDrawable = new StringBuilder();
for(String s : itm.getTitle().split(" "))
{
textDrawable.append(s.charAt(0));
}
TextDrawable.IBuilder builder = TextDrawable.builder().round();
TextDrawable drawable = builder.build(textDrawable.toString(), color);
holder.sourceImage.setImageDrawable(drawable);
} else {
Glide.with(c).load(itm.getIcon(c)).asBitmap().centerCrop().into(new BitmapImageViewTarget(holder.sourceImage) {
@Override
protected void setResource(Bitmap resource) {
RoundedBitmapDrawable circularBitmapDrawable =
RoundedBitmapDrawableFactory.create(c.getResources(), resource);
circularBitmapDrawable.setCircular(true);
fHolder.sourceImage.setImageDrawable(circularBitmapDrawable);
}
});
}
holder.sourceTitle.setText(itm.getTitle());
}
@Override
public int getItemCount() {
return items.size();
}
public class ViewHolder extends RecyclerView.ViewHolder {
ConstraintLayout mView;
ImageView sourceImage;
TextView sourceTitle;
Button deleteBtn;
public ViewHolder(ConstraintLayout itemView) {
super(itemView);
mView = itemView;
handleClickListeners();
}
private void handleClickListeners() {
sourceImage = (ImageView) mView.findViewById(R.id.itemImage);
sourceTitle = (TextView) mView.findViewById(R.id.sourceTitle);
deleteBtn = (Button) mView.findViewById(R.id.deleteBtn);
deleteBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Sources i = items.get(getAdapterPosition());
api.deleteSource(i.getId()).enqueue(new Callback<SuccessResponse>() {
@Override
public void onResponse(Call<SuccessResponse> call, Response<SuccessResponse> response) {
if (response.body() != null && response.body().isSuccess()) {
items.remove(getAdapterPosition());
notifyItemRemoved(getAdapterPosition());
notifyItemRangeChanged(getAdapterPosition(), getItemCount());
}
else {
Toast.makeText(app, "Petit soucis lors de la suppression de la source.", Toast.LENGTH_SHORT).show();
}
}
@Override
public void onFailure(Call<SuccessResponse> call, Throwable t) {
Toast.makeText(app, "Petit soucis lors de la suppression de la source.", Toast.LENGTH_SHORT).show();
}
});
}
});
}
}
}

View File

@ -0,0 +1,36 @@
package apps.amine.bou.readerforselfoss.api.mercury;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import okhttp3.OkHttpClient;
import okhttp3.logging.HttpLoggingInterceptor;
import retrofit2.Call;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;
public class MercuryApi {
private final MercuryService service;
private final String key;
public MercuryApi(String key) {
HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor();
interceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
OkHttpClient client = new OkHttpClient.Builder().addInterceptor(interceptor).build();
Gson gson = new GsonBuilder()
.setLenient()
.create();
this.key = key;
Retrofit retrofit = new Retrofit.Builder().baseUrl("https://mercury.postlight.com").client(client)
.addConverterFactory(GsonConverterFactory.create(gson)).build();
service = retrofit.create(MercuryService.class);
}
public Call<ParsedContent> parseUrl(String url) {
return service.parseUrl(url, this.key);
}
}

View File

@ -0,0 +1,55 @@
package apps.amine.bou.readerforselfoss.api.mercury
import android.os.Parcel
import android.os.Parcelable
class ParsedContent(val title: String,
val content: String,
val date_published: String,
val lead_image_url: String,
val dek: String,
val url: String,
val domain: String,
val excerpt: String,
val total_pages: Int,
val rendered_pages: Int,
val next_page_url: String) : Parcelable {
companion object {
@JvmField val CREATOR: Parcelable.Creator<ParsedContent> = object : Parcelable.Creator<ParsedContent> {
override fun createFromParcel(source: Parcel): ParsedContent = ParsedContent(source)
override fun newArray(size: Int): Array<ParsedContent?> = arrayOfNulls(size)
}
}
constructor(source: Parcel) : this(
title = source.readString(),
content = source.readString(),
date_published = source.readString(),
lead_image_url = source.readString(),
dek = source.readString(),
url = source.readString(),
domain = source.readString(),
excerpt = source.readString(),
total_pages = source.readInt(),
rendered_pages = source.readInt(),
next_page_url = source.readString()
)
override fun describeContents() = 0
override fun writeToParcel(dest: Parcel, flags: Int) {
dest.writeString(title)
dest.writeString(content)
dest.writeString(date_published)
dest.writeString(lead_image_url)
dest.writeString(dek)
dest.writeString(url)
dest.writeString(domain)
dest.writeString(excerpt)
dest.writeInt(total_pages)
dest.writeInt(rendered_pages)
dest.writeString(next_page_url)
}
}

View File

@ -0,0 +1,13 @@
package apps.amine.bou.readerforselfoss.api.mercury;
import retrofit2.Call;
import retrofit2.http.GET;
import retrofit2.http.Header;
import retrofit2.http.Query;
public interface MercuryService {
@GET("parser")
Call<ParsedContent> parseUrl(@Query("url") String url, @Header("x-api-key") String key);
}

View File

@ -0,0 +1,19 @@
package apps.amine.bou.readerforselfoss.api.selfoss
import com.google.gson.JsonParseException
import com.google.gson.JsonDeserializationContext
import com.google.gson.JsonElement
import com.google.gson.JsonDeserializer
import java.lang.reflect.Type
internal class BooleanTypeAdapter : JsonDeserializer<Boolean> {
@Throws(JsonParseException::class)
override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): Boolean? =
try {
json.asInt == 1
} catch (e: Exception) {
json.asBoolean
}
}

View File

@ -0,0 +1,138 @@
package apps.amine.bou.readerforselfoss.api.selfoss;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import android.content.Context;
import apps.amine.bou.readerforselfoss.utils.Config;
import com.burgstaller.okhttp.AuthenticationCacheInterceptor;
import com.burgstaller.okhttp.CachingAuthenticatorDecorator;
import com.burgstaller.okhttp.DispatchingAuthenticator;
import com.burgstaller.okhttp.basic.BasicAuthenticator;
import com.burgstaller.okhttp.digest.CachingAuthenticator;
import com.burgstaller.okhttp.digest.Credentials;
import com.burgstaller.okhttp.digest.DigestAuthenticator;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import okhttp3.OkHttpClient;
import okhttp3.logging.HttpLoggingInterceptor;
import retrofit2.Call;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;
public class SelfossApi {
private final SelfossService service;
private final Config config;
private final String userName;
private final String password;
public SelfossApi(Context c) {
this.config = new Config(c);
HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor();
interceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
OkHttpClient.Builder httpBuilder = new OkHttpClient.Builder();
final Map<String, CachingAuthenticator> authCache = new ConcurrentHashMap<>();
String httpUserName = config.getHttpUserLogin();
String httpPassword = config.getHttpUserPassword();
Credentials credentials = new Credentials(httpUserName, httpPassword);
final BasicAuthenticator basicAuthenticator = new BasicAuthenticator(credentials);
final DigestAuthenticator digestAuthenticator = new DigestAuthenticator(credentials);
// note that all auth schemes should be registered as lowercase!
DispatchingAuthenticator authenticator = new DispatchingAuthenticator.Builder()
.with("digest", digestAuthenticator)
.with("basic", basicAuthenticator)
.build();
OkHttpClient client = httpBuilder
.authenticator(new CachingAuthenticatorDecorator(authenticator, authCache))
.addInterceptor(new AuthenticationCacheInterceptor(authCache))
.addInterceptor(interceptor)
.build();
GsonBuilder builder = new GsonBuilder();
builder.registerTypeAdapter(boolean.class, new BooleanTypeAdapter());
Gson gson = builder
.setLenient()
.create();
userName = config.getUserLogin();
password = config.getUserPassword();
Retrofit retrofit = new Retrofit.Builder().baseUrl(config.getBaseUrl()).client(client)
.addConverterFactory(GsonConverterFactory.create(gson)).build();
service = retrofit.create(SelfossService.class);
}
public Call<SuccessResponse> login() {
return service.loginToSelfoss(config.getUserLogin(), config.getUserPassword());
}
public Call<List<Item>> getReadItems() {
return getItems("read");
}
public Call<List<Item>> getUnreadItems() {
return getItems("unread");
}
public Call<List<Item>> getStarredItems() {
return getItems("starred");
}
private Call<List<Item>> getItems(String type) {
return service.getItems(type, userName, password);
}
public Call<SuccessResponse> markItem(String itemId) {
return service.markAsRead(itemId, userName, password);
}
public Call<SuccessResponse> unmarkItem(String itemId) {
return service.unmarkAsRead(itemId, userName, password);
}
public Call<SuccessResponse> readAll(List<String> ids) {
return service.markAllAsRead(ids, userName, password);
}
public Call<SuccessResponse> starrItem(String itemId) {
return service.starr(itemId, userName, password);
}
public Call<SuccessResponse> unstarrItem(String itemId) {
return service.unstarr(itemId, userName, password);
}
public Call<Stats> getStats() {
return service.stats(userName, password);
}
public Call<List<Tag>> getTags() {
return service.tags(userName, password);
}
public Call<String> update() {
return service.update(userName, password);
}
public Call<List<Sources>> getSources() { return service.sources(userName, password); }
public Call<SuccessResponse> deleteSource(String id) { return service.deleteSource(id, userName, password);}
public Call<Map<String, Spout>> spouts() { return service.spouts(userName, password); }
public Call<SuccessResponse> createSource(String title, String url, String spout, String tags, String filter) {return service.createSource(title, url, spout, tags, filter, userName, password);}
}

View File

@ -0,0 +1,127 @@
package apps.amine.bou.readerforselfoss.api.selfoss
import android.content.Context
import android.net.Uri
import android.os.Parcel
import android.os.Parcelable
import apps.amine.bou.readerforselfoss.utils.Config
import apps.amine.bou.readerforselfoss.utils.isEmptyOrNullOrNullString
private fun constructUrl(config: Config?, path: String, file: String): String {
val baseUriBuilder = Uri.parse(config!!.baseUrl).buildUpon()
baseUriBuilder.appendPath(path).appendPath(file)
return if (isEmptyOrNullOrNullString(file)) ""
else baseUriBuilder.toString()
}
data class Tag(val tag: String, val color: String, val unread: Int)
class SuccessResponse(val success: Boolean) {
val isSuccess: Boolean
get() = success
}
class Stats(val total: Int, val unread: Int, val starred: Int)
data class Spout(val name: String, val description: String)
data class Sources(val id: String,
val title: String,
val tags: String,
val spout: String,
val error: String,
val icon: String) {
var config: Config? = null
fun getIcon(app: Context): String {
if (config == null) {
config = Config(app)
}
return constructUrl(config,"favicons", icon)
}
}
data class Item(val id: String,
val datetime: String,
val title: String,
val unread: Boolean,
val starred: Boolean,
val thumbnail: String,
val icon: String,
val link: String,
val sourcetitle: String) : Parcelable {
var config: Config? = null
companion object {
@JvmField val CREATOR: Parcelable.Creator<Item> = object : Parcelable.Creator<Item> {
override fun createFromParcel(source: Parcel): Item = Item(source)
override fun newArray(size: Int): Array<Item?> = arrayOfNulls(size)
}
}
constructor(source: Parcel) : this(
id = source.readString(),
datetime = source.readString(),
title = source.readString(),
unread = source.readByte() != 0,
starred = source.readByte() != 0,
thumbnail = source.readString(),
icon = source.readString(),
link = source.readString(),
sourcetitle = source.readString()
)
override fun describeContents() = 0
override fun writeToParcel(dest: Parcel, flags: Int) {
dest.writeString(id)
dest.writeString(datetime)
dest.writeString(title)
dest.writeByte((if (unread) 1 else 0))
dest.writeByte((if (starred) 1 else 0))
dest.writeString(thumbnail)
dest.writeString(icon)
dest.writeString(link)
dest.writeString(sourcetitle)
}
fun getIcon(app: Context): String {
if (config == null) {
config = Config(app)
}
return constructUrl(config, "favicons", icon)
}
fun getThumbnail(app: Context): String {
if (config == null) {
config = Config(app)
}
return constructUrl(config, "thumbnails", thumbnail)
}
// TODO: maybe find a better way to handle these kind of urls
fun getLinkDecoded(): String {
var stringUrl: String
if (link.startsWith("http://news.google.com/news/") || link.startsWith("https://news.google.com/news/")) {
if (link.contains("&amp;url=")) {
stringUrl = link.substringAfter("&amp;url=")
} else {
stringUrl = this.link.replace("&amp;", "&")
}
} else {
stringUrl = this.link.replace("&amp;", "&")
}
// handle :443 => https
if (stringUrl.contains(":443")) {
stringUrl = stringUrl.replace(":443", "").replace("http://", "https://")
}
return stringUrl
}
}

View File

@ -0,0 +1,68 @@
package apps.amine.bou.readerforselfoss.api.selfoss;
import java.util.List;
import java.util.Map;
import retrofit2.Call;
import retrofit2.http.DELETE;
import retrofit2.http.Field;
import retrofit2.http.FormUrlEncoded;
import retrofit2.http.GET;
import retrofit2.http.POST;
import retrofit2.http.Path;
import retrofit2.http.Query;
interface SelfossService {
@GET("login")
Call<SuccessResponse> loginToSelfoss(@Query("username") String username, @Query("password") String password);
@GET("items")
Call<List<Item>> getItems(@Query("type") String type, @Query("username") String username, @Query("password") String password);
@POST("mark/{id}")
Call<SuccessResponse> markAsRead(@Path("id") String id, @Query("username") String username, @Query("password") String password);
@POST("unmark/{id}")
Call<SuccessResponse> unmarkAsRead(@Path("id") String id, @Query("username") String username, @Query("password") String password);
@FormUrlEncoded
@POST("mark")
Call<SuccessResponse> markAllAsRead(@Field("ids[]") List<String> ids, @Query("username") String username, @Query("password") String password);
@POST("starr/{id}")
Call<SuccessResponse> starr(@Path("id") String id, @Query("username") String username, @Query("password") String password);
@POST("unstarr/{id}")
Call<SuccessResponse> unstarr(@Path("id") String id, @Query("username") String username, @Query("password") String password);
@GET("stats")
Call<Stats> stats(@Query("username") String username, @Query("password") String password);
@GET("tags")
Call<List<Tag>> tags(@Query("username") String username, @Query("password") String password);
@GET("update")
Call<String> update(@Query("username") String username, @Query("password") String password);
@GET("sources/spouts")
Call<Map<String, Spout>> spouts(@Query("username") String username, @Query("password") String password);
@GET("sources/list")
Call<List<Sources>> sources(@Query("username") String username, @Query("password") String password);
@DELETE("source/{id}")
Call<SuccessResponse> deleteSource(@Path("id") String id, @Query("username") String username, @Query("password") String password);
@FormUrlEncoded
@POST("source")
Call<SuccessResponse> createSource(@Field("title") String title, @Field("url") String url, @Field("spout") String spout, @Field("tags") String tags, @Field("filter") String filter, @Query("username") String username, @Query("password") String password);
}

View File

@ -0,0 +1,111 @@
package apps.amine.bou.readerforselfoss.settings;
import android.content.res.Configuration;
import android.os.Bundle;
import android.preference.PreferenceActivity;
import android.support.annotation.LayoutRes;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v7.app.ActionBar;
import android.support.v7.app.AppCompatDelegate;
import android.support.v7.widget.Toolbar;
import android.view.MenuInflater;
import android.view.View;
import android.view.ViewGroup;
/**
* A {@link PreferenceActivity} which implements and proxies the necessary calls
* to be used with AppCompat.
*/
public abstract class AppCompatPreferenceActivity extends PreferenceActivity {
private AppCompatDelegate mDelegate;
@Override
protected void onCreate(Bundle savedInstanceState) {
getDelegate().installViewFactory();
getDelegate().onCreate(savedInstanceState);
super.onCreate(savedInstanceState);
}
@Override
protected void onPostCreate(Bundle savedInstanceState) {
super.onPostCreate(savedInstanceState);
getDelegate().onPostCreate(savedInstanceState);
}
ActionBar getSupportActionBar() {
return getDelegate().getSupportActionBar();
}
public void setSupportActionBar(@Nullable Toolbar toolbar) {
getDelegate().setSupportActionBar(toolbar);
}
@NonNull
@Override
public MenuInflater getMenuInflater() {
return getDelegate().getMenuInflater();
}
@Override
public void setContentView(@LayoutRes int layoutResID) {
getDelegate().setContentView(layoutResID);
}
@Override
public void setContentView(View view) {
getDelegate().setContentView(view);
}
@Override
public void setContentView(View view, ViewGroup.LayoutParams params) {
getDelegate().setContentView(view, params);
}
@Override
public void addContentView(View view, ViewGroup.LayoutParams params) {
getDelegate().addContentView(view, params);
}
@Override
protected void onPostResume() {
super.onPostResume();
getDelegate().onPostResume();
}
@Override
protected void onTitleChanged(CharSequence title, int color) {
super.onTitleChanged(title, color);
getDelegate().setTitle(title);
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
getDelegate().onConfigurationChanged(newConfig);
}
@Override
protected void onStop() {
super.onStop();
getDelegate().onStop();
}
@Override
protected void onDestroy() {
super.onDestroy();
getDelegate().onDestroy();
}
public void invalidateOptionsMenu() {
getDelegate().invalidateOptionsMenu();
}
private AppCompatDelegate getDelegate() {
if (mDelegate == null) {
mDelegate = AppCompatDelegate.create(this, null);
}
return mDelegate;
}
}

View File

@ -0,0 +1,211 @@
package apps.amine.bou.readerforselfoss.settings;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.Intent;
import android.content.res.Configuration;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.preference.Preference;
import android.preference.Preference.OnPreferenceChangeListener;
import android.preference.PreferenceActivity;
import android.preference.SwitchPreference;
import android.support.v7.app.ActionBar;
import android.preference.PreferenceFragment;
import android.preference.PreferenceManager;
import android.view.MenuItem;
import java.util.List;
import apps.amine.bou.readerforselfoss.R;
/**
* A {@link PreferenceActivity} that presents a set of application settings. On
* handset devices, settings are presented as a single list. On tablets,
* settings are split by category, with category headers shown to the left of
* the list of settings.
* <p>
* See <a href="http://developer.android.com/design/patterns/settings.html">
* Android Design: Settings</a> for design guidelines and the <a
* href="http://developer.android.com/guide/topics/ui/settings.html">Settings
* API Guide</a> for more information on developing a Settings UI.
*/
public class SettingsActivity extends AppCompatPreferenceActivity {
/**
* A preference value change listener that updates the preference's summary
* to reflect its new value.
*/
private static Preference.OnPreferenceChangeListener sBindPreferenceSummaryToValueListener = new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object value) {
String stringValue = value.toString();
preference.setSummary(stringValue);
return true;
}
};
/**
* Helper method to determine if the device has an extra-large screen. For
* example, 10" tablets are extra-large.
*/
private static boolean isXLargeTablet(Context context) {
return (context.getResources().getConfiguration().screenLayout
& Configuration.SCREENLAYOUT_SIZE_MASK) >= Configuration.SCREENLAYOUT_SIZE_XLARGE;
}
/**
* Binds a preference's summary to its value. More specifically, when the
* preference's value is changed, its summary (line of text below the
* preference title) is updated to reflect the value. The summary is also
* immediately updated upon calling this method. The exact display format is
* dependent on the type of preference.
*
* @see #sBindPreferenceSummaryToValueListener
*/
private static void bindPreferenceSummaryToValue(Preference preference) {
// Set the listener to watch for value changes.
preference.setOnPreferenceChangeListener(sBindPreferenceSummaryToValueListener);
// Trigger the listener immediately with the preference's
// current value.
sBindPreferenceSummaryToValueListener.onPreferenceChange(preference,
PreferenceManager
.getDefaultSharedPreferences(preference.getContext())
.getString(preference.getKey(), ""));
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setupActionBar();
}
/**
* Set up the {@link android.app.ActionBar}, if the API is available.
*/
private void setupActionBar() {
ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {
// Show the Up button in the action bar.
actionBar.setDisplayHomeAsUpEnabled(true);
}
}
/**
* {@inheritDoc}
*/
@Override
public boolean onIsMultiPane() {
return isXLargeTablet(this);
}
/**
* {@inheritDoc}
*/
@Override
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
public void onBuildHeaders(List<Header> target) {
loadHeadersFromResource(R.xml.pref_headers, target);
}
/**
* This method stops fragment injection in malicious applications.
* Make sure to deny any unknown fragments here.
*/
protected boolean isValidFragment(String fragmentName) {
return PreferenceFragment.class.getName().equals(fragmentName)
|| GeneralPreferenceFragment.class.getName().equals(fragmentName)
|| LinksPreferenceFragment.class.getName().equals(fragmentName);
}
/**
* This fragment shows general preferences only. It is used when the
* activity is showing a two-pane settings UI.
*/
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
public static class GeneralPreferenceFragment extends PreferenceFragment {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
addPreferencesFromResource(R.xml.pref_general);
setHasOptionsMenu(true);
SwitchPreference cardViewActive = (SwitchPreference) findPreference("card_view_active");
final SwitchPreference tabOnTap = (SwitchPreference) findPreference("tab_on_tap");
tabOnTap.setEnabled(!cardViewActive.isChecked());
cardViewActive.setOnPreferenceChangeListener(new OnPreferenceChangeListener() {
public boolean onPreferenceChange(Preference preference, Object newValue){
boolean isEnabled = (Boolean) newValue;
tabOnTap.setEnabled(!isEnabled);
return true;
}
});
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int id = item.getItemId();
if (id == android.R.id.home) {
getActivity().finish();
return true;
}
return super.onOptionsItemSelected(item);
}
}
/**
* This fragment shows general preferences only. It is used when the
* activity is showing a two-pane settings UI.
*/
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
public static class LinksPreferenceFragment extends PreferenceFragment {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
addPreferencesFromResource(R.xml.pref_links);
setHasOptionsMenu(true);
Preference tracker = findPreference( "trackerLink" );
tracker.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.tracker_url)));
startActivity(browserIntent);
return true;
}
});
findPreference("sourceLink").setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.source_url)));
startActivity(browserIntent);
return false;
}
});
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int id = item.getItemId();
if (id == android.R.id.home) {
getActivity().finish();
return true;
}
return super.onOptionsItemSelected(item);
}
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int id = item.getItemId();
if (id == android.R.id.home) {
finish();
return true;
}
return super.onOptionsItemSelected(item);
}
}

View File

@ -0,0 +1,93 @@
package apps.amine.bou.readerforselfoss.utils
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.content.SharedPreferences
import android.net.Uri
import android.support.v7.app.AlertDialog
import android.text.TextUtils
import android.util.Log
import android.util.Patterns
import apps.amine.bou.readerforselfoss.BuildConfig
import apps.amine.bou.readerforselfoss.R
import com.google.firebase.crash.FirebaseCrash
import com.google.firebase.remoteconfig.FirebaseRemoteConfig
import okhttp3.HttpUrl
private fun isStoreVersion(context: Context): Boolean {
var result = false
try {
val installer = context.packageManager
.getInstallerPackageName(context.packageName)
result = !TextUtils.isEmpty(installer)
} catch (e: Throwable) {
}
return result
}
fun checkAndDisplayStoreApk(context: Context) =
if (!isStoreVersion(context) && !BuildConfig.GITHUB_VERSION) {
val alertDialog = AlertDialog.Builder(context).create()
alertDialog.setTitle(context.getString(R.string.warning_version))
alertDialog.setMessage(context.getString(R.string.text_version))
alertDialog.setButton(AlertDialog.BUTTON_NEUTRAL, "OK",
{ dialog, _ -> dialog.dismiss() })
alertDialog.show()
} else Unit
fun isUrlValid(url: String): Boolean {
val baseUrl = HttpUrl.parse(url)
var existsAndEndsWithSlash = false
if (baseUrl != null) {
val pathSegments = baseUrl.pathSegments()
existsAndEndsWithSlash = "" == pathSegments[pathSegments.size - 1]
}
return Patterns.WEB_URL.matcher(url).matches() && existsAndEndsWithSlash
}
fun isEmptyOrNullOrNullString(str: String?): Boolean =
str == null || str == "null" || str.isEmpty()
fun checkApkVersion(settings: SharedPreferences, editor: SharedPreferences.Editor, context: Context, mFirebaseRemoteConfig: FirebaseRemoteConfig) {
mFirebaseRemoteConfig.fetch(43200)
.addOnCompleteListener { task ->
if (task.isSuccessful) {
mFirebaseRemoteConfig.activateFetched()
} else {
FirebaseCrash.logcat(Log.DEBUG, "CONFIG FETCH", "remote config task unsuccessful")
FirebaseCrash.report(Exception(task.exception))
}
isThereAnUpdate(settings, editor, context, mFirebaseRemoteConfig)
}
}
private fun isThereAnUpdate(settings: SharedPreferences, editor: SharedPreferences.Editor, context: Context, mFirebaseRemoteConfig: FirebaseRemoteConfig) {
val APK_LINK = "github_apk"
val apkLink = mFirebaseRemoteConfig.getString(APK_LINK)
val storedLink = settings.getString(APK_LINK, "")
if (apkLink != storedLink && !apkLink.isEmpty()) {
val alertDialog = AlertDialog.Builder(context).create()
alertDialog.setTitle(context.getString(R.string.new_apk_available_title))
alertDialog.setMessage(context.getString(R.string.new_apk_available_message))
alertDialog.setButton(AlertDialog.BUTTON_POSITIVE, context.getString(R.string.new_apk_available_get)) { _, _ ->
editor.putString(APK_LINK, apkLink)
editor.apply()
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(apkLink))
context.startActivity(browserIntent)
}
alertDialog.setButton(AlertDialog.BUTTON_NEUTRAL, context.getString(R.string.new_apk_available_no),
{ dialog, _ ->
editor.putString(APK_LINK, apkLink)
editor.apply()
dialog.dismiss()
})
alertDialog.show()
}
}

View File

@ -0,0 +1,34 @@
package apps.amine.bou.readerforselfoss.utils
import android.content.Context
import android.content.SharedPreferences
class Config(c: Context) {
private val settings: SharedPreferences
init {
this.settings = c.getSharedPreferences(settingsName, Context.MODE_PRIVATE)
}
val baseUrl: String
get() = settings.getString("url", "")
val userLogin: String
get() = settings.getString("login", "")
val userPassword: String
get() = settings.getString("password", "")
val httpUserLogin: String
get() = settings.getString("httpUserName", "")
val httpUserPassword: String
get() = settings.getString("httpPassword", "")
companion object {
val settingsName = "paramsselfoss"
}
}

View File

@ -0,0 +1,81 @@
package apps.amine.bou.readerforselfoss.utils
import android.app.Activity
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.graphics.BitmapFactory
import android.net.Uri
import android.support.customtabs.CustomTabsIntent
import apps.amine.bou.readerforselfoss.R
import apps.amine.bou.readerforselfoss.ReaderActivity
import apps.amine.bou.readerforselfoss.api.selfoss.Item
import apps.amine.bou.readerforselfoss.utils.customtabs.CustomTabActivityHelper
import xyz.klinker.android.drag_dismiss.DragDismissIntentBuilder
fun buildCustomTabsIntent(c: Context): CustomTabsIntent {
fun createPendingShareIntent(c: Context): PendingIntent {
val actionIntent = Intent(Intent.ACTION_SEND)
actionIntent.type = "text/plain"
return PendingIntent.getActivity(
c, 0, actionIntent, 0)
}
val intentBuilder = CustomTabsIntent.Builder()
// TODO: change to primary when it's possible to customize custom tabs title color
//intentBuilder.setToolbarColor(c.getResources().getColor(R.color.colorPrimary));
intentBuilder.setToolbarColor(c.resources.getColor(R.color.colorAccentDark))
intentBuilder.setShowTitle(true)
intentBuilder.setStartAnimations(c,
R.anim.slide_in_right,
R.anim.slide_out_left)
intentBuilder.setExitAnimations(c,
android.R.anim.slide_in_left,
android.R.anim.slide_out_right)
val closeicon = BitmapFactory.decodeResource(c.resources, R.drawable.ic_close_white_24dp)
intentBuilder.setCloseButtonIcon(closeicon)
val shareLabel = c.getString(R.string.label_share)
val icon = BitmapFactory.decodeResource(c.resources,
R.drawable.ic_share_white_24dp)
intentBuilder.setActionButton(icon, shareLabel, createPendingShareIntent(c))
return intentBuilder.build()
}
fun openItemUrl(i: Item,
customTabsIntent: CustomTabsIntent,
internalBrowser: Boolean,
articleViewer: Boolean,
app: Activity,
c: Context) {
if (!internalBrowser) {
val intent = Intent(Intent.ACTION_VIEW)
intent.data = Uri.parse(i.getLinkDecoded())
app.startActivity(intent)
} else {
if (articleViewer) {
val intent = Intent(c, ReaderActivity::class.java)
DragDismissIntentBuilder(c)
.setFullscreenOnTablets(true) // defaults to false, tablets will have padding on each side
.setDragElasticity(DragDismissIntentBuilder.DragElasticity.NORMAL) // Larger elasticities will make it easier to dismiss.
.build(intent)
intent.putExtra("url", i.getLinkDecoded())
app.startActivity(intent)
} else {
CustomTabActivityHelper.openCustomTab(app, customTabsIntent, Uri.parse(i.getLinkDecoded())
) { _, uri ->
val intent = Intent(Intent.ACTION_VIEW, uri)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
c.startActivity(intent)
}
}
}
}

View File

@ -0,0 +1,146 @@
package apps.amine.bou.readerforselfoss.utils.customtabs;
import android.app.Activity;
import android.content.ComponentName;
import android.net.Uri;
import android.os.Bundle;
import android.support.customtabs.CustomTabsClient;
import android.support.customtabs.CustomTabsIntent;
import android.support.customtabs.CustomTabsServiceConnection;
import android.support.customtabs.CustomTabsSession;
import java.util.List;
@SuppressWarnings("ALL")
public class CustomTabActivityHelper {
private CustomTabsSession mCustomTabsSession;
private CustomTabsClient mClient;
private CustomTabsServiceConnection mConnection;
private ConnectionCallback mConnectionCallback;
/**
* Opens the URL on a Custom Tab if possible. Otherwise fallsback to opening it on a WebView
*
* @param activity The host activity
* @param customTabsIntent a CustomTabsIntent to be used if Custom Tabs is available
* @param uri the Uri to be opened
* @param fallback a CustomTabFallback to be used if Custom Tabs is not available
*/
public static void openCustomTab(Activity activity,
CustomTabsIntent customTabsIntent,
Uri uri,
CustomTabFallback fallback) {
String packageName = CustomTabsHelper.getPackageNameToUse(activity);
//If we cant find a package name, it means there's no browser that supports
//Chrome Custom Tabs installed. So, we fallback to the webview
if (packageName == null) {
if (fallback != null) {
fallback.openUri(activity, uri);
}
} else {
customTabsIntent.intent.setPackage(packageName);
customTabsIntent.launchUrl(activity, uri);
}
}
/**
* Unbinds the Activity from the Custom Tabs Service
* @param activity the activity that is connected to the service
*/
public void unbindCustomTabsService(Activity activity) {
try {
if (mConnection == null) return;
activity.unbindService(mConnection);
mClient = null;
mCustomTabsSession = null;
} catch (RuntimeException e) {}
}
/**
* Creates or retrieves an exiting CustomTabsSession
*
* @return a CustomTabsSession
*/
public CustomTabsSession getSession() {
if (mClient == null) {
mCustomTabsSession = null;
} else if (mCustomTabsSession == null) {
mCustomTabsSession = mClient.newSession(null);
}
return mCustomTabsSession;
}
/**
* Register a Callback to be called when connected or disconnected from the Custom Tabs Service
* @param connectionCallback
*/
public void setConnectionCallback(ConnectionCallback connectionCallback) {
this.mConnectionCallback = connectionCallback;
}
/**
* Binds the Activity to the Custom Tabs Service
* @param activity the activity to be binded to the service
*/
public void bindCustomTabsService(Activity activity) {
if (mClient != null) return;
String packageName = CustomTabsHelper.getPackageNameToUse(activity);
if (packageName == null) return;
mConnection = new CustomTabsServiceConnection() {
@Override
public void onCustomTabsServiceConnected(ComponentName name, CustomTabsClient client) {
mClient = client;
mClient.warmup(0L);
if (mConnectionCallback != null) mConnectionCallback.onCustomTabsConnected();
//Initialize a session as soon as possible.
getSession();
}
@Override
public void onServiceDisconnected(ComponentName name) {
mClient = null;
if (mConnectionCallback != null) mConnectionCallback.onCustomTabsDisconnected();
}
};
CustomTabsClient.bindCustomTabsService(activity, packageName, mConnection);
}
public boolean mayLaunchUrl(Uri uri, Bundle extras, List<Bundle> otherLikelyBundles) {
if (mClient == null) return false;
CustomTabsSession session = getSession();
return session != null && session.mayLaunchUrl(uri, extras, otherLikelyBundles);
}
/**
* A Callback for when the service is connected or disconnected. Use those callbacks to
* handle UI changes when the service is connected or disconnected
*/
public interface ConnectionCallback {
/**
* Called when the service is connected
*/
void onCustomTabsConnected();
/**
* Called when the service is disconnected
*/
void onCustomTabsDisconnected();
}
/**
* To be used as a fallback to open the Uri when Custom Tabs is not available
*/
public interface CustomTabFallback {
/**
*
* @param activity The Activity that wants to open the Uri
* @param uri The uri to be opened by the fallback
*/
void openUri(Activity activity, Uri uri);
}
}

View File

@ -0,0 +1,126 @@
package apps.amine.bou.readerforselfoss.utils.customtabs;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.net.Uri;
import android.support.customtabs.CustomTabsService;
import android.text.TextUtils;
import android.util.Log;
import apps.amine.bou.readerforselfoss.utils.customtabs.helpers.KeepAliveService;
import java.util.ArrayList;
import java.util.List;
@SuppressWarnings("ALL")
class CustomTabsHelper {
private static final String TAG = "CustomTabsHelper";
private static final String STABLE_PACKAGE = "com.android.chrome";
private static final String BETA_PACKAGE = "com.chrome.beta";
private static final String DEV_PACKAGE = "com.chrome.dev";
private static final String LOCAL_PACKAGE = "com.google.android.apps.chrome";
private static final String EXTRA_CUSTOM_TABS_KEEP_ALIVE =
"android.support.customtabs.extra.KEEP_ALIVE";
private static String sPackageNameToUse;
private CustomTabsHelper() {}
public static void addKeepAliveExtra(Context context, Intent intent) {
Intent keepAliveIntent = new Intent().setClassName(
context.getPackageName(), KeepAliveService.class.getCanonicalName());
intent.putExtra(EXTRA_CUSTOM_TABS_KEEP_ALIVE, keepAliveIntent);
}
/**
* Goes through all apps that handle VIEW intents and have a warmup service. Picks
* the one chosen by the user if there is one, otherwise makes a best effort to return a
* valid package name.
*
* This is <strong>not</strong> threadsafe.
*
* @param context {@link Context} to use for accessing {@link PackageManager}.
* @return The package name recommended to use for connecting to custom tabs related components.
*/
public static String getPackageNameToUse(Context context) {
if (sPackageNameToUse != null) return sPackageNameToUse;
PackageManager pm = context.getPackageManager();
// Get default VIEW intent handler.
Intent activityIntent = new Intent(Intent.ACTION_VIEW, Uri.parse("http://www.example.com"));
ResolveInfo defaultViewHandlerInfo = pm.resolveActivity(activityIntent, 0);
String defaultViewHandlerPackageName = null;
if (defaultViewHandlerInfo != null) {
defaultViewHandlerPackageName = defaultViewHandlerInfo.activityInfo.packageName;
}
// Get all apps that can handle VIEW intents.
List<ResolveInfo> resolvedActivityList = pm.queryIntentActivities(activityIntent, 0);
List<String> packagesSupportingCustomTabs = new ArrayList<>();
for (ResolveInfo info : resolvedActivityList) {
Intent serviceIntent = new Intent();
serviceIntent.setAction(CustomTabsService.ACTION_CUSTOM_TABS_CONNECTION);
serviceIntent.setPackage(info.activityInfo.packageName);
if (pm.resolveService(serviceIntent, 0) != null) {
packagesSupportingCustomTabs.add(info.activityInfo.packageName);
}
}
// Now packagesSupportingCustomTabs contains all apps that can handle both VIEW intents
// and service calls.
if (packagesSupportingCustomTabs.isEmpty()) {
sPackageNameToUse = null;
} else if (packagesSupportingCustomTabs.size() == 1) {
sPackageNameToUse = packagesSupportingCustomTabs.get(0);
} else if (!TextUtils.isEmpty(defaultViewHandlerPackageName)
&& !hasSpecializedHandlerIntents(context, activityIntent)
&& packagesSupportingCustomTabs.contains(defaultViewHandlerPackageName)) {
sPackageNameToUse = defaultViewHandlerPackageName;
} else if (packagesSupportingCustomTabs.contains(STABLE_PACKAGE)) {
sPackageNameToUse = STABLE_PACKAGE;
} else if (packagesSupportingCustomTabs.contains(BETA_PACKAGE)) {
sPackageNameToUse = BETA_PACKAGE;
} else if (packagesSupportingCustomTabs.contains(DEV_PACKAGE)) {
sPackageNameToUse = DEV_PACKAGE;
} else if (packagesSupportingCustomTabs.contains(LOCAL_PACKAGE)) {
sPackageNameToUse = LOCAL_PACKAGE;
}
return sPackageNameToUse;
}
/**
* Used to check whether there is a specialized handler for a given intent.
* @param intent The intent to check with.
* @return Whether there is a specialized handler for the given intent.
*/
private static boolean hasSpecializedHandlerIntents(Context context, Intent intent) {
try {
PackageManager pm = context.getPackageManager();
List<ResolveInfo> handlers = pm.queryIntentActivities(
intent,
PackageManager.GET_RESOLVED_FILTER);
if (handlers == null || handlers.size() == 0) {
return false;
}
for (ResolveInfo resolveInfo : handlers) {
IntentFilter filter = resolveInfo.filter;
if (filter == null) continue;
if (filter.countDataAuthorities() == 0 || filter.countDataPaths() == 0) continue;
if (resolveInfo.activityInfo == null) continue;
return true;
}
} catch (RuntimeException e) {
Log.e(TAG, "Runtime exception while getting specialized handlers");
}
return false;
}
/**
* @return All possible chrome package names that provide custom tabs feature.
*/
public static String[] getPackages() {
return new String[]{"", STABLE_PACKAGE, BETA_PACKAGE, DEV_PACKAGE, LOCAL_PACKAGE};
}
}

View File

@ -0,0 +1,15 @@
package apps.amine.bou.readerforselfoss.utils.customtabs.helpers;
import android.app.Service;
import android.content.Intent;
import android.os.Binder;
import android.os.IBinder;
public class KeepAliveService extends Service {
private static final Binder sBinder = new Binder();
@Override
public IBinder onBind(Intent intent) {
return sBinder;
}
}

View File

@ -1,14 +0,0 @@
package bou.amine.apps.readerforselfoss
import android.support.v7.app.AppCompatActivity
import android.os.Bundle
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
}

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2015 Google Inc. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate android:fromXDelta="100%p" android:toXDelta="0"
android:duration="@android:integer/config_mediumAnimTime"/>
</set>

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2015 Google Inc. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate android:fromXDelta="0" android:toXDelta="-100%p"
android:duration="@android:integer/config_mediumAnimTime"/>
</set>

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 239 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 221 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 275 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 361 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 301 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 355 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 953 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 187 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 473 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 453 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 398 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 397 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 434 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 213 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 247 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 208 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 241 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 655 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 309 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 322 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 262 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 268 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 307 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 261 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 257 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 300 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 437 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 308 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 464 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 208 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 625 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 557 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 483 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 496 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 542 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 377 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 347 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 398 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 636 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 448 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 649 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 283 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 907 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 827 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 675 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 698 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 768 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 483 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 436 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 473 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 802 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 527 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 874 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 359 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Some files were not shown because too many files have changed in this diff Show More