├── .github └── FUNDING.yml ├── .gitignore ├── Audio-Note-Demo-Google-Play.mp4 ├── CONTRIBUTING.md ├── CONTRIBUTORS.md ├── LICENSE.md ├── Privacy-Policy.txt ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro ├── schemas │ └── com.omgodse.notally.room.NotallyDatabase │ │ ├── 1.json │ │ ├── 2.json │ │ ├── 3.json │ │ ├── 4.json │ │ └── 5.json └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ ├── android │ │ └── print │ │ │ └── PostPDFGenerator.kt │ └── com │ │ └── omgodse │ │ └── notally │ │ ├── ActionMode.kt │ │ ├── AttachmentDeleteService.kt │ │ ├── AutoBackupWorker.kt │ │ ├── Cache.kt │ │ ├── LinkMovementMethod.kt │ │ ├── MenuDialog.kt │ │ ├── NotallyApplication.kt │ │ ├── OverflowEditText.kt │ │ ├── Progress.kt │ │ ├── ReminderReceiver.kt │ │ ├── activities │ │ ├── ConfigureWidget.kt │ │ ├── MainActivity.kt │ │ ├── MakeList.kt │ │ ├── NotallyActivity.kt │ │ ├── PlayAudio.kt │ │ ├── RecordAudio.kt │ │ ├── SelectLabels.kt │ │ ├── TakeNote.kt │ │ └── ViewImage.kt │ │ ├── audio │ │ ├── AudioControlView.kt │ │ ├── AudioPlayService.kt │ │ ├── AudioRecordService.kt │ │ ├── LocalBinder.kt │ │ └── Status.kt │ │ ├── fragments │ │ ├── Archived.kt │ │ ├── Deleted.kt │ │ ├── DisplayLabel.kt │ │ ├── Labels.kt │ │ ├── NotallyFragment.kt │ │ ├── Notes.kt │ │ ├── Search.kt │ │ └── Settings.kt │ │ ├── image │ │ ├── AspectRatioRecyclerView.kt │ │ ├── Event.kt │ │ └── ImageError.kt │ │ ├── legacy │ │ ├── Migrations.kt │ │ └── XMLUtils.kt │ │ ├── miscellaneous │ │ ├── Constants.kt │ │ ├── Export.kt │ │ ├── Extensions.kt │ │ ├── IO.kt │ │ └── Operations.kt │ │ ├── preferences │ │ ├── BetterLiveData.kt │ │ ├── ListInfo.kt │ │ ├── Preferences.kt │ │ ├── SeekbarInfo.kt │ │ └── TextInfo.kt │ │ ├── recyclerview │ │ ├── DragCallback.kt │ │ ├── ItemListener.kt │ │ ├── ListItemListener.kt │ │ ├── StringDiffCallback.kt │ │ ├── adapter │ │ │ ├── AudioAdapter.kt │ │ │ ├── BaseNoteAdapter.kt │ │ │ ├── ColorAdapter.kt │ │ │ ├── ErrorAdapter.kt │ │ │ ├── ImageAdapter.kt │ │ │ ├── LabelAdapter.kt │ │ │ ├── MakeListAdapter.kt │ │ │ ├── PreviewImageAdapter.kt │ │ │ └── SelectableLabelAdapter.kt │ │ └── viewholder │ │ │ ├── AudioVH.kt │ │ │ ├── BaseNoteVH.kt │ │ │ ├── ColorVH.kt │ │ │ ├── ErrorVH.kt │ │ │ ├── HeaderVH.kt │ │ │ ├── ImageVH.kt │ │ │ ├── LabelVH.kt │ │ │ ├── MakeListVH.kt │ │ │ ├── PreviewImageVH.kt │ │ │ └── SelectableLabelVH.kt │ │ ├── room │ │ ├── Attachment.kt │ │ ├── Audio.kt │ │ ├── BaseNote.kt │ │ ├── Color.kt │ │ ├── Converters.kt │ │ ├── Folder.kt │ │ ├── Frequency.kt │ │ ├── Header.kt │ │ ├── IdLabels.kt │ │ ├── IdReminder.kt │ │ ├── Image.kt │ │ ├── Item.kt │ │ ├── Label.kt │ │ ├── ListItem.kt │ │ ├── NotallyDatabase.kt │ │ ├── Reminder.kt │ │ ├── SpanRepresentation.kt │ │ ├── Type.kt │ │ ├── dao │ │ │ ├── BaseNoteDao.kt │ │ │ ├── CommonDao.kt │ │ │ └── LabelDao.kt │ │ └── livedata │ │ │ ├── Content.kt │ │ │ └── SearchResult.kt │ │ ├── viewmodels │ │ ├── BaseNoteModel.kt │ │ ├── Extensions.kt │ │ ├── LabelModel.kt │ │ └── NotallyModel.kt │ │ └── widget │ │ ├── WidgetFactory.kt │ │ ├── WidgetProvider.kt │ │ └── WidgetService.kt │ └── res │ ├── color │ ├── chip_background.xml │ ├── chip_stroke.xml │ ├── navigation_view_item.xml │ ├── stop_background.xml │ └── stop_text.xml │ ├── drawable-v26 │ ├── make_list.xml │ └── take_note.xml │ ├── drawable │ ├── add.xml │ ├── add_16.xml │ ├── add_images.xml │ ├── archive.xml │ ├── change_color.xml │ ├── checkbox.xml │ ├── checkbox_16.xml │ ├── checkbox_fill.xml │ ├── checkbox_outline.xml │ ├── checkbox_outline_16.xml │ ├── checked_circle.xml │ ├── copy.xml │ ├── delete.xml │ ├── delete_all.xml │ ├── drag_handle.xml │ ├── edit.xml │ ├── export.xml │ ├── home.xml │ ├── label.xml │ ├── make_list.xml │ ├── make_list_foreground.xml │ ├── notally_foreground.xml │ ├── notebook.xml │ ├── notification_delete.xml │ ├── notification_reminder.xml │ ├── pin.xml │ ├── record_audio.xml │ ├── reminder.xml │ ├── reminder_16.xml │ ├── reminder_20.xml │ ├── restore.xml │ ├── rounded_rectangle.xml │ ├── save.xml │ ├── search.xml │ ├── settings.xml │ ├── share.xml │ ├── take_note.xml │ ├── take_note_foreground.xml │ ├── unarchive.xml │ └── unpin.xml │ ├── layout-v31 │ └── widget_list_item.xml │ ├── layout │ ├── activity_configure_widget.xml │ ├── activity_label.xml │ ├── activity_main.xml │ ├── activity_notally.xml │ ├── activity_play_audio.xml │ ├── activity_record_audio.xml │ ├── activity_view_image.xml │ ├── dialog_color.xml │ ├── dialog_input.xml │ ├── dialog_progress.xml │ ├── dialog_reminder.xml │ ├── drawer_header.xml │ ├── error.xml │ ├── fragment_notes.xml │ ├── fragment_settings.xml │ ├── label.xml │ ├── menu_item.xml │ ├── preference.xml │ ├── preference_seekbar.xml │ ├── recycler_audio.xml │ ├── recycler_base_note.xml │ ├── recycler_color.xml │ ├── recycler_header.xml │ ├── recycler_image.xml │ ├── recycler_label.xml │ ├── recycler_list_item.xml │ ├── recycler_preview_image.xml │ ├── recycler_selectable_label.xml │ ├── widget.xml │ ├── widget_list_header.xml │ ├── widget_list_item.xml │ ├── widget_note.xml │ └── widget_preview.xml │ ├── mipmap-anydpi-v26 │ ├── notally.xml │ └── notally_round.xml │ ├── mipmap-hdpi │ ├── notally.png │ └── notally_round.png │ ├── mipmap-mdpi │ ├── notally.png │ └── notally_round.png │ ├── mipmap-nodpi │ └── widget_preview.png │ ├── mipmap-xhdpi │ ├── notally.png │ └── notally_round.png │ ├── mipmap-xxhdpi │ ├── notally.png │ └── notally_round.png │ ├── mipmap-xxxhdpi │ ├── notally.png │ └── notally_round.png │ ├── navigation │ └── navigation.xml │ ├── raw │ └── keep.xml │ ├── resources.properties │ ├── values-ca │ └── strings.xml │ ├── values-cs │ └── strings.xml │ ├── values-da │ └── strings.xml │ ├── values-de │ └── strings.xml │ ├── values-el │ └── strings.xml │ ├── values-es │ └── strings.xml │ ├── values-fr │ └── strings.xml │ ├── values-hu │ └── strings.xml │ ├── values-in │ └── strings.xml │ ├── values-it │ └── strings.xml │ ├── values-ja │ └── strings.xml │ ├── values-ko │ └── strings.xml │ ├── values-my │ └── strings.xml │ ├── values-nb │ └── strings.xml │ ├── values-night │ ├── colors.xml │ └── styles.xml │ ├── values-nl │ └── strings.xml │ ├── values-nn │ └── strings.xml │ ├── values-pl │ └── strings.xml │ ├── values-pt-rBR │ └── strings.xml │ ├── values-pt-rPT │ └── strings.xml │ ├── values-ro │ └── strings.xml │ ├── values-ru │ └── strings.xml │ ├── values-sk │ └── strings.xml │ ├── values-sl │ └── strings.xml │ ├── values-sv │ └── strings.xml │ ├── values-tl │ └── strings.xml │ ├── values-tr │ └── strings.xml │ ├── values-uk │ └── strings.xml │ ├── values-v23 │ └── styles.xml │ ├── values-vi │ └── strings.xml │ ├── values-zh-rCN │ └── strings.xml │ ├── values │ ├── arrays.xml │ ├── attrs.xml │ ├── colors.xml │ ├── dimens.xml │ ├── strings.xml │ └── styles.xml │ └── xml │ ├── backup_content.xml │ ├── data_rules.xml │ ├── provider_paths.xml │ ├── shortcuts.xml │ └── widget.xml ├── build.gradle ├── fastlane └── metadata │ └── android │ ├── en-US │ ├── changelogs │ │ ├── 40.txt │ │ ├── 41.txt │ │ ├── 43.txt │ │ ├── 44.txt │ │ ├── 45.txt │ │ ├── 46.txt │ │ ├── 47.txt │ │ ├── 50.txt │ │ ├── 51.txt │ │ ├── 52.txt │ │ ├── 53.txt │ │ ├── 54.txt │ │ ├── 55.txt │ │ └── 56.txt │ ├── full_description.txt │ ├── images │ │ ├── icon.png │ │ └── phoneScreenshots │ │ │ ├── 1.png │ │ │ ├── 2.png │ │ │ ├── 3.png │ │ │ ├── 4.png │ │ │ ├── 5.png │ │ │ ├── 6.png │ │ │ ├── 7.png │ │ │ └── 8.png │ ├── short_description.txt │ └── title.txt │ └── fr-FR │ ├── full_description.txt │ ├── short_description.txt │ └── title.txt ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | patreon: omgodse -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | -------------------------------------------------------------------------------- /Audio-Note-Demo-Google-Play.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OmGodse/Notally/f6569646d84b7be9d188735caf8a99cf61fa5275/Audio-Note-Demo-Google-Play.mp4 -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ### Contributing 2 | 3 | Issues are currently disabled. 4 | 5 | Please use the pull requests tab only for translations or bug fixes. -------------------------------------------------------------------------------- /CONTRIBUTORS.md: -------------------------------------------------------------------------------- 1 | ### Translations 2 | 1. 🇬🇧 English 3 | 2. 🇭🇺 Hungarian 4 | 3. 🇬🇷 Greek by xamps 5 | 4. 🇳🇱 Dutch by [tlmnot](https://github.com/tlmnot) 6 | 5. 🇨🇿 Czech by [tomo90](https://github.com/tomo90) 7 | 6. 🇯🇵 Japanese by [kato-k](https://github.com/kato-k) 8 | 7. 🇦🇩 Catalan by retiolus 9 | 8. 🇵🇱 Polish by [ZiomaleQ](https://github.com/ZiomaleQ), [rehork](https://github.com/rehork) 10 | 9. 🇸🇰 Slovak by [Juraj Liso](https://github.com/LiJu09) 11 | 10. 🇮🇩 Indonesian by [zmni](https://github.com/zmni) 12 | 11. 🇮🇹 Italian by Luigi Sforza, [IlmastroStefanuzzo](https://github.com/IlmastroStefanuzzo) 13 | 12. 🇪🇸 Spanish by Jose Casas 14 | 13. 🇹🇷 Turkish by Helpful User 15 | 14. 🇺🇦 Ukrainian by Alex Shpak 16 | 15. 🇩🇰 Danish by [shoddysheep](https://github.com/shoddysheep) 17 | 16. 🇸🇪 Swedish by Erik Lindström 18 | 17. 🇷🇺 Russian by Denis Bondarenko, Lyyako 19 | 18. 🇫🇷 French by Arnaud Dieumegard, [Co-7](https://github.com/Co-7) 20 | 19. 🇧🇷 Portuguese (Brazil) by [fabianski7](https://github.com/fabianski7) 21 | 20. 🇵🇹 Portuguese (Portugal) by [joaopmatos](https://github.com/joaopmatos) 22 | 21. 🇳🇴 Norwegian (Bokmål) by Fredrik Magnussen, [Erik Thom](https://github.com/erikthm) 23 | 22. 🇳🇴 Norwegian (Nynorsk) by [Erik Thom](https://github.com/erikthm) 24 | 23. 🇵🇭 Tagalog by Isaiah Collins Abetong 25 | 24. 🇨🇳 Chinese (Simplified) by [Austin Huang](https://github.com/austinhuang0131), [sr093906](https://github.com/sr093906) 26 | 25. 🇩🇪 German by Maximilian Braunschmied, [jonas-haeusler](https://github.com/jonas-haeusler), 27 | [samuel141](https://github.com/samuel141), [nautilusx](https://github.com/nautilusx) 28 | 26. 🇻🇳 Vietnamese by [mastoduy](https://github.com/mastoduy) -------------------------------------------------------------------------------- /Privacy-Policy.txt: -------------------------------------------------------------------------------- 1 | No user data is collected -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### Background 2 | Notally was created because I wanted to make something that was beautiful and at the same time, useful. It's extremely light, there are minimal dependencies and lines of code. 3 | 4 | ### Features 5 | * Widgets 6 | * Auto backup 7 | * Adjustable text size 8 | * Support for Lollipop devices and up 9 | * APK size of 1.4 MB (1.8 MB uncompressed) 10 | * Color, pin and label your notes for quick organisation 11 | * Complement your notes with pictures (JPG, PNG, WEBP) 12 | * Export notes as TXT, JSON, HTML or PDF files with formatting 13 | * Create rich text notes with support for bold, italics, mono space and strike-through 14 | * Add clickable links to notes with support for phone numbers, email addresses and web urls 15 | 16 | [Get it on Google Play](https://play.google.com/store/apps/details?id=com.omgodse.notally) 17 | [Get it on F-Droid](https://f-droid.org/packages/com.omgodse.notally/) 18 | 19 | ### Translations 20 | All translations are crowd sourced. To contribute, follow these [guidelines](https://m2.material.io/design/communication/writing.html) and email me or open a pull request. 21 | 22 | 23 | 24 | ### Hall of fame 25 | * [Top 20 Android Apps 2021!](https://www.youtube.com/watch?v=bwz13aM0qJk) 26 | * [De-Googling Any Android Phone! (Google Apps Alternatives)](https://www.youtube.com/watch?v=RQUEgwgV99I) 27 | * [The BEST Private Notetaking Apps Explained](https://www.youtube.com/watch?v=BJw5tKPP1PY) 28 | * [Notally](https://www.noteapps.ca/notally/) 29 | * [The 9 Best Simple Note-Taking Apps for Android](https://www.makeuseof.com/simple-note-apps-android/) 30 | 31 | ### Copycats 32 | Clones of Notally keep popping up on the Play Store. They are not licensed under GPL3 and usually change a few colors, include ads, etc. Please [report them](https://support.google.com/googleplay/android-developer/contact/takedown) and [inform me](mailto:omgodseapps@gmail.com) if you find a new one. 33 | 34 | * https://play.google.com/store/apps/details?id=com.sladjan.notes 35 | * https://play.google.com/store/apps/details?id=com.sladjan.notespro -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile 2 | 3 | plugins { 4 | id 'com.android.application' 5 | id 'kotlin-android' 6 | id 'kotlin-parcelize' 7 | id 'com.google.devtools.ksp' 8 | } 9 | 10 | android { 11 | compileSdk 34 12 | namespace 'com.omgodse.notally' 13 | 14 | defaultConfig { 15 | applicationId 'com.omgodse.notally' 16 | minSdk 21 17 | targetSdk 34 18 | versionCode 56 19 | versionName "6.1" 20 | resourceConfigurations += ['en', 'ca', 'cs', 'da', 'de', 'el', 'es', 'fr', 'hu', 'in', 'it', 'ja', 'ko', 'my', 'nb', 'nl', 'nn', 'pl', 'pt-rBR', 'pt-rPT', 'ro', 'ru', 'sk', 'sl', 'sv', 'tl', 'tr', 'uk', 'vi', 'zh-rCN'] 21 | vectorDrawables.generatedDensities = [] 22 | } 23 | 24 | ksp { 25 | arg("room.generateKotlin", "true") 26 | arg("room.schemaLocation", "$projectDir/schemas") 27 | } 28 | 29 | buildTypes { 30 | debug { 31 | applicationIdSuffix ".debug" 32 | } 33 | release { 34 | crunchPngs false 35 | minifyEnabled true 36 | shrinkResources true 37 | proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" 38 | } 39 | } 40 | 41 | kotlinOptions { jvmTarget = "1.8" } 42 | 43 | buildFeatures { 44 | viewBinding true 45 | buildConfig true 46 | } 47 | 48 | packagingOptions.resources { 49 | excludes += ["DebugProbesKt.bin", "META-INF/**.version", "kotlin/**.kotlin_builtins", "kotlin-tooling-metadata.json"] 50 | } 51 | 52 | androidResources { 53 | generateLocaleConfig = true 54 | } 55 | } 56 | 57 | tasks.withType(KotlinCompile).configureEach { 58 | kotlinOptions.jvmTarget = "1.8" 59 | } 60 | 61 | dependencies { 62 | final def navVersion = "2.3.5" 63 | final def roomVersion = "2.6.1" 64 | 65 | ksp "androidx.room:room-compiler:$roomVersion" 66 | implementation "androidx.room:room-ktx:$roomVersion" 67 | implementation "androidx.room:room-runtime:$roomVersion" 68 | 69 | implementation "androidx.work:work-runtime:2.9.0" 70 | 71 | implementation "androidx.navigation:navigation-ui-ktx:$navVersion" 72 | implementation "androidx.navigation:navigation-fragment-ktx:$navVersion" 73 | 74 | implementation "org.ocpsoft.prettytime:prettytime:4.0.6.Final" 75 | implementation "com.google.android.material:material:1.4.0" 76 | 77 | implementation "com.github.bumptech.glide:glide:4.15.1" 78 | implementation "com.github.rambler-digital-solutions:swipe-layout-android:1.0.17" 79 | implementation "com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0" 80 | } -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | -keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | -renamesourcefileattribute SourceFile 22 | -keep class ** extends androidx.navigation.Navigator 23 | -keep class ** implements org.ocpsoft.prettytime.TimeUnit -------------------------------------------------------------------------------- /app/src/main/java/android/print/PostPDFGenerator.kt: -------------------------------------------------------------------------------- 1 | package android.print 2 | 3 | import android.content.Context 4 | import android.os.ParcelFileDescriptor 5 | import android.webkit.WebView 6 | import android.webkit.WebViewClient 7 | import java.io.File 8 | 9 | /** 10 | * This class needs to be in android.print package to access the package private 11 | * methods of [PrintDocumentAdapter] 12 | */ 13 | object PostPDFGenerator { 14 | 15 | fun create(file: File, content: String, context: Context, onResult: OnResult) { 16 | val webView = WebView(context) 17 | webView.loadDataWithBaseURL(null, content, "text/html", "utf-8", null) 18 | webView.webViewClient = object : WebViewClient() { 19 | 20 | override fun onPageFinished(view: WebView?, url: String?) { 21 | val adapter = webView.createPrintDocumentAdapter(file.nameWithoutExtension) 22 | print(file, adapter, onResult) 23 | } 24 | } 25 | } 26 | 27 | 28 | private fun print(file: File, adapter: PrintDocumentAdapter, onResult: OnResult) { 29 | val onLayoutResult = object : PrintDocumentAdapter.LayoutResultCallback() { 30 | 31 | override fun onLayoutFailed(error: CharSequence?) { 32 | onResult.onFailure(error) 33 | } 34 | 35 | override fun onLayoutFinished(info: PrintDocumentInfo?, changed: Boolean) { 36 | writeToFile(file, adapter, onResult) 37 | } 38 | } 39 | 40 | adapter.onLayout(null, getPrintAttributes(), null, onLayoutResult, null) 41 | } 42 | 43 | private fun writeToFile(file: File, adapter: PrintDocumentAdapter, onResult: OnResult) { 44 | val onWriteResult = object : PrintDocumentAdapter.WriteResultCallback() { 45 | 46 | override fun onWriteFailed(error: CharSequence?) { 47 | onResult.onFailure(error) 48 | } 49 | 50 | override fun onWriteFinished(pages: Array?) { 51 | onResult.onSuccess(file) 52 | } 53 | } 54 | 55 | val pages = arrayOf(PageRange.ALL_PAGES) 56 | if (!file.exists()) { 57 | file.createNewFile() 58 | } 59 | val fileDescriptor = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_WRITE) 60 | adapter.onWrite(pages, fileDescriptor, null, onWriteResult) 61 | } 62 | 63 | 64 | private fun getPrintAttributes(): PrintAttributes { 65 | val builder = PrintAttributes.Builder() 66 | builder.setMediaSize(PrintAttributes.MediaSize.ISO_A4) 67 | builder.setMinMargins(PrintAttributes.Margins(250, 250, 250, 250)) 68 | builder.setResolution(PrintAttributes.Resolution("Standard", "Standard", 100, 100)) 69 | return builder.build() 70 | } 71 | 72 | interface OnResult { 73 | 74 | fun onSuccess(file: File) 75 | 76 | fun onFailure(message: CharSequence?) 77 | } 78 | } -------------------------------------------------------------------------------- /app/src/main/java/com/omgodse/notally/ActionMode.kt: -------------------------------------------------------------------------------- 1 | package com.omgodse.notally 2 | 3 | import androidx.lifecycle.MutableLiveData 4 | import com.omgodse.notally.image.Event 5 | import com.omgodse.notally.preferences.BetterLiveData 6 | import com.omgodse.notally.room.BaseNote 7 | 8 | class ActionMode { 9 | 10 | val enabled = BetterLiveData(false) 11 | val count = BetterLiveData(0) 12 | val selectedNotes = HashMap() 13 | val selectedIds = selectedNotes.keys 14 | val closeListener = MutableLiveData>>() 15 | 16 | private fun refresh() { 17 | count.value = selectedNotes.size 18 | enabled.value = selectedNotes.size != 0 19 | } 20 | 21 | fun add(id: Long, baseNote: BaseNote) { 22 | selectedNotes[id] = baseNote 23 | refresh() 24 | } 25 | 26 | fun remove(id: Long) { 27 | selectedNotes.remove(id) 28 | refresh() 29 | } 30 | 31 | fun close(notify: Boolean) { 32 | val previous = HashSet(selectedIds) 33 | selectedNotes.clear() 34 | refresh() 35 | if (notify && selectedNotes.size == 0) { 36 | closeListener.value = Event(previous) 37 | } 38 | } 39 | 40 | fun isEnabled() = enabled.value 41 | 42 | // We assume selectedNotes.size is 1 43 | fun getFirstNote() = selectedNotes.values.first() 44 | } -------------------------------------------------------------------------------- /app/src/main/java/com/omgodse/notally/AutoBackupWorker.kt: -------------------------------------------------------------------------------- 1 | package com.omgodse.notally 2 | 3 | import android.app.Application 4 | import android.content.Context 5 | import android.net.Uri 6 | import androidx.documentfile.provider.DocumentFile 7 | import androidx.work.Worker 8 | import androidx.work.WorkerParameters 9 | import com.omgodse.notally.miscellaneous.Export 10 | import com.omgodse.notally.miscellaneous.IO 11 | import com.omgodse.notally.miscellaneous.Operations 12 | import com.omgodse.notally.preferences.AutoBackup 13 | import com.omgodse.notally.preferences.Preferences 14 | import com.omgodse.notally.room.Converters 15 | import com.omgodse.notally.room.NotallyDatabase 16 | import java.text.SimpleDateFormat 17 | import java.util.Locale 18 | import java.util.zip.ZipOutputStream 19 | 20 | class AutoBackupWorker(private val context: Context, params: WorkerParameters) : Worker(context, params) { 21 | 22 | override fun doWork(): Result { 23 | val app = context.applicationContext as Application 24 | val preferences = Preferences.getInstance(app) 25 | val backupPath = preferences.autoBackup.value 26 | 27 | if (backupPath != AutoBackup.emptyPath) { 28 | val uri = Uri.parse(backupPath) 29 | val folder = requireNotNull(DocumentFile.fromTreeUri(app, uri)) 30 | 31 | if (folder.exists()) { 32 | val formatter = SimpleDateFormat("yyyyMMdd HHmmss '(Notally Backup)'", Locale.ENGLISH) 33 | val name = formatter.format(System.currentTimeMillis()) 34 | val file = requireNotNull(folder.createFile("application/zip", name)) 35 | val outputStream = requireNotNull(app.contentResolver.openOutputStream(file.uri)) 36 | 37 | val zipStream = ZipOutputStream(outputStream) 38 | 39 | val database = NotallyDatabase.getDatabase(app) 40 | database.checkpoint() 41 | 42 | Export.backupDatabase(app, zipStream) 43 | 44 | val imageRoot = IO.getExternalImagesDirectory(app) 45 | val audioRoot = IO.getExternalAudioDirectory(app) 46 | database.getBaseNoteDao().getAllImages() 47 | .asSequence() 48 | .flatMap { string -> Converters.jsonToImages(string) } 49 | .forEach { image -> 50 | try { 51 | Export.backupFile(zipStream, imageRoot, "Images", image.name) 52 | } catch (exception: Exception) { 53 | Operations.log(app, exception) 54 | } 55 | } 56 | database.getBaseNoteDao().getAllAudios() 57 | .asSequence() 58 | .flatMap { string -> Converters.jsonToAudios(string) } 59 | .forEach { audio -> 60 | try { 61 | Export.backupFile(zipStream, audioRoot, "Audios", audio.name) 62 | } catch (exception: Exception) { 63 | Operations.log(app, exception) 64 | } 65 | } 66 | 67 | zipStream.close() 68 | } 69 | } 70 | 71 | return Result.success() 72 | } 73 | } -------------------------------------------------------------------------------- /app/src/main/java/com/omgodse/notally/Cache.kt: -------------------------------------------------------------------------------- 1 | package com.omgodse.notally 2 | 3 | import com.omgodse.notally.room.BaseNote 4 | 5 | object Cache { 6 | 7 | var list: List = ArrayList() 8 | } -------------------------------------------------------------------------------- /app/src/main/java/com/omgodse/notally/MenuDialog.kt: -------------------------------------------------------------------------------- 1 | package com.omgodse.notally 2 | 3 | import android.content.Context 4 | import android.view.ViewGroup.LayoutParams 5 | import android.widget.LinearLayout 6 | import androidx.core.widget.NestedScrollView 7 | import com.google.android.material.bottomsheet.BottomSheetDialog 8 | import com.omgodse.notally.databinding.MenuItemBinding 9 | 10 | class MenuDialog(context: Context) : BottomSheetDialog(context) { 11 | 12 | private val linearLayout: LinearLayout 13 | 14 | init { 15 | val params = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT) 16 | 17 | linearLayout = LinearLayout(context) 18 | linearLayout.layoutParams = params 19 | linearLayout.orientation = LinearLayout.VERTICAL 20 | 21 | val scrollView = NestedScrollView(context) 22 | scrollView.layoutParams = params 23 | 24 | scrollView.addView(linearLayout) 25 | setContentView(scrollView) 26 | } 27 | 28 | fun add(title: Int, onClick: () -> Unit): MenuDialog { 29 | val item = MenuItemBinding.inflate(layoutInflater, linearLayout, true).root 30 | item.setText(title) 31 | item.setOnClickListener { 32 | dismiss() 33 | onClick() 34 | } 35 | return this 36 | } 37 | } -------------------------------------------------------------------------------- /app/src/main/java/com/omgodse/notally/NotallyApplication.kt: -------------------------------------------------------------------------------- 1 | package com.omgodse.notally 2 | 3 | import android.app.Application 4 | import androidx.appcompat.app.AppCompatDelegate 5 | import androidx.work.ExistingPeriodicWorkPolicy 6 | import androidx.work.PeriodicWorkRequest 7 | import androidx.work.WorkManager 8 | import com.omgodse.notally.preferences.Preferences 9 | import com.omgodse.notally.preferences.Theme 10 | import java.util.concurrent.TimeUnit 11 | 12 | class NotallyApplication : Application() { 13 | 14 | override fun onCreate() { 15 | super.onCreate() 16 | 17 | val preferences = Preferences.getInstance(this) 18 | preferences.theme.observeForever { theme -> 19 | when (theme) { 20 | Theme.dark -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES) 21 | Theme.light -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO) 22 | Theme.followSystem -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM) 23 | } 24 | } 25 | 26 | val request = PeriodicWorkRequest.Builder(AutoBackupWorker::class.java, 12, TimeUnit.HOURS).build() 27 | WorkManager.getInstance(this).enqueueUniquePeriodicWork("Auto Backup", ExistingPeriodicWorkPolicy.KEEP, request) 28 | } 29 | } -------------------------------------------------------------------------------- /app/src/main/java/com/omgodse/notally/OverflowEditText.kt: -------------------------------------------------------------------------------- 1 | package com.omgodse.notally 2 | 3 | import android.content.Context 4 | import android.util.AttributeSet 5 | import androidx.appcompat.widget.AppCompatEditText 6 | 7 | /** 8 | * Implementation that fixes a bug in Lollipop where clicking on the overflow icon 9 | * in the custom text selection mode causes it to end. 10 | * For more information, see this -> https://issuetracker.google.com/issues/36937508 11 | */ 12 | class OverflowEditText(context: Context, attrs: AttributeSet) : AppCompatEditText(context, attrs) { 13 | 14 | var isActionModeOn = false 15 | 16 | override fun onWindowFocusChanged(hasWindowFocus: Boolean) { 17 | if (!isActionModeOn) { 18 | super.onWindowFocusChanged(hasWindowFocus) 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /app/src/main/java/com/omgodse/notally/Progress.kt: -------------------------------------------------------------------------------- 1 | package com.omgodse.notally 2 | 3 | class Progress(val inProgress: Boolean, val current: Int, val total: Int, val indeterminate: Boolean) -------------------------------------------------------------------------------- /app/src/main/java/com/omgodse/notally/activities/MakeList.kt: -------------------------------------------------------------------------------- 1 | package com.omgodse.notally.activities 2 | 3 | import android.view.inputmethod.InputMethodManager 4 | import com.omgodse.notally.miscellaneous.setOnNextAction 5 | import com.omgodse.notally.recyclerview.ListItemListener 6 | import com.omgodse.notally.recyclerview.adapter.MakeListAdapter 7 | import com.omgodse.notally.recyclerview.viewholder.MakeListVH 8 | import com.omgodse.notally.room.ListItem 9 | import com.omgodse.notally.room.Type 10 | 11 | class MakeList : NotallyActivity(Type.LIST) { 12 | 13 | private lateinit var adapter: MakeListAdapter 14 | 15 | override fun configureUI() { 16 | binding.EnterTitle.setOnNextAction { 17 | moveToNext(-1) 18 | } 19 | 20 | if (model.isNewNote) { 21 | if (model.items.isEmpty()) { 22 | addListItem() 23 | } 24 | } 25 | } 26 | 27 | 28 | override fun setupListeners() { 29 | super.setupListeners() 30 | binding.AddItem.setOnClickListener { 31 | addListItem() 32 | } 33 | } 34 | 35 | override fun setStateFromModel() { 36 | super.setStateFromModel() 37 | val elevation = resources.displayMetrics.density * 2 38 | 39 | adapter = MakeListAdapter(model.textSize, elevation, model.items, object : ListItemListener { 40 | 41 | override fun delete(position: Int) { 42 | model.items.removeAt(position) 43 | adapter.notifyItemRemoved(position) 44 | } 45 | 46 | override fun moveToNext(position: Int) { 47 | this@MakeList.moveToNext(position) 48 | } 49 | 50 | override fun textChanged(position: Int, text: String) { 51 | model.items[position].body = text 52 | } 53 | 54 | override fun checkedChanged(position: Int, checked: Boolean) { 55 | model.items[position].checked = checked 56 | } 57 | }) 58 | 59 | binding.RecyclerView.adapter = adapter 60 | } 61 | 62 | 63 | private fun addListItem() { 64 | val position = model.items.size 65 | val listItem = ListItem(String(), false) 66 | model.items.add(listItem) 67 | adapter.notifyItemInserted(position) 68 | val inputManager = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager 69 | binding.RecyclerView.post { 70 | val viewHolder = binding.RecyclerView.findViewHolderForAdapterPosition(position) as MakeListVH? 71 | if (viewHolder != null) { 72 | viewHolder.binding.EditText.requestFocus() 73 | inputManager.showSoftInput(viewHolder.binding.EditText, InputMethodManager.SHOW_IMPLICIT) 74 | } 75 | } 76 | } 77 | 78 | private fun moveToNext(currentPosition: Int) { 79 | val viewHolder = binding.RecyclerView.findViewHolderForAdapterPosition(currentPosition + 1) as MakeListVH? 80 | if (viewHolder != null) { 81 | if (viewHolder.binding.CheckBox.isChecked) { 82 | moveToNext(currentPosition + 1) 83 | } else viewHolder.binding.EditText.requestFocus() 84 | } else addListItem() 85 | } 86 | } -------------------------------------------------------------------------------- /app/src/main/java/com/omgodse/notally/audio/AudioPlayService.kt: -------------------------------------------------------------------------------- 1 | package com.omgodse.notally.audio 2 | 3 | import android.app.Service 4 | import android.content.Intent 5 | import android.media.MediaPlayer 6 | import android.os.IBinder 7 | import com.omgodse.notally.miscellaneous.IO 8 | import com.omgodse.notally.miscellaneous.Operations 9 | import com.omgodse.notally.room.Audio 10 | import java.io.File 11 | 12 | class AudioPlayService : Service() { 13 | 14 | private var state = IDLE 15 | private var stateBeforeSeeking = -1 16 | private var errorType = 0 17 | private var errorCode = 0 18 | private lateinit var player: MediaPlayer 19 | 20 | var onStateChange: (() -> Unit)? = null 21 | 22 | override fun onCreate() { 23 | super.onCreate() 24 | player = MediaPlayer() 25 | player.setOnPreparedListener { setState(PREPARED) } 26 | player.setOnCompletionListener { setState(COMPLETED) } 27 | player.setOnSeekCompleteListener { setState(stateBeforeSeeking) } 28 | player.setOnErrorListener { _, what, extra -> 29 | errorType = what 30 | errorCode = extra 31 | setState(ERROR) 32 | return@setOnErrorListener true 33 | } 34 | } 35 | 36 | override fun onDestroy() { 37 | super.onDestroy() 38 | player.release() 39 | } 40 | 41 | override fun onBind(intent: Intent?): IBinder { 42 | return LocalBinder(this) 43 | } 44 | 45 | 46 | fun initialise(audio: Audio) { 47 | if (state == IDLE) { 48 | val audioRoot = IO.getExternalAudioDirectory(application) 49 | if (audioRoot != null) { 50 | try { 51 | val file = File(audioRoot, audio.name) 52 | player.setDataSource(file.absolutePath) 53 | setState(INITIALISED) 54 | player.prepareAsync() 55 | } catch (exception: Exception) { 56 | setIOError() 57 | Operations.log(application, exception) 58 | } 59 | } else setIOError() 60 | } 61 | } 62 | 63 | fun play() { 64 | when (state) { 65 | PREPARED, PAUSED, COMPLETED -> { 66 | player.start() 67 | setState(STARTED) 68 | } 69 | STARTED -> { 70 | player.pause() 71 | setState(PAUSED) 72 | } 73 | } 74 | } 75 | 76 | fun seek(milliseconds: Long) { 77 | if (state == PREPARED || state == STARTED || state == PAUSED || state == COMPLETED) { 78 | stateBeforeSeeking = state 79 | player.seekTo(milliseconds.toInt()) 80 | setState(SEEKING) 81 | } 82 | } 83 | 84 | 85 | fun getState(): Int { 86 | return state 87 | } 88 | 89 | fun getErrorType(): Int { 90 | return errorType 91 | } 92 | 93 | fun getErrorCode(): Int { 94 | return errorCode 95 | } 96 | 97 | fun getCurrentPosition(): Int { 98 | return if (state != IDLE && state != ERROR) player.currentPosition else 0 99 | } 100 | 101 | 102 | private fun setState(state: Int) { 103 | this.state = state 104 | onStateChange?.invoke() 105 | } 106 | 107 | private fun setIOError() { 108 | errorCode = 0 109 | errorType = 15 110 | setState(ERROR) 111 | } 112 | 113 | companion object { 114 | const val IDLE = 0 115 | const val INITIALISED = 1 116 | const val PREPARED = 2 117 | const val STARTED = 3 118 | const val PAUSED = 4 119 | const val SEEKING = 5 120 | const val COMPLETED = 6 121 | const val ERROR = 7 122 | } 123 | } -------------------------------------------------------------------------------- /app/src/main/java/com/omgodse/notally/audio/LocalBinder.kt: -------------------------------------------------------------------------------- 1 | package com.omgodse.notally.audio 2 | 3 | import android.app.Service 4 | import android.os.Binder 5 | 6 | class LocalBinder(private val service: T) : Binder() { 7 | 8 | fun getService(): T = service 9 | } -------------------------------------------------------------------------------- /app/src/main/java/com/omgodse/notally/audio/Status.kt: -------------------------------------------------------------------------------- 1 | package com.omgodse.notally.audio 2 | 3 | enum class Status { READY, PAUSED, RECORDING } -------------------------------------------------------------------------------- /app/src/main/java/com/omgodse/notally/fragments/Archived.kt: -------------------------------------------------------------------------------- 1 | package com.omgodse.notally.fragments 2 | 3 | import com.omgodse.notally.R 4 | 5 | class Archived : NotallyFragment() { 6 | 7 | override fun getBackground() = R.drawable.archive 8 | 9 | override fun getObservable() = model.archivedNotes 10 | } -------------------------------------------------------------------------------- /app/src/main/java/com/omgodse/notally/fragments/Deleted.kt: -------------------------------------------------------------------------------- 1 | package com.omgodse.notally.fragments 2 | 3 | import android.view.Menu 4 | import android.view.MenuInflater 5 | import com.google.android.material.dialog.MaterialAlertDialogBuilder 6 | import com.omgodse.notally.R 7 | import com.omgodse.notally.miscellaneous.add 8 | 9 | class Deleted : NotallyFragment() { 10 | 11 | override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { 12 | menu.add(R.string.delete_all, R.drawable.delete_all) { deleteAllNotes() } 13 | } 14 | 15 | 16 | private fun deleteAllNotes() { 17 | MaterialAlertDialogBuilder(requireContext()) 18 | .setMessage(R.string.delete_all_notes) 19 | .setPositiveButton(R.string.delete) { _, _ -> model.deleteAllBaseNotes() } 20 | .setNegativeButton(R.string.cancel, null) 21 | .show() 22 | } 23 | 24 | 25 | override fun getBackground() = R.drawable.delete 26 | 27 | override fun getObservable() = model.deletedNotes 28 | } -------------------------------------------------------------------------------- /app/src/main/java/com/omgodse/notally/fragments/DisplayLabel.kt: -------------------------------------------------------------------------------- 1 | package com.omgodse.notally.fragments 2 | 3 | import androidx.lifecycle.LiveData 4 | import com.omgodse.notally.R 5 | import com.omgodse.notally.miscellaneous.Constants 6 | import com.omgodse.notally.room.Item 7 | 8 | class DisplayLabel : NotallyFragment() { 9 | 10 | override fun getBackground() = R.drawable.label 11 | 12 | override fun getObservable(): LiveData> { 13 | val label = requireNotNull(requireArguments().getString(Constants.SelectedLabel)) 14 | return model.getNotesByLabel(label) 15 | } 16 | } -------------------------------------------------------------------------------- /app/src/main/java/com/omgodse/notally/fragments/Notes.kt: -------------------------------------------------------------------------------- 1 | package com.omgodse.notally.fragments 2 | 3 | import android.view.Menu 4 | import android.view.MenuInflater 5 | import androidx.navigation.fragment.findNavController 6 | import com.omgodse.notally.R 7 | import com.omgodse.notally.miscellaneous.add 8 | 9 | class Notes : NotallyFragment() { 10 | 11 | override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { 12 | menu.add(R.string.search, R.drawable.search) { findNavController().navigate(R.id.NotesToSearch) } 13 | } 14 | 15 | 16 | override fun getObservable() = model.baseNotes 17 | 18 | override fun getBackground() = R.drawable.notebook 19 | } -------------------------------------------------------------------------------- /app/src/main/java/com/omgodse/notally/fragments/Search.kt: -------------------------------------------------------------------------------- 1 | package com.omgodse.notally.fragments 2 | 3 | import android.os.Build 4 | import android.os.Bundle 5 | import android.view.View 6 | import com.omgodse.notally.R 7 | import com.omgodse.notally.room.Folder 8 | 9 | class Search : NotallyFragment() { 10 | 11 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 12 | binding?.ChipGroup?.visibility = View.VISIBLE 13 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 14 | binding?.RecyclerView?.scrollIndicators = View.SCROLL_INDICATOR_TOP 15 | } 16 | super.onViewCreated(view, savedInstanceState) 17 | 18 | val checked = when (model.folder) { 19 | Folder.NOTES -> R.id.Notes 20 | Folder.DELETED -> R.id.Deleted 21 | Folder.ARCHIVED -> R.id.Archived 22 | } 23 | 24 | binding?.ChipGroup?.check(checked) 25 | 26 | binding?.ChipGroup?.setOnCheckedChangeListener { _, checkedId -> 27 | when (checkedId) { 28 | R.id.Notes -> model.folder = Folder.NOTES 29 | R.id.Deleted -> model.folder = Folder.DELETED 30 | R.id.Archived -> model.folder = Folder.ARCHIVED 31 | } 32 | } 33 | } 34 | 35 | 36 | override fun getBackground() = R.drawable.search 37 | 38 | override fun getObservable() = model.searchResults 39 | } -------------------------------------------------------------------------------- /app/src/main/java/com/omgodse/notally/image/AspectRatioRecyclerView.kt: -------------------------------------------------------------------------------- 1 | package com.omgodse.notally.image 2 | 3 | import android.content.Context 4 | import android.util.AttributeSet 5 | import androidx.recyclerview.widget.RecyclerView 6 | 7 | class AspectRatioRecyclerView(context: Context, attrs: AttributeSet) : RecyclerView(context, attrs) { 8 | 9 | override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { 10 | super.onMeasure(widthMeasureSpec, heightMeasureSpec) 11 | val height = (measuredWidth * 0.75).toInt() 12 | if (height != measuredHeight) { 13 | setMeasuredDimension(measuredWidth, height) 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /app/src/main/java/com/omgodse/notally/image/Event.kt: -------------------------------------------------------------------------------- 1 | package com.omgodse.notally.image 2 | 3 | class Event(val data: T) { 4 | 5 | private var isHandled = false 6 | 7 | fun handle(function: (data: T) -> Unit) { 8 | if (!isHandled) { 9 | function(data) 10 | isHandled = true 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /app/src/main/java/com/omgodse/notally/image/ImageError.kt: -------------------------------------------------------------------------------- 1 | package com.omgodse.notally.image 2 | 3 | class ImageError (val name: String, val description: String) -------------------------------------------------------------------------------- /app/src/main/java/com/omgodse/notally/legacy/Migrations.kt: -------------------------------------------------------------------------------- 1 | package com.omgodse.notally.legacy 2 | 3 | import android.app.Application 4 | import android.content.Context 5 | import android.content.SharedPreferences 6 | import com.omgodse.notally.room.BaseNote 7 | import com.omgodse.notally.room.Folder 8 | import com.omgodse.notally.room.Label 9 | import java.io.File 10 | 11 | // Backwards compatibility from v3.2 to v3.3 12 | object Migrations { 13 | 14 | fun clearAllLabels(app: Application) { 15 | val preferences = getLabelsPreferences(app) 16 | preferences.edit().clear().commit() 17 | } 18 | 19 | fun clearAllFolders(app: Application) { 20 | getNotePath(app).listFiles()?.forEach { file -> file.delete() } 21 | getDeletedPath(app).listFiles()?.forEach { file -> file.delete() } 22 | getArchivedPath(app).listFiles()?.forEach { file -> file.delete() } 23 | } 24 | 25 | fun getPreviousLabels(app: Application): List