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 @@ - - Get it on IzzyOnDroid - Get it on F-Droid + 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.