diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
index 4cf90ec92..a7ab944b1 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -8,3 +8,9 @@ assignees: ''
---
- I have searched for existing issues that may be the same as or related to mine.
+
+Please complete the following information:
+
+ Device:
+ Android Version:
+ Orgzly Revived Version:
diff --git a/.github/workflows/android-build-master.yml b/.github/workflows/android-build-master.yml
index b1aecf981..62d57ba12 100644
--- a/.github/workflows/android-build-master.yml
+++ b/.github/workflows/android-build-master.yml
@@ -8,6 +8,9 @@ on:
branches:
- 'master'
+ # Allows you to run this workflow manually from the Actions tab
+ workflow_dispatch:
+
jobs:
build:
name: Generate APK
@@ -24,6 +27,8 @@ jobs:
- name: Build APK
run: ./gradlew assembleDebug
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload APK
uses: actions/upload-artifact@v2
diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml
index 901b9b58e..ba06f26da 100644
--- a/.github/workflows/prerelease.yml
+++ b/.github/workflows/prerelease.yml
@@ -25,9 +25,18 @@ jobs:
distribution: 'zulu'
java-version: '11'
+ - name: Setup build tool version variable
+ shell: bash
+ run: |
+ BUILD_TOOL_VERSION=$(ls /usr/local/lib/android/sdk/build-tools/ | tail -n 1)
+ echo "BUILD_TOOL_VERSION=$BUILD_TOOL_VERSION" >> $GITHUB_ENV
+ echo Last build tool version is: $BUILD_TOOL_VERSION
+
# Build the standard version binary.
- name: Generate "premium" release APK
run: ./gradlew assemblePremiumRelease --stacktrace
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Sign APK using key store from repo secrets
uses: r0adkll/sign-android-release@v1
@@ -37,6 +46,8 @@ jobs:
signingKeyBase64: ${{ secrets.APK_SIGNING_KEYSTORE_FILE }}
alias: orgzly-revived-20231013
keyStorePassword: ${{ secrets.APK_SIGNING_KEYSTORE_PASSWORD }}
+ env:
+ BUILD_TOOLS_VERSION: ${{ env.BUILD_TOOL_VERSION }}
- name: Get version name from git tag
run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
@@ -47,6 +58,8 @@ jobs:
# Now do the same for the F-Droid flavor.
- name: Generate "fdroid" release APK
run: ./gradlew assembleFdroidRelease --stacktrace
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Sign APK using key store from repo secrets
uses: r0adkll/sign-android-release@v1
@@ -56,6 +69,8 @@ jobs:
signingKeyBase64: ${{ secrets.APK_SIGNING_KEYSTORE_FILE }}
alias: orgzly-revived-20231013
keyStorePassword: ${{ secrets.APK_SIGNING_KEYSTORE_PASSWORD }}
+ env:
+ BUILD_TOOLS_VERSION: ${{ env.BUILD_TOOL_VERSION }}
- name: Rename APK file
run: mv ${{steps.sign_fdroid_apk.outputs.signedReleaseFile}} orgzly-revived-fdroid-${{ env.VERSION }}.apk
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 521634c57..e24f57c8d 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -26,9 +26,18 @@ jobs:
distribution: 'zulu'
java-version: '11'
+ - name: Setup build tool version variable
+ shell: bash
+ run: |
+ BUILD_TOOL_VERSION=$(ls /usr/local/lib/android/sdk/build-tools/ | tail -n 1)
+ echo "BUILD_TOOL_VERSION=$BUILD_TOOL_VERSION" >> $GITHUB_ENV
+ echo Last build tool version is: $BUILD_TOOL_VERSION
+
# Build the standard version binary.
- name: Generate "premium" release APK
run: ./gradlew assemblePremiumRelease --stacktrace
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Sign APK using key store from repo secrets
uses: r0adkll/sign-android-release@v1
@@ -38,6 +47,8 @@ jobs:
signingKeyBase64: ${{ secrets.APK_SIGNING_KEYSTORE_FILE }}
alias: orgzly-revived-20231013
keyStorePassword: ${{ secrets.APK_SIGNING_KEYSTORE_PASSWORD }}
+ env:
+ BUILD_TOOLS_VERSION: ${{ env.BUILD_TOOL_VERSION }}
- name: Get version name from git tag
run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
@@ -48,6 +59,8 @@ jobs:
# Now do the same for the F-Droid flavor.
- name: Generate "fdroid" release APK
run: ./gradlew assembleFdroidRelease --stacktrace
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Sign APK using key store from repo secrets
uses: r0adkll/sign-android-release@v1
@@ -57,6 +70,8 @@ jobs:
signingKeyBase64: ${{ secrets.APK_SIGNING_KEYSTORE_FILE }}
alias: orgzly-revived-20231013
keyStorePassword: ${{ secrets.APK_SIGNING_KEYSTORE_PASSWORD }}
+ env:
+ BUILD_TOOLS_VERSION: ${{ env.BUILD_TOOL_VERSION }}
- name: Rename APK file
run: mv ${{steps.sign_fdroid_apk.outputs.signedReleaseFile}} orgzly-revived-fdroid-${{ env.VERSION }}.apk
@@ -67,4 +82,3 @@ jobs:
with:
files: 'orgzly-revived-*.apk'
draft: true
-
\ No newline at end of file
diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml
new file mode 100644
index 000000000..1ea1dd9e3
--- /dev/null
+++ b/.github/workflows/test.yaml
@@ -0,0 +1,56 @@
+name: Test
+
+on:
+ pull_request:
+ branches:
+ - 'master'
+ push:
+ branches:
+ - 'master'
+ workflow_dispatch:
+
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ steps:
+ - name: checkout
+ uses: actions/checkout@v4
+
+ - name: Enable KVM
+ run: |
+ echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
+ sudo udevadm control --reload-rules
+ sudo udevadm trigger --name-match=kvm
+
+ - name: Gradle cache
+ uses: gradle/actions/setup-gradle@v3
+
+ - name: AVD cache
+ uses: actions/cache@v4
+ id: avd-cache
+ with:
+ path: |
+ ~/.android/avd/*
+ ~/.android/adb*
+ key: avd-29
+
+ - name: create AVD and generate snapshot for caching
+ if: steps.avd-cache.outputs.cache-hit != 'true'
+ uses: reactivecircus/android-emulator-runner@v2
+ with:
+ api-level: 29
+ force-avd-creation: false
+ emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
+ disable-animations: true
+ profile: Nexus 6
+ script: echo "Generated AVD snapshot for caching."
+
+ - name: run tests
+ uses: reactivecircus/android-emulator-runner@v2
+ with:
+ api-level: 29
+ force-avd-creation: false
+ emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
+ disable-animations: true
+ profile: Nexus 6
+ script: ./gradlew connectedCheck
diff --git a/README.org b/README.org
index 946b57691..9027369aa 100644
--- a/README.org
+++ b/README.org
@@ -3,11 +3,6 @@
-
-
-
+ android:maxSdkVersion="29" />
@@ -34,7 +34,6 @@
android:supportsRtl="true"
android:icon="@mipmap/cic_launcher"
android:label="@string/app_name"
- android:theme="@style/AppLightTheme.Light"
android:fullBackupContent="@xml/backup_config"
android:requestLegacyExternalStorage="true">
@@ -228,6 +227,10 @@
+
+
+
diff --git a/app/src/main/java/com/orgzly/android/data/DataRepository.kt b/app/src/main/java/com/orgzly/android/data/DataRepository.kt
index d27570804..f427efb05 100644
--- a/app/src/main/java/com/orgzly/android/data/DataRepository.kt
+++ b/app/src/main/java/com/orgzly/android/data/DataRepository.kt
@@ -613,7 +613,7 @@ class DataRepository @Inject constructor(
} else {
db.runInTransaction(Callable {
- moveSubtrees(noteIds, Place.UNDER, target.noteId)
+ moveSubtrees(noteIds, target.place, target.noteId)
})
}
}
@@ -1280,6 +1280,22 @@ class DataRepository @Inject constructor(
return db.note().getNoteAndAncestors(noteId)
}
+ fun getNoteAtPath(fullPath: String): NoteView? {
+ val (bookName, path) = run {
+ val pathParts = fullPath.split("/")
+ if (pathParts.isEmpty()) return null
+ pathParts[0] to pathParts.drop(1).joinToString("/")
+ }
+ return if (path.split("/").any { it.isNotEmpty() })
+ getNotes(bookName)
+ .filter { ("/$path").endsWith("/" + it.note.title) }
+ .firstOrNull { view ->
+ getNoteAndAncestors(view.note.id)
+ .joinToString("/") { it.title } == path
+ }
+ else null
+ }
+
fun getNotesAndSubtrees(ids: Set): List {
return db.note().getNotesForSubtrees(ids)
}
diff --git a/app/src/main/java/com/orgzly/android/data/logs/AppLogsRepository.kt b/app/src/main/java/com/orgzly/android/data/logs/AppLogsRepository.kt
index b64c49b84..98fe13d03 100644
--- a/app/src/main/java/com/orgzly/android/data/logs/AppLogsRepository.kt
+++ b/app/src/main/java/com/orgzly/android/data/logs/AppLogsRepository.kt
@@ -5,5 +5,5 @@ import kotlinx.coroutines.flow.Flow
interface AppLogsRepository {
fun log(type: String, str: String)
- fun getFlow(type: String): Flow>
+ fun getFlow(): Flow>
}
\ No newline at end of file
diff --git a/app/src/main/java/com/orgzly/android/data/logs/DatabaseAppLogsRepository.kt b/app/src/main/java/com/orgzly/android/data/logs/DatabaseAppLogsRepository.kt
index 6f21f8184..34c2b43ae 100644
--- a/app/src/main/java/com/orgzly/android/data/logs/DatabaseAppLogsRepository.kt
+++ b/app/src/main/java/com/orgzly/android/data/logs/DatabaseAppLogsRepository.kt
@@ -18,8 +18,8 @@ class DatabaseAppLogsRepository @Inject constructor(db: OrgzlyDatabase) : AppLog
dbAppLog.insert(entry)
}
- override fun getFlow(type: String): Flow> {
- return dbAppLog.getFlow(type).map { logEntries ->
+ override fun getFlow(): Flow> {
+ return dbAppLog.getFlow().map { logEntries ->
logEntries.map { entry ->
LogEntry(entry.timestamp, entry.name, entry.message)
}
diff --git a/app/src/main/java/com/orgzly/android/db/dao/AppLogDao.kt b/app/src/main/java/com/orgzly/android/db/dao/AppLogDao.kt
index 8267f3a06..7cd46ea46 100644
--- a/app/src/main/java/com/orgzly/android/db/dao/AppLogDao.kt
+++ b/app/src/main/java/com/orgzly/android/db/dao/AppLogDao.kt
@@ -7,6 +7,6 @@ import kotlinx.coroutines.flow.Flow
@Dao
abstract class AppLogDao : BaseDao {
- @Query("SELECT * FROM app_logs WHERE name = :name ORDER BY timestamp")
- abstract fun getFlow(name: String): Flow>
+ @Query("SELECT * FROM app_logs ORDER BY timestamp")
+ abstract fun getFlow(): Flow>
}
diff --git a/app/src/main/java/com/orgzly/android/di/AppComponent.kt b/app/src/main/java/com/orgzly/android/di/AppComponent.kt
index f5b3efb86..d89a48fa4 100644
--- a/app/src/main/java/com/orgzly/android/di/AppComponent.kt
+++ b/app/src/main/java/com/orgzly/android/di/AppComponent.kt
@@ -7,6 +7,7 @@ import com.orgzly.android.TimeChangeBroadcastReceiver
import com.orgzly.android.di.module.ApplicationModule
import com.orgzly.android.di.module.DataModule
import com.orgzly.android.di.module.DatabaseModule
+import com.orgzly.android.external.actionhandlers.ExternalAccessActionHandler
import com.orgzly.android.reminders.NoteReminders
import com.orgzly.android.reminders.RemindersBroadcastReceiver
import com.orgzly.android.sync.SyncWorker
@@ -86,4 +87,5 @@ interface AppComponent {
fun inject(arg: RemindersBroadcastReceiver)
fun inject(arg: NotificationBroadcastReceiver)
fun inject(arg: SharingShortcutsManager)
+ fun inject(arg: ExternalAccessActionHandler)
}
\ No newline at end of file
diff --git a/app/src/main/java/com/orgzly/android/external/ExternalAccessReceiver.kt b/app/src/main/java/com/orgzly/android/external/ExternalAccessReceiver.kt
new file mode 100644
index 000000000..97b8ec116
--- /dev/null
+++ b/app/src/main/java/com/orgzly/android/external/ExternalAccessReceiver.kt
@@ -0,0 +1,27 @@
+package com.orgzly.android.external
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import com.google.gson.GsonBuilder
+import com.orgzly.android.external.actionhandlers.*
+import com.orgzly.android.external.types.Response
+
+class ExternalAccessReceiver : BroadcastReceiver() {
+ val actionHandlers = listOf(
+ GetOrgInfo(),
+ RunSearch(),
+ EditNotes(),
+ EditSavedSearches(),
+ ManageWidgets()
+ )
+
+ override fun onReceive(context: Context?, intent: Intent?) {
+ val response = actionHandlers.asSequence()
+ .mapNotNull { it.handle(intent!!, context!!) }
+ .firstOrNull()
+ ?: Response(false, "Invalid action")
+ val gson = GsonBuilder().serializeNulls().create()
+ resultData = gson.toJson(response)
+ }
+}
diff --git a/app/src/main/java/com/orgzly/android/external/actionhandlers/EditNotes.kt b/app/src/main/java/com/orgzly/android/external/actionhandlers/EditNotes.kt
new file mode 100644
index 000000000..94d1cd749
--- /dev/null
+++ b/app/src/main/java/com/orgzly/android/external/actionhandlers/EditNotes.kt
@@ -0,0 +1,53 @@
+package com.orgzly.android.external.actionhandlers
+
+import android.content.Intent
+import com.orgzly.android.external.types.ExternalHandlerFailure
+
+class EditNotes : ExternalAccessActionHandler() {
+ override val actions = listOf(
+ action(::addNote, "ADD_NOTE"),
+ action(::editNote, "EDIT_NOTE"),
+ action(::refileNote, "REFILE_NOTE", "REFILE_NOTES"),
+ action(::moveNote, "MOVE_NOTE", "MOVE_NOTES"),
+ action(::deleteNote, "DELETE_NOTE", "DELETE_NOTES")
+ )
+
+ private fun addNote(intent: Intent): String {
+ val place = intent.getNotePlace()
+ val newNote = intent.getNotePayload()
+ val note = dataRepository.createNote(newNote, place)
+ return "${note.id}"
+ }
+
+ private fun editNote(intent: Intent) {
+ val noteView = intent.getNote()
+ val newNote = intent.getNotePayload(title=noteView.note.title)
+ dataRepository.updateNote(noteView.note.id, newNote)
+ }
+
+ private fun refileNote(intent: Intent) {
+ val notes = intent.getNoteIds()
+ val place = intent.getNotePlace()
+ dataRepository.refileNotes(notes, place)
+ }
+
+ private fun moveNote(intent: Intent) {
+ val notes = intent.getNoteIds()
+ with(dataRepository) { when (intent.getStringExtra("DIRECTION")) {
+ "UP" -> moveNote(intent.getBook().id, notes, -1)
+ "DOWN" -> moveNote(intent.getBook().id, notes, 1)
+ "LEFT" -> promoteNotes(notes)
+ "RIGHT" -> demoteNotes(notes)
+ else -> throw ExternalHandlerFailure("invalid direction")
+ } }
+ }
+
+ private fun deleteNote(intent: Intent) {
+ intent.getNoteIds().groupBy {
+ dataRepository.getNoteView(it)?.bookName
+ ?: throw ExternalHandlerFailure("invalid note id $it")
+ }.forEach { (bookName, notes) ->
+ dataRepository.deleteNotes(dataRepository.getBook(bookName)!!.id, notes.toSet())
+ }
+ }
+}
diff --git a/app/src/main/java/com/orgzly/android/external/actionhandlers/EditSavedSearches.kt b/app/src/main/java/com/orgzly/android/external/actionhandlers/EditSavedSearches.kt
new file mode 100644
index 000000000..4f8ced4ef
--- /dev/null
+++ b/app/src/main/java/com/orgzly/android/external/actionhandlers/EditSavedSearches.kt
@@ -0,0 +1,45 @@
+package com.orgzly.android.external.actionhandlers
+
+import android.content.Intent
+import com.orgzly.android.db.entity.SavedSearch
+import com.orgzly.android.external.types.ExternalHandlerFailure
+
+class EditSavedSearches : ExternalAccessActionHandler() {
+ override val actions = listOf(
+ action(::addSavedSearch, "ADD_SAVED_SEARCH"),
+ action(::editSavedSearch, "EDIT_SAVED_SEARCH"),
+ action(::moveSavedSearch, "MOVE_SAVED_SEARCH"),
+ action(::deleteSavedSearch, "DELETE_SAVED_SEARCH"),
+ )
+
+ private fun addSavedSearch(intent: Intent): String {
+ val savedSearch = intent.getNewSavedSearch()
+ val id = dataRepository.createSavedSearch(savedSearch)
+ return "$id"
+ }
+
+ private fun editSavedSearch(intent: Intent) {
+ val savedSearch = intent.getSavedSearch()
+ val newSavedSearch = intent.getNewSavedSearch(allowBlank = true)
+ dataRepository.updateSavedSearch(SavedSearch(
+ savedSearch.id,
+ newSavedSearch.name.ifBlank { savedSearch.name },
+ newSavedSearch.query.ifBlank { savedSearch.query },
+ savedSearch.position
+ ))
+ }
+
+ private fun moveSavedSearch(intent: Intent) {
+ val savedSearch = intent.getSavedSearch()
+ when (intent.getStringExtra("DIRECTION")) {
+ "UP" -> dataRepository.moveSavedSearchUp(savedSearch.id)
+ "DOWN" -> dataRepository.moveSavedSearchDown(savedSearch.id)
+ else -> throw ExternalHandlerFailure("invalid direction")
+ }
+ }
+
+ private fun deleteSavedSearch(intent: Intent) {
+ val savedSearch = intent.getSavedSearch()
+ dataRepository.deleteSavedSearches(setOf(savedSearch.id))
+ }
+}
diff --git a/app/src/main/java/com/orgzly/android/external/actionhandlers/ExternalAccessActionHandler.kt b/app/src/main/java/com/orgzly/android/external/actionhandlers/ExternalAccessActionHandler.kt
new file mode 100644
index 000000000..a7cce4f05
--- /dev/null
+++ b/app/src/main/java/com/orgzly/android/external/actionhandlers/ExternalAccessActionHandler.kt
@@ -0,0 +1,45 @@
+package com.orgzly.android.external.actionhandlers
+
+import android.content.Context
+import android.content.Intent
+import com.orgzly.android.App
+import com.orgzly.android.data.DataRepository
+import com.orgzly.android.external.types.Response
+import javax.inject.Inject
+
+abstract class ExternalAccessActionHandler : ExternalIntentParser {
+ @Inject
+ override lateinit var dataRepository: DataRepository
+
+ init {
+ @Suppress("LeakingThis")
+ App.appComponent.inject(this)
+ }
+
+ abstract val actions: List Any>>>
+ private val fullNameActions by lazy {
+ actions.flatten().toMap().mapKeys { (key, _) -> "com.orgzly.android.$key" }
+ }
+
+ fun action(f: (Intent, Context) -> Any, vararg names: String) = names.map { it to f }
+
+ @JvmName("intentAction")
+ fun action(f: (Intent) -> Any, vararg names: String) =
+ action({ i, _ -> f(i) }, *names)
+
+ @JvmName("contextAction")
+ fun action(f: (Context) -> Any, vararg names: String) =
+ action({ _, c -> f(c) }, *names)
+
+ fun action(f: () -> Any, vararg names: String) =
+ action({ _, _ -> f() }, *names)
+
+
+ fun handle(intent: Intent, context: Context) = try {
+ fullNameActions[intent.action!!]
+ ?.let { it(intent, context) }
+ ?.let { Response(true, if (it is Unit) null else it) }
+ } catch (e: Exception) {
+ Response(false, e.message)
+ }
+}
diff --git a/app/src/main/java/com/orgzly/android/external/actionhandlers/ExternalIntentParser.kt b/app/src/main/java/com/orgzly/android/external/actionhandlers/ExternalIntentParser.kt
new file mode 100644
index 000000000..f8478c30b
--- /dev/null
+++ b/app/src/main/java/com/orgzly/android/external/actionhandlers/ExternalIntentParser.kt
@@ -0,0 +1,135 @@
+package com.orgzly.android.external.actionhandlers
+
+import android.content.Intent
+import com.google.gson.JsonElement
+import com.google.gson.JsonObject
+import com.google.gson.JsonParser
+import com.orgzly.android.data.DataRepository
+import com.orgzly.android.db.entity.NoteView
+import com.orgzly.android.db.entity.SavedSearch
+import com.orgzly.android.external.types.ExternalHandlerFailure
+import com.orgzly.android.query.user.InternalQueryParser
+import com.orgzly.android.ui.NotePlace
+import com.orgzly.android.ui.Place
+import com.orgzly.android.ui.note.NotePayload
+import com.orgzly.org.OrgProperties
+
+interface ExternalIntentParser {
+ val dataRepository: DataRepository
+
+ fun Intent.getNotePayload(title: String? = null): NotePayload {
+ val rawJson = getStringExtra("NOTE_PAYLOAD")
+ val json = try {
+ JsonParser.parseString(rawJson)
+ .let { if (it.isJsonObject) it.asJsonObject else null }!!
+ } catch (e: Exception) {
+ throw ExternalHandlerFailure("failed to parse json: ${e.message}\n$rawJson")
+ }
+ return NotePayload(
+ json.getString("title") ?: title
+ ?: throw ExternalHandlerFailure("no title supplied!\n$rawJson"),
+ json.getString("content"),
+ json.getString("state"),
+ json.getString("priority"),
+ json.getString("scheduled"),
+ json.getString("deadline"),
+ json.getString("closed"),
+ (json.getString("tags") ?: "")
+ .split(" +".toRegex())
+ .filter { it.isNotEmpty() },
+ OrgProperties().apply {
+ json["properties"]?.asMap?.forEach { (k, v) -> this[k] = v }
+ }
+ )
+ }
+
+ private fun getNoteByQuery(rawQuery: String?): NoteView {
+ if (rawQuery == null)
+ throw ExternalHandlerFailure("couldn't find note")
+ val query = InternalQueryParser().parse(rawQuery)
+ val notes = dataRepository.selectNotesFromQuery(query)
+ if (notes.isEmpty())
+ throw ExternalHandlerFailure("couldn't find note")
+ if (notes.size > 1)
+ throw ExternalHandlerFailure("query \"$rawQuery\" gave multiple results")
+ return notes[0]
+ }
+
+ fun Intent.getNote(prefix: String = "") =
+ dataRepository.getNoteView(getLongExtra("${prefix}NOTE_ID", -1))
+ ?: dataRepository.getNoteAtPath(getStringExtra("${prefix}NOTE_PATH") ?: "")
+ ?: getNoteByQuery(getStringExtra("${prefix}NOTE_QUERY"))
+
+ fun Intent.getNoteAndProps(prefix: String = "") = getNote(prefix).let {
+ it to dataRepository.getNoteProperties(it.note.id)
+ }
+
+ fun Intent.getBook(prefix: String = "") =
+ dataRepository.getBook(getLongExtra("${prefix}BOOK_ID", -1))
+ ?: dataRepository.getBook(getStringExtra("${prefix}BOOK_NAME") ?: "")
+ ?: throw ExternalHandlerFailure("couldn't find book")
+
+ fun Intent.getNotePlace() = try {
+ getNote(prefix="PARENT_").let { noteView ->
+ val place = try {
+ Place.valueOf(getStringExtra("PLACEMENT") ?: "")
+ } catch (e: IllegalArgumentException) { Place.UNDER }
+ dataRepository.getBook(noteView.bookName)?.let { book ->
+ NotePlace(book.id, noteView.note.id, place)
+ }
+ }
+ } catch (e: ExternalHandlerFailure) { null } ?: try {
+ NotePlace(getBook(prefix="PARENT_").id)
+ } catch (e: ExternalHandlerFailure) {
+ throw ExternalHandlerFailure("couldn't find parent note/book")
+ }
+
+ fun Intent.getNoteIds(allowSingle: Boolean = true, allowEmpty: Boolean = false): Set {
+ val id = if (allowSingle) getLongExtra("NOTE_ID", -1) else null
+ val ids = getLongArrayExtra("NOTE_IDS")?.toTypedArray() ?: emptyArray()
+ val path =
+ if (allowSingle)
+ getStringExtra("NOTE_PATH")
+ ?.let { dataRepository.getNoteAtPath(it)?.note?.id }
+ else null
+ val paths = (getStringArrayExtra("NOTE_PATHS") ?: emptyArray())
+ .mapNotNull { dataRepository.getNoteAtPath(it)?.note?.id }
+ .toTypedArray()
+ return listOfNotNull(id, *ids, path, *paths).filter { it >= 0 }.toSet().also {
+ if (it.isEmpty() && !allowEmpty)
+ throw ExternalHandlerFailure("no notes specified")
+ }
+ }
+
+ fun Intent.getSavedSearch() =
+ dataRepository.getSavedSearch(getLongExtra("SAVED_SEARCH_ID", -1))
+ ?: dataRepository.getSavedSearches()
+ .find { it.name == getStringExtra("SAVED_SEARCH_NAME") }
+ ?: throw ExternalHandlerFailure("couldn't find saved search")
+
+ fun Intent.getNewSavedSearch(allowBlank: Boolean = false): SavedSearch {
+ val name = getStringExtra("SAVED_SEARCH_NEW_NAME")
+ val query = getStringExtra("SAVED_SEARCH_NEW_QUERY")
+ if (!allowBlank && (name.isNullOrBlank() || query.isNullOrBlank()))
+ throw ExternalHandlerFailure("invalid parameters for new saved search")
+ return SavedSearch(0, name ?: "", query ?: "", 0)
+ }
+
+ private fun JsonObject.getString(name: String) = this[name]?.let {
+ if (it.isJsonPrimitive && it.asJsonPrimitive.isString)
+ it.asJsonPrimitive.asString
+ else null
+ }
+
+ private val JsonElement.asMap: Map?
+ get() = if (this.isJsonObject) {
+ this.asJsonObject
+ .entrySet()
+ .map {
+ if (it.value.isJsonPrimitive)
+ it.key to it.value.asJsonPrimitive.asString
+ else return null
+ }
+ .toMap()
+ } else null
+}
diff --git a/app/src/main/java/com/orgzly/android/external/actionhandlers/GetOrgInfo.kt b/app/src/main/java/com/orgzly/android/external/actionhandlers/GetOrgInfo.kt
new file mode 100644
index 000000000..2f7442f41
--- /dev/null
+++ b/app/src/main/java/com/orgzly/android/external/actionhandlers/GetOrgInfo.kt
@@ -0,0 +1,21 @@
+package com.orgzly.android.external.actionhandlers
+
+import android.content.Intent
+import com.orgzly.android.external.types.*
+
+class GetOrgInfo : ExternalAccessActionHandler() {
+ override val actions = listOf(
+ action(::getBooks, "GET_BOOKS"),
+ action(::getSavedSearches, "GET_SAVED_SEARCHES"),
+ action(::getNote, "GET_NOTE")
+ )
+
+ private fun getBooks() =
+ dataRepository.getBooks().map(Book::from).toTypedArray()
+
+ private fun getSavedSearches() =
+ dataRepository.getSavedSearches().map(SavedSearch::from).toTypedArray()
+
+ private fun getNote(intent: Intent) =
+ Note.from(intent.getNoteAndProps())
+}
diff --git a/app/src/main/java/com/orgzly/android/external/actionhandlers/ManageWidgets.kt b/app/src/main/java/com/orgzly/android/external/actionhandlers/ManageWidgets.kt
new file mode 100644
index 000000000..fb9374bfc
--- /dev/null
+++ b/app/src/main/java/com/orgzly/android/external/actionhandlers/ManageWidgets.kt
@@ -0,0 +1,36 @@
+package com.orgzly.android.external.actionhandlers
+
+import android.appwidget.AppWidgetManager
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import com.orgzly.android.AppIntent
+import com.orgzly.android.db.entity.SavedSearch
+import com.orgzly.android.external.types.ExternalHandlerFailure
+import com.orgzly.android.widgets.ListWidgetProvider
+
+class ManageWidgets : ExternalAccessActionHandler() {
+ override val actions = listOf(
+ action(::getWidgets, "GET_WIDGETS"),
+ action(::setWidget, "SET_WIDGET")
+ )
+
+ private fun getWidgets(context: Context): Map {
+ val widgetManager = AppWidgetManager.getInstance(context)
+ val componentName = ComponentName(context.packageName, ListWidgetProvider::class.java.name)
+ return widgetManager.getAppWidgetIds(componentName)
+ .associateWith { ListWidgetProvider.getSavedSearch(context, it, dataRepository) }
+ }
+
+ private fun setWidget(intent: Intent, context: Context) {
+ val widgetId = intent.getIntExtra("WIDGET_ID", -1)
+ if (widgetId < 0) throw ExternalHandlerFailure("invalid widget id")
+ val savedSearch = intent.getSavedSearch()
+
+ context.sendBroadcast(Intent(context, ListWidgetProvider::class.java).apply {
+ action = AppIntent.ACTION_SET_LIST_WIDGET_SELECTION
+ putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, widgetId)
+ putExtra(AppIntent.EXTRA_SAVED_SEARCH_ID, savedSearch.id)
+ })
+ }
+}
diff --git a/app/src/main/java/com/orgzly/android/external/actionhandlers/RunSearch.kt b/app/src/main/java/com/orgzly/android/external/actionhandlers/RunSearch.kt
new file mode 100644
index 000000000..6542f7cc0
--- /dev/null
+++ b/app/src/main/java/com/orgzly/android/external/actionhandlers/RunSearch.kt
@@ -0,0 +1,21 @@
+package com.orgzly.android.external.actionhandlers
+
+import android.content.Intent
+import com.orgzly.android.external.types.ExternalHandlerFailure
+import com.orgzly.android.external.types.Note
+import com.orgzly.android.query.user.InternalQueryParser
+
+class RunSearch : ExternalAccessActionHandler() {
+ override val actions = listOf(
+ action(::runSearch, "SEARCH")
+ )
+
+ private fun runSearch(intent: Intent): List {
+ val searchTerm = intent.getStringExtra("QUERY")
+ if (searchTerm.isNullOrBlank()) throw ExternalHandlerFailure("invalid search term")
+ val query = InternalQueryParser().parse(searchTerm)
+ val notes = dataRepository.selectNotesFromQuery(query)
+ val notesWithProps = notes.map { it to dataRepository.getNoteProperties(it.note.id) }
+ return notesWithProps.map(Note::from)
+ }
+}
diff --git a/app/src/main/java/com/orgzly/android/external/types/Book.kt b/app/src/main/java/com/orgzly/android/external/types/Book.kt
new file mode 100644
index 000000000..31b624aaf
--- /dev/null
+++ b/app/src/main/java/com/orgzly/android/external/types/Book.kt
@@ -0,0 +1,10 @@
+package com.orgzly.android.external.types
+
+import com.orgzly.android.db.entity.BookView
+
+data class Book(val id: Long, val title: String) {
+ companion object {
+ fun from(view: BookView) =
+ Book(view.book.id, view.book.name)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/orgzly/android/external/types/ExternalHandlerFailure.kt b/app/src/main/java/com/orgzly/android/external/types/ExternalHandlerFailure.kt
new file mode 100644
index 000000000..81030ead5
--- /dev/null
+++ b/app/src/main/java/com/orgzly/android/external/types/ExternalHandlerFailure.kt
@@ -0,0 +1,3 @@
+package com.orgzly.android.external.types
+
+class ExternalHandlerFailure(msg: String) : Exception(msg)
\ No newline at end of file
diff --git a/app/src/main/java/com/orgzly/android/external/types/Note.kt b/app/src/main/java/com/orgzly/android/external/types/Note.kt
new file mode 100644
index 000000000..66c3fd03b
--- /dev/null
+++ b/app/src/main/java/com/orgzly/android/external/types/Note.kt
@@ -0,0 +1,82 @@
+package com.orgzly.android.external.types
+
+import com.orgzly.android.db.entity.NoteProperty
+import com.orgzly.android.db.entity.NoteView
+
+data class Note(
+ val id: Long,
+ val title: String,
+ val content: String?,
+ val tags: List,
+ val inheritedTags: List,
+ val bookName: String,
+ val scheduled: Timestamp?,
+ val deadline: Timestamp?,
+ val closed: Timestamp?,
+ val priority: String?,
+ val state: String?,
+ val createdAt: Long?,
+ val properties: Map
+) {
+ companion object {
+ fun from(view: NoteView, props: List): Note {
+ val note = view.note
+ return Note(
+ note.id,
+ note.title,
+ note.content,
+ note.tags?.split(" +".toRegex())
+ ?.filter { it.isNotEmpty() }
+ ?: emptyList(),
+ view.getInheritedTagsList()
+ .filter { it.isNotEmpty() },
+ view.bookName,
+ Timestamp.from(
+ view.scheduledRangeString,
+ view.scheduledTimeTimestamp,
+ view.scheduledTimeString,
+ view.scheduledTimeEndString,
+ ),
+ Timestamp.from(
+ view.deadlineRangeString,
+ view.deadlineTimeTimestamp,
+ view.deadlineTimeString,
+ view.deadlineTimeEndString,
+ ),
+ Timestamp.from(
+ view.closedRangeString,
+ view.closedTimeTimestamp,
+ view.closedTimeString,
+ view.closedTimeEndString,
+ ),
+ note.priority,
+ note.state,
+ note.createdAt,
+ props.associate { it.name to it.value }
+ )
+ }
+
+ fun from(noteAndProps: Pair>) =
+ from(noteAndProps.first, noteAndProps.second)
+ }
+
+ data class Timestamp(
+ val rangeString: String,
+ val timeTimestamp: Long,
+ val timeString: String?,
+ val timeEndString: String? = null,
+ ) {
+ companion object {
+ fun from(
+ rangeString: String?,
+ timeTimestamp: Long?,
+ timeString: String?,
+ timeEndString: String?,
+ ): Timestamp? {
+ return if (rangeString != null && timeTimestamp != null)
+ Timestamp(rangeString, timeTimestamp, timeString, timeEndString)
+ else null
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/orgzly/android/external/types/Response.kt b/app/src/main/java/com/orgzly/android/external/types/Response.kt
new file mode 100644
index 000000000..41bf95860
--- /dev/null
+++ b/app/src/main/java/com/orgzly/android/external/types/Response.kt
@@ -0,0 +1,8 @@
+package com.orgzly.android.external.types
+
+import java.io.Serializable
+
+data class Response(val success: Boolean = true, val result: Any? = null) {
+ constructor(success: Boolean, result: List) :
+ this(success, result.toTypedArray())
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/orgzly/android/external/types/SavedSearch.kt b/app/src/main/java/com/orgzly/android/external/types/SavedSearch.kt
new file mode 100644
index 000000000..4200e3e5b
--- /dev/null
+++ b/app/src/main/java/com/orgzly/android/external/types/SavedSearch.kt
@@ -0,0 +1,8 @@
+package com.orgzly.android.external.types
+
+data class SavedSearch(val id: Long, val name: String, val position: Int, val query: String) {
+ companion object {
+ fun from(search: com.orgzly.android.db.entity.SavedSearch) =
+ SavedSearch(search.id, search.name, search.position, search.query)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/orgzly/android/git/GitFileSynchronizer.java b/app/src/main/java/com/orgzly/android/git/GitFileSynchronizer.java
index 43830fc2d..e976c95f2 100644
--- a/app/src/main/java/com/orgzly/android/git/GitFileSynchronizer.java
+++ b/app/src/main/java/com/orgzly/android/git/GitFileSynchronizer.java
@@ -38,6 +38,7 @@
public class GitFileSynchronizer {
private final static String TAG = GitFileSynchronizer.class.getName();
+ public final static String PRE_SYNC_MARKER_BRANCH = "orgzly-pre-sync-marker";
private final Git git;
private final GitPreferences preferences;
@@ -138,8 +139,8 @@ public boolean updateAndCommitFileFromRevisionAndMerge(
boolean mergeSucceeded = false;
try {
RevCommit mergeTarget = currentHead();
- // Try to use the branch "orgzly-pre-sync-marker" to find a good point for branching off.
- RevCommit branchStartPoint = getCommit("orgzly-pre-sync-marker");
+ // Try to use our "pre sync marker" to find a good point in history for branching off.
+ RevCommit branchStartPoint = getCommit(PRE_SYNC_MARKER_BRANCH);
if (branchStartPoint == null) {
branchStartPoint = revision;
}
@@ -295,13 +296,19 @@ public void setBranchAndGetLatest() throws IOException {
try {
// Point a "marker" branch to the current head, so that we know a good starting commit
// for merge conflict branches.
- git.branchCreate().setName("orgzly-pre-sync-marker").setForce(true).call();
+ git.branchCreate().setName(PRE_SYNC_MARKER_BRANCH).setForce(true).call();
} catch (GitAPIException e) {
- throw new IOException(context.getString(R.string.git_sync_error_failed_set_marker_branch));
+ // We may end up here when syncing an empty Git repo for the first time. So don't
+ // panic, just log an info message.
+ Log.i(TAG, context.getString(R.string.git_sync_error_failed_set_marker_branch));
}
fetch();
try {
RevCommit current = currentHead();
+ if (current == null) {
+ Log.i(TAG, "Git repo does not seem to have any commits.");
+ return;
+ }
RevCommit mergeTarget = getCommit(
String.format("%s/%s", preferences.remoteName(), git.getRepository().getBranch()));
if (mergeTarget != null) {
diff --git a/app/src/main/java/com/orgzly/android/git/GitSshKeyTransportSetter.kt b/app/src/main/java/com/orgzly/android/git/GitSshKeyTransportSetter.kt
index 551359066..b13e0565b 100644
--- a/app/src/main/java/com/orgzly/android/git/GitSshKeyTransportSetter.kt
+++ b/app/src/main/java/com/orgzly/android/git/GitSshKeyTransportSetter.kt
@@ -3,7 +3,6 @@ package com.orgzly.android.git
import android.os.Build
import androidx.annotation.RequiresApi
import com.orgzly.android.App
-import org.apache.sshd.common.util.OsUtils
import org.eclipse.jgit.annotations.NonNull
import org.eclipse.jgit.api.TransportCommand
import org.eclipse.jgit.api.TransportConfigCallback
@@ -38,7 +37,7 @@ class GitSshKeyTransportSetter: GitTransportSetter {
)
}
- @RequiresApi(Build.VERSION_CODES.M)
+ @RequiresApi(Build.VERSION_CODES.N)
override fun getDefaultKeys(@NonNull sshDir: File): Iterable? {
return if (SshKey.exists) {
listOf(SshKey.getKeyPair())
@@ -53,8 +52,6 @@ class GitSshKeyTransportSetter: GitTransportSetter {
// org.apache.sshd.common.config.keys.IdentityUtils freaks out if user.home is not set
System.setProperty("user.home", context.filesDir.toString())
- // org.apache.sshd.common.util.OsUtils has trouble recognizing Android
- OsUtils.setAndroid(true)
configCallback = TransportConfigCallback { transport: Transport ->
val sshTransport = transport as SshTransport
diff --git a/app/src/main/java/com/orgzly/android/git/SshKey.kt b/app/src/main/java/com/orgzly/android/git/SshKey.kt
index d7c9ed723..f8906b58e 100644
--- a/app/src/main/java/com/orgzly/android/git/SshKey.kt
+++ b/app/src/main/java/com/orgzly/android/git/SshKey.kt
@@ -5,10 +5,8 @@ import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import android.security.keystore.KeyGenParameterSpec
-import android.security.keystore.KeyInfo
import android.security.keystore.KeyProperties
import android.security.keystore.UserNotAuthenticatedException
-import android.util.Log
import androidx.annotation.RequiresApi
import androidx.core.content.edit
import androidx.security.crypto.EncryptedFile
@@ -22,6 +20,7 @@ import com.orgzly.android.util.BiometricAuthenticator
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
+import net.i2p.crypto.eddsa.EdDSAEngine
import net.i2p.crypto.eddsa.EdDSAPrivateKey
import net.i2p.crypto.eddsa.spec.EdDSANamedCurveTable
import net.i2p.crypto.eddsa.spec.EdDSAPrivateKeySpec
@@ -31,8 +30,6 @@ import java.io.File
import java.io.IOException
import java.security.*
import java.security.interfaces.RSAKey
-import javax.crypto.SecretKey
-import javax.crypto.SecretKeyFactory
private const val PROVIDER_ANDROID_KEY_STORE = "AndroidKeyStore"
private const val KEYSTORE_ALIAS = "orgzly_sshkey"
@@ -60,63 +57,29 @@ fun toSshPublicKey(publicKey: PublicKey): String {
return PublicKeyEntry.toString(publicKey)
}
+@RequiresApi(Build.VERSION_CODES.N)
object SshKey {
- private val TAG = SshKey::class.java.name
val sshPublicKey
get() = if (publicKeyFile.exists()) publicKeyFile.readText() else null
val canShowSshPublicKey
get() = type in listOf(Type.KeystoreNative, Type.KeystoreWrappedEd25519)
val exists
get() = type != null
- private val mustAuthenticate: Boolean
- @RequiresApi(Build.VERSION_CODES.M)
- get() {
- return runCatching {
- if (type !in listOf(Type.KeystoreNative, Type.KeystoreWrappedEd25519)) return false
- when (val key = androidKeystore.getKey(KEYSTORE_ALIAS, null)) {
- is PrivateKey -> {
- val factory =
- KeyFactory.getInstance(key.algorithm, PROVIDER_ANDROID_KEY_STORE)
- return factory.getKeySpec(
- key,
- KeyInfo::class.java
- ).isUserAuthenticationRequired
- }
- is SecretKey -> {
- val factory =
- SecretKeyFactory.getInstance(key.algorithm, PROVIDER_ANDROID_KEY_STORE)
- (factory.getKeySpec(
- key,
- KeyInfo::class.java
- ) as KeyInfo).isUserAuthenticationRequired
- }
- else -> throw IllegalStateException("SSH key does not exist in Keystore")
- }
- }
- .getOrElse {
- // It is fine to swallow the exception here since it will reappear when the key
- // is used for SSH authentication and can then be shown in the UI.
- false
- }
- }
-
private val context: Context
get() = App.getAppContext()
-
private val privateKeyFile
get() = File(context.filesDir, "ssh_key")
private val publicKeyFile
get() = File(context.filesDir, "ssh_key.pub")
-
private var type: Type?
get() = Type.fromValue(AppPreferences.gitSshKeyType(context))
set(value) = AppPreferences.gitSshKeyType(context, value?.value)
-
private val isStrongBoxSupported by unsafeLazy {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P)
context.packageManager.hasSystemFeature(PackageManager.FEATURE_STRONGBOX_KEYSTORE)
else false
}
+ private var privateKeyLoadAttempts = 0
private enum class Type(val value: String) {
KeystoreNative("keystore_native"),
@@ -128,7 +91,6 @@ object SshKey {
}
}
- @RequiresApi(Build.VERSION_CODES.M)
enum class Algorithm(
val algorithm: String,
val applyToSpec: KeyGenParameterSpec.Builder.() -> Unit
@@ -217,7 +179,6 @@ object SshKey {
type = Type.KeystoreWrappedEd25519
}
- @RequiresApi(Build.VERSION_CODES.M)
fun generateKeystoreNativeKey(algorithm: Algorithm, requireAuthentication: Boolean) {
delete()
@@ -248,10 +209,9 @@ object SshKey {
type = Type.KeystoreNative
}
- @RequiresApi(Build.VERSION_CODES.M)
fun getKeyPair(): KeyPair {
+ privateKeyLoadAttempts = 0
var privateKey: PrivateKey? = null
- var privateKeyLoadAttempts = 0
val publicKey: PublicKey? = when (type) {
Type.KeystoreNative -> {
kotlin.runCatching { androidKeystore.sshPublicKey }
@@ -282,73 +242,78 @@ object SshKey {
}
}
Type.KeystoreWrappedEd25519 -> {
- runCatching {
- // The current MasterKey API does not allow getting a reference to an existing
- // one without specifying the KeySpec for a new one. However, the value for
- // passed here for `requireAuthentication` is not used as the key already exists
- // at this point.
- val encryptedPrivateKeyFile = runBlocking {
- getOrCreateWrappedPrivateKeyFile(false)
- }
- val rawPrivateKey =
- encryptedPrivateKeyFile.openFileInput().use { it.readBytes() }
- EdDSAPrivateKey(
- EdDSAPrivateKeySpec(
- rawPrivateKey, EdDSANamedCurveTable.ED_25519_CURVE_SPEC
- )
- )
- }.getOrElse { error ->
- throw IOException(context.getString(R.string.ssh_key_failed_get_private), error)
+ try {
+ tryToReadEd25519PrivateKey()
+ } catch (e: UserNotAuthenticatedException) {
+ tryBiometricAuthOrFail(e)
+ tryToReadEd25519PrivateKey()
+ } catch (e: Exception) {
+ throw IOException(context.getString(R.string.ssh_key_failed_get_private), e)
}
}
else -> throw IllegalStateException("SSH key does not exist in Keystore")
}
try {
// Try to sign something to see if the key is unlocked
- val algorithm: String = if (privateKey is RSAKey) {
- "SHA256withRSA"
- } else {
- "SHA256withECDSA"
+ val signature = when (privateKey) {
+ is EdDSAPrivateKey -> EdDSAEngine(MessageDigest.getInstance(
+ EdDSANamedCurveTable.getByName(EdDSANamedCurveTable.ED_25519).hashAlgorithm))
+ is RSAKey -> Signature.getInstance("SHA256withRSA")
+ else -> Signature.getInstance("SHA256withECDSA")
}
- Signature.getInstance(algorithm).apply {
+ signature.apply {
initSign(privateKey)
update("loremipsum".toByteArray())
}.sign()
// The key is unlocked; exit the loop.
break
} catch (e: UserNotAuthenticatedException) {
- if (privateKeyLoadAttempts > 0) {
- // We expect this exception before trying auth, but after that, this means
- // we have failed to unlock the SSH key.
- Log.e(TAG, context.getString(R.string.ssh_key_failed_unlock_private), e)
- throw IOException(context.getString(R.string.ssh_key_failed_unlock_private))
- }
+ tryBiometricAuthOrFail(e)
} catch (e: Exception) {
- // Our attempt to use the key for signing may go wrong in many unforeseen ways.
- // Such failures are unimportant.
- e.printStackTrace()
+ throw e
}
- if (mustAuthenticate && privateKeyLoadAttempts == 0) {
- // Time to try biometric auth
- val currentActivity = App.getCurrentActivity()
- checkNotNull(currentActivity) {
- throw IOException(context.getString(R.string.ssh_key_locked_and_no_activity))
- }
- val biometricAuthenticator = BiometricAuthenticator(currentActivity)
- runBlocking(Dispatchers.Main) {
- biometricAuthenticator.authenticate(
- context.getString(
- R.string.biometric_prompt_title_unlock_ssh_key
- )
+ }
+ return KeyPair(publicKey, privateKey)
+ }
+
+ private fun tryToReadEd25519PrivateKey(): EdDSAPrivateKey {
+ // The current MasterKey API does not allow getting a reference to an existing
+ // one without specifying the KeySpec for a new one. However, the value for
+ // passed here for `requireAuthentication` is not used as the key already exists
+ // at this point.
+ val encryptedPrivateKeyFile = runBlocking {
+ getOrCreateWrappedPrivateKeyFile(false)
+ }
+ val rawPrivateKey =
+ encryptedPrivateKeyFile.openFileInput().use { it.readBytes() }
+ return EdDSAPrivateKey(
+ EdDSAPrivateKeySpec(
+ rawPrivateKey, EdDSANamedCurveTable.ED_25519_CURVE_SPEC
+ )
+ )
+ }
+
+ private fun tryBiometricAuthOrFail(e: UserNotAuthenticatedException) {
+ if (privateKeyLoadAttempts == 0) {
+ val currentActivity = App.getCurrentActivity()
+ checkNotNull(currentActivity) {
+ throw IOException(context.getString(R.string.ssh_key_locked_and_no_activity))
+ }
+ val biometricAuthenticator = BiometricAuthenticator(currentActivity)
+ runBlocking(Dispatchers.Main) {
+ biometricAuthenticator.authenticate(
+ context.getString(
+ R.string.biometric_prompt_title_unlock_ssh_key
)
- }
+ )
}
privateKeyLoadAttempts++
+ } else {
+ throw e
}
- return KeyPair(publicKey, privateKey)
}
- @RequiresApi(Build.VERSION_CODES.M)
+ @RequiresApi(Build.VERSION_CODES.N)
fun promptForKeyGeneration() {
val activity = App.getCurrentActivity()
if (activity != null) {
diff --git a/app/src/main/java/com/orgzly/android/repos/GitRepo.java b/app/src/main/java/com/orgzly/android/repos/GitRepo.java
index 652c89c98..09634a242 100644
--- a/app/src/main/java/com/orgzly/android/repos/GitRepo.java
+++ b/app/src/main/java/com/orgzly/android/repos/GitRepo.java
@@ -1,10 +1,14 @@
package com.orgzly.android.repos;
+import static com.orgzly.android.git.GitFileSynchronizer.PRE_SYNC_MARKER_BRANCH;
+
import android.content.Context;
import android.net.Uri;
import android.util.Log;
import com.orgzly.BuildConfig;
+import com.orgzly.R;
+import com.orgzly.android.App;
import com.orgzly.android.BookName;
import com.orgzly.android.db.entity.Repo;
import com.orgzly.android.git.GitFileSynchronizer;
@@ -40,6 +44,7 @@
public class GitRepo implements SyncRepo, TwoWaySyncRepo {
private final static String TAG = GitRepo.class.getName();
private final long repoId;
+ private final Context context = App.getAppContext();
/**
* Used as cause when we try to clone into a non-empty directory
@@ -158,7 +163,6 @@ private static Git cloneRepo(Uri repoUri, File directoryFile, GitTransportSetter
} catch (IOException ex) {
ex.printStackTrace();
}
- Log.e(TAG, "JGit error:", e);
throw new IOException(e);
}
}
@@ -185,6 +189,8 @@ public boolean isAutoSyncSupported() {
public VersionedRook storeBook(File file, String fileName) throws IOException {
File destination = synchronizer.repoDirectoryFile(fileName);
+ ensureRepoPathIsNotIgnored(destination.getPath());
+
if (destination.exists()) {
synchronizer.updateAndCommitExistingFile(file, fileName);
} else {
@@ -230,7 +236,7 @@ private VersionedRook currentVersionedRook(Uri uri) {
private IgnoreNode getIgnores() throws IOException {
IgnoreNode ignores = new IgnoreNode();
- File ignoreFile = synchronizer.repoDirectoryFile(".orgzlyignore");
+ File ignoreFile = synchronizer.repoDirectoryFile(context.getString(R.string.repo_ignore_rules_file));
if (ignoreFile.exists()) {
FileInputStream in = new FileInputStream(ignoreFile);
try {
@@ -242,11 +248,24 @@ private IgnoreNode getIgnores() throws IOException {
return ignores;
}
+ private void ensureRepoPathIsNotIgnored(String filePath) throws IOException {
+ IgnoreNode ignores = getIgnores();
+ if (ignores.isIgnored(filePath, false) == IgnoreNode.MatchResult.IGNORED) {
+ throw new IOException(context.getString(R.string.error_file_matches_repo_ignore_rule,
+ context.getString(R.string.repo_ignore_rules_file)));
+ }
+ }
+
public boolean isUnchanged() throws IOException {
// Check if the current head is unchanged.
// If so, we can read all the VersionedRooks from the database.
synchronizer.setBranchAndGetLatest();
- return synchronizer.currentHead().equals(synchronizer.getCommit("orgzly-pre-sync-marker"));
+ // If current HEAD is null, there are no commits, and this means there are no remote
+ // changes to handle.
+ if (synchronizer.currentHead() == null) return true;
+ if (synchronizer.currentHead().equals(synchronizer.getCommit(PRE_SYNC_MARKER_BRANCH)))
+ return true;
+ return false;
}
public List getBooks() throws IOException {
@@ -304,6 +323,7 @@ public void delete(Uri uri) throws IOException {
public VersionedRook renameBook(Uri oldUri, String newRookName) throws IOException {
String oldFileName = oldUri.toString().replaceFirst("^/", "");
String newFileName = newRookName + ".org";
+ ensureRepoPathIsNotIgnored(newFileName);
if (synchronizer.renameFileInRepo(oldFileName, newFileName)) {
synchronizer.tryPush();
return currentVersionedRook(Uri.EMPTY.buildUpon().appendPath(newFileName).build());
diff --git a/app/src/main/java/com/orgzly/android/sync/SyncWorker.kt b/app/src/main/java/com/orgzly/android/sync/SyncWorker.kt
index 9ccb8bc34..e06428a94 100644
--- a/app/src/main/java/com/orgzly/android/sync/SyncWorker.kt
+++ b/app/src/main/java/com/orgzly/android/sync/SyncWorker.kt
@@ -9,6 +9,7 @@ import com.orgzly.R
import com.orgzly.android.App
import com.orgzly.android.SharingShortcutsManager
import com.orgzly.android.data.DataRepository
+import com.orgzly.android.data.logs.AppLogsRepository
import com.orgzly.android.db.entity.BookAction
import com.orgzly.android.prefs.AppPreferences
import com.orgzly.android.reminders.RemindersScheduler
@@ -16,6 +17,7 @@ import com.orgzly.android.repos.*
import com.orgzly.android.ui.notifications.SyncNotifications
import com.orgzly.android.ui.util.haveNetworkConnection
import com.orgzly.android.util.AppPermissions
+import com.orgzly.android.util.LogMajorEvents
import com.orgzly.android.util.LogUtils
import com.orgzly.android.widgets.ListWidgetProvider
import kotlinx.coroutines.Dispatchers
@@ -29,6 +31,9 @@ class SyncWorker(val context: Context, val params: WorkerParameters) :
@Inject
lateinit var dataRepository: DataRepository
+ @Inject
+ lateinit var appLogs: AppLogsRepository
+
override suspend fun doWork(): Result {
App.appComponent.inject(this)
@@ -84,15 +89,28 @@ class SyncWorker(val context: Context, val params: WorkerParameters) :
checkConditions()?.let { return it }
+ val syncStartTime = System.currentTimeMillis()
+
syncRepos()?.let { return it }
RemindersScheduler.notifyDataSetChanged(App.getAppContext())
ListWidgetProvider.notifyDataSetChanged(App.getAppContext())
SharingShortcutsManager().replaceDynamicShortcuts(App.getAppContext())
+ val syncEndTime = System.currentTimeMillis()
+
// Save last successful sync time to preferences
- val time = System.currentTimeMillis()
- AppPreferences.lastSuccessfulSyncTime(context, time)
+ AppPreferences.lastSuccessfulSyncTime(context, syncEndTime)
+
+ if (LogMajorEvents.isEnabled()) {
+ val syncDuration = (syncEndTime - syncStartTime)
+ val numberOfRepos = dataRepository.getRepos().size
+ val numberOfBooks = dataRepository.getBooks().size
+ appLogs.log(
+ LogMajorEvents.SYNC,
+ "Sync took $syncDuration milliseconds. Synced $numberOfBooks books in $numberOfRepos repos."
+ )
+ }
return SyncState.getInstance(SyncState.Type.FINISHED)
}
@@ -275,4 +293,4 @@ class SyncWorker(val context: Context, val params: WorkerParameters) :
companion object {
private val TAG: String = SyncWorker::class.java.name
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/orgzly/android/ui/SshKeygenActivity.kt b/app/src/main/java/com/orgzly/android/ui/SshKeygenActivity.kt
index 5f0eeb2fc..aeb446577 100644
--- a/app/src/main/java/com/orgzly/android/ui/SshKeygenActivity.kt
+++ b/app/src/main/java/com/orgzly/android/ui/SshKeygenActivity.kt
@@ -21,7 +21,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
-@RequiresApi(Build.VERSION_CODES.M)
+@RequiresApi(Build.VERSION_CODES.N)
private enum class KeyGenType(val generateKey: suspend (requireAuthentication: Boolean) -> Unit) {
Rsa({ requireAuthentication ->
SshKey.generateKeystoreNativeKey(SshKey.Algorithm.Rsa, requireAuthentication)
@@ -34,7 +34,7 @@ private enum class KeyGenType(val generateKey: suspend (requireAuthentication: B
}),
}
-@RequiresApi(Build.VERSION_CODES.M)
+@RequiresApi(Build.VERSION_CODES.N)
class SshKeygenActivity : CommonActivity() {
private var keyGenType = KeyGenType.Ecdsa
@@ -84,7 +84,7 @@ class SshKeygenActivity : CommonActivity() {
}
}
val keyguardManager: KeyguardManager = getSystemService(KEYGUARD_SERVICE) as KeyguardManager
- keyRequireAuthentication.isEnabled = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
+ keyRequireAuthentication.isEnabled = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
false
} else {
keyguardManager.isDeviceSecure
diff --git a/app/src/main/java/com/orgzly/android/ui/logs/AppLogsActivity.kt b/app/src/main/java/com/orgzly/android/ui/logs/AppLogsActivity.kt
index bb503d83d..ebd223d20 100644
--- a/app/src/main/java/com/orgzly/android/ui/logs/AppLogsActivity.kt
+++ b/app/src/main/java/com/orgzly/android/ui/logs/AppLogsActivity.kt
@@ -16,6 +16,7 @@ import com.orgzly.android.ui.util.getAlarmManager
import com.orgzly.android.ui.util.sharePlainText
import com.orgzly.android.ui.util.userFriendlyPeriod
import com.orgzly.databinding.ActivityLogsBinding
+import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import org.joda.time.DateTime
import javax.inject.Inject
diff --git a/app/src/main/java/com/orgzly/android/ui/logs/AppLogsViewModel.kt b/app/src/main/java/com/orgzly/android/ui/logs/AppLogsViewModel.kt
index 385bc3d46..0208ec83c 100644
--- a/app/src/main/java/com/orgzly/android/ui/logs/AppLogsViewModel.kt
+++ b/app/src/main/java/com/orgzly/android/ui/logs/AppLogsViewModel.kt
@@ -7,7 +7,7 @@ import kotlinx.coroutines.flow.map
import java.util.*
class AppLogsViewModel(appLogsRepository: AppLogsRepository) : CommonViewModel() {
- val logs = appLogsRepository.getFlow(LogMajorEvents.REMINDERS).map {
+ val logs = appLogsRepository.getFlow().map {
it.map { logEntry ->
val date = Date(logEntry.time)
val type = logEntry.type
diff --git a/app/src/main/java/com/orgzly/android/ui/repo/git/GitRepoActivity.kt b/app/src/main/java/com/orgzly/android/ui/repo/git/GitRepoActivity.kt
index efeea20f9..0493289d4 100644
--- a/app/src/main/java/com/orgzly/android/ui/repo/git/GitRepoActivity.kt
+++ b/app/src/main/java/com/orgzly/android/ui/repo/git/GitRepoActivity.kt
@@ -3,6 +3,7 @@ package com.orgzly.android.ui.repo.git
import android.app.Activity
import android.app.ProgressDialog
+import android.content.DialogInterface
import android.content.Intent
import android.content.SharedPreferences
import android.net.Uri
@@ -21,6 +22,7 @@ import android.widget.EditText
import androidx.annotation.RequiresApi
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.textfield.TextInputLayout
import com.orgzly.R
import com.orgzly.android.App
@@ -38,15 +40,14 @@ import com.orgzly.android.ui.repo.BrowserActivity
import com.orgzly.android.ui.repo.RepoViewModel
import com.orgzly.android.ui.repo.RepoViewModelFactory
import com.orgzly.android.ui.showSnackbar
+import com.orgzly.android.ui.util.copyPlainTextToClipboard
import com.orgzly.android.util.AppPermissions
import com.orgzly.android.util.MiscUtils
import com.orgzly.databinding.ActivityRepoGitBinding
-import org.eclipse.jgit.errors.TransportException
import org.eclipse.jgit.errors.NoRemoteRepositoryException
import org.eclipse.jgit.errors.NotSupportedException
import org.eclipse.jgit.lib.ProgressMonitor
import java.io.File
-import java.io.FileNotFoundException
import java.io.IOException
class GitRepoActivity : CommonActivity(), GitPreferences {
@@ -124,10 +125,14 @@ class GitRepoActivity : CommonActivity(), GitPreferences {
createDefaultRepoFolder()
binding.activityRepoGitAuthor.setText("Orgzly")
binding.activityRepoGitBranch.setText(R.string.git_default_branch)
- val userDeviceName: String = if (Build.VERSION.SDK_INT > Build.VERSION_CODES.S) {
- Settings.Global.getString(contentResolver, Settings.Global.DEVICE_NAME)
- } else {
- Settings.Secure.getString(contentResolver, "bluetooth_name")
+ val userDeviceName: String = try {
+ if (Build.VERSION.SDK_INT > Build.VERSION_CODES.S) {
+ Settings.Global.getString(contentResolver, Settings.Global.DEVICE_NAME)
+ } else {
+ Settings.Secure.getString(contentResolver, "bluetooth_name")
+ }
+ } catch (e: Exception) {
+ "MyPhone"
}
binding.activityRepoGitEmail.setText(String.format("orgzly@%s", userDeviceName))
}
@@ -275,12 +280,18 @@ class GitRepoActivity : CommonActivity(), GitPreferences {
save()
} else {
val targetDirectory = File(binding.activityRepoGitDirectory.text.toString())
+ if (!targetDirectory.exists()) {
+ binding.activityRepoGitDirectoryLayout.error =
+ getString(R.string.git_clone_error_invalid_target_dir)
+ return
+ }
if (targetDirectory.list()!!.isNotEmpty()) {
- binding.activityRepoGitDirectoryLayout.error = getString(R.string.git_clone_error_target_not_empty)
+ binding.activityRepoGitDirectoryLayout.error =
+ getString(R.string.git_clone_error_target_not_empty)
return
}
val repoScheme = getRepoScheme()
- @RequiresApi(Build.VERSION_CODES.M)
+ @RequiresApi(Build.VERSION_CODES.N)
if (repoScheme != "https" && !SshKey.exists) {
SshKey.promptForKeyGeneration()
return
@@ -295,28 +306,28 @@ class GitRepoActivity : CommonActivity(), GitPreferences {
if (e == null) {
save()
} else {
- val error = when (e.cause) {
- is NoRemoteRepositoryException -> R.string.git_clone_error_invalid_repo
- is TransportException -> {
- // JGit's catch-all "remote hung up unexpectedly" message is not very useful.
- if (Regex("hung up unexpectedly").containsMatchIn(e.cause!!.message!!)) {
- String.format(getString(R.string.git_clone_error_ssh), e.cause!!.cause!!.message)
- } else {
- String.format(getString(R.string.git_clone_error_ssh), e.cause!!.message)
- }
- }
- // TODO: This should be checked when the user enters a directory by hand
- is FileNotFoundException -> R.string.git_clone_error_invalid_target_dir
- is GitRepo.DirectoryNotEmpty -> R.string.git_clone_error_target_not_empty
- is NotSupportedException -> R.string.git_clone_error_uri_not_supported
- else -> R.string.git_clone_error_unknown
- }
- when (error) {
- is Int -> { showSnackbar(error) }
- is String -> { showSnackbar(error) }
+ val error = when (val rootException = getRootException(e)) {
+ is NoRemoteRepositoryException -> getString(R.string.git_clone_error_invalid_repo)
+ is NotSupportedException -> getString(R.string.git_clone_error_uri_not_supported)
+ else -> rootException.localizedMessage?.toString()
}
- e.printStackTrace()
+ MaterialAlertDialogBuilder(this)
+ .setPositiveButton(R.string.ok, null)
+ .setNeutralButton("Copy stack trace") { _: DialogInterface?, _: Int ->
+ this.copyPlainTextToClipboard("Error during cloning", e.stackTraceToString())
+ }
+ .setMessage(error)
+ .show()
+ Log.e(TAG, "Error during repo cloning:", e)
+ }
+ }
+
+ private fun getRootException(e: Throwable): Throwable {
+ var result = e
+ while (result.cause != null) {
+ result = result.cause as Throwable
}
+ return result
}
private fun saveToPreferences(id: Long): Boolean {
@@ -355,7 +366,7 @@ class GitRepoActivity : CommonActivity(), GitPreferences {
for (field in fields) {
if (field.layout.visibility == View.GONE || field.allowEmpty) {
- continue;
+ continue
}
if (errorIfEmpty(field.editText, field.layout)) {
hasEmptyFields = true
@@ -472,10 +483,8 @@ class GitRepoActivity : CommonActivity(), GitPreferences {
try {
GitRepo.ensureRepositoryExists(fragment, true, this)
} catch (e: IOException) {
- Log.e(TAG, "Error while cloning Git repository:", e)
return e
}
-
return null
}
@@ -520,10 +529,6 @@ class GitRepoActivity : CommonActivity(), GitPreferences {
override fun endTask() {
}
-
- override fun showDuration(enabled: Boolean) {
- TODO("Not yet implemented")
- }
}
companion object {
diff --git a/app/src/main/java/com/orgzly/android/ui/settings/SettingsFragment.kt b/app/src/main/java/com/orgzly/android/ui/settings/SettingsFragment.kt
index 6556ec27a..838aee7b0 100644
--- a/app/src/main/java/com/orgzly/android/ui/settings/SettingsFragment.kt
+++ b/app/src/main/java/com/orgzly/android/ui/settings/SettingsFragment.kt
@@ -100,7 +100,7 @@ class SettingsFragment : PreferenceFragmentCompat(), SharedPreferences.OnSharedP
}
// Disable Git repos completely on API < 23
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
preference(R.string.pref_key_git_is_enabled)?.let {
preferenceScreen.removePreference(it)
}
diff --git a/app/src/main/java/com/orgzly/android/util/LogMajorEvents.kt b/app/src/main/java/com/orgzly/android/util/LogMajorEvents.kt
index 7418af14d..2c47910b3 100644
--- a/app/src/main/java/com/orgzly/android/util/LogMajorEvents.kt
+++ b/app/src/main/java/com/orgzly/android/util/LogMajorEvents.kt
@@ -13,6 +13,7 @@ class LogMajorEvents {
companion object {
const val REMINDERS = "reminders"
+ const val SYNC = "sync"
fun isEnabled(): Boolean {
return AppPreferences.logMajorEvents(App.getAppContext())
diff --git a/app/src/main/java/com/orgzly/android/widgets/ListWidgetProvider.java b/app/src/main/java/com/orgzly/android/widgets/ListWidgetProvider.java
index 259924ae1..a6274ac51 100644
--- a/app/src/main/java/com/orgzly/android/widgets/ListWidgetProvider.java
+++ b/app/src/main/java/com/orgzly/android/widgets/ListWidgetProvider.java
@@ -230,6 +230,10 @@ private void setSelectionFromIntent(Context context, Intent intent) {
}
private SavedSearch getSavedSearch(Context context, int appWidgetId) {
+ return getSavedSearch(context, appWidgetId, dataRepository);
+ }
+
+ public static SavedSearch getSavedSearch(Context context, int appWidgetId, DataRepository dataRepository) {
long filterId = context.getSharedPreferences(PREFERENCES_ID, Context.MODE_PRIVATE).getLong(getFilterPreferenceKey(appWidgetId), -1);
SavedSearch savedSearch = null;
@@ -253,7 +257,7 @@ private void setFilter(Context context, int appWidgetId, long id) {
editor.apply();
}
- private String getFilterPreferenceKey(int appWidgetId) {
+ private static String getFilterPreferenceKey(int appWidgetId) {
return "widget-filter-" + appWidgetId;
}
diff --git a/app/src/main/res/layout/dialog_whats_new.xml b/app/src/main/res/layout/dialog_whats_new.xml
index 517655969..1ab1983f8 100644
--- a/app/src/main/res/layout/dialog_whats_new.xml
+++ b/app/src/main/res/layout/dialog_whats_new.xml
@@ -20,6 +20,71 @@
android:layout_height="wrap_content" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+SCHEDULED: <2024-01-10 Çrş 22:09>
-Belirlenen zamanlara tekrarlayıcı ekleyebilirsiniz
-SCHEDULED: <2015-02-16 Pzt. .+1d>
+Belirlenen zamanlara tekrarlayıcı eklenebilir
+SCHEDULED: <2024-01-10 Çrş .+3d>
-** Notlara son teslim tarihi de ekleyebilirsiniz
-DEADLINE: <2015-02-20 Cuma>
+** Notlara bitiş tarihi de eklenebilir
+DEADLINE: <2024-01-10 Çrş>
-** Belirlenen zamanlar ve son teslim tarihleri için hatırlatıcılar desteklenmektedir
+** Planlamalar ve bitiş tarihleri için hatırlatma desteği
-Hatırlatıcılar başlangıçta devredışı olacak şekilde tanımlandılar ve ayarlardan etkinleştirilebilinirler.
+Varsayılan olarak devre dışıdırlar ve Ayarlar'da etkinleştirilmeleri gerekir.
** [#A] Notların öncelikleri olabilir
-Önceliklerin numarasını ayarlardan değiştirebilirsiniz. You can also change default priority - assumed priority for notes without one set.
+Önceliklerin numarasını ayarlardan değiştirebilirsiniz. Ayrıca varsayılan önceliği değiştirebilirsiniz - tek notlar için varsayılan öncelik.
** Not bağlantılar içerebilir
-Bir telefon numarası çevirin (tel:555-0199), SMS gönderin (sms:555-0199), e-posta oluşturun (mailto:support@orgzly.com) veya bir web sayfasını ziyaret edin ([[https://www.orgzly.com][Orgzly.com]]).
+Bir telefon numarası çevirin (tel:555-0199), SMS gönderin (sms:555-0199), e-posta oluşturun (mailto:support@orgzly.com) veya bir web sayfasını ziyaret edin ([[http://www.orgzly.com][Orgzly.com]]).
-Ayrıca uygulama içinde başka bir nota veya not defterine bağlantı verebilirsiniz.
+Ayrıca uygulama içindeki başka bir nota veya deftere bağlantı verilebilir.
-Daha fazla bilgi için https://www.orgzly.com/help#links adresine bakın.
+Daha fazla bilgi için http://www.orgzly.com/help#links adresine bakın.
-** Temel yazı karakteri önemi desteklenmektedir
+** Temel tipografik vurgu desteklenmekte
-Kelimeleri * kalın *, / italik /, _altıçizili_, = kelimesi kelimesine =, ~ kod ~ ve + doğrudan vurgulama + yapabilirsiniz.
+Kelimeler *kalın*, /italik/, _altı çizili_, =verbatim=, ~kod~ ve +üstü çizili+ yapılabilir.
** Onay kutusu listesi kullanılabilir
@@ -73,22 +73,22 @@ Değiştirmek için onay kutusunu tıklayın. Yeni bir öğe oluşturmak için s
* Arama
** Birçok arama operatörü desteklenmektedir
-Notları sınıfına, etiketine, zamanlanmış veya son teslim zamanı vb. Göre arayabilirsiniz.
+Notlar durumlara, etiketlere, planlanma durumuna veya bitiş tarihlerine vb. göre aranabilir.
-Daha fazlası için https://www.orgzly.com/help#search siteyi görün.
+Daha fazla bilgi edinmek için http://www.orgzly.com/help#search adresine göz at.
-** Arama sorguları hızlı sorgu için kaydedilebilir
+** Hızlı erişim için arama sorguları kaydedilebilir
-Gezinme penceresinden örnek aramaları deneyin ve kullandıkları soruları not edin.
+Gezinme çekmecesinden örnek aramaları dene ve kullandıkları sorguları not et.
-Gezinme penceresinde bulunan "Aramalar" ı tıklayarak kendi kaydedilmiş aramalarınızı oluşturabilirsiniz.
+Gezinme çekmecesindeki "Aramalar"ı tıklayarak kendi kayıtlı aramalarını oluşturabilirsin.
* Senkronizasyon
-** Dizüstü bilgisayarlar düz metin dosyaları olarak kaydedilebilir
+** Defterler düz metin dosyaları olarak kaydedilebilir
-Dosyalar "Org mode" tarafından kullanılmış formatları içerir.
+Dosyalar “Org mod” tarafından kullanılan formattadır.
** Konum (depo) türü
-Dizüstü bilgisayarlarınızı mobil cihazınızdaki, SD kartınızdaki veya Dropbox'taki bir dizinde senkronize şekilde tutabilirsiniz.
+Defterleri mobil cihazındaki bir dizine, SD kartına veya Dropbox'a eşitleyebilirsin.
diff --git a/app/src/main/res/values-tr-rTR/strings.xml b/app/src/main/res/values-tr-rTR/strings.xml
index ec96bf892..bab4ddb12 100644
--- a/app/src/main/res/values-tr-rTR/strings.xml
+++ b/app/src/main/res/values-tr-rTR/strings.xml
@@ -18,9 +18,9 @@
Kaydet
Vazgeç
Bağlantıyı kaldır
- Araştır
+ Gözat
Dropbox
- Gör
+ Görüntüle
Düzenle
Notu düzenle
Sil
@@ -29,23 +29,23 @@
Sorgu
Uygula
Tamamlandı olarak işaretle
- %s \'yi yeniden planlayın
+ Yeniden planla %s
Ertele
Kapat
- İçeri al
+ İçe aktar
- Dşarı Aktar
+ Dışa aktar
Üzerine yaz
- İçeri alındı
- Oluştur
- Durum
+ İçe aktarıldı
+ Oluşturuldu
+ Durumlar
Not
Notlar
Özellik
Olarak içe aktar…
- Yeni Not
- Yeni Dosya
- Planlama
+ Yeni defter
+ Yeni dosya
+ Zamanlandı
Gündem
Sabah gündemi
Zaman
@@ -60,7 +60,7 @@
Etkinlik
- Notu kes
- - Kes %d notları
+ - %d notu kes
Notlar kesilmedi
@@ -71,32 +71,32 @@
- Not yapıştırıldı
- Yapıştırıldı %d notlar
- Yapışacak bir şey yok
+ Yapıştırılacak birşey yok
Başlık
- etiket tag
- Org dosyasını içeri al
- Dropbox\'tan kaldırıldı
+ etiket etiket
+ Org dosyasını içeri aktar
+ Dropbox bağlantısı kaldırıldı
Dropbox\'a başarıyla bağlandı
Depolar
- Defterinizin senkronize edilmesi için gereken konum
- Bağlantı yok.
- Not Oluşturuldu
- Not oluşturma başarısız oldu
- Not güncelleme işlemi başarısız oldu
- Çekmeyi kapa
+ Defterlerin senkronize edilmesi için gereken konum
+ Bağlantı yok
+ Not oluşturuldu
+ Not oluşturulamadı
+ Not güncellenemedi
+ Çekmeceyi kapat
Çekmeyi aç
- Planlama
- Refile
- Refile to
- Son tarih
+ Zamanla
+ Yerleştir
+ Yeniden dosyala
+ Bitiş tarihi
Kapalı
- Zaman Planla
+ Planlanan zaman
Bitiş tarihi
- Zamanı Kapa
- Not İçeriği
- Yeniden Adlandır
+ Kapanış saati
+ Not içeriği
+ Yeniden adlandır
Bağlantı kur
Kaydetmeye Zorla
Yüklemeye Zorla
@@ -104,82 +104,82 @@
Kes
Kopyala
Yapıştır
- Üstüne Yapıştır
- Altına Yapıştır
- En Alta Yapıştır
- Tanıt
+ Üstüne yapıştır
+ Altına yapıştır
+ En alta yapıştır
+ Yükselt
Aşağı
Yukarı
Düşür
Yeni not
Hızlı not
- Yeni bir not oluşturmak için dokunun
+ Yeni bir not oluşturmak için dokun
Arama
Henüz yapılandırılmadı
Yeni Depo
- NotDefteri %s zaten var
+ %s defteri zaten mevcut
Aynı isim ile arama zaten var
- Dizin %s oluşturulamadı.
- Ne\ler yeni
- Notları ara
- Veritabanını Temizle
- Yerel veritabanını temizle?
- Yerel veritabanını temizle
- Veritabanı temizlendi
+ %s dizini oluşturulamadı
+ Yenilikler
+ Notlarda ara
+ Veri tabanını temizle
+ Yerel veri tabanı temizlensin mi?
+ Yerel veri tabanını temizle
+ Veri tabanı temizlendi
Orgzly\'ye Başlarken
\"Orgzly\'ye Başlarken\" öğreticisini içeri aktar
- Not defterinin son sürümünü içe aktarın
+ Defterin son halini içe aktar
Defter içe aktarıldı
- Bu defter artık yok.
+ Bu defter artık mevcut değil.
Bu not artık mevcut değil.
Bu arama artık mevcut değil.
- Arama
- Dropbox\'a bağlantı
- Dropbox ile bağlantısını kaldır
+ Ara
+ Dropbox bağlantısı
+ Dropbox bağlantısını kaldır
Ayarlar
Tercih notları etkiler
- Değişiklikleri yansıtacak notları güncelleme
+ Değişiklikleri yansıtacak şekilde notlar güncellensin mi?
Notlar güncelleniyor…
- Ah ah
- Aramanız herhangi bir notla eşleşmedi
+ Tüh
+ Sorgu hiçbir bir notla eşleşmedi
Bugün
Yarın
Gelecek Hafta
Etiketler
- Note Defteri Düzenleme Zamanı
- Not Defteri Bağlantı
- Not Defteri Bağlantı ayrıntıları
- Not Defteri son eylem
+ Defter değiştirilme zamanı
+ Not defteri bağlantısı
+ Not defteri bağlantı ayrıntıları
+ Defter son eylemi
Not sayısı
- Not Defterleri
- Not defteri
- Not Defterleri
+ Notlar & Defterler
+ Defter
+ Defterler
Aramalar
Arama
Yeni Arama
- Birden fazla depo var
- Depolamalar yapılandırılmadı
- No notebooks found
- NotDefter\'leri kodlamaları
- Collecting notebooks…
+ Birden fazla depo mevcut
+ Depolar yapılandırılmadı
+ Defter bulunamadı
+ Defter kodlamaları
+ Defterler getiriliyor…
Senkronize ediliyor…
- Senkronize ediliyor %s…
+ %s senkronize ediliyor…
Senkronize: %s
Senkronizasyon başarısız
İptal edildi
İptal ediliyor…
Dizin
- Dropbox içindeki Dizin (Örneğin. “/Documents/Orgzly”)
- URI dizini
+ Dropbox içindeki dizin (Örn. “/Belgeler/Orgzly”)
+ Dizin URI
Depo klonlanıyor
- Ensuring repository settings will work.
- Uzak Bağlantı (ör. git@github.com:orgzly/orgzly-android.git)
- Dizin konumu (ör. \"/sdcard/orgzly\")
+ Depo ayarlarının çalışacağından emin ol.
+ Uzak Bağlantı (örn. git@github.com:orgzly/orgzly-android.git)
+ Dizin konumu (örn. \"/sdcard/orgzly\")
Kullanıcı adı
Parola
- Yazar (ör. Sarı Çizmeli Mehmet)
- Eposta (ör. saricizmelimehmet@gmail.com)
- Dal (ör. master)
+ Yazar (örn. Sarı Çizmeli Mehmet)
+ E-posta (örn. saricizmelimehmet@email.com)
+ Dal (örn. master)
Kullanıcı adı
Parola
Anahtar çifti oluştur
@@ -195,139 +195,139 @@
Hedef dizin mevcut değil
Hedef dizin boş değil
Uzak bağlantı adresi desteklenmiyor
- Bağlantı (ör. webdavs://ornek.com/org-dosyalarim/)
+ Bağlantı (örn. webdavs://ornek.com/org-dosyalarim/)
Kullanıcı adı
Parola
Kullanıcı adı veya parola hatalı
Bilinmeyen bir hata oluştu
- Paylaşma ayarlı değil
- Paylaşma %s Desteklenmiyor
+ Paylaşma işlemi ayarlanmadı
+ %s paylaşım eylemi desteklenmiyor
Paylaşım türü ayarlanmadı
- Paylaşım tür %s Desteklenmiyor
- Depolamalar
- Repo geçersiz: %s
- Geçersiz depo URL “%s”
+ %s paylaşım türü desteklenmiyor
+ Depolar
+ Depo geçersiz: %s
+ Geçersiz depo bağlantısı “%s”
Bu bağlantıya sahip bir depo zaten var
- Sol çekme
- Çekme Öğesi
+ Sol çekmece
+ Çekmece öğesi
Önceki Durum
Sonraki Durum
- Yeni üstüne
- Yeni altına
- Yeni aşağı
+ Üstüne yeni
+ Altına yeni
+ Aşağı yeni
Dropbox ile bağlantı kaldırılsın mı?
- Dropbox\'a tekrar bağlanana kadar senkronize edemezsiniz.
- Not defteri ayarlanmadı
+ Dropbox\'a tekrar bağlanana kadar senkronize edilemez.
+ Defter ayarlanmadı
Başlık boş olamaz
Boş olamaz
Geçersiz bağlantı
- Not Defteriniz yok
- Not Defterinizde notlar yok
- Not defterlerinizi senkronize etmek için yeni depo oluşturun
- Kaydettiğiniz aramanız yok
- Not defteri
+ Hiç defter yok
+ Defterde not yok
+ Defterleri senkronize etmek için yeni depo oluştur
+ Kayıtlı arama yok
+ Defter
Yeni not
Bir defter seç
Aşağı
Yukarı
- Not defteri değiştirildi
- Yerel bir dizine senkronize etmek için depolama izniniz gerekiyor
- Not defteri dışa aktarmak için depolama izni gerekiyor
+ Defter değiştirildi
+ Yerel bir dizinle senkronizasyon, depolama izni gerektirir
+ Defteri dışa aktarmak için depolama izni gerekiyor
Yerel depolarla senkronize etmek depolama iznini gerektirir
Harici dosyalara erişim, depolama izni gerektirir
- Hepsini Katla/Aç
+ Tümünü Katla/Aç
Veritabanı yükseltiliyor…
Not değiştirildi
- Değişiklikleri Kaydet?
- Adını değiştir
- Ad değiştir %s
- Not defterini yeniden adlandırma başarısız oldu
- Yeniden adlandırma başarısız oldu %s
- Yeniden adlandırıldı “%s”
- Uzak not defteri Linkini sil
- Not Defteri silinemedi: %s
- Not Defteri Silindi
+ Değişiklikler kaydedilsin mi?
+ Yeniden adlandır
+ %s yeniden adlandır
+ Defter yeniden adlandırılamadı
+ Yeniden adlandırılamadı: %s
+ “%s” yeniden adlandırıldı
+ Bağlantılı uzak defteri sil
+ Defter silinemedi: %s
+ Defter silindi
Öncelik %s
Varsayılan öncelik
- Düşük Öncelik
- Etkin (yapılacak) görevler için anahtar kelimeler
- Tamamlanmış görevler için (Biten) anahtar kelimeler
+ Düşük öncelik
+ Aktif (yapılacak) görevler için anahtar kelimeler
+ Tamamlanan (biten) görevler için anahtar kelimeler
Oluşturuldu
- Not\'s oluşturma zamanı ekle
- Yaratılan zaman için özellik kullan
- Senkron yaratılan zamanda kullanılan bir özellik
+ Not oluşturulma zamanını ekle
+ Zamanında oluşturulanlar için özelliği kullan
+ Bir özellik kullanılarak aynı anda oluşturulan senkronizasyon
Özellik Oluşturuldu
Özellikler
- Özellik Adı
+ Özellik adı
Özellik değeri
- Zamanlanan: %s
- Son bildiri tarihi: %s
+ Zamanlandı: %s
+ Bitiş tarihi: %s
Etkinlik:%s
Bugün için yeni notlar planla
- Katlanır içerik
- Içeriğin katlanmasına izin ver
- Aramada içeriği göster
- Arama sonuçlarında içeriği göster
- İçeriği göster
+ İçeriği katla
+ İçeriğin katlanmasına izin ver
+ Aramada içeriği görüntüle
+ Arama sonuçlarında içeriği görüntüle
+ İçeriği görüntüle
Hem başlık hem de içeriği görüntüle
- Planlama sürelerini Görüntüle
- Planlamaları, sona erenleri ve zamanı dolanları göster
+ Planlama sürelerini görüntüle
+ Planlamaları, sona erenleri ve zamanı dolanları görüntüle
Sıkışık
Ayrıntılar
- NotDefteri ayrıntıları
- Defterle ilgili ayrıntıları göster
+ Defter ayrıntıları
+ Defterle alakalı ayrıntıları görüntüle
Sıralama düzeni
- Not defterleri sıralama düzeni
- Yazı Boyutu
- Tema Rengi
- Other
- Theme
- Light scheme
- Dark scheme
+ Defter sıralama düzeni
+ Yazı boyutu
+ Renk düzeni
+ Diğer
+ Tema
+ Aydınlık düzen
+ Koyu düzen
Eş aralıklı yazı tipi
- Not içeriği\'s ve kitap\'s önsözü için eş aralıklı font kullanın
- Notlara tıklama ve uzun tıklama eylemlerini değiştirin
- Notu seçmek için tıklayın, açmak için uzun tıklayın
- Look & Feel
- Notlar listesi
- Yeni Not
+ Not içeriği ve defter ön sözü için tek aralıklı yazı tipi kullan
+ Nota tıklama ve uzun tıklama eylemlerini değiştir
+ Notu seçmek için tıkla, açmak için basılı tut
+ Görünüm & His
+ Notların listesi
+ Yeni not
Bildirimler
Uygulama
- Depolar Yapılandırılmadı.
+ Yapılandırılmış depo yok
Test bağlantısı
Bağlanıyor…
- Bağlantı Yok.
+ Bağlantı yok
Bağlantı başarılı
- Son senkronize: %s
- Tarafından zorla yükleniyor %s …
- Tarafından zorla yüklendi %s
- Kaydetmek için zorla %s …
- Için zorla kaydedildi %s
- Zorla kaydetme başarısız oldu: %s
- Zorla yükleme başarısız oldu: %s
- Kaynaktan yüklendi %s
- Dizüstü bilgisayar dosyaları
+ Son senkronizasyon: %s
+ %s konumundan zorla getiriliyor…
+ %s konumundan zorla yüklendi
+ %s konumuna kaydetmek için zorla…
+ %s konumuna zorla kaydedildi
+ %s konumuna zorla kaydedilemedi
+ %s konumundan zorla getirilemedi
+ %s konumundan getirildi
+ Defter dosyaları
Org dosya formatı
Dışa aktarılmış defter tipi
- Yeni satırla notları ayır
- Başlık ve içerik
- Önbilgiyi ve içeriği boş satırla ayırın
+ Notlar boş bir satırla ayrılsın
+ Ayrı başlık ve içerik
+ Başlık ve içerik boş bir satırla ayrılsın
Girili etiketler
- Etiketleri kontrol eden seçenekler
+ Etiketlerin girintisini kontrol eden seçenekler
Etiketler sütunu (org-etiketler-sütun)
Girdi moduna göre ayarla
- Org-çentik-mod\'u hesaba kat
- Seviye başına girdi
- Özellik Ekle
- A (Yüksek Öncelik)
- Z (Düşük Öncelik)
+ Org-indent-mode\'u dikkate al
+ Düzey başına girinti
+ Özellik ekle
+ A (yüksek öncelik)
+ Z (düşük öncelik)
Saat
Gün
Hafta
Ay
Yıl
- Bitince, Tekrarla
+ Tekrarla
Görev tamamlandı olarak işaretlendikten sonra tekrarlayıcı saatini değiştirir
Önceki zamana aralık ekle (biriktir)
@@ -337,24 +337,24 @@
Gecikme
Görevin ajanda da görüntülenmesini geciktirme
- Affects all occurrences
- Affects only the first scheduled occurrence
- Warning period
- Warn about the approaching or missed deadline
+ Tüm olayları etkiler
+ Yalnızca ilk programlanmış olayı etkiler
+ Uyarı periyodu
+ Zamanı yaklaşan yada geçen olayları uyar
Yerel değişiklik zamanı
- URL Bağlantısı
+ Bağlantı adresi
URL Senkronizasyonu
- Senkronizasyon değiştirme zamanı
- Senkronizasyon gözden geçirme
- Seçilen kod
+ Değiştirilme zamanını senkronize et
+ Revizyonu senkronize et
+ Seçilen kodlama
Algılanan kodlama
Kullanılanan kodlama
- Son Eylem
+ Son eylem
Not sayısı
- System
- Dynamic color
- Beyaz - Parlak
- Hafif karanlık
+ Sistem
+ Dinamik renk
+ Açık
+ Koyu
Siyah
Küçük
Varsayılan
@@ -364,92 +364,92 @@
Not altına
Konforlu
Rahat
- Sıkışık
+ Kompakt
İsim
- Değiştirme zamanı
+ Değiştirilme zamanı
- Not silinsin mi?
- %d not silinsin mi?
- Not sil
+ Notu sil
Notları sil
- Bu not ve alt notları silinsin mi?
+ Bu not ve tüm alt notları silinsin mi?
Notlar ve alt notlar silinsin mi?
- - Not Silindi
- - Silindi %d notlar
+ - Not silindi
+ - %d not silindi
Silinen not yok
Içeriği göster
Içeriği düzenle
- Anahtar kelime %s zaten kullanılıyor
- Tanımlanan sınıf yok
- NotDefteri içinde arama sonuçları
- Not defteri adını görüntüle
+ %s anahtar kelimesi zaten kullanılıyor
+ Tanımlanmış durum yok
+ Arama sonuçlarında defter adı
+ Defter adını görüntüle
Arama sonuçlarında devralınan etiketler
- Devralınan etiketleri arama sonuçlarında göster
+ Devralınan etiketleri arama sonuçlarında görüntüle
Devam ediyor
- Quickly create new note or sync notebooks from notification drawer
+ Bildirim sekmesinden hızlıca yeni not oluştur veya defterleri senkronize et
Devam eden öncelik
- Senkron başarısızlığı
- Senkronun kesilmesi üzerine görüntü gösterimi bildirimi
+ Senkronizasyon hatası
+ Senkronizasyon hatasında bildirim görüntüle
Hatırlatıcılar
- Zamanlanmış
- Zamanlanmış notlar için bildirimleri görüntüle
- Son bildiri tarihi
- Son tarihi belirlenmiş notlar için bildirimleri görüntüle
+ Planlanmış
+ Planlanan notlar için bildirim görüntüle
+ Bitiş tarihleri
+ Bitiş tarihi belirlenmiş notlar için bildirim görüntüle
Etkinlikler
- Display notification for notes with event time
+ Etkinlik zamanı olan notlar için bildirim görüntüle
Ses
Titreşim
Işık
Erteleme zamanı (dakika)
Erteleme şekli
Günlük hatırlatma zamanı
- Ana Depolama
- Not defteri dışa aktarıldı %s
- Kitap ihraç edilemedi: %s
- Not defteri içe aktarılamadı: %s
- Önsözü düzenle
+ Ana depo
+ Defter dışa aktarıldı %s
+ Defter dışa aktarılamadı: %s
+ Defter içe aktarılamadı: %s
+ Ön sözü düzenle
İlk nottan önce görüntülenen metin
Ekranı açık tutmak için seçeneği etkinleştir
- Display menu item for toggling the option
+ Seçeneği değiştirmek için menü öğesini görüntüle
Ekranı açık tut
- Siz ayrılana veya bu seçeneği devre dışı bırakana kadar ekranınız kapanmayacak
+ Ayrılana veya bu seçeneği devre dışı bırakana kadar ekran kapanmayacak
Yerleşim yönü
Varsayılan
Soldan sağa
Sağdan sola
- NotDefteri\'s Önsözü
+ Defter ön sözü
Göster
Gizle
Birkaç satır göster
- OLUŞTURULUYOR
+ OLUŞTURULDU
Her zaman
Yalnızca çok satırlı notlar
Asla
- Varsayılan NotDefteri
+ Varsayılan defter
Orgzly kullanıcısı ile NotDefteri paylaşıldı veya devam eden bildirim yeni bir not oluştur
- UTF-8\'i kodlamaya zorla
- Kodlamayı anlamaya çalışmayın, her zaman UTF-8 kullanın
+ UTF-8 kodlamasına zorla
+ Kodlamayı tespit etmeye çalışma, her zaman UTF-8 kullan
Katlı olarak başla
- Not defteri yüklendiğinde tüm notlar katlanır
+ Defter yüklendiğinde tüm notlar katlansın
Kaydedilmiş arama
Arama seç
Arama seçilmedi
- Otomatik eşitleme
- Sadece dahili depolar için kullanılır
- Not supported for Dropbox
- Eşitleme işlemi seçtiğiniz durumlara göre işleme konulabilir. Eğer defter değiştirilirse, tüm içerik depodan aktarılır. Eşitleme devam ederken bazı işlemler gecikebilir.
+ Otomatik senkronizasyon
+ Yalnızca yerel depolar için kullanılır
+ Dropbox desteklenmiyor
+ Hangi seçeneklerin seçildiğine bağlı olarak senkronizasyon sıklıkla tetiklenebilir. Eğer defter değiştirilirse içeriğin tamamı depoya veya depodan buraya aktarılır. Senkronizasyon devam ederken bazı işlemler gecikebilir.
Not oluşturuldu
- Yeni notu oluşturduktan sonra eşitle
- Not güncellendi veya silindi
- Bir notu güncelledikten veya sildikten sonra eşitle
- Uygulama başlatıldı veya devam ettirildi
- Uygulama açıldığında eşitle
- Depolar değiştirildi (henüz değil)
- Depolarda bir güncelleme oluştuğunda eşitle
+ Yeni bir not oluşturduktan sonra senkronize et
+ Not güncellenir veya silinirse
+ Bir not güncellendikten veya silindikten sonra senkronize et
+ Uygulama açılışında veya kullanım esnasında
+ Uygulama ön plana çıktığında senkronize et
+ Depolar değiştirildi (henüz uygulanmadı)
+ Depolarda bir değişiklik algılandığında senkronize et
Değiştirilmedi
Not içermiyor
@@ -457,16 +457,16 @@
- Notlar %d içerir
- - %d not defteri bulundu
- - %d not defteri bulundu
+ - %d defter bulundu
+ - %d defter bulundu
- Çizgi sayısı içeriğini göster
- İçerik gösterilmediği zaman satır sayısını göster
+ İçerik satır sayısını görüntüle
+ İçerik görüntülenmediğinde satır sayısını görüntüle
Metin stili
- Vurgu ve tek boşluk için özel işaretler kullan
+ Vurgu ve eş aralıklı metin için özel işaretler kullan
İşaretler içeren metnin stili
Gösterilen işaretleri kalıcı kıl (örn. *kalın*)
- \"%2$s\" bulunamadığı için notu \"%1$s\" özelliği ile kur
+ “%1$s” özelliğinin “%2$s” olarak ayarlandığı not bulunamadı
- Bir arama içe aktarıldı
- %d arama içe aktarıldı
@@ -477,14 +477,14 @@
Depolama izni yok
Dosya mevcut değil: %s
- \"%1$s\" \' den içeri aktarılsın mı?
- \"%1$s\" e aktarılsın mı?
- Birincil depolama alanı mevcut değil
+ \"%1$s\" içeri aktarılsın mı?
+ “%1$s” dışa aktarılsın mı?
+ Birincil depolama kullanılamıyor
- Hatırlatmalar
- Zamanlanmış ya da görevler için göster
+ Hatırlatıcılar
+ Zamanlanmış veya süresi dolmuş görevler için görüntüle
Devam ediyor (yeni not)
- Yeni notu hemen oluşturmak veya senkronizasyon başlatmak
+ Hızlıca yeni not oluştur veya senkronizasyon başlat
Senkron ilerlemesi
Senkron ilerlemesini göster
Başarısız Senkronizasyon
@@ -492,10 +492,10 @@
Bildirimler
Ses, titreşim, bildirim noktası
Meta verileri gizle
- Yerel not defterinin üzerine yazılsın mı?
- Uzak not defterinin üzerine yazılsın mı?
- Drawers folded
- Fold drawers by default
+ Yerel defterin üzerine yazılsın mı?
+ Uzak defterin üzerine yazılsın mı?
+ Katla
+ Çekmeceler varsayılan olarak katlı olsun
Log to drawer on time shift
Log time when note with repeater is marked as done
Set property on time shift
@@ -504,9 +504,9 @@
Güncelleme sıklığı
Git
Meta veri
- Metadata you can choose to always display in note
+ Notta her zaman görüntülenebilir meta veriler
Hepsini göster
- Always show set
+ Her zaman seti göster
Seçileni göster
Tümünü gizle
Dosyayı açmak için uygun uygulama bulunamadı
@@ -516,44 +516,44 @@
Notlardaki görselleri göster
Genişliğe küçült
Görseli belirtilen genişliğe küçült
- Root for absolute links (e.g. file:/readme.txt)
- Root for relative links (e.g. file:readme.txt)
+ Sabit bağlantılar için ana dizin (örn. dosya:/benioku.txt)
+ Göreli bağlantılar için ana dizin (örn. dosya:benioku.txt)
%s kullanıldı
%s algılandı
%s seçildi
Genişlik (piksel cinsinden)
- Share to Orgzly notebook
+ Orgzly defterine paylaş
Altında
Üzerinde
Altında
Yükleniyor…
Git
- Cannot refile to the same subtree
+ Aynı alt ağaca yeniden dosyalanamıyor
Maksimum
Yüksek
Varsayılan
Düşük
Minimum
- Breadcrumbs target
+ Gezinme çubuğu hedefi
Bağlantı hedefi
Not ayrıntıları
- Not defter (not için kaydırın)
- Notebook (focus on note)
+ Defter (nota gitmek için kaydır)
+ Defter (nota odaklan)
- - Refile note
- - Refile %d notes
+ - Notu yeniden dosyala
+ - %d notu yerleştir
- Display checkmarks
- Allows marking notes as done
+ Onay işaretlerini görüntüle
+ Notları tamamlandı olarak işaretlemeye izin verir
Son kullanım
Açılış modu
Cleartext traffic
- Are you sure you want to use this URL?
- Passing credentials in URL is not supported
- Access token
- Trusted certificates
- Add trusted certificates (optional)
- Edit trusted certificates
+ Bu bağlantıyı kullanmak istediğinden emin misin?
+ Kimlik bilgilerinin bağlantıda iletilmesi desteklenmiyor
+ Erişim anahtarı
+ Güvenilen sertifikalar
+ Güvenilir sertifika ekle (isteğe bağlı)
+ Güvenilen sertifikaları düzenle
Bu işlem geri alınamaz.
- Not yapıştır
@@ -564,21 +564,21 @@
Bitiş zamanı
Tekrarlayıcı
Gecikme
- Warning period
+ Uyarı periyodu
Süresi geçmiş
Yardım
İçerik
- Prepend
- Insert new note at beginning
- Developer options
- Git repository type
- In development
- Select notebook
- Highlight rich text being edited
- Use alarm clock
+ Başa ekle
+ Başa yeni not ekle
+ Geliştirici seçenekleri
+ Git depo türü
+ Geliştirilmekte
+ Defter seç
+ Düzenlenmekte olan zengin metni vurgula
+ Çalar saati kullan
For reminders with the time of day set
- Log major events
- Logs
- Share
- Refresh
+ Önemli olayları günlüğe kaydet
+ Günlükler
+ Paylaş
+ Yenile
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index d8666b0d4..117c05927 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -766,6 +766,7 @@
Error while trying to generate the SSH key pair
Message: \n
Error: SSH key can only be unlocked from an activity
+ Repository filename matches a rule in %s
No change
diff --git a/app/src/main/res/values/strings_untranslatable.xml b/app/src/main/res/values/strings_untranslatable.xml
index 0b9f463e9..383695c12 100644
--- a/app/src/main/res/values/strings_untranslatable.xml
+++ b/app/src/main/res/values/strings_untranslatable.xml
@@ -42,6 +42,7 @@
master
-----BEGIN CERTIFICATE-----
WebDAV
+ .orgzlyignore
Lorem ipsum dolor sit amet, consectetur
Lorem *ipsum* dolor sit amet, /consectetur/
diff --git a/app/src/main/res/xml/prefs_screen_developer.xml b/app/src/main/res/xml/prefs_screen_developer.xml
index 193feef32..efb8a859f 100644
--- a/app/src/main/res/xml/prefs_screen_developer.xml
+++ b/app/src/main/res/xml/prefs_screen_developer.xml
@@ -21,7 +21,7 @@
android:title="@string/logs">
diff --git a/app/src/main/res/xml/prefs_screen_reminders.xml b/app/src/main/res/xml/prefs_screen_reminders.xml
index 16a33ce8f..8bb70730e 100644
--- a/app/src/main/res/xml/prefs_screen_reminders.xml
+++ b/app/src/main/res/xml/prefs_screen_reminders.xml
@@ -62,7 +62,7 @@
android:summary="@string/notification_channel_settings_summary">
-
+
diff --git a/app/src/main/res/xml/prefs_screen_sync.xml b/app/src/main/res/xml/prefs_screen_sync.xml
index 1dc86abc1..78c2d01cf 100644
--- a/app/src/main/res/xml/prefs_screen_sync.xml
+++ b/app/src/main/res/xml/prefs_screen_sync.xml
@@ -11,7 +11,7 @@
android:summary="@string/repos_preference_summary">
@@ -27,7 +27,7 @@
android:summary="@string/ssh_keygen_preference_summary">
diff --git a/build.gradle b/build.gradle
index 0323f7530..3926e82c0 100644
--- a/build.gradle
+++ b/build.gradle
@@ -45,7 +45,7 @@ buildscript {
versions.android_test_uiautomator = '2.2.0'
- versions.org_java = '1.3-SNAPSHOT'
+ versions.org_java = 'v1.3.2'
versions.loremipsum = '1.0'
@@ -65,9 +65,9 @@ buildscript {
versions.okhttp_digest = '2.7'
- versions.jgit = '6.7.0.202309050840-r'
+ versions.jgit = '5.13.3.202401111512-r'
- versions.security_crypto = '1.1.0-alpha03'
+ versions.security_crypto = '1.1.0-alpha06'
versions.biometric_ktx = '1.2.0-alpha04'
@@ -94,7 +94,7 @@ allprojects {
url 'https://oss.sonatype.org/content/repositories/snapshots'
}
- // For sardine-android
+ // For sardine-android and org-java
maven {
url 'https://jitpack.io'
}
diff --git a/gradle.properties b/gradle.properties
index 807552ddd..8fb6ad324 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -16,5 +16,13 @@ android.enableJetifier = true
android.databinding.incremental = true
kotlin.code.style = official
+org.gradle.unsafe.configuration-cache=true
-# org.gradle.warning.mode=all
\ No newline at end of file
+# org.gradle.warning.mode=all
+systemProp.DDMLIB_JDWP_PROXY_ENABLED=false
+
+org.gradle.caching=true
+
+org.gradle.unsafe.configuration-cache-problems=warn
+
+org.gradle.parallel=true
diff --git a/metadata/en-US/changelogs/175.txt b/metadata/en-US/changelogs/175.txt
new file mode 100644
index 000000000..5b84d9926
--- /dev/null
+++ b/metadata/en-US/changelogs/175.txt
@@ -0,0 +1,6 @@
+• Add clocking/time capture feature
+• Add support for opening org-protocol://org-id-goto links
+• Make popup buttons configurable
+• Generate SSH key for Git syncing on the device
+• Various Git-related improvements (feature still in beta)
+• Add monochrome icon definition
diff --git a/metadata/en-US/changelogs/177.txt b/metadata/en-US/changelogs/177.txt
new file mode 100644
index 000000000..5cb941429
--- /dev/null
+++ b/metadata/en-US/changelogs/177.txt
@@ -0,0 +1,8 @@
+• Show ranged timestamps in agenda queries
+• Consider time and date ranges when querying events
+• Hide empty days in agenda
+• Add menu option to sort lines in a note
+• Add auto-sync on suspend
+• Disable widget opacity on unsupported color scheme
+• Add tooltips to popup buttons
+• Set a lower threshold for Git garbage collection
diff --git a/metadata/en-US/changelogs/183.txt b/metadata/en-US/changelogs/183.txt
new file mode 100644
index 000000000..d4f38949e
--- /dev/null
+++ b/metadata/en-US/changelogs/183.txt
@@ -0,0 +1,5 @@
+• Fix reminder notification settings opening
+• Fix agenda jitter
+• Git: Further lower the garbage collection threshold
+• Git: Show branch information in namesake status
+• Git: Fix some SSH keygen bugs
diff --git a/metadata/en-US/changelogs/188.txt b/metadata/en-US/changelogs/188.txt
new file mode 100644
index 000000000..c2b410695
--- /dev/null
+++ b/metadata/en-US/changelogs/188.txt
@@ -0,0 +1,5 @@
+• Add a public receiver for Tasker, Automate, etc.
+• Disable Git repos on API <24 (Android <7)
+• Turkish translation updated
+• Removed forced Light theme for splash screen during cold start
+• Add sync times to "major events" log
diff --git a/metadata/en-US/changelogs/191.txt b/metadata/en-US/changelogs/191.txt
new file mode 100644
index 000000000..2d2505135
--- /dev/null
+++ b/metadata/en-US/changelogs/191.txt
@@ -0,0 +1 @@
+• Prevent adding of undesired newline between drawers in header
diff --git a/metadata/en-US/changelogs/194.txt b/metadata/en-US/changelogs/194.txt
new file mode 100644
index 000000000..68cb4e030
--- /dev/null
+++ b/metadata/en-US/changelogs/194.txt
@@ -0,0 +1,2 @@
+• Various fixes for Git repos
+• Fix broken app shortcuts
diff --git a/metadata/en-US/changelogs/196.txt b/metadata/en-US/changelogs/196.txt
new file mode 100644
index 000000000..2c36d29db
--- /dev/null
+++ b/metadata/en-US/changelogs/196.txt
@@ -0,0 +1 @@
+• Support syncing to empty Git repository
diff --git a/sample.app.properties b/sample.app.properties
index 5e57531c0..19e1886d8 100644
--- a/sample.app.properties
+++ b/sample.app.properties
@@ -15,6 +15,13 @@ dropbox.app_key = "appkey"
# Same as above, but prefixed with "db-"
dropbox.app_key_schema = "db-appkey"
+
+# Github Package Registry access credentials.
+# see https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-gradle-registry
+# used to pull the com.orgzlyrevived.org-java dependency from https://github.com/orgzly-revived/org-java
+gpr.user = ""
+gpr.key = ""
+
# Use org-java from local directory instead of the Maven repository.
# If you are working on org-java project, this can make development process easier.
# Don't forget to sync project with Gradle files (in Android Studio) if you change this value.