Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
661 changes: 661 additions & 0 deletions NoteFileParser.ipynb

Large diffs are not rendered by default.

29 changes: 17 additions & 12 deletions app/build.gradle
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
// Kotlin Android plugin removed - no longer required in AGP 9.0 (Kotlin is built-in)
id 'com.google.devtools.ksp'
id 'kotlin-parcelize'
id 'com.google.gms.google-services'
Expand Down Expand Up @@ -31,9 +31,6 @@ android {
ksp {
arg('room.schemaLocation', "$projectDir/schemas")
}
ndk {
abiFilters 'arm64-v8a'
}
javaCompileOptions {
annotationProcessorOptions {
arguments = ["room.schemaLocation":
Expand Down Expand Up @@ -86,8 +83,7 @@ android {
}
}
release {
minifyEnabled true
shrinkResources true
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
buildConfigField("boolean", "IS_NEXT", IS_NEXT)
buildConfigField "String", "SHIPBOOK_APP_ID", "\"${System.getenv("SHIPBOOK_APP_ID") ?: "default-secret"}\""
Expand All @@ -101,12 +97,12 @@ android {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = '17'
}
// kotlinOptions removed - use compileOptions instead with AGP 9.0 built-in Kotlin
// The JVM target is already configured in compileOptions above
buildFeatures {
compose true
buildConfig true
aidl true
}
composeOptions {
kotlinCompilerExtensionVersion compose_version
Expand All @@ -127,7 +123,7 @@ android {
}

dependencies {
implementation 'androidx.core:core-ktx:1.15.0'
implementation 'androidx.core:core-ktx:1.17.0'
implementation "androidx.compose.ui:ui:$compose_version"
implementation "androidx.compose.material:material:$compose_version"
implementation "androidx.compose.ui:ui-tooling-preview:$compose_version"
Expand All @@ -137,6 +133,10 @@ dependencies {
implementation 'androidx.fragment:fragment-ktx:1.8.9'
implementation 'androidx.graphics:graphics-core:1.0.4'
implementation 'androidx.input:input-motionprediction:1.0.0'

// DataStore for Note Converter settings
implementation 'androidx.datastore:datastore-preferences:1.1.1'
implementation 'androidx.documentfile:documentfile:1.1.0'

//implementation fileTree(dir: 'libs', include: ['*.aar'])
implementation('com.onyx.android.sdk:onyxsdk-device:1.3.2') {
Expand Down Expand Up @@ -206,8 +206,13 @@ dependencies {
// for PDF support:
implementation("com.artifex.mupdf:fitz:1.26.10")

// ML Kit Digital Ink Recognition
implementation 'com.google.mlkit:digital-ink-recognition:19.0.0'
// Jetpack Ink API — stroke rendering + geometry
def ink_version = "1.0.0"
implementation "androidx.ink:ink-nativeloader:$ink_version"
implementation "androidx.ink:ink-brush:$ink_version"
implementation "androidx.ink:ink-geometry:$ink_version"
implementation "androidx.ink:ink-rendering:$ink_version"
implementation "androidx.ink:ink-strokes:$ink_version"

// Hilt
implementation "com.google.dagger:hilt-android:2.59.2"
Expand Down
117 changes: 25 additions & 92 deletions app/proguard-rules.pro
Original file line number Diff line number Diff line change
@@ -1,92 +1,25 @@
# Notable ProGuard/R8 rules


# ── Room database entities & DAOs ──
-keep class com.ethran.notable.data.db.** { *; }

# ── Kotlin serialization ──
-keepattributes *Annotation*, InnerClasses
-dontnote kotlinx.serialization.AnnotationsKt

-keepclassmembers @kotlinx.serialization.Serializable class com.ethran.notable.** {
*** Companion;
*** serializer(...);
}
-keep class com.ethran.notable.**$$serializer { *; }
-keepclasseswithmembers class com.ethran.notable.** {
kotlinx.serialization.KSerializer serializer(...);
}

# ── Onyx SDK (accessed via reflection by the device firmware) ──
-keep class com.onyx.** { *; }
-dontwarn com.onyx.**

# ── HWR Parcelables (IPC with ksync service) ──
-keep class com.onyx.android.sdk.hwr.service.** { *; }

# ── Hilt / Dagger ──
-dontwarn dagger.**
-keep class dagger.** { *; }
-keep class * extends dagger.hilt.android.internal.managers.ViewComponentManager$FragmentContextWrapper { *; }

# ── Firebase ──
-keep class com.google.firebase.** { *; }
-dontwarn com.google.firebase.**

# ── ShipBook logging ──
-keep class io.shipbook.** { *; }
-dontwarn io.shipbook.**

# ── MuPDF (native JNI) ──
-keep class com.artifex.mupdf.fitz.** { *; }

# ── Jetpack Ink (native JNI) ──
-keep class androidx.ink.** { *; }

# ── Coil (image loading, uses reflection) ──
-dontwarn coil.**

# ── RxJava ──
-dontwarn io.reactivex.**

# ── LZ4 (native loader uses reflection) ──
-keep class net.jpountz.** { *; }
-dontwarn net.jpountz.**

# ── Apache Commons Compress ──
-dontwarn org.apache.commons.compress.**

# ── Kotlin enums (used by name in serialization/Room converters) ──
-keepclassmembers enum com.ethran.notable.** {
<fields>;
public static **[] values();
public static ** valueOf(java.lang.String);
}

# ── Keep Parcelable CREATOR fields ──
-keepclassmembers class * implements android.os.Parcelable {
public static final ** CREATOR;
}

# ── Keep Android entry points (activities, services, receivers) ──
-keep public class * extends android.app.Activity
-keep public class * extends android.app.Application
-keep public class * extends android.app.Service
-keep public class * extends android.content.BroadcastReceiver
-keep public class * extends android.inputmethodservice.InputMethodService

# ── Hidden API bypass (loaded by reflection) ──
-keep class org.lsposed.hiddenapibypass.** { *; }
-dontwarn org.lsposed.hiddenapibypass.**

# ── MMKV (used by Onyx SDK, native JNI) ──
-keep class com.tencent.mmkv.** { *; }

# ── Standard suppressions ──
-dontwarn javax.**
-dontwarn org.joda.**
-dontwarn sun.misc.Unsafe
-dontwarn org.slf4j.impl.StaticLoggerBinder
-dontwarn org.slf4j.impl.StaticMDCBinder
-dontwarn org.slf4j.impl.StaticMarkerBinder

# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html

# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}

# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable

# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

# Ignore optional dependencies used by libraries
#-dontwarn org.joda.convert.FromString
#-dontwarn org.joda.convert.ToString
7 changes: 7 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,13 @@
</intent-filter>
</activity>

<activity
android:name=".noteconverter.NoteConverterActivity"
android:exported="true"
android:theme="@style/Theme.Inka"
android:label="Note Converter" />


<provider
android:name="com.ethran.notable.editor.utils.Provider"
android:authorities="com.ethran.notable.provider"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package com.onyx.android.sdk.hwr.service;

parcelable HWRCommandArgs;
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package com.onyx.android.sdk.hwr.service;

parcelable HWRInputArgs;
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package com.onyx.android.sdk.hwr.service;

parcelable HWROutputArgs;
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.onyx.android.sdk.hwr.service;

import com.onyx.android.sdk.hwr.service.HWROutputArgs;

interface HWROutputCallback {
void read(in HWROutputArgs args);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.onyx.android.sdk.hwr.service;

import android.os.ParcelFileDescriptor;
import com.onyx.android.sdk.hwr.service.HWROutputCallback;
import com.onyx.android.sdk.hwr.service.HWRInputArgs;
import com.onyx.android.sdk.hwr.service.HWRCommandArgs;

// Method order determines transaction codes — must match the service exactly.
// init=1, compileRecognizeText=2, batchRecognize=3, openIncrementalRecognizer=4,
// execCommand=5, closeRecognizer=6
// All methods are oneway (async) to match the service's original interface.
oneway interface IHWRService {
void init(in HWRInputArgs args, boolean forceReinit, HWROutputCallback callback);
void compileRecognizeText(String text, String language, HWROutputCallback callback);
void batchRecognize(in ParcelFileDescriptor pfd, HWROutputCallback callback);
void openIncrementalRecognizer(in HWRInputArgs args, HWROutputCallback callback);
void execCommand(in HWRInputArgs args, in HWRCommandArgs cmdArgs, HWROutputCallback callback);
void closeRecognizer();
}
6 changes: 4 additions & 2 deletions app/src/main/java/com/ethran/notable/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -111,8 +111,10 @@ class MainActivity : ComponentActivity() {
editorSettingCacheManager.get().init()
strokeMigrationHelper.get().reencodeStrokePointsToSB1()

// Pre-populate inbox tag cache
VaultTagScanner.refreshCache(savedSettings.obsidianInboxPath)
// Pre-populate inbox tag cache (only works with legacy text paths, not SAF URIs)
if (savedSettings.obsidianOutputUri.isEmpty() && savedSettings.obsidianInboxPath.isNotEmpty()) {
VaultTagScanner.refreshCache(savedSettings.obsidianInboxPath)
}
}
}
isInitialized = true
Expand Down
15 changes: 13 additions & 2 deletions app/src/main/java/com/ethran/notable/data/datastore/AppSettings.kt
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,19 @@ data class AppSettings(
val enableQuickNav: Boolean = true,


// Inbox Capture
val obsidianInboxPath: String = "Documents/primary/inbox",
// Handwriting Recognition
val recognitionLanguage: String = "cs_CZ",

// Inbox Capture & Batch Converter - shared output directory
val obsidianInboxPath: String = "Documents/primary/inbox", // Legacy text path
val obsidianOutputUri: String = "", // SAF URI for unified output (both features use this)
val obsidianTemplateUri: String = "", // Optional markdown template file (SAF URI)

// Batch Converter
val batchConverterInputUri: String = "",
val batchConverterLastScan: Long = 0,
val batchConverterPdfMode: Boolean = false,
val batchConverterPdfUri: String = "",

// Debug
val showWelcome: Boolean = true,
Expand Down
2 changes: 1 addition & 1 deletion app/src/main/java/com/ethran/notable/editor/EditorView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@ fun EditorView(
onTagRemove = { tag -> selectedTags.remove(tag) },
onSave = {
SyncState.launchSync(
appRepository, pageId, selectedTags.toList()
appRepository, pageId, selectedTags.toList(), context
)
navController.popBackStack()
},
Expand Down
87 changes: 87 additions & 0 deletions app/src/main/java/com/ethran/notable/editor/drawing/InkBrushes.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package com.ethran.notable.editor.drawing

import androidx.compose.ui.geometry.Offset
import androidx.ink.brush.Brush
import androidx.ink.brush.InputToolType
import androidx.ink.brush.StockBrushes
import androidx.ink.rendering.android.canvas.CanvasStrokeRenderer
import androidx.ink.strokes.MutableStrokeInputBatch
import androidx.ink.strokes.Stroke as InkStroke
import com.ethran.notable.data.db.Stroke
import com.ethran.notable.editor.utils.Pen
import com.ethran.notable.editor.utils.offsetStroke

/**
* Singleton renderer — thread-safe, reuse across draw calls.
*/
val inkRenderer: CanvasStrokeRenderer by lazy { CanvasStrokeRenderer.create() }

// Cache brush families (they're lazy-computed internally, but let's not re-invoke every stroke)
private val markerFamily by lazy { StockBrushes.marker() }
private val pressurePenFamily by lazy { StockBrushes.pressurePen() }
private val highlighterFamily by lazy { StockBrushes.highlighter() }
private val dashedLineFamily by lazy { StockBrushes.dashedLine() }

/**
* Map our Pen types to Ink API BrushFamily + create a Brush with the stroke's color/size.
*/
fun brushForStroke(stroke: Stroke): Brush {
val family = when (stroke.pen) {
Pen.BALLPEN, Pen.REDBALLPEN, Pen.GREENBALLPEN, Pen.BLUEBALLPEN ->
markerFamily
Pen.FOUNTAIN ->
pressurePenFamily
Pen.BRUSH ->
pressurePenFamily
Pen.PENCIL ->
markerFamily
Pen.MARKER ->
highlighterFamily
Pen.DASHED ->
dashedLineFamily
}
return Brush.createWithColorIntArgb(
family = family,
colorIntArgb = stroke.color,
size = stroke.size,
epsilon = 0.1f
)
}

// Spacing between synthetic timestamps when dt is missing (ms per point).
// 5ms ≈ 200Hz stylus sample rate — realistic enough for Ink API's stroke smoothing.
private const val SYNTHETIC_DT_MS = 5L

/**
* Convert our Stroke data model to an Ink API Stroke for rendering.
*/
fun Stroke.toInkStroke(offset: Offset = Offset.Zero): InkStroke {
val src = if (offset != Offset.Zero) offsetStroke(this, offset) else this
val batch = MutableStrokeInputBatch()
val points = src.points
if (points.isEmpty()) {
return InkStroke(brushForStroke(this), batch.toImmutable())
}

for (i in points.indices) {
val pt = points[i]
// Use real dt if available, otherwise synthesize realistic timing
val elapsedMs = pt.dt?.toLong() ?: (i * SYNTHETIC_DT_MS)
val pressure = pt.pressure?.let { it / maxPressure.toFloat() } ?: 0.5f
// tiltRadians must be in [0, π/2] or -1 (unset). Our tiltX is degrees [-90, 90].
val tiltRad = pt.tiltX?.let {
Math.toRadians(it.toDouble()).toFloat().coerceIn(0f, Math.PI.toFloat() / 2f)
} ?: -1f

batch.add(
type = InputToolType.STYLUS,
x = pt.x,
y = pt.y,
elapsedTimeMillis = elapsedMs,
pressure = pressure.coerceIn(0f, 1f),
tiltRadians = tiltRad,
orientationRadians = -1f // not captured by our hardware
)
}
return InkStroke(brushForStroke(this), batch)
}
Loading