Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implementation changes and extensions #20

Open
wants to merge 24 commits into
base: main
Choose a base branch
from
Open
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Intermediate commit
- Comments improved
- Preferences as central place for settings
- Other improvements
- Begin with unit tests
Problems
- UpdateService not working with Documents folder
  • Loading branch information
Mick2nd committed May 25, 2024
commit d048c8dc60a83e3577d11eacc320724c00b0100f
3 changes: 3 additions & 0 deletions .idea/misc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 6 additions & 1 deletion app/build.gradle
Original file line number Diff line number Diff line change
@@ -4,6 +4,7 @@ plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id 'org.jetbrains.kotlin.kapt'
id("org.jetbrains.dokka") version "1.9.20"
}

android {
@@ -68,11 +69,15 @@ dependencies {
// implementation 'com.google.dagger:dagger:2.51.1'
// kapt 'com.google.dagger:dagger-compiler:2.51.1'
implementation 'com.google.dagger:dagger-android:2.51.1'
implementation 'com.google.dagger:dagger-android-support:2.51.1' // if you use the support libraries
implementation 'com.google.dagger:dagger-android-support:2.51.1'
implementation 'androidx.test:monitor:1.6.1'// if you use the support libraries
kapt 'com.google.dagger:dagger-android-processor:2.51.1'
kapt 'com.google.dagger:dagger-compiler:2.51.1'

dokkaPlugin("org.jetbrains.dokka:android-documentation-plugin:1.9.20")

testImplementation 'junit:junit:4.13.2'
testImplementation "org.mockito.kotlin:mockito-kotlin:5.3.1"
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
}
Original file line number Diff line number Diff line change
@@ -2,14 +2,10 @@ package ch.tiim.markdown_widget

import android.content.Context
import android.net.Uri
import android.provider.DocumentsContract
import android.provider.OpenableColumns
import android.util.Log
import android.webkit.WebResourceResponse
import androidx.webkit.WebViewAssetLoader
import ch.tiim.markdown_widget.di.Wrapper
import java.io.File
import java.io.FileNotFoundException
import java.io.InputStream
import javax.inject.Inject
import javax.inject.Singleton
Original file line number Diff line number Diff line change
@@ -8,10 +8,9 @@ import java.io.FileNotFoundException
import java.io.InputStream
import java.io.InputStreamReader
import javax.inject.Inject
import javax.inject.Named
import javax.inject.Singleton

private const val TAG = "FileChecker"
private const val TAG = "FileServices"

/**
* This class is meant as checker for the *userstyle.css* file changes
@@ -20,27 +19,27 @@ private const val TAG = "FileChecker"
* @param uri the complete Uri of the file to be checked
*/
@Singleton
class FileChecker @Inject constructor (val context: Context, private val prefs: Preferences) {
class FileServices @Inject constructor (val context: Context, private val uri: Uri) {

var content: String = ""
private var err: Exception? = null
private var state: String = ""

init {
Log.d(TAG, "File Checker $this instantiated")
Log.i(TAG, "File Checker $this instantiated with uri $uri")
updateState()
}

fun updateState() {
state = loadFile()
content = loadFile()
}

fun stateChanged(): Boolean {
return loadFile() != state
return loadFile() != content
}

private fun loadFile(): String {
try {
val ins: InputStream = context.contentResolver.openInputStream(prefs["userstyle.css"])!!
val ins: InputStream = context.contentResolver.openInputStream(uri)!!
val reader = BufferedReader(InputStreamReader(ins, "utf-8"))
val data = reader.lines().reduce { s, t -> s + "\n" + t }
reader.close()
10 changes: 0 additions & 10 deletions app/src/main/java/ch/tiim/markdown_widget/MainActivity.kt
Original file line number Diff line number Diff line change
@@ -1,24 +1,14 @@
package ch.tiim.markdown_widget

import android.content.Intent
import android.content.res.Configuration
import android.net.Uri
import android.os.Bundle
import android.os.Environment
import android.os.PersistableBundle
import android.util.Log
import android.view.View
import android.widget.Button
import android.widget.LinearLayout
import androidx.appcompat.app.AppCompatActivity
import ch.tiim.markdown_widget.di.AppComponent
import ch.tiim.markdown_widget.di.ContextModule
import ch.tiim.markdown_widget.di.DaggerAppComponent
import ch.tiim.markdown_widget.di.StringModule
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking

private const val DEBUG = true
private const val TAG = "MainActivity"
110 changes: 41 additions & 69 deletions app/src/main/java/ch/tiim/markdown_widget/MarkdownFileWidget.kt
Original file line number Diff line number Diff line change
@@ -18,11 +18,6 @@ import android.util.Log
import android.util.SparseArray
import android.widget.RemoteViews
import ch.tiim.markdown_widget.di.AppComponent
import java.io.BufferedReader
import java.io.FileNotFoundException
import java.io.InputStream
import java.io.InputStreamReader


private const val TAG = "MarkdownFileWidget"

@@ -39,14 +34,15 @@ class MarkdownFileWidget : AppWidgetProvider() {

/**
* Updates all requested Widgets
*
* @param context the [Context] instance
* @param appWidgetIds array of requested widgets
*/
override fun onUpdate(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray
) {
// There may be multiple widgets active, so update all of them
Log.i(TAG, "onUpdate")
for (appWidgetId in appWidgetIds) {
updateAppWidget(context, appWidgetManager, appWidgetId)
@@ -55,7 +51,11 @@ class MarkdownFileWidget : AppWidgetProvider() {

/**
* Handles changed options, but especially View dimension changes
*
* @param context the [Context] instance
* @param appWidgetManager the [AppWidgetManager]
* @param appWidgetId id of the Widget to be changed
* @param newOptions
*/
override fun onAppWidgetOptionsChanged(
context: Context,
@@ -87,6 +87,9 @@ class MarkdownFileWidget : AppWidgetProvider() {

/**
* When the user deletes the widget, delete the preference associated with it.
*
* @param context the [Context] instance
* @param appWidgetIds array of app widgets
*/
override fun onDeleted(context: Context, appWidgetIds: IntArray) {
Log.i(TAG, "onDeleted")
@@ -97,29 +100,38 @@ class MarkdownFileWidget : AppWidgetProvider() {
}

/**
* Enter relevant functionality for when the first widget is created
* Enter relevant functionality for when the first widget is created.
*
* @param context the [Context] instance
*
*/
override fun onEnabled(context: Context) {
Log.i(TAG, "onEnabled")
}

/**
* Enter relevant functionality for when the last widget is disabled
* Enter relevant functionality for when the last widget is disabled.
*
* @param context the [Context] instance
*/
override fun onDisabled(context: Context) {
Log.i(TAG, "onDisabled")
}

/**
* THIS OVERRIDE IS NORMALLY NOT REQUIRED
* THIS OVERRIDE IS NORMALLY NOT REQUIRED. BASE CLASS IMPLEMENTATION IS SUFFICIENT.
*/
override fun onReceive(context: Context?, intent: Intent?) {
Log.d(TAG, "onReceive ${intent!!.action!!}")
super.onReceive(context, intent)
}

/**
* Updates a single widget
* Updates a single widget.
*
* @param context the [Context] instance
* @param appWidgetManager the [AppWidgetManager]
* @param appWidgetId the integer id of the app widget
*/
private fun updateAppWidget(
context: Context,
@@ -142,7 +154,7 @@ class MarkdownFileWidget : AppWidgetProvider() {
if (tapBehavior != TAP_BEHAVIOUR_NONE) {
views.setOnClickPendingIntent(
R.id.renderImg,
getIntent(context, fileUri, tapBehavior, context.contentResolver)
getIntent(context, fileUri, tapBehavior)
)
}

@@ -157,16 +169,17 @@ class MarkdownFileWidget : AppWidgetProvider() {
}

/**
* Either instantiates a Renderer instance or extracts it from Cache
* In either case the content of the Widget is drawn
* Finally the given callback is invoked
* Either instantiates a Renderer instance or extracts it from Cache.
* In either case the content of the Widget is drawn.
* Finally the given callback is invoked.
*
* @param appWidgetId the Widget
* @param checkForChange defines the relevance of the *needsUpdate* call
* @param cb the callback to be invoked
*/
private fun loadRenderer(context: Context, appWidgetId: Int, checkForChange: Boolean, cb: (()->Unit)) {
val fileUri = Uri.parse(prefs[appWidgetId, PREF_FILE, ""])
val s = loadMarkdown(context, fileUri)
val fileUri = prefs.markdownUriOf(appWidgetId)
val s = FileServices(context, fileUri).content
var md = cachedMarkdown[appWidgetId]
if (md == null || (checkForChange && md.needsUpdate(s))) {
val widgetSizeProvider = WidgetSizeProvider(context)
@@ -184,7 +197,11 @@ class MarkdownFileWidget : AppWidgetProvider() {
}

/**
* Can be used to Broadcast update requests to the own Widget
* Can be used to Broadcast update requests to the own Widget.
*
* @param context the [Context] instance
* @param appWidgetId the integer id of the app widget
* @return intent of type [PendingIntent]
*/
internal fun getUpdatePendingIntent(context: Context, appWidgetId: Int): PendingIntent {
val intentUpdate = Intent(context, MarkdownFileWidget::class.java)
@@ -203,79 +220,33 @@ internal fun getUpdatePendingIntent(context: Context, appWidgetId: Int): Pending
/**
* Returns the Intent to be used to request the invocation of the Markdown editor.
* Depends on the configured method during Widget creation.
*
* @param uri Uri of the file
* @param tapBehavior configured Tap method
* @return pending intent
*/
fun getIntent(context: Context, uri: Uri, tapBehavior: String, c: ContentResolver): PendingIntent {
fun getIntent(context: Context, uri: Uri, tapBehavior: String): PendingIntent {
val intent = Intent(Intent.ACTION_EDIT)
if (tapBehavior == TAP_BEHAVIOUR_DEFAULT_APP) {
intent.setDataAndType(uri.normalizeScheme(), "text/plain")
//intent.addCategory(Intent.CATEGORY_OPENABLE)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
intent.flags = Intent.FLAG_GRANT_WRITE_URI_PERMISSION
} else if (tapBehavior == TAP_BEHAVIOUR_OBSIDIAN) {
intent.data = Uri.parse("obsidian://open?file=" + Uri.encode(getFileName(uri, c)))
intent.data = uri.toObsidian(context)
}
return PendingIntent.getActivity(context, 555, intent, PendingIntent.FLAG_IMMUTABLE)
}

/**
* Uri to File name converter
*/
fun getFileName(uri: Uri, c: ContentResolver): String? {
var result: String? = null
if (uri.scheme == "content") {
val cursor: Cursor? = c.query(uri, null, null, null, null)
try {
if (cursor != null && cursor.moveToFirst()) {
val i = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
result = cursor.getString(i)
}
} finally {
cursor?.close()
}
}
if (result == null) {
result = uri.path
val cut = result!!.lastIndexOf('/')
if (cut != -1) {
result = result.substring(cut + 1)
}
}
return result
}

/**
* Loads the Markdown given the Uri of a file
* @param uri the Uri of a file
* @return the string containing the Markdown
*/
fun loadMarkdown(context: Context, uri: Uri): String {
try {
Log.i(TAG, uri.toString())
val ins: InputStream = context.contentResolver.openInputStream(uri)!!
val reader = BufferedReader(InputStreamReader(ins, "utf-8"))
val data = reader.lines().reduce { s, t -> s + "\n" + t }
reader.close()
ins.close()
return data.get()
} catch (err: FileNotFoundException) {
return err.toString()
} catch (err: Exception) {
return err.toString()
} finally {
}
}

/**
* Extracts Dimensions of Widget or Screen in Density Points
*/
class WidgetSizeProvider(
private val context: Context // Do not pass Application context
) {
/**
* Returns Dimensions of Widget as stored in the options
* Returns Dimensions of Widget as stored in the options.
*
* @param widgetId the id of the widget
* @return Dimensions in Density Points
*/
@@ -294,7 +265,8 @@ class WidgetSizeProvider(
}

/**
* Returns the Dimensions of the Screen
* Returns the Dimensions of the Screen.
*
* @return Dimensions in Density Points
*/
fun getScreenSize() : Pair<Int, Int> {
Original file line number Diff line number Diff line change
@@ -10,11 +10,8 @@ import android.webkit.MimeTypeMap
import android.widget.EditText
import android.widget.RadioGroup
import ch.tiim.markdown_widget.databinding.MarkdownFileWidgetConfigureBinding
import ch.tiim.markdown_widget.di.ActivityScope
import ch.tiim.markdown_widget.di.AppComponent
import ch.tiim.markdown_widget.di.DaggerActivityComponent
import javax.inject.Inject
import javax.inject.Singleton

internal const val TAP_BEHAVIOUR_NONE = "none"
internal const val TAP_BEHAVIOUR_DEFAULT_APP = "default_app"
@@ -30,7 +27,8 @@ class MarkdownFileWidgetConfigureActivity @Inject constructor() : Activity() {
// We can either use this construct and use Preferences as "Singleton"
// Or inject Preferences without being Singleton (@Inject lateinit var)
// Or we inject and make Preference de facto a real Singleton
val prefs: Preferences = AppComponent.instance.preferences()
// val prefs: Preferences = AppComponent.instance.preferences()
@Inject lateinit var prefs: Preferences

private var appWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID
private lateinit var inputFilePath: EditText
@@ -111,8 +109,8 @@ class MarkdownFileWidgetConfigureActivity @Inject constructor() : Activity() {
public override fun onCreate(icicle: Bundle?) {
super.onCreate(icicle)

DaggerActivityComponent.factory()
.create("test", AppComponent.instance)
AppComponent.instance.activityComponentFactory()
.create()
.inject(this)

// Set the result to CANCELED. This will cause the widget host to cancel
Loading