Skip to content

Commit 0ef2466

Browse files
Implement simple read-only mode
See #81
1 parent f5e50da commit 0ef2466

File tree

14 files changed

+111
-8
lines changed

14 files changed

+111
-8
lines changed

README.md

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,9 +66,7 @@ If you're any good at Android app development using Kotlin, feel free to contrib
6666

6767
### Testing
6868

69-
- `app/test/setup-test-server.sh`
70-
- Gradle action `pixel9api35DebugAndroidTest`
71-
- `app/test/compare-test-images.sh`
69+
The app is tested using both unit tests and emulator tests ([details](./app/test/release-testing.md)).
7270

7371
## License
7472

app/src/androidTest/kotlin/eu/fliegendewurst/triliumdroid/InitialSyncTest.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ class InitialSyncTest {
137137
val id = Notes.getNotesByType("canvas")[0]
138138
Preferences.setCanvasViewportOverride(id, CanvasNoteViewport(12.7F, 307.4F, 0.6F))
139139
}
140+
Thread.sleep(2000) // wait until ready
140141

141142
openActionBarOverflowOrOptionsMenu(getInstrumentation().targetContext)
142143
onView(withText(R.string.jump_to_dialog))
@@ -153,6 +154,7 @@ class InitialSyncTest {
153154

154155
@Test
155156
fun test_011_jumpToNote() {
157+
Thread.sleep(2000) // wait until ready
156158
openActionBarOverflowOrOptionsMenu(getInstrumentation().targetContext)
157159
onView(withText(R.string.jump_to_dialog))
158160
.perform(click())
@@ -195,6 +197,7 @@ class InitialSyncTest {
195197

196198
@Test
197199
fun test_012_globalNoteMap() {
200+
Thread.sleep(2000) // wait until ready
198201
onView(withId(R.id.drawer_layout))
199202
.perform(DrawerActions.open(Gravity.START))
200203
onView(withText(R.string.sidebar_tab_2))

app/src/main/kotlin/eu/fliegendewurst/triliumdroid/activity/SetupActivity.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package eu.fliegendewurst.triliumdroid.activity
33
import android.app.Activity
44
import android.content.Intent
55
import android.os.Bundle
6+
import android.widget.Toast
67
import androidx.activity.result.contract.ActivityResultContracts
78
import androidx.appcompat.app.AlertDialog
89
import androidx.appcompat.app.AppCompatActivity
@@ -17,6 +18,7 @@ import eu.fliegendewurst.triliumdroid.dialog.ConfigureSyncDialog
1718
import eu.fliegendewurst.triliumdroid.dialog.YesNoDialog
1819
import eu.fliegendewurst.triliumdroid.service.Option
1920
import eu.fliegendewurst.triliumdroid.sync.ConnectionUtil
21+
import eu.fliegendewurst.triliumdroid.util.Preferences
2022
import kotlinx.coroutines.launch
2123
import kotlinx.coroutines.runBlocking
2224
import java.io.File
@@ -181,6 +183,8 @@ class SetupActivity : AppCompatActivity() {
181183
binding.status.setText(R.string.status_unknown)
182184
}
183185
}
186+
187+
binding.checkboxReadOnly.isChecked = Preferences.readOnlyMode()
184188
}
185189

186190
override fun onStop() {
@@ -195,6 +199,12 @@ class SetupActivity : AppCompatActivity() {
195199
Option.revisionIntervalUpdate(newInterval)
196200
}
197201
}
202+
203+
val newReadOnly = binding.checkboxReadOnly.isChecked
204+
if (newReadOnly != Preferences.readOnlyMode()) {
205+
Toast.makeText(this, R.string.hint_read_only_restart, Toast.LENGTH_LONG).show()
206+
}
207+
Preferences.setReadOnlyMode(newReadOnly)
198208
}
199209

200210
private fun setText() {

app/src/main/kotlin/eu/fliegendewurst/triliumdroid/activity/main/MainActivity.kt

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,8 @@ class MainActivity : AppCompatActivity() {
124124
binding = ActivityMainBinding.inflate(layoutInflater)
125125
setContentView(binding.root)
126126

127+
val readOnly = Preferences.readOnlyMode()
128+
127129
val toolbar = binding.toolbar
128130
toolbar.title = ""
129131
setSupportActionBar(toolbar)
@@ -227,15 +229,27 @@ class MainActivity : AppCompatActivity() {
227229
findViewById<Spinner>(R.id.widget_basic_properties_type_content).adapter = adapter
228230
}
229231

230-
binding.root.findViewById<Button>(R.id.button_labels_modify).setOnClickListener {
232+
val btnLabelsModify = binding.root.findViewById<Button>(R.id.button_labels_modify)
233+
btnLabelsModify.setOnClickListener {
231234
ModifyLabelsDialog.showDialog(this, getNoteLoaded() ?: return@setOnClickListener)
232235
}
233-
binding.root.findViewById<Button>(R.id.button_relations_edit).setOnClickListener {
236+
val btnRelationsEdit = binding.root.findViewById<Button>(R.id.button_relations_edit)
237+
btnRelationsEdit.setOnClickListener {
234238
ModifyRelationsDialog.showDialog(this, getNoteLoaded() ?: return@setOnClickListener)
235239
}
236-
binding.root.findViewById<Button>(R.id.button_note_paths_add).setOnClickListener {
240+
val btnAddNotePath = binding.root.findViewById<Button>(R.id.button_note_paths_add)
241+
btnAddNotePath.setOnClickListener {
237242
controller.addNotePath(this)
238243
}
244+
if (readOnly) {
245+
btnLabelsModify.visibility = View.GONE
246+
btnRelationsEdit.visibility = View.GONE
247+
btnAddNotePath.visibility = View.GONE
248+
} else {
249+
btnLabelsModify.visibility = View.VISIBLE
250+
btnRelationsEdit.visibility = View.VISIBLE
251+
btnAddNotePath.visibility = View.VISIBLE
252+
}
239253

240254
binding.fab.setOnClickListener {
241255
val action = ConfigureFabsDialog.getRightAction()
@@ -379,6 +393,7 @@ class MainActivity : AppCompatActivity() {
379393
}
380394
val frag = getFragment()
381395
val item = menu.findItem(R.id.action_edit)
396+
item.isVisible = !Preferences.readOnlyMode()
382397
if (frag is NoteEditFragment) {
383398
item?.setIcon(R.drawable.bx_save)
384399
} else {

app/src/main/kotlin/eu/fliegendewurst/triliumdroid/controller/MainController.kt

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,9 @@ class MainController {
301301
}
302302

303303
R.id.action_edit -> run {
304+
if (Preferences.readOnlyMode()) {
305+
return@run true
306+
}
304307
when (val fragment = activity.getFragment()) {
305308
is NoteFragment -> {
306309
val id = fragment.getNoteId() ?: return true
@@ -378,7 +381,10 @@ class MainController {
378381
true
379382
}
380383

381-
R.id.action_delete -> {
384+
R.id.action_delete -> run {
385+
if (Preferences.readOnlyMode()) {
386+
return@run true
387+
}
382388
val note = activity.getNoteLoaded()
383389
if (note == null || note.id == Notes.ROOT) {
384390
Toast.makeText(
@@ -530,6 +536,9 @@ class MainController {
530536
* User clicked on the note icon in the toolbar.
531537
*/
532538
fun titleIconClicked(activity: MainActivity) {
539+
if (Preferences.readOnlyMode()) {
540+
return
541+
}
533542
val note = activity.getNoteLoaded() ?: return
534543
NoteIconDialog.showDialogReturningIcon(activity) {
535544
activity.lifecycleScope.launch {
@@ -544,6 +553,9 @@ class MainController {
544553
* User clicked on the note title in the action bar.
545554
*/
546555
fun titleClicked(activity: MainActivity) {
556+
if (Preferences.readOnlyMode()) {
557+
return
558+
}
547559
val note = activity.getNoteLoaded() ?: return
548560
if (note.isProtected && !ProtectedSession.isActive()) {
549561
return

app/src/main/kotlin/eu/fliegendewurst/triliumdroid/database/Blobs.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import eu.fliegendewurst.triliumdroid.data.RevisionId
1111
import eu.fliegendewurst.triliumdroid.database.Cache.dateModified
1212
import eu.fliegendewurst.triliumdroid.database.Cache.utcDateModified
1313
import eu.fliegendewurst.triliumdroid.service.ProtectedSession
14+
import eu.fliegendewurst.triliumdroid.util.Preferences
1415
import kotlinx.coroutines.Dispatchers
1516
import kotlinx.coroutines.withContext
1617
import java.security.MessageDigest
@@ -118,6 +119,10 @@ object Blobs {
118119
* @return whether the blob was deleted
119120
*/
120121
suspend fun delete(id: BlobId) = withContext(Dispatchers.IO) {
122+
if (Preferences.readOnlyMode()) {
123+
Log.w(TAG, "read-only mode ignoring blob delete!")
124+
return@withContext false
125+
}
121126
if (notesWithBlob(id).isNotEmpty() || attachmentsWithBlob(id).isNotEmpty() ||
122127
revisionsWithBlob(id).isNotEmpty()
123128
) {

app/src/main/kotlin/eu/fliegendewurst/triliumdroid/database/Cache.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@ object Cache {
4646
toHash: List<ByteArray?>,
4747
isErased: Boolean = false
4848
) = withContext(Dispatchers.IO) {
49+
if (Preferences.readOnlyMode()) {
50+
Log.w(TAG, "read-only mode ignoring entity change!")
51+
return@withContext
52+
}
4953
val utc = utcDateModified()
5054
// https://github.com/TriliumNext/Notes/blob/v0.93.0/src/becca/entities/abstract_becca_entity.ts#L63-L76
5155
val md = MessageDigest.getInstance("SHA-1")

app/src/main/kotlin/eu/fliegendewurst/triliumdroid/database/DB.kt

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,14 @@ object DB {
3232

3333
var lastSync: Long? = null
3434

35+
/**
36+
* @return -1 on error
37+
*/
3538
suspend fun insert(table: String, vararg columns: Pair<String, Any?>): Long {
39+
if (Preferences.readOnlyMode()) {
40+
Log.w(TAG, "read-only mode ignoring database insert!")
41+
return 0
42+
}
3643
ensureDatabase()
3744
val cv = valuesFromPairs(columns)
3845
return withContext(Dispatchers.IO) {
@@ -48,6 +55,10 @@ object DB {
4855
onConflict: Int,
4956
vararg columns: Pair<String, Any?>
5057
): Long {
58+
if (Preferences.readOnlyMode()) {
59+
Log.w(TAG, "read-only mode ignoring database insert!")
60+
return 0
61+
}
5162
ensureDatabase()
5263
val cv = valuesFromPairs(columns)
5364
return withContext(Dispatchers.IO) {
@@ -70,6 +81,10 @@ object DB {
7081

7182
suspend fun delete(table: String, primaryKey: String, selectionArgs: Array<String>) =
7283
withContext(Dispatchers.IO) {
84+
if (Preferences.readOnlyMode()) {
85+
Log.w(TAG, "read-only mode ignoring database delete!")
86+
return@withContext 0
87+
}
7388
ensureDatabase()
7489
db!!.delete(table, "$primaryKey = ?", selectionArgs)
7590
}
@@ -83,6 +98,10 @@ object DB {
8398
columnName: String,
8499
generator: () -> String
85100
): String = withContext(Dispatchers.IO) {
101+
if (Preferences.readOnlyMode()) {
102+
Log.w(TAG, "read-only mode ignoring database ID generation!")
103+
return@withContext "INVALID"
104+
}
86105
ensureDatabase()
87106
while (true) {
88107
val candidate = generator.invoke()
@@ -113,6 +132,11 @@ object DB {
113132
return db!!.rawQueryWithFactory(factory, sql, selectionArgs, editTable)
114133
}
115134

135+
/**
136+
* Make sure to respect [Preferences.readOnlyMode] when using the handle.
137+
*
138+
* @return raw database handle
139+
*/
116140
suspend fun internalGetDatabase(): SQLiteDatabase? {
117141
ensureDatabase()
118142
return db

app/src/main/kotlin/eu/fliegendewurst/triliumdroid/sync/Sync.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ object Sync {
9797
val utc =
9898
DateTimeFormatter.ISO_INSTANT.format(OffsetDateTime.now(ZoneOffset.UTC))
9999
.replace('T', ' ')
100+
// read-only mode: sync must still work
100101
DB.internalGetDatabase()!!.execSQL(
101102
"INSERT OR REPLACE INTO options (name, value, isSynced, utcDateModified) VALUES (?, ?, 0, ?)",
102103
arrayOf("lastSyncedPush", largestId.toString(), utc)
@@ -295,6 +296,7 @@ object Sync {
295296
}
296297
}
297298
}
299+
// read-only mode: should still receive database updates
298300
DB.internalGetDatabase()!!.insertWithOnConflict(
299301
entityName,
300302
null,

app/src/main/kotlin/eu/fliegendewurst/triliumdroid/util/Preferences.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ object Preferences {
2727
private const val DB_MIGRATION = "dbMigration"
2828
private const val DATABASE_VERSION = "databaseVersion"
2929
private const val WEB_ASSETS_VERSION = "webAssetsVersion"
30+
private const val READ_ONLY_MODE = "readOnlyMode"
3031

3132
fun init(applicationContext: Context) {
3233
prefs = PreferenceManager.getDefaultSharedPreferences(applicationContext)
@@ -89,6 +90,8 @@ object Preferences {
8990
return CanvasNoteViewport(x, y, zoom)
9091
}
9192

93+
fun readOnlyMode(): Boolean = prefs.getBoolean(READ_ONLY_MODE, false)
94+
9295
fun isLeftAction(action: String) = prefs.getBoolean("fab_${action}_left", false)
9396
fun isRightAction(action: String) = prefs.getBoolean("fab_${action}_right", false)
9497

@@ -117,6 +120,8 @@ object Preferences {
117120
putFloat(keyZoom, view.zoom)
118121
}
119122

123+
fun setReadOnlyMode(readOnly: Boolean) = prefs.edit { putBoolean(READ_ONLY_MODE, readOnly) }
124+
120125
fun setWebAssetsVersion(version: Int) = prefs.edit { putInt(WEB_ASSETS_VERSION, version) }
121126

122127
fun clearMTLS() = prefs.edit { remove(MTLS_CERT) }

0 commit comments

Comments
 (0)