Drawables and strings and everything else. Some unconverted java classes.
@ -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
|
||||
|
||||
|
@ -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'
|
6
app/proguard-rules.pro
vendored
@ -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>;
|
||||
}
|
@ -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>
|
@ -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()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
||||
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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);}
|
||||
|
||||
}
|
@ -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("&url=")) {
|
||||
stringUrl = link.substringAfter("&url=")
|
||||
} else {
|
||||
stringUrl = this.link.replace("&", "&")
|
||||
}
|
||||
} else {
|
||||
stringUrl = this.link.replace("&", "&")
|
||||
}
|
||||
|
||||
// handle :443 => https
|
||||
if (stringUrl.contains(":443")) {
|
||||
stringUrl = stringUrl.replace(":443", "").replace("http://", "https://")
|
||||
}
|
||||
return stringUrl
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
|
||||
}
|
@ -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"
|
||||
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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};
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
16
app/src/main/res/anim/slide_in_right.xml
Normal 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>
|
16
app/src/main/res/anim/slide_out_left.xml
Normal 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>
|
BIN
app/src/main/res/drawable-hdpi/ic_add_black_24dp.png
Normal file
After Width: | Height: | Size: 124 B |
BIN
app/src/main/res/drawable-hdpi/ic_archive_black_24dp.png
Normal file
After Width: | Height: | Size: 239 B |
BIN
app/src/main/res/drawable-hdpi/ic_close_white_24dp.png
Normal file
After Width: | Height: | Size: 221 B |
BIN
app/src/main/res/drawable-hdpi/ic_done_all_white_24dp.png
Normal file
After Width: | Height: | Size: 275 B |
BIN
app/src/main/res/drawable-hdpi/ic_favorite_black_24dp.png
Normal file
After Width: | Height: | Size: 361 B |
BIN
app/src/main/res/drawable-hdpi/ic_fiber_new_black_24dp.png
Normal file
After Width: | Height: | Size: 301 B |
BIN
app/src/main/res/drawable-hdpi/ic_info_black_24.png
Normal file
After Width: | Height: | Size: 355 B |
BIN
app/src/main/res/drawable-hdpi/ic_info_outline_white_48dp.png
Normal file
After Width: | Height: | Size: 953 B |
BIN
app/src/main/res/drawable-hdpi/ic_open_in_browser_black_24dp.png
Normal file
After Width: | Height: | Size: 187 B |
After Width: | Height: | Size: 473 B |
BIN
app/src/main/res/drawable-hdpi/ic_settings_black_24dp.png
Normal file
After Width: | Height: | Size: 453 B |
BIN
app/src/main/res/drawable-hdpi/ic_share_black_24dp.png
Normal file
After Width: | Height: | Size: 398 B |
BIN
app/src/main/res/drawable-hdpi/ic_share_white_24dp.png
Normal file
After Width: | Height: | Size: 397 B |
BIN
app/src/main/res/drawable-hdpi/ic_thumb_up_white_48dp.png
Normal file
After Width: | Height: | Size: 434 B |
BIN
app/src/main/res/drawable-mdpi/ic_add_black_24dp.png
Normal file
After Width: | Height: | Size: 86 B |
BIN
app/src/main/res/drawable-mdpi/ic_archive_black_24dp.png
Normal file
After Width: | Height: | Size: 174 B |
BIN
app/src/main/res/drawable-mdpi/ic_close_white_24dp.png
Normal file
After Width: | Height: | Size: 175 B |
BIN
app/src/main/res/drawable-mdpi/ic_done_all_white_24dp.png
Normal file
After Width: | Height: | Size: 213 B |
BIN
app/src/main/res/drawable-mdpi/ic_favorite_black_24dp.png
Normal file
After Width: | Height: | Size: 247 B |
BIN
app/src/main/res/drawable-mdpi/ic_fiber_new_black_24dp.png
Normal file
After Width: | Height: | Size: 208 B |
BIN
app/src/main/res/drawable-mdpi/ic_info_black_24.png
Normal file
After Width: | Height: | Size: 241 B |
BIN
app/src/main/res/drawable-mdpi/ic_info_outline_white_48dp.png
Normal file
After Width: | Height: | Size: 655 B |
BIN
app/src/main/res/drawable-mdpi/ic_open_in_browser_black_24dp.png
Normal file
After Width: | Height: | Size: 144 B |
After Width: | Height: | Size: 309 B |
BIN
app/src/main/res/drawable-mdpi/ic_settings_black_24dp.png
Normal file
After Width: | Height: | Size: 322 B |
BIN
app/src/main/res/drawable-mdpi/ic_share_black_24dp.png
Normal file
After Width: | Height: | Size: 262 B |
BIN
app/src/main/res/drawable-mdpi/ic_share_white_24dp.png
Normal file
After Width: | Height: | Size: 268 B |
BIN
app/src/main/res/drawable-mdpi/ic_thumb_up_white_48dp.png
Normal file
After Width: | Height: | Size: 307 B |
BIN
app/src/main/res/drawable-xhdpi/ic_add_black_24dp.png
Normal file
After Width: | Height: | Size: 108 B |
BIN
app/src/main/res/drawable-xhdpi/ic_archive_black_24dp.png
Normal file
After Width: | Height: | Size: 261 B |
BIN
app/src/main/res/drawable-xhdpi/ic_close_white_24dp.png
Normal file
After Width: | Height: | Size: 257 B |
BIN
app/src/main/res/drawable-xhdpi/ic_done_all_white_24dp.png
Normal file
After Width: | Height: | Size: 300 B |
BIN
app/src/main/res/drawable-xhdpi/ic_favorite_black_24dp.png
Normal file
After Width: | Height: | Size: 437 B |
BIN
app/src/main/res/drawable-xhdpi/ic_fiber_new_black_24dp.png
Normal file
After Width: | Height: | Size: 308 B |
BIN
app/src/main/res/drawable-xhdpi/ic_info_black_24.png
Normal file
After Width: | Height: | Size: 464 B |
BIN
app/src/main/res/drawable-xhdpi/ic_info_outline_white_48dp.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 208 B |
After Width: | Height: | Size: 625 B |
BIN
app/src/main/res/drawable-xhdpi/ic_settings_black_24dp.png
Normal file
After Width: | Height: | Size: 557 B |
BIN
app/src/main/res/drawable-xhdpi/ic_share_black_24dp.png
Normal file
After Width: | Height: | Size: 483 B |
BIN
app/src/main/res/drawable-xhdpi/ic_share_white_24dp.png
Normal file
After Width: | Height: | Size: 496 B |
BIN
app/src/main/res/drawable-xhdpi/ic_thumb_up_white_48dp.png
Normal file
After Width: | Height: | Size: 542 B |
BIN
app/src/main/res/drawable-xxhdpi/ic_add_black_24dp.png
Normal file
After Width: | Height: | Size: 114 B |
BIN
app/src/main/res/drawable-xxhdpi/ic_archive_black_24dp.png
Normal file
After Width: | Height: | Size: 377 B |
BIN
app/src/main/res/drawable-xxhdpi/ic_close_white_24dp.png
Normal file
After Width: | Height: | Size: 347 B |
BIN
app/src/main/res/drawable-xxhdpi/ic_done_all_white_24dp.png
Normal file
After Width: | Height: | Size: 398 B |
BIN
app/src/main/res/drawable-xxhdpi/ic_favorite_black_24dp.png
Normal file
After Width: | Height: | Size: 636 B |
BIN
app/src/main/res/drawable-xxhdpi/ic_fiber_new_black_24dp.png
Normal file
After Width: | Height: | Size: 448 B |
BIN
app/src/main/res/drawable-xxhdpi/ic_info_black_24.png
Normal file
After Width: | Height: | Size: 649 B |
BIN
app/src/main/res/drawable-xxhdpi/ic_info_outline_white_48dp.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 283 B |
After Width: | Height: | Size: 907 B |
BIN
app/src/main/res/drawable-xxhdpi/ic_settings_black_24dp.png
Normal file
After Width: | Height: | Size: 827 B |
BIN
app/src/main/res/drawable-xxhdpi/ic_share_black_24dp.png
Normal file
After Width: | Height: | Size: 675 B |
BIN
app/src/main/res/drawable-xxhdpi/ic_share_white_24dp.png
Normal file
After Width: | Height: | Size: 698 B |
BIN
app/src/main/res/drawable-xxhdpi/ic_thumb_up_white_48dp.png
Normal file
After Width: | Height: | Size: 768 B |
BIN
app/src/main/res/drawable-xxxhdpi/ic_add_black_24dp.png
Normal file
After Width: | Height: | Size: 119 B |
BIN
app/src/main/res/drawable-xxxhdpi/ic_archive_black_24dp.png
Normal file
After Width: | Height: | Size: 483 B |
BIN
app/src/main/res/drawable-xxxhdpi/ic_close_white_24dp.png
Normal file
After Width: | Height: | Size: 436 B |
BIN
app/src/main/res/drawable-xxxhdpi/ic_done_all_white_24dp.png
Normal file
After Width: | Height: | Size: 473 B |
BIN
app/src/main/res/drawable-xxxhdpi/ic_favorite_black_24dp.png
Normal file
After Width: | Height: | Size: 802 B |
BIN
app/src/main/res/drawable-xxxhdpi/ic_fiber_new_black_24dp.png
Normal file
After Width: | Height: | Size: 527 B |
BIN
app/src/main/res/drawable-xxxhdpi/ic_info_black_24.png
Normal file
After Width: | Height: | Size: 874 B |
BIN
app/src/main/res/drawable-xxxhdpi/ic_info_outline_white_48dp.png
Normal file
After Width: | Height: | Size: 2.6 KiB |
After Width: | Height: | Size: 359 B |
After Width: | Height: | Size: 1.2 KiB |
BIN
app/src/main/res/drawable-xxxhdpi/ic_settings_black_24dp.png
Normal file
After Width: | Height: | Size: 1.0 KiB |