diff --git a/README.md b/README.md index bf660a0..d5e5491 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/app/build.gradle b/app/build.gradle index 5f902b7..822a678 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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' \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 44927d8..623ce19 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -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$* { + ; +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f961aa7..8751646 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,6 +1,6 @@ + package="apps.amine.bou.readerforselfoss"> @@ -20,7 +20,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/apps/amine/bou/readerforselfoss/AddSourceActivity.kt b/app/src/main/java/apps/amine/bou/readerforselfoss/AddSourceActivity.kt new file mode 100644 index 0000000..76ddc1f --- /dev/null +++ b/app/src/main/java/apps/amine/bou/readerforselfoss/AddSourceActivity.kt @@ -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() + 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 + api.spouts().enqueue(object : Callback> { + override fun onResponse(call: Call>, response: Response>) { + 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>, 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 { + override fun onResponse(call: Call, response: Response) { + 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, t: Throwable) { + Toast.makeText(this@AddSourceActivity, R.string.cant_create_source, Toast.LENGTH_SHORT).show() + } + }) + } + } +} diff --git a/app/src/main/java/apps/amine/bou/readerforselfoss/HomeActivity.kt b/app/src/main/java/apps/amine/bou/readerforselfoss/HomeActivity.kt new file mode 100644 index 0000000..6519a10 --- /dev/null +++ b/app/src/main/java/apps/amine/bou/readerforselfoss/HomeActivity.kt @@ -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? = 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> { + override fun onResponse(call: Call>, response: Response>) { + if (response.body() != null && response.body().isNotEmpty()) { + items = response.body() + } else { + items = ArrayList() + } + handleListResult() + mSwipeRefreshLayout!!.isRefreshing = false + } + + override fun onFailure(call: Call>, 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> { + override fun onResponse(call: Call>, response: Response>) { + if (response.body() != null && response.body().isNotEmpty()) { + items = response.body() + } else { + items = ArrayList() + } + handleListResult() + mSwipeRefreshLayout!!.isRefreshing = false + } + + override fun onFailure(call: Call>, 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> { + override fun onResponse(call: Call>, response: Response>) { + if (response.body() != null && response.body().isNotEmpty()) { + items = response.body() + } else { + items = ArrayList() + } + handleListResult() + mSwipeRefreshLayout!!.isRefreshing = false + } + + override fun onFailure(call: Call>, 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 { + override fun onResponse(call: Call, response: Response) { + Toast.makeText(this@HomeActivity, + R.string.refresh_success_response, Toast.LENGTH_LONG) + .show() + } + + override fun onFailure(call: Call, 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 { + override fun onResponse(call: Call, response: Response) { + 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, 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 { + override fun onResponse(call: Call, response: Response) { + 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, 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() + } +} diff --git a/app/src/main/java/apps/amine/bou/readerforselfoss/IntroActivity.kt b/app/src/main/java/apps/amine/bou/readerforselfoss/IntroActivity.kt new file mode 100644 index 0000000..61720a6 --- /dev/null +++ b/app/src/main/java/apps/amine/bou/readerforselfoss/IntroActivity.kt @@ -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() + } +} diff --git a/app/src/main/java/apps/amine/bou/readerforselfoss/LoginActivity.kt b/app/src/main/java/apps/amine/bou/readerforselfoss/LoginActivity.kt new file mode 100644 index 0000000..c06d0fb --- /dev/null +++ b/app/src/main/java/apps/amine/bou/readerforselfoss/LoginActivity.kt @@ -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 { + 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, response: Response) { + if (response.body() != null && response.body().isSuccess) { + goToMain() + } else { + preferenceError() + } + } + + override fun onFailure(call: Call, 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) + } + } +} diff --git a/app/src/main/java/apps/amine/bou/readerforselfoss/MainActivity.kt b/app/src/main/java/apps/amine/bou/readerforselfoss/MainActivity.kt new file mode 100644 index 0000000..c8b3692 --- /dev/null +++ b/app/src/main/java/apps/amine/bou/readerforselfoss/MainActivity.kt @@ -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() + + } +} diff --git a/app/src/main/java/bou/amine/apps/readerforselfoss/MyApp.kt b/app/src/main/java/apps/amine/bou/readerforselfoss/MyApp.kt similarity index 50% rename from app/src/main/java/bou/amine/apps/readerforselfoss/MyApp.kt rename to app/src/main/java/apps/amine/bou/readerforselfoss/MyApp.kt index 014911f..9e2f304 100644 --- a/app/src/main/java/bou/amine/apps/readerforselfoss/MyApp.kt +++ b/app/src/main/java/apps/amine/bou/readerforselfoss/MyApp.kt @@ -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() } } \ No newline at end of file diff --git a/app/src/main/java/apps/amine/bou/readerforselfoss/ReaderActivity.kt b/app/src/main/java/apps/amine/bou/readerforselfoss/ReaderActivity.kt new file mode 100644 index 0000000..0243284 --- /dev/null +++ b/app/src/main/java/apps/amine/bou/readerforselfoss/ReaderActivity.kt @@ -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 { + override fun onResponse(call: Call, response: Response) { + 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, 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 + } +} diff --git a/app/src/main/java/apps/amine/bou/readerforselfoss/SourcesActivity.kt b/app/src/main/java/apps/amine/bou/readerforselfoss/SourcesActivity.kt new file mode 100644 index 0000000..fbffbf3 --- /dev/null +++ b/app/src/main/java/apps/amine/bou/readerforselfoss/SourcesActivity.kt @@ -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 = ArrayList() + + mFab.attachToRecyclerView(mRecyclerView) + mRecyclerView.setHasFixedSize(true) + mRecyclerView.layoutManager = mLayoutManager + + api.sources.enqueue(object : Callback> { + override fun onResponse(call: Call>, response: Response>) { + 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>, t: Throwable) { + Toast.makeText(this@SourcesActivity, R.string.cant_get_sources, Toast.LENGTH_SHORT).show() + } + }) + + mFab.setOnClickListener { + startActivity(Intent(this@SourcesActivity, AddSourceActivity::class.java)) + } + } +} diff --git a/app/src/main/java/apps/amine/bou/readerforselfoss/adapters/ItemCardAdapter.java b/app/src/main/java/apps/amine/bou/readerforselfoss/adapters/ItemCardAdapter.java new file mode 100644 index 0000000..8a987d1 --- /dev/null +++ b/app/src/main/java/apps/amine/bou/readerforselfoss/adapters/ItemCardAdapter.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 { + private final List 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 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() { + @Override + public void onResponse(Call call, Response response) {} + + @Override + public void onFailure(Call 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() { + @Override + public void onResponse(Call call, Response response) { + + doUnmark(i, position); + } + + @Override + public void onFailure(Call 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() { + @Override + public void onResponse(Call call, Response response) {} + + @Override + public void onFailure(Call 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() { + @Override + public void onResponse(Call call, Response response) {} + + @Override + public void onFailure(Call 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); + } + }); + } + } +} diff --git a/app/src/main/java/apps/amine/bou/readerforselfoss/adapters/ItemListAdapter.java b/app/src/main/java/apps/amine/bou/readerforselfoss/adapters/ItemListAdapter.java new file mode 100644 index 0000000..b972b52 --- /dev/null +++ b/app/src/main/java/apps/amine/bou/readerforselfoss/adapters/ItemListAdapter.java @@ -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 { + private final boolean clickBehavior; + private final boolean articleViewer; + private final boolean internalBrowser; + private final ColorGenerator generator; + private SelfossApi api; + private Context c; + private List items; + private List bars; + private Activity app; + private CustomTabActivityHelper helper; + + public ItemListAdapter(Activity a, List 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() { + @Override + public void onResponse(Call call, Response response) {} + + @Override + public void onFailure(Call 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() { + @Override + public void onResponse(Call call, Response response) { + doUnmark(i, position); + + } + + @Override + public void onFailure(Call 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() { + @Override + public void onResponse(Call call, Response response) {} + + @Override + public void onFailure(Call 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() { + @Override + public void onResponse(Call call, Response response) {} + + @Override + public void onFailure(Call 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); + } + } +} diff --git a/app/src/main/java/apps/amine/bou/readerforselfoss/adapters/SourcesListAdapter.java b/app/src/main/java/apps/amine/bou/readerforselfoss/adapters/SourcesListAdapter.java new file mode 100644 index 0000000..b65d98d --- /dev/null +++ b/app/src/main/java/apps/amine/bou/readerforselfoss/adapters/SourcesListAdapter.java @@ -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 { + private final List items; + private final Activity app; + private final SelfossApi api; + private final Context c; + private final ColorGenerator generator; + + public SourcesListAdapter(Activity activity, List 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() { + @Override + public void onResponse(Call call, Response 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 call, Throwable t) { + Toast.makeText(app, "Petit soucis lors de la suppression de la source.", Toast.LENGTH_SHORT).show(); + } + }); + } + }); + + + } + } +} diff --git a/app/src/main/java/apps/amine/bou/readerforselfoss/api/mercury/MercuryApi.java b/app/src/main/java/apps/amine/bou/readerforselfoss/api/mercury/MercuryApi.java new file mode 100644 index 0000000..d3d4332 --- /dev/null +++ b/app/src/main/java/apps/amine/bou/readerforselfoss/api/mercury/MercuryApi.java @@ -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 parseUrl(String url) { + return service.parseUrl(url, this.key); + } +} diff --git a/app/src/main/java/apps/amine/bou/readerforselfoss/api/mercury/MercuryModels.kt b/app/src/main/java/apps/amine/bou/readerforselfoss/api/mercury/MercuryModels.kt new file mode 100644 index 0000000..1b05115 --- /dev/null +++ b/app/src/main/java/apps/amine/bou/readerforselfoss/api/mercury/MercuryModels.kt @@ -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 = object : Parcelable.Creator { + override fun createFromParcel(source: Parcel): ParsedContent = ParsedContent(source) + override fun newArray(size: Int): Array = 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) + } +} diff --git a/app/src/main/java/apps/amine/bou/readerforselfoss/api/mercury/MercuryService.java b/app/src/main/java/apps/amine/bou/readerforselfoss/api/mercury/MercuryService.java new file mode 100644 index 0000000..85fc610 --- /dev/null +++ b/app/src/main/java/apps/amine/bou/readerforselfoss/api/mercury/MercuryService.java @@ -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 parseUrl(@Query("url") String url, @Header("x-api-key") String key); +} diff --git a/app/src/main/java/apps/amine/bou/readerforselfoss/api/selfoss/BooleanTypeAdapter.kt b/app/src/main/java/apps/amine/bou/readerforselfoss/api/selfoss/BooleanTypeAdapter.kt new file mode 100644 index 0000000..69acad7 --- /dev/null +++ b/app/src/main/java/apps/amine/bou/readerforselfoss/api/selfoss/BooleanTypeAdapter.kt @@ -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 { + + @Throws(JsonParseException::class) + override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): Boolean? = + try { + json.asInt == 1 + } catch (e: Exception) { + json.asBoolean + } +} diff --git a/app/src/main/java/apps/amine/bou/readerforselfoss/api/selfoss/SelfossApi.java b/app/src/main/java/apps/amine/bou/readerforselfoss/api/selfoss/SelfossApi.java new file mode 100644 index 0000000..b966f2e --- /dev/null +++ b/app/src/main/java/apps/amine/bou/readerforselfoss/api/selfoss/SelfossApi.java @@ -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 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 login() { + return service.loginToSelfoss(config.getUserLogin(), config.getUserPassword()); + } + + public Call> getReadItems() { + return getItems("read"); + } + + public Call> getUnreadItems() { + return getItems("unread"); + } + + public Call> getStarredItems() { + return getItems("starred"); + } + + private Call> getItems(String type) { + return service.getItems(type, userName, password); + } + + public Call markItem(String itemId) { + return service.markAsRead(itemId, userName, password); + } + + public Call unmarkItem(String itemId) { + return service.unmarkAsRead(itemId, userName, password); + } + + public Call readAll(List ids) { + return service.markAllAsRead(ids, userName, password); + } + + public Call starrItem(String itemId) { + return service.starr(itemId, userName, password); + } + + + public Call unstarrItem(String itemId) { + return service.unstarr(itemId, userName, password); + } + + public Call getStats() { + return service.stats(userName, password); + } + + public Call> getTags() { + return service.tags(userName, password); + } + + public Call update() { + return service.update(userName, password); + } + + public Call> getSources() { return service.sources(userName, password); } + + public Call deleteSource(String id) { return service.deleteSource(id, userName, password);} + + public Call> spouts() { return service.spouts(userName, password); } + + public Call createSource(String title, String url, String spout, String tags, String filter) {return service.createSource(title, url, spout, tags, filter, userName, password);} + +} diff --git a/app/src/main/java/apps/amine/bou/readerforselfoss/api/selfoss/SelfossModels.kt b/app/src/main/java/apps/amine/bou/readerforselfoss/api/selfoss/SelfossModels.kt new file mode 100644 index 0000000..f87e67e --- /dev/null +++ b/app/src/main/java/apps/amine/bou/readerforselfoss/api/selfoss/SelfossModels.kt @@ -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 = object : Parcelable.Creator { + override fun createFromParcel(source: Parcel): Item = Item(source) + override fun newArray(size: Int): Array = 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 + } + +} \ No newline at end of file diff --git a/app/src/main/java/apps/amine/bou/readerforselfoss/api/selfoss/SelfossService.java b/app/src/main/java/apps/amine/bou/readerforselfoss/api/selfoss/SelfossService.java new file mode 100644 index 0000000..973fe60 --- /dev/null +++ b/app/src/main/java/apps/amine/bou/readerforselfoss/api/selfoss/SelfossService.java @@ -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 loginToSelfoss(@Query("username") String username, @Query("password") String password); + + @GET("items") + Call> getItems(@Query("type") String type, @Query("username") String username, @Query("password") String password); + + @POST("mark/{id}") + Call markAsRead(@Path("id") String id, @Query("username") String username, @Query("password") String password); + + + @POST("unmark/{id}") + Call unmarkAsRead(@Path("id") String id, @Query("username") String username, @Query("password") String password); + + @FormUrlEncoded + @POST("mark") + Call markAllAsRead(@Field("ids[]") List ids, @Query("username") String username, @Query("password") String password); + + + @POST("starr/{id}") + Call starr(@Path("id") String id, @Query("username") String username, @Query("password") String password); + + + @POST("unstarr/{id}") + Call unstarr(@Path("id") String id, @Query("username") String username, @Query("password") String password); + + + @GET("stats") + Call stats(@Query("username") String username, @Query("password") String password); + + + @GET("tags") + Call> tags(@Query("username") String username, @Query("password") String password); + + + @GET("update") + Call update(@Query("username") String username, @Query("password") String password); + + @GET("sources/spouts") + Call> spouts(@Query("username") String username, @Query("password") String password); + + @GET("sources/list") + Call> sources(@Query("username") String username, @Query("password") String password); + + + @DELETE("source/{id}") + Call deleteSource(@Path("id") String id, @Query("username") String username, @Query("password") String password); + + @FormUrlEncoded + @POST("source") + Call 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); +} diff --git a/app/src/main/java/apps/amine/bou/readerforselfoss/settings/AppCompatPreferenceActivity.java b/app/src/main/java/apps/amine/bou/readerforselfoss/settings/AppCompatPreferenceActivity.java new file mode 100644 index 0000000..ef49cfd --- /dev/null +++ b/app/src/main/java/apps/amine/bou/readerforselfoss/settings/AppCompatPreferenceActivity.java @@ -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; + } +} diff --git a/app/src/main/java/apps/amine/bou/readerforselfoss/settings/SettingsActivity.java b/app/src/main/java/apps/amine/bou/readerforselfoss/settings/SettingsActivity.java new file mode 100644 index 0000000..36d0fb5 --- /dev/null +++ b/app/src/main/java/apps/amine/bou/readerforselfoss/settings/SettingsActivity.java @@ -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. + *

+ * See + * Android Design: Settings for design guidelines and the Settings + * API Guide 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

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); + } +} diff --git a/app/src/main/java/apps/amine/bou/readerforselfoss/utils/AppUtils.kt b/app/src/main/java/apps/amine/bou/readerforselfoss/utils/AppUtils.kt new file mode 100644 index 0000000..bf123fc --- /dev/null +++ b/app/src/main/java/apps/amine/bou/readerforselfoss/utils/AppUtils.kt @@ -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() + } + +} \ No newline at end of file diff --git a/app/src/main/java/apps/amine/bou/readerforselfoss/utils/Config.kt b/app/src/main/java/apps/amine/bou/readerforselfoss/utils/Config.kt new file mode 100644 index 0000000..7e9f037 --- /dev/null +++ b/app/src/main/java/apps/amine/bou/readerforselfoss/utils/Config.kt @@ -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" + + } +} diff --git a/app/src/main/java/apps/amine/bou/readerforselfoss/utils/LinksUtils.kt b/app/src/main/java/apps/amine/bou/readerforselfoss/utils/LinksUtils.kt new file mode 100644 index 0000000..466e825 --- /dev/null +++ b/app/src/main/java/apps/amine/bou/readerforselfoss/utils/LinksUtils.kt @@ -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) + } + } + } +} diff --git a/app/src/main/java/apps/amine/bou/readerforselfoss/utils/customtabs/CustomTabActivityHelper.java b/app/src/main/java/apps/amine/bou/readerforselfoss/utils/customtabs/CustomTabActivityHelper.java new file mode 100644 index 0000000..8c287df --- /dev/null +++ b/app/src/main/java/apps/amine/bou/readerforselfoss/utils/customtabs/CustomTabActivityHelper.java @@ -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 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); + } + +} diff --git a/app/src/main/java/apps/amine/bou/readerforselfoss/utils/customtabs/CustomTabsHelper.java b/app/src/main/java/apps/amine/bou/readerforselfoss/utils/customtabs/CustomTabsHelper.java new file mode 100644 index 0000000..19ac086 --- /dev/null +++ b/app/src/main/java/apps/amine/bou/readerforselfoss/utils/customtabs/CustomTabsHelper.java @@ -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 not 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 resolvedActivityList = pm.queryIntentActivities(activityIntent, 0); + List 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 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}; + } +} diff --git a/app/src/main/java/apps/amine/bou/readerforselfoss/utils/customtabs/helpers/KeepAliveService.java b/app/src/main/java/apps/amine/bou/readerforselfoss/utils/customtabs/helpers/KeepAliveService.java new file mode 100644 index 0000000..9758d72 --- /dev/null +++ b/app/src/main/java/apps/amine/bou/readerforselfoss/utils/customtabs/helpers/KeepAliveService.java @@ -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; + } +} diff --git a/app/src/main/java/bou/amine/apps/readerforselfoss/MainActivity.kt b/app/src/main/java/bou/amine/apps/readerforselfoss/MainActivity.kt deleted file mode 100644 index 04628ee..0000000 --- a/app/src/main/java/bou/amine/apps/readerforselfoss/MainActivity.kt +++ /dev/null @@ -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) - } -} diff --git a/app/src/main/res/anim/slide_in_right.xml b/app/src/main/res/anim/slide_in_right.xml new file mode 100644 index 0000000..3189c25 --- /dev/null +++ b/app/src/main/res/anim/slide_in_right.xml @@ -0,0 +1,16 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/slide_out_left.xml b/app/src/main/res/anim/slide_out_left.xml new file mode 100644 index 0000000..0ec7682 --- /dev/null +++ b/app/src/main/res/anim/slide_out_left.xml @@ -0,0 +1,16 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-hdpi/ic_add_black_24dp.png b/app/src/main/res/drawable-hdpi/ic_add_black_24dp.png new file mode 100644 index 0000000..c04b523 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_add_black_24dp.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_archive_black_24dp.png b/app/src/main/res/drawable-hdpi/ic_archive_black_24dp.png new file mode 100644 index 0000000..94d0646 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_archive_black_24dp.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_close_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_close_white_24dp.png new file mode 100644 index 0000000..ceb1a1e Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_close_white_24dp.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_done_all_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_done_all_white_24dp.png new file mode 100644 index 0000000..8c83ff9 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_done_all_white_24dp.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_favorite_black_24dp.png b/app/src/main/res/drawable-hdpi/ic_favorite_black_24dp.png new file mode 100644 index 0000000..b8a2be6 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_favorite_black_24dp.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_fiber_new_black_24dp.png b/app/src/main/res/drawable-hdpi/ic_fiber_new_black_24dp.png new file mode 100644 index 0000000..c701cda Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_fiber_new_black_24dp.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_info_black_24.png b/app/src/main/res/drawable-hdpi/ic_info_black_24.png new file mode 100644 index 0000000..d65e6aa Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_info_black_24.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_info_outline_white_48dp.png b/app/src/main/res/drawable-hdpi/ic_info_outline_white_48dp.png new file mode 100644 index 0000000..c41a5fc Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_info_outline_white_48dp.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_open_in_browser_black_24dp.png b/app/src/main/res/drawable-hdpi/ic_open_in_browser_black_24dp.png new file mode 100644 index 0000000..5dc9b66 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_open_in_browser_black_24dp.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_remove_circle_outline_black_24dp.png b/app/src/main/res/drawable-hdpi/ic_remove_circle_outline_black_24dp.png new file mode 100644 index 0000000..21d5ee8 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_remove_circle_outline_black_24dp.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_settings_black_24dp.png b/app/src/main/res/drawable-hdpi/ic_settings_black_24dp.png new file mode 100644 index 0000000..acf1ddf Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_settings_black_24dp.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_share_black_24dp.png b/app/src/main/res/drawable-hdpi/ic_share_black_24dp.png new file mode 100644 index 0000000..20ba480 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_share_black_24dp.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_share_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_share_white_24dp.png new file mode 100644 index 0000000..b09a692 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_share_white_24dp.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_thumb_up_white_48dp.png b/app/src/main/res/drawable-hdpi/ic_thumb_up_white_48dp.png new file mode 100644 index 0000000..cedf001 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_thumb_up_white_48dp.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_add_black_24dp.png b/app/src/main/res/drawable-mdpi/ic_add_black_24dp.png new file mode 100644 index 0000000..23bf119 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_add_black_24dp.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_archive_black_24dp.png b/app/src/main/res/drawable-mdpi/ic_archive_black_24dp.png new file mode 100644 index 0000000..bdcd478 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_archive_black_24dp.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_close_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_close_white_24dp.png new file mode 100644 index 0000000..af7f828 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_close_white_24dp.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_done_all_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_done_all_white_24dp.png new file mode 100644 index 0000000..4724d66 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_done_all_white_24dp.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_favorite_black_24dp.png b/app/src/main/res/drawable-mdpi/ic_favorite_black_24dp.png new file mode 100644 index 0000000..b3659bc Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_favorite_black_24dp.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_fiber_new_black_24dp.png b/app/src/main/res/drawable-mdpi/ic_fiber_new_black_24dp.png new file mode 100644 index 0000000..f98336a Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_fiber_new_black_24dp.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_info_black_24.png b/app/src/main/res/drawable-mdpi/ic_info_black_24.png new file mode 100644 index 0000000..2eceb97 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_info_black_24.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_info_outline_white_48dp.png b/app/src/main/res/drawable-mdpi/ic_info_outline_white_48dp.png new file mode 100644 index 0000000..c571b2e Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_info_outline_white_48dp.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_open_in_browser_black_24dp.png b/app/src/main/res/drawable-mdpi/ic_open_in_browser_black_24dp.png new file mode 100644 index 0000000..bbe5c14 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_open_in_browser_black_24dp.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_remove_circle_outline_black_24dp.png b/app/src/main/res/drawable-mdpi/ic_remove_circle_outline_black_24dp.png new file mode 100644 index 0000000..844e340 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_remove_circle_outline_black_24dp.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_settings_black_24dp.png b/app/src/main/res/drawable-mdpi/ic_settings_black_24dp.png new file mode 100644 index 0000000..c59419c Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_settings_black_24dp.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_share_black_24dp.png b/app/src/main/res/drawable-mdpi/ic_share_black_24dp.png new file mode 100644 index 0000000..f02d360 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_share_black_24dp.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_share_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_share_white_24dp.png new file mode 100644 index 0000000..e944fd7 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_share_white_24dp.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_thumb_up_white_48dp.png b/app/src/main/res/drawable-mdpi/ic_thumb_up_white_48dp.png new file mode 100644 index 0000000..75ae1f5 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_thumb_up_white_48dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_add_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_add_black_24dp.png new file mode 100644 index 0000000..3191d52 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_add_black_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_archive_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_archive_black_24dp.png new file mode 100644 index 0000000..9b88218 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_archive_black_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_close_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_close_white_24dp.png new file mode 100644 index 0000000..b7c7ffd Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_close_white_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_done_all_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_done_all_white_24dp.png new file mode 100644 index 0000000..6ace20e Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_done_all_white_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_favorite_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_favorite_black_24dp.png new file mode 100644 index 0000000..460bfb6 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_favorite_black_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_fiber_new_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_fiber_new_black_24dp.png new file mode 100644 index 0000000..3788145 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_fiber_new_black_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_info_black_24.png b/app/src/main/res/drawable-xhdpi/ic_info_black_24.png new file mode 100644 index 0000000..7aa8b51 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_info_black_24.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_info_outline_white_48dp.png b/app/src/main/res/drawable-xhdpi/ic_info_outline_white_48dp.png new file mode 100644 index 0000000..3a82cab Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_info_outline_white_48dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_open_in_browser_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_open_in_browser_black_24dp.png new file mode 100644 index 0000000..d87cbcc Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_open_in_browser_black_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_remove_circle_outline_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_remove_circle_outline_black_24dp.png new file mode 100644 index 0000000..dc99afc Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_remove_circle_outline_black_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_settings_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_settings_black_24dp.png new file mode 100644 index 0000000..e84e188 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_settings_black_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_share_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_share_black_24dp.png new file mode 100644 index 0000000..81c80b7 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_share_black_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_share_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_share_white_24dp.png new file mode 100644 index 0000000..22a8783 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_share_white_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_thumb_up_white_48dp.png b/app/src/main/res/drawable-xhdpi/ic_thumb_up_white_48dp.png new file mode 100644 index 0000000..f43cef8 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_thumb_up_white_48dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_add_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_add_black_24dp.png new file mode 100644 index 0000000..a84106b Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_add_black_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_archive_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_archive_black_24dp.png new file mode 100644 index 0000000..d6d60f6 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_archive_black_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_close_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_close_white_24dp.png new file mode 100644 index 0000000..6b717e0 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_close_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_done_all_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_done_all_white_24dp.png new file mode 100644 index 0000000..1f3132a Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_done_all_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_favorite_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_favorite_black_24dp.png new file mode 100644 index 0000000..1139f5f Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_favorite_black_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_fiber_new_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_fiber_new_black_24dp.png new file mode 100644 index 0000000..aca9b73 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_fiber_new_black_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_info_black_24.png b/app/src/main/res/drawable-xxhdpi/ic_info_black_24.png new file mode 100644 index 0000000..3c29783 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_info_black_24.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_info_outline_white_48dp.png b/app/src/main/res/drawable-xxhdpi/ic_info_outline_white_48dp.png new file mode 100644 index 0000000..bc0eda9 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_info_outline_white_48dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_open_in_browser_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_open_in_browser_black_24dp.png new file mode 100644 index 0000000..4324fbf Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_open_in_browser_black_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_remove_circle_outline_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_remove_circle_outline_black_24dp.png new file mode 100644 index 0000000..63ae14b Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_remove_circle_outline_black_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_settings_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_settings_black_24dp.png new file mode 100644 index 0000000..3023ff8 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_settings_black_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_share_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_share_black_24dp.png new file mode 100644 index 0000000..784933a Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_share_black_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_share_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_share_white_24dp.png new file mode 100644 index 0000000..a35b3cd Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_share_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_thumb_up_white_48dp.png b/app/src/main/res/drawable-xxhdpi/ic_thumb_up_white_48dp.png new file mode 100644 index 0000000..db84e27 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_thumb_up_white_48dp.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_add_black_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_add_black_24dp.png new file mode 100644 index 0000000..3cb1092 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_add_black_24dp.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_archive_black_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_archive_black_24dp.png new file mode 100644 index 0000000..b8c0376 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_archive_black_24dp.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_close_white_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_close_white_24dp.png new file mode 100644 index 0000000..3964192 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_close_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_done_all_white_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_done_all_white_24dp.png new file mode 100644 index 0000000..82dc1e2 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_done_all_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_favorite_black_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_favorite_black_24dp.png new file mode 100644 index 0000000..b46fa0a Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_favorite_black_24dp.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_fiber_new_black_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_fiber_new_black_24dp.png new file mode 100644 index 0000000..38e9fb7 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_fiber_new_black_24dp.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_info_black_24.png b/app/src/main/res/drawable-xxxhdpi/ic_info_black_24.png new file mode 100644 index 0000000..2dc9634 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_info_black_24.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_info_outline_white_48dp.png b/app/src/main/res/drawable-xxxhdpi/ic_info_outline_white_48dp.png new file mode 100644 index 0000000..939ee3a Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_info_outline_white_48dp.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_open_in_browser_black_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_open_in_browser_black_24dp.png new file mode 100644 index 0000000..f0e1888 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_open_in_browser_black_24dp.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_remove_circle_outline_black_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_remove_circle_outline_black_24dp.png new file mode 100644 index 0000000..0861db8 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_remove_circle_outline_black_24dp.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_settings_black_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_settings_black_24dp.png new file mode 100644 index 0000000..476d5c9 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_settings_black_24dp.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_share_black_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_share_black_24dp.png new file mode 100644 index 0000000..5a8544c Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_share_black_24dp.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_share_white_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_share_white_24dp.png new file mode 100644 index 0000000..e351c7b Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_share_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_thumb_up_white_48dp.png b/app/src/main/res/drawable-xxxhdpi/ic_thumb_up_white_48dp.png new file mode 100644 index 0000000..7a025a9 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_thumb_up_white_48dp.png differ diff --git a/app/src/main/res/layout-land/activity_home.xml b/app/src/main/res/layout-land/activity_home.xml new file mode 100644 index 0000000..c52ed6c --- /dev/null +++ b/app/src/main/res/layout-land/activity_home.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_add_source.xml b/app/src/main/res/layout/activity_add_source.xml new file mode 100644 index 0000000..1c8749a --- /dev/null +++ b/app/src/main/res/layout/activity_add_source.xml @@ -0,0 +1,118 @@ + + + + + + + + + + + + + + + +