diff --git a/.idea/dictionaries/naresh.xml b/.idea/dictionaries/naresh.xml new file mode 100644 index 0000000..4547fe3 --- /dev/null +++ b/.idea/dictionaries/naresh.xml @@ -0,0 +1,9 @@ + + + + elektro + hsdsans + weblog + + + \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 6374795..20e8765 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -35,16 +35,19 @@ android { dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" - implementation 'androidx.core:core-ktx:1.2.0' - implementation 'androidx.appcompat:appcompat:1.1.0' - implementation 'com.google.android.material:material:1.1.0' - implementation 'androidx.constraintlayout:constraintlayout:1.1.3' - implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.2.0' - implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0' - testImplementation 'junit:junit:4.+' - androidTestImplementation 'androidx.test.ext:junit:1.1.1' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' + implementation 'androidx.core:core-ktx:1.3.2' + implementation 'androidx.appcompat:appcompat:1.2.0' + implementation 'com.google.android.material:material:1.3.0' + implementation 'androidx.constraintlayout:constraintlayout:2.0.4' + implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.3.0' + implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.0' + implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' + testImplementation 'junit:junit:4.13.2' + androidTestImplementation 'androidx.test.ext:junit:1.1.2' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' implementation 'com.android.volley:volley:1.2.0' - implementation "com.squareup.okhttp3:okhttp:4.9.0" + implementation "androidx.work:work-runtime-ktx:2.5.0" + + } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 48d335b..21250d8 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,7 +1,8 @@ - + + + + - - \ No newline at end of file diff --git a/app/src/main/ic_launcher-playstore.png b/app/src/main/ic_launcher-playstore.png new file mode 100644 index 0000000..08a37e8 Binary files /dev/null and b/app/src/main/ic_launcher-playstore.png differ diff --git a/app/src/main/java/com/nareshkumarrao/eiweblog/AboutActivity.kt b/app/src/main/java/com/nareshkumarrao/eiweblog/AboutActivity.kt new file mode 100644 index 0000000..dec743a --- /dev/null +++ b/app/src/main/java/com/nareshkumarrao/eiweblog/AboutActivity.kt @@ -0,0 +1,36 @@ +package com.nareshkumarrao.eiweblog + +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.view.View +import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.widget.AppCompatButton +import androidx.appcompat.widget.Toolbar + + +class AboutActivity : AppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_about) + + val aboutToolbar = this.findViewById(R.id.about_toolbar) + setSupportActionBar(aboutToolbar) + supportActionBar?.setDisplayHomeAsUpEnabled(true) + supportActionBar?.setDisplayShowTitleEnabled(false) + } + + fun sendBeer(view: View) { + startActivity( Intent( + Intent.ACTION_VIEW, + Uri.parse("https://paypal.me/naresh97") + )) + } + + fun githubRepo(view: View){ + startActivity( Intent( + Intent.ACTION_VIEW, + Uri.parse("https://github.com/naresh97/ei-weblog-android") + )) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/nareshkumarrao/eiweblog/MainActivity.kt b/app/src/main/java/com/nareshkumarrao/eiweblog/MainActivity.kt index 99f6fb0..cfeb7ec 100644 --- a/app/src/main/java/com/nareshkumarrao/eiweblog/MainActivity.kt +++ b/app/src/main/java/com/nareshkumarrao/eiweblog/MainActivity.kt @@ -1,17 +1,23 @@ package com.nareshkumarrao.eiweblog +import android.app.NotificationChannel +import android.app.NotificationManager +import android.content.Context +import android.content.Intent +import android.os.Build import android.os.Bundle -import android.util.Log import android.view.Menu +import android.view.MenuItem import android.view.View import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.Toolbar import androidx.viewpager.widget.ViewPager -import com.android.volley.VolleyLog +import androidx.work.PeriodicWorkRequestBuilder +import androidx.work.WorkManager +import androidx.work.WorkRequest import com.google.android.material.tabs.TabLayout -import com.nareshkumarrao.eiweblog.ui.main.Article import com.nareshkumarrao.eiweblog.ui.main.SectionsPagerAdapter -import okhttp3.OkHttpClient +import java.util.concurrent.TimeUnit class MainActivity : AppCompatActivity() { @@ -26,50 +32,32 @@ class MainActivity : AppCompatActivity() { val tabs: TabLayout = findViewById(R.id.tabs) tabs.setupWithViewPager(viewPager) - val myToolbar = findViewById(R.id.toolbar) as Toolbar + val myToolbar = findViewById(R.id.about_toolbar) as Toolbar setSupportActionBar(myToolbar) - VolleyLog.DEBUG = true + Utilities.createNotificationChannel(this) - /*val queue = Volley.newRequestQueue(this) - val url = "https://www.reddit.com" - val stringRequest = StringRequest( - Request.Method.GET, url, - { response -> - Log.d("XMLLIST", "got response!") - Log.d("XMLLIST", "$response") - }, - { error -> Log.e("XMLLIST", error.toString()) }) - stringRequest.retryPolicy = DefaultRetryPolicy( - DefaultRetryPolicy.DEFAULT_TIMEOUT_MS * 2, - DefaultRetryPolicy.DEFAULT_MAX_RETRIES, - DefaultRetryPolicy.DEFAULT_BACKOFF_MULT - ) - queue.add(stringRequest)*/ + val uploadWorkRequest: WorkRequest = + PeriodicWorkRequestBuilder(1, TimeUnit.HOURS) + .build() + WorkManager.getInstance(this).enqueue(uploadWorkRequest) - val thread = Thread { - try { - Log.d("XMLLIST", "Starting network request thread.") - val client = OkHttpClient() - val request = okhttp3.Request.Builder().url("https://google.com/").build() - val response = client.newCall(request).execute() - Log.d("XMLLIST", response.toString()) - } catch (e: Exception) { - e.printStackTrace() - } - } - thread.start() } - fun logArticles(articles: List
){ - Log.i("XMLLIST", articles.toString()) - } - override fun onCreateOptionsMenu(menu: Menu?): Boolean { - //Utilities.weblogXML(baseContext, ::logArticles) menuInflater.inflate(R.menu.toolbar_menu, menu); return true; } + + fun showNotificationSettings(item: MenuItem){ + val intent = Intent(this, NotificationSettingsActivity::class.java) + startActivity(intent) + } + + fun showAbout(item: MenuItem){ + val intent = Intent(this, AboutActivity::class.java) + startActivity(intent) + } } \ No newline at end of file diff --git a/app/src/main/java/com/nareshkumarrao/eiweblog/NotificationSettingsActivity.kt b/app/src/main/java/com/nareshkumarrao/eiweblog/NotificationSettingsActivity.kt new file mode 100644 index 0000000..4dd2d7a --- /dev/null +++ b/app/src/main/java/com/nareshkumarrao/eiweblog/NotificationSettingsActivity.kt @@ -0,0 +1,17 @@ +package com.nareshkumarrao.eiweblog + +import androidx.appcompat.app.AppCompatActivity +import android.os.Bundle +import androidx.appcompat.widget.Toolbar + +class NotificationSettingsActivity : AppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_notification_settings) + + val notificationsToolbar = this.findViewById(R.id.notification_toolbar) + setSupportActionBar(notificationsToolbar) + supportActionBar?.setDisplayHomeAsUpEnabled(true) + supportActionBar?.setDisplayShowTitleEnabled(false) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/nareshkumarrao/eiweblog/UpdateWorker.kt b/app/src/main/java/com/nareshkumarrao/eiweblog/UpdateWorker.kt new file mode 100644 index 0000000..bbc3760 --- /dev/null +++ b/app/src/main/java/com/nareshkumarrao/eiweblog/UpdateWorker.kt @@ -0,0 +1,36 @@ +package com.nareshkumarrao.eiweblog + +import android.content.Context +import androidx.work.Worker +import androidx.work.WorkerParameters +import java.math.BigInteger +import java.security.MessageDigest + +class UpdateWorker(private val context: Context, workerParams: WorkerParameters) : Worker(context, workerParams) { + override fun doWork(): Result { + + Utilities.weblogList(context) { articles -> + val lastArticle = Utilities.getLatestRelevantArticle(articles)!! + val hashString = lastArticle.title + lastArticle.content + lastArticle.date + val oldHash = md5(hashString) + + Utilities.fetchWeblogXML(applicationContext){newArticles -> + val lastNewArticle = Utilities.getLatestRelevantArticle(newArticles)!! + val newHashString = lastNewArticle.title + lastNewArticle.content + lastNewArticle.date + val newHash = md5(newHashString) + + if(oldHash != newHash){ + Utilities.sendNotification(context, lastNewArticle, newArticles.size) + } + } + } + + return Result.success() + } + + private fun md5(input:String): String { + val md = MessageDigest.getInstance("MD5") + return BigInteger(1, md.digest(input.toByteArray())).toString(16).padStart(32, '0') + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/nareshkumarrao/eiweblog/Utilities.kt b/app/src/main/java/com/nareshkumarrao/eiweblog/Utilities.kt index 199cc08..07489f3 100644 --- a/app/src/main/java/com/nareshkumarrao/eiweblog/Utilities.kt +++ b/app/src/main/java/com/nareshkumarrao/eiweblog/Utilities.kt @@ -1,35 +1,74 @@ package com.nareshkumarrao.eiweblog -import android.content.ContentValues +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.content.Intent import android.content.Context +import android.os.Build import android.util.Log import android.util.Xml +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import androidx.core.text.HtmlCompat import com.android.volley.Request -import com.android.volley.Response import com.android.volley.toolbox.StringRequest import com.android.volley.toolbox.Volley import com.nareshkumarrao.eiweblog.ui.main.Article import org.xmlpull.v1.XmlPullParser import java.io.StringReader + internal object Utilities { - fun weblogXML(context: Context, function: (d: List
) -> Unit) { + + fun weblogList(context: Context?, function: (d: List
) -> Unit){ + val sharedPref = context?.getSharedPreferences(context?.getString(R.string.preference_file_key), Context.MODE_PRIVATE) + val weblogResponse = sharedPref?.getString( context?.getString(R.string.weblog_response_key), null) + if (weblogResponse == null){ + fetchWeblogXML(context, function) + return + } + + val parser: XmlPullParser = Xml.newPullParser() + parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false) + parser.setInput( StringReader(weblogResponse) ) + parser.nextTag() + function(parseXML(parser)) + } + + fun fetchWeblogXML(context: Context?, function: (d: List
) -> Unit) { val queue = Volley.newRequestQueue(context) - val url = "https://www.google.com" + val url = context?.getString(R.string.weblog_xml_url) val stringRequest = StringRequest(Request.Method.GET, url, { response -> - Log.d("XMLLIST", "got response!") - // Display the first 500 characters of the response string. - Log.d("XMLLIST", "$response") + val responseStr = String(response.toByteArray(Charsets.ISO_8859_1), Charsets.UTF_8) + + val sharedPref = context?.getSharedPreferences(context?.getString(R.string.preference_file_key), Context.MODE_PRIVATE) + if (sharedPref != null) { + with (sharedPref.edit()) { + putString(context?.getString(R.string.weblog_response_key), responseStr) + apply() + } + } + + + + val parser: XmlPullParser = Xml.newPullParser() + parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false) + //Log.d("XMLLIST", responseStr ) + parser.setInput( StringReader(responseStr) ) + parser.nextTag() + + val articles = parseXML(parser) + function(articles) + }, { error -> Log.e("XMLLIST", error.toString()) }) - queue.add(stringRequest) - Log.e("XMLLIST", "Adding request to queue from: $url") } private fun parseXML(parser: XmlPullParser): List
{ @@ -56,7 +95,7 @@ internal object Utilities { continue } if (parser.name == "z:row") { - articles.add(parseZROW(parser)) + parseZROW(parser)?.let { articles.add(it) } } else { parseSkip(parser) } @@ -64,16 +103,21 @@ internal object Utilities { return articles } - private fun parseZROW(parser: XmlPullParser): Article { + private fun parseZROW(parser: XmlPullParser): Article? { parser.require(XmlPullParser.START_TAG, null, "z:row") val title = parser.getAttributeValue(null, "ows_Title") val content = parser.getAttributeValue(null, "ows_Body") val date = parser.getAttributeValue(null, "ows_Created") val author = parser.getAttributeValue(null, "ows_Autor2") + val category = parser.getAttributeValue(null, "ows_Kategorie") parser.nextTag() - parser.require(XmlPullParser.END_TAG, null, "link") + parser.require(XmlPullParser.END_TAG, null, "z:row") + + if(title == null || content == null || date == null || author == null || category == null){ + return null + } - return Article(title, content, date, author) + return Article(title, content, date, author, category) } private fun parseSkip(parser: XmlPullParser) { @@ -88,4 +132,49 @@ internal object Utilities { } } } + + fun sendNotification(context: Context?, article: Article, id:Int) { + val intent = Intent(context, MainActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + } + val pendingIntent: PendingIntent = PendingIntent.getActivity(context, 0, intent, 0) + + var builder = NotificationCompat.Builder(context!!, context.getString(R.string.channel_id)) + .setSmallIcon(R.drawable.ic_stat_name) + .setContentTitle(article.title) + .setStyle(NotificationCompat.BigTextStyle() + .bigText(HtmlCompat.fromHtml(article.content, HtmlCompat.FROM_HTML_MODE_COMPACT))) + .setContentIntent(pendingIntent) + .setPriority(NotificationCompat.PRIORITY_DEFAULT) + .setAutoCancel(true) + + with(NotificationManagerCompat.from(context)) { + // notificationId is a unique int for each notification that you must define + notify(id, builder.build()) + } + + } + + fun createNotificationChannel(context: Context?){ + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val name = context?.getString(R.string.channel_name) + val descriptionText = context?.getString(R.string.channel_description) + val importance = NotificationManager.IMPORTANCE_DEFAULT + val channel = NotificationChannel(context?.getString(R.string.channel_id), name, importance).apply { + description = descriptionText + } + val notificationManager: NotificationManager = context?.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + notificationManager.createNotificationChannel(channel) + } + } + + fun getLatestRelevantArticle(articles: List
): Article? { + val sortedArticles = articles.sortedByDescending { it.date } + for (article in sortedArticles){ + if( article.category == "Lehre" || article.category == "Prüfung" || article.category == "Sonstiges"){ + return article + } + } + return null + } } diff --git a/app/src/main/java/com/nareshkumarrao/eiweblog/ui/main/ItemArticleAdapter.kt b/app/src/main/java/com/nareshkumarrao/eiweblog/ui/main/ItemArticleAdapter.kt index ed6d1fa..8beabbf 100644 --- a/app/src/main/java/com/nareshkumarrao/eiweblog/ui/main/ItemArticleAdapter.kt +++ b/app/src/main/java/com/nareshkumarrao/eiweblog/ui/main/ItemArticleAdapter.kt @@ -1,26 +1,38 @@ package com.nareshkumarrao.eiweblog.ui.main +import android.os.Build +import android.text.Html +import android.text.util.Linkify import android.view.LayoutInflater import android.view.ViewGroup import android.widget.TextView +import androidx.core.text.HtmlCompat import androidx.recyclerview.widget.RecyclerView import com.nareshkumarrao.eiweblog.R +import org.w3c.dom.Text -data class Article(val title: String, val content: String, val date: String, val author: String) +data class Article(val title: String, val content: String, val date: String, val author: String, val category: String) class ItemArticleAdapter(private val articles: List
) : RecyclerView.Adapter() { inner class ViewHolder(inflater: LayoutInflater, parent: ViewGroup) : RecyclerView.ViewHolder(inflater.inflate(R.layout.item_article, parent, false)) { private var title: TextView? = null private var content: TextView? = null + private var date: TextView? = null + private var author: TextView? = null init { title = itemView.findViewById(R.id.titleText) content = itemView.findViewById(R.id.contentText) + date = itemView.findViewById(R.id.dateText) + author = itemView.findViewById(R.id.authorText) } fun bind(article: Article) { title?.text = article.title - content?.text = article.content + content?.text = HtmlCompat.fromHtml(article.content, HtmlCompat.FROM_HTML_MODE_COMPACT) + content?.let { Linkify.addLinks(it, Linkify.WEB_URLS) }; + author?.text = article.author + date?.text = article.date } } diff --git a/app/src/main/java/com/nareshkumarrao/eiweblog/ui/main/SectionsFragment.kt b/app/src/main/java/com/nareshkumarrao/eiweblog/ui/main/SectionsFragment.kt index 59c49c0..8383f51 100644 --- a/app/src/main/java/com/nareshkumarrao/eiweblog/ui/main/SectionsFragment.kt +++ b/app/src/main/java/com/nareshkumarrao/eiweblog/ui/main/SectionsFragment.kt @@ -7,11 +7,23 @@ import android.view.ViewGroup import androidx.fragment.app.Fragment import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import com.nareshkumarrao.eiweblog.R +import com.nareshkumarrao.eiweblog.Utilities + +class SectionsFragment() : Fragment() { + + private var swipeRefreshLayout: SwipeRefreshLayout? = null -class SectionsFragment : Fragment() { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return inflater.inflate(R.layout.fragment_sections, container, false) + val rootView = inflater.inflate(R.layout.fragment_sections, container, false) + + this.swipeRefreshLayout = rootView.findViewById(R.id.sectionsSwipeRefresh) + this.swipeRefreshLayout?.setOnRefreshListener { + Utilities.fetchWeblogXML(this.context, ::updateView) + } + + return rootView } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -20,12 +32,36 @@ class SectionsFragment : Fragment() { layoutManager = LinearLayoutManager(activity) adapter = ItemArticleAdapter(listOf()) } + Utilities.weblogList(this.context, ::updateView) + } + companion object { + private const val ARG_SECTION_NAME = "section_title" + + @JvmStatic + fun newInstance(title: String): SectionsFragment { + return SectionsFragment().apply { + arguments = Bundle().apply { + putString(ARG_SECTION_NAME, title) + } + } + } } - fun updateView(get_articles: List
){ + + private fun updateView(get_articles: List
){ + this.swipeRefreshLayout?.isRefreshing=false + var articles: MutableList
= mutableListOf() + val title = arguments?.getString(ARG_SECTION_NAME) ?: return + for (article in get_articles){ + if(article.category == title){ + articles.add(article) + } + } + articles.sortByDescending { it.date } + view?.findViewById(R.id.sectionsRecylerView)?.apply { layoutManager = LinearLayoutManager(activity) - adapter = ItemArticleAdapter(get_articles) + adapter = ItemArticleAdapter(articles) } } } \ No newline at end of file diff --git a/app/src/main/java/com/nareshkumarrao/eiweblog/ui/main/SectionsPagerAdapter.kt b/app/src/main/java/com/nareshkumarrao/eiweblog/ui/main/SectionsPagerAdapter.kt index 74bcc15..c3a230a 100644 --- a/app/src/main/java/com/nareshkumarrao/eiweblog/ui/main/SectionsPagerAdapter.kt +++ b/app/src/main/java/com/nareshkumarrao/eiweblog/ui/main/SectionsPagerAdapter.kt @@ -16,7 +16,7 @@ class SectionsPagerAdapter(private val context: Context, fm: FragmentManager) : FragmentPagerAdapter(fm) { override fun getItem(position: Int): Fragment { - return SectionsFragment() + return SectionsFragment.newInstance( context.resources.getString(TAB_TITLES[position]) ) } override fun getPageTitle(position: Int): CharSequence? { diff --git a/app/src/main/res/drawable-hdpi/ic_stat_name.png b/app/src/main/res/drawable-hdpi/ic_stat_name.png new file mode 100644 index 0000000..d4cbf09 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_stat_name.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_stat_name.png b/app/src/main/res/drawable-mdpi/ic_stat_name.png new file mode 100644 index 0000000..5464f21 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_stat_name.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_stat_name.png b/app/src/main/res/drawable-xhdpi/ic_stat_name.png new file mode 100644 index 0000000..c026274 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_stat_name.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_stat_name.png b/app/src/main/res/drawable-xxhdpi/ic_stat_name.png new file mode 100644 index 0000000..1db9724 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_stat_name.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_stat_name.png b/app/src/main/res/drawable-xxxhdpi/ic_stat_name.png new file mode 100644 index 0000000..ef47fb6 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_stat_name.png differ diff --git a/app/src/main/res/drawable/toolbar_dropshadow.xml b/app/src/main/res/drawable/toolbar_dropshadow.xml new file mode 100644 index 0000000..e7d6ab9 --- /dev/null +++ b/app/src/main/res/drawable/toolbar_dropshadow.xml @@ -0,0 +1,7 @@ + + + + diff --git a/app/src/main/res/layout/activity_about.xml b/app/src/main/res/layout/activity_about.xml new file mode 100644 index 0000000..4efb1cf --- /dev/null +++ b/app/src/main/res/layout/activity_about.xml @@ -0,0 +1,116 @@ + + + + + + + + + + + + + + + + + + + +