├── .gitattributes ├── .gitignore ├── LICENSE.txt ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── org │ │ └── standardnotes │ │ └── notes │ │ ├── NoteStoreTest.kt │ │ ├── OffineSyncTest.java │ │ ├── ProgressIdlingResource.java │ │ └── StarterActivityTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── org │ │ │ └── standardnotes │ │ │ └── notes │ │ │ ├── BaseActivity.kt │ │ │ ├── DebugActivity.kt │ │ │ ├── LoginActivity.kt │ │ │ ├── MainActivity.kt │ │ │ ├── NoteActivity.kt │ │ │ ├── SApplication.kt │ │ │ ├── SearchActivity.kt │ │ │ ├── SettingsActivity.kt │ │ │ ├── StarterActivity.kt │ │ │ ├── TagListActivity.kt │ │ │ ├── UiUtil.kt │ │ │ ├── comms │ │ │ ├── CommsManager.kt │ │ │ ├── Crypt.java │ │ │ ├── ExportUtil.kt │ │ │ ├── ServerApi.java │ │ │ ├── SyncManager.kt │ │ │ └── data │ │ │ │ ├── AuthParamsResponse.java │ │ │ │ ├── ContentType.kt │ │ │ │ ├── EncryptableItem.java │ │ │ │ ├── EncryptedItem.java │ │ │ │ ├── ExportItems.java │ │ │ │ ├── Note.java │ │ │ │ ├── NoteContent.java │ │ │ │ ├── PlaintextItem.java │ │ │ │ ├── Reference.java │ │ │ │ ├── SigninResponse.java │ │ │ │ ├── SyncItems.java │ │ │ │ ├── Tag.java │ │ │ │ ├── TagContent.java │ │ │ │ ├── UnsavedItem.java │ │ │ │ ├── UnsavedItemError.java │ │ │ │ └── UploadSyncItems.java │ │ │ ├── frag │ │ │ ├── NoteFragment.kt │ │ │ └── NoteListFragment.kt │ │ │ ├── store │ │ │ ├── NoteStore.kt │ │ │ └── ValueStore.kt │ │ │ └── widget │ │ │ ├── AppWidgetRefreshService.java │ │ │ └── NoteListWidget.java │ └── res │ │ ├── color │ │ └── drawer_item.xml │ │ ├── drawable-nodpi │ │ └── appwidget_preview.png │ │ ├── drawable │ │ ├── divider.xml │ │ ├── fab_add.xml │ │ ├── ic_magnify.xml │ │ ├── ic_settings_24dp.xml │ │ ├── ic_share.xml │ │ ├── ic_tag.xml │ │ └── tag_bg.xml │ │ ├── layout │ │ ├── activity_debug.xml │ │ ├── activity_login.xml │ │ ├── activity_main.xml │ │ ├── activity_search.xml │ │ ├── activity_settings.xml │ │ ├── activity_tags.xml │ │ ├── frag_note.xml │ │ ├── frag_note_list.xml │ │ ├── item_note.xml │ │ ├── item_tag.xml │ │ ├── item_tag_lozenge.xml │ │ ├── view_navigation_header.xml │ │ ├── view_new_tag.xml │ │ ├── view_password_confirm.xml │ │ └── widget_note_list.xml │ │ ├── menu │ │ ├── drawer_tags.xml │ │ ├── logged_in.xml │ │ ├── note.xml │ │ └── search.xml │ │ ├── mipmap-hdpi │ │ └── ic_launcher.png │ │ ├── mipmap-mdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxxhdpi │ │ └── ic_launcher.png │ │ ├── values-v14 │ │ └── dimens.xml │ │ ├── values-v21 │ │ └── styles.xml │ │ ├── values-w820dp │ │ └── dimens.xml │ │ ├── values │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml │ │ └── xml │ │ ├── filepaths.xml │ │ ├── note_list_widget_info.xml │ │ └── settings.xml │ └── test │ └── java │ └── org │ └── standardnotes │ └── notes │ ├── ContentTypeTest.kt │ └── CryptTest.kt ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── schema ├── AuthParamsResponse.jschema ├── EncryptableItem.jschema ├── EncryptedItem.jschema ├── Note.jschema ├── NoteContent.jschema ├── Reference.jschema ├── SigninResponse.jschema ├── SyncItems.jschema ├── Tag.jschema ├── TagContent.jschema ├── UnsavedItem.jschema ├── UnsavedItemError.jschema └── UploadSyncItems.jschema ├── settings.gradle ├── tools └── jsonschema2pojo-0.4.15 │ ├── .gitignore │ ├── .travis.yml │ ├── generate_pojo.sh │ ├── generate_pojo.sh~ │ ├── jsonschema2pojo │ ├── jsonschema2pojo-0.4.15-javadoc.jar │ ├── jsonschema2pojo-cli-0.4.15-sources.jar │ ├── jsonschema2pojo-cli-0.4.15.jar │ ├── jsonschema2pojo-core-0.4.15-sources.jar │ ├── jsonschema2pojo.bat │ ├── jsonschema2pojo~ │ └── lib │ ├── android-4.1.1.4.jar │ ├── annotations-1.3.9.jar │ ├── codemodel-2.6.jar │ ├── commons-io-2.4.jar │ ├── commons-lang-2.6.jar │ ├── commons-lang3-3.2.1.jar │ ├── commons-logging-1.1.1.jar │ ├── gson-2.2.4.jar │ ├── httpclient-4.0.1.jar │ ├── httpcore-4.0.1.jar │ ├── jackson-annotations-2.2.0.jar │ ├── jackson-core-2.2.0.jar │ ├── jackson-core-asl-1.9.11.jar │ ├── jackson-databind-2.2.0.jar │ ├── jackson-mapper-asl-1.9.11.jar │ ├── javaparser-1.0.11.jar │ ├── jcommander-1.30.jar │ ├── joda-time-2.2.jar │ ├── json-20080701.jar │ ├── jsonschema2pojo-core-0.4.15.jar │ ├── opengl-api-gl1.1-android-2.1_r1.jar │ ├── validation-api-1.0.0.GA.jar │ ├── xmlParserAPIs-2.6.2.jar │ └── xpp3-1.1.4c.jar └── web_hi_res_512.png /.gitattributes: -------------------------------------------------------------------------------- 1 | *.bat eol=crlf 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # From https://github.com/github/gitignore/blob/master/Android.gitignore 2 | # Built application files 3 | *.apk 4 | *.ap_ 5 | 6 | # Files for the ART/Dalvik VM 7 | *.dex 8 | 9 | # Java class files 10 | *.class 11 | 12 | # Generated files 13 | bin/ 14 | gen/ 15 | out/ 16 | 17 | # Gradle files 18 | .gradle/ 19 | build/ 20 | 21 | # Local configuration file (sdk path, etc) 22 | local.properties 23 | 24 | # Proguard folder generated by Eclipse 25 | proguard/ 26 | 27 | # Log Files 28 | *.log 29 | 30 | # Android Studio Navigation editor temp files 31 | .navigation/ 32 | 33 | # Android Studio captures folder 34 | captures/ 35 | 36 | # Intellij 37 | *.iml 38 | .idea 39 | 40 | # Keystore files 41 | *.jks 42 | 43 | # External native build folder generated in Android Studio 2.2 and later 44 | .externalNativeBuild 45 | 46 | # Google Services (e.g. APIs or Firebase) 47 | google-services.json 48 | 49 | # Mac auto-generated files 50 | .DS_Store -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Deprecated 2 | 3 | __This codebase has been deprecated in favor of our new implementation, found here:__ 4 | 5 | [https://github.com/standardnotes/mobile](https://github.com/standardnotes/mobile) 6 | 7 | What follows remains online for historical reasons. 8 | 9 | # Standard Notes for Android (Classic) 10 | 11 | Available on F-Droid: https://apt.izzysoft.de/fdroid/index/apk/org.standardnotes.notes 12 | 13 | Or you can clone this repository and compile the code. If you would like to get involved in the development & testing, please also join the [Slack group](https://standardnotes.org/slack) 14 | 15 | https://standardnotes.org/ 16 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlin-android-extensions' 4 | 5 | android { 6 | compileSdkVersion 25 7 | buildToolsVersion '25.0.3' 8 | defaultConfig { 9 | applicationId "org.standardnotes.notes" 10 | minSdkVersion 21 11 | targetSdkVersion 25 12 | versionCode 10403 13 | versionName "1.4.3" 14 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 15 | buildConfigField "String", "SERVER_DEFAULT", "\"https://sync.standardnotes.org/\"" 16 | } 17 | // The release password should not be stored on the repository. Use a build parameter, eg: 18 | // gradlew assembleRelease -PsignPass=xxxx 19 | if (project.hasProperty('signPass')) { 20 | signingConfigs { 21 | release { 22 | storeFile file(keystore) 23 | storePassword signPass 24 | keyAlias "notes" 25 | keyPassword signPass 26 | } 27 | } 28 | } 29 | buildTypes { 30 | if (project.hasProperty('signPass')) { 31 | release { 32 | minifyEnabled false 33 | signingConfig signingConfigs.release 34 | } 35 | } 36 | } 37 | flavorDimensions "runmethod" 38 | productFlavors { 39 | normal { 40 | resValue "string", "app_name", "Notes" 41 | dimension "runmethod" 42 | } 43 | emulatortest { 44 | resValue "string", "app_name", "Notes (staging)" 45 | applicationId "org.standardnotes.notes.buildfortest" 46 | buildConfigField "String", "SERVER_DEFAULT", "\"https://stagingapi.standardnotes.org/\"" 47 | dimension "runmethod" 48 | } 49 | } 50 | sourceSets { 51 | main.java.srcDirs += 'src/main/kotlin' 52 | test.java.srcDirs += 'src/test/kotlin' 53 | } 54 | 55 | testOptions { 56 | 57 | unitTests.all { 58 | testLogging { 59 | events "passed", "skipped", "failed", "standardOut", "standardError" 60 | outputs.upToDateWhen { false } 61 | showStandardStreams = true 62 | } 63 | } 64 | } 65 | } 66 | 67 | afterEvaluate { 68 | android.sourceSets.all { sourceSet -> 69 | if (!sourceSet.name.startsWith("test")) { 70 | sourceSet.kotlin.setSrcDirs([]) 71 | } 72 | } 73 | } 74 | 75 | dependencies { 76 | compile fileTree(include: ['*.jar'], dir: 'libs') 77 | androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', { 78 | exclude group: 'com.android.support', module: 'support-annotations' 79 | }) 80 | androidTestCompile 'com.android.support.test:runner:0.5' 81 | compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" 82 | compile "com.android.support:appcompat-v7:$android_support" 83 | compile "com.android.support:design:$android_support" 84 | compile "com.android.support:support-v4:$android_support" 85 | compile "com.android.support:recyclerview-v7:$android_support" 86 | compile "com.android.support:preference-v7:$android_support" 87 | compile "com.android.support:preference-v14:$android_support" 88 | androidTestCompile 'com.android.support.test.espresso:espresso-contrib:2.2.2', { 89 | exclude group: 'com.android.support', module: 'support-annotations' 90 | exclude group: 'com.android.support', module: 'support-v4' 91 | exclude group: 'com.android.support', module: 'design' 92 | exclude group: 'com.android.support', module: 'recyclerview-v7' 93 | } 94 | compile 'org.glassfish:javax.annotation:10.0-b28' 95 | compile 'com.google.code.gson:gson:2.8.0' 96 | compile 'com.squareup.retrofit2:retrofit:2.1.0' 97 | compile 'com.squareup.retrofit2:converter-gson:2.1.0' 98 | compile 'com.squareup.okhttp3:okhttp:3.6.0' 99 | compile 'com.squareup.okhttp3:logging-interceptor:3.6.0' 100 | compile 'com.madgag.spongycastle:core:1.54.0.0' 101 | compile 'com.madgag.spongycastle:prov:1.54.0.0' 102 | compile 'com.madgag.spongycastle:pkix:1.54.0.0' 103 | compile 'com.madgag.spongycastle:pg:1.54.0.0' 104 | compile 'net.danlew:android.joda:2.9.4.1' 105 | compile 'ch.acra:acra:4.9.2' 106 | testCompile 'junit:junit:4.12' 107 | } 108 | repositories { 109 | mavenCentral() 110 | } 111 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /home/carl/android-sdk-linux_86/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /app/src/androidTest/java/org/standardnotes/notes/NoteStoreTest.kt: -------------------------------------------------------------------------------- 1 | package org.standardnotes.notes 2 | 3 | import android.content.Context 4 | import android.support.test.InstrumentationRegistry 5 | import android.support.test.runner.AndroidJUnit4 6 | 7 | import org.joda.time.DateTime 8 | import org.junit.Assert 9 | import org.junit.Test 10 | import org.junit.runner.RunWith 11 | import org.standardnotes.notes.comms.data.Note 12 | import org.standardnotes.notes.comms.data.Tag 13 | import org.standardnotes.notes.store.NoteStore 14 | 15 | import org.junit.Assert.* 16 | import org.junit.Before 17 | import org.standardnotes.notes.comms.Crypt 18 | import org.standardnotes.notes.comms.data.ContentType 19 | import org.standardnotes.notes.comms.data.Reference 20 | import org.standardnotes.notes.frag.newNote 21 | import java.util.* 22 | 23 | /** 24 | * Instrumentation test, which will execute on an Android device. 25 | 26 | * @see [Testing documentation](http://d.android.com/tools/testing) 27 | */ 28 | class NoteStoreTest { 29 | 30 | @Before 31 | @Throws(Exception::class) 32 | fun useAppContext() { 33 | // Context of the app under test. 34 | val appContext = InstrumentationRegistry.getTargetContext() 35 | 36 | assertEquals("Run tests with the emulatortest product flavor, otherwise you will write over your app data", 37 | "org.standardnotes.notes.buildfortest", appContext.packageName) 38 | } 39 | 40 | @Test 41 | fun noteStore() { 42 | val ns = SApplication.instance.noteStore 43 | val n = Note() 44 | n.text = "text" 45 | n.title = "title" 46 | n.uuid = "uuid" 47 | n.encItemKey = "123" 48 | val time = DateTime.now() 49 | n.createdAt = time 50 | n.updatedAt = time 51 | n.references = ArrayList() 52 | ns.putNote(n.uuid, n) 53 | val n1 = ns.getNote(n.uuid) 54 | assertEquals(n.uuid, n1!!.uuid) 55 | Assert.assertEquals(n.title, n1.title) 56 | Assert.assertEquals(n.text, n1.text) 57 | Assert.assertEquals(n.createdAt, n1.createdAt) 58 | 59 | n.title = UUID.randomUUID().toString() 60 | ns.putNote(n.uuid, n) 61 | val changedNote = ns.getNote(n.uuid) 62 | Assert.assertEquals(changedNote!!.title, n.title) 63 | 64 | val oldTitle = changedNote.title 65 | changedNote.title = UUID.randomUUID().toString() 66 | changedNote.updatedAt = DateTime.now() 67 | ns.mergeNote(changedNote.uuid, changedNote) 68 | val mergedNote = ns.getNote(changedNote.uuid) 69 | Assert.assertEquals(oldTitle, mergedNote!!.title) 70 | Assert.assertEquals(n.text, mergedNote.text) 71 | Assert.assertEquals(changedNote.updatedAt, mergedNote.updatedAt) 72 | 73 | } 74 | 75 | @Test 76 | fun tagStore() { 77 | val ns = SApplication.instance.noteStore 78 | val t = Tag() 79 | t.title = "title" 80 | t.uuid = "uuid2" 81 | t.encItemKey = "123" 82 | val time = DateTime.now() 83 | t.createdAt = time 84 | t.updatedAt = time 85 | ns.putTag(t.uuid, t) 86 | val n1 = ns.getTag(t.uuid) 87 | Assert.assertEquals(t.uuid, n1!!.uuid) 88 | Assert.assertEquals(t.title, n1.title) 89 | Assert.assertEquals(t.createdAt, n1.createdAt) 90 | } 91 | 92 | @Test 93 | fun noteStoreWithTag() { 94 | val ns = SApplication.instance.noteStore 95 | val n = Note() 96 | n.text = UUID.randomUUID().toString() 97 | n.title = UUID.randomUUID().toString() 98 | n.uuid = UUID.randomUUID().toString() 99 | n.encItemKey = "123" 100 | val time = DateTime.now() 101 | n.createdAt = time 102 | n.updatedAt = time 103 | 104 | val t = Tag() 105 | t.title = UUID.randomUUID().toString() 106 | t.uuid = UUID.randomUUID().toString() 107 | t.encItemKey = "123" 108 | t.createdAt = time 109 | t.updatedAt = time 110 | ns.putTag(t.uuid, t) 111 | 112 | val ref = Reference() 113 | ref.uuid = t.uuid 114 | ref.contentType = ContentType.Tag.toString() 115 | n.references = ArrayList() 116 | n.references.add(ref) 117 | 118 | ns.putNote(n.uuid, n) 119 | 120 | val n1 = ns.getNote(n.uuid) 121 | Assert.assertEquals(n.uuid, n1!!.uuid) 122 | Assert.assertEquals(n.title, n1.title) 123 | Assert.assertEquals(n.text, n1.text) 124 | Assert.assertEquals(n.createdAt, n1.createdAt) 125 | 126 | val nTags = ns.getTagsForNote(n.uuid) 127 | Assert.assertEquals(1, nTags.count()) 128 | Assert.assertEquals(ref.uuid, nTags[0].uuid) 129 | Assert.assertEquals(n.references[0].uuid, nTags[0].uuid) 130 | 131 | Assert.assertEquals(1, ns.getNotesForTag(t.uuid).count()) 132 | Assert.assertEquals(n1.uuid, ns.getNotesForTag(t.uuid)[0].uuid) 133 | 134 | val t2 = Tag() 135 | t2.title = UUID.randomUUID().toString() 136 | t2.uuid = UUID.randomUUID().toString() 137 | t2.encItemKey = "123" 138 | t2.createdAt = time 139 | t2.updatedAt = time 140 | ns.putTag(t2.uuid, t2) 141 | 142 | val ref2 = Reference() 143 | ref2.uuid = t2.uuid 144 | ref2.contentType = ContentType.Tag.toString() 145 | n.references.add(ref2) 146 | ns.putNote(n.uuid, n) 147 | Assert.assertEquals(1, ns.getNotesForTag(t2.uuid).count()) 148 | Assert.assertEquals(2, ns.getTagsForNote(n.uuid).count()) 149 | 150 | n.references.clear() 151 | ns.putNote(n.uuid, n) 152 | 153 | Assert.assertEquals(0, ns.getNotesForTag(t.uuid).count()) 154 | Assert.assertEquals(0, ns.getTagsForNote(n.uuid).count()) 155 | } 156 | 157 | @Test 158 | fun encrypt001() { 159 | val mk = "96fbfbace17d0d268cc5a57900fe785e50a40cf7ae2d23a3dcdd2f28d5fd09d8" 160 | SApplication.instance.valueStore.setTokenAndMasterKey("", mk, null) 161 | 162 | val n = newNote() 163 | n.text = UUID.randomUUID().toString() 164 | n.title = UUID.randomUUID().toString() 165 | 166 | val cn = Crypt.encrypt(n, "001") 167 | val n2 = Crypt.decryptNote(cn) 168 | Assert.assertEquals(n.uuid, n2.uuid) 169 | Assert.assertEquals(n.title, n2.title) 170 | Assert.assertEquals(n.text, n2.text) 171 | Assert.assertEquals(n.createdAt, n2.createdAt) 172 | Assert.assertEquals(n.updatedAt, n2.updatedAt) 173 | 174 | } 175 | 176 | @Test 177 | fun encrypt002() { 178 | val mk = "96fbfbace17d0d268cc5a57900fe785e50a40cf7ae2d23a3dcdd2f28d5fd09d8" 179 | val ak = "f6fsfbacd17d0d268cc5a57900fe785e50a40cf7ae2d23a3dcdd2f28d5fd09d2" 180 | SApplication.instance.valueStore.setTokenAndMasterKey("", mk, ak) 181 | 182 | val n = Note() 183 | n.uuid = UUID.randomUUID().toString() 184 | n.encItemKey = Crypt.generateEncryptedKey(512, "002", n.uuid) 185 | n.createdAt = DateTime.now() 186 | n.updatedAt = n.createdAt 187 | n.text = UUID.randomUUID().toString() 188 | n.title = UUID.randomUUID().toString() 189 | 190 | val cn = Crypt.encrypt(n, "002") 191 | val n2 = Crypt.decryptNote(cn) 192 | Assert.assertEquals(n.uuid, n2.uuid) 193 | Assert.assertEquals(n.title, n2.title) 194 | Assert.assertEquals(n.text, n2.text) 195 | Assert.assertEquals(n.createdAt, n2.createdAt) 196 | Assert.assertEquals(n.updatedAt, n2.updatedAt) 197 | 198 | } 199 | 200 | fun createString(length: Int): String { 201 | val chars = "abcdefghijklmnopqrstuvwxyz".toCharArray() 202 | val sb = StringBuilder(length) 203 | val random = Random() 204 | for (i in 0..length) { 205 | val c = chars[random.nextInt(chars.size)] 206 | sb.append(c) 207 | } 208 | return sb.toString() 209 | } 210 | 211 | @Test 212 | fun bigStoreThreadedRead() { 213 | val ns = SApplication.instance.noteStore 214 | val n1 = createLargeNote() 215 | for (i in 1..10) { 216 | createLargeNote() 217 | createLargeNote() 218 | createLargeNote() 219 | Thread(Runnable { ns.getAllNotes() }).start() 220 | } 221 | val n2 = createLargeNote() 222 | val n3 = createLargeNote() 223 | val n4 = createLargeNote() 224 | val n1r = ns.getNote(n1.uuid) 225 | Assert.assertEquals(n1r?.uuid, n1.uuid) 226 | } 227 | 228 | fun createLargeNote(): Note { 229 | val ns = SApplication.instance.noteStore 230 | val n = Note() 231 | n.text = createString(50000) 232 | n.encItemKey = "123" 233 | n.uuid = UUID.randomUUID().toString() 234 | n.title = n.uuid 235 | val time = DateTime.now() 236 | n.createdAt = time 237 | n.updatedAt = time 238 | n.references = ArrayList() 239 | ns.putNote(n.uuid, n) 240 | return n 241 | } 242 | 243 | } 244 | -------------------------------------------------------------------------------- /app/src/androidTest/java/org/standardnotes/notes/OffineSyncTest.java: -------------------------------------------------------------------------------- 1 | package org.standardnotes.notes; 2 | 3 | 4 | import android.content.Intent; 5 | import android.support.test.espresso.Espresso; 6 | import android.support.test.espresso.IdlingResource; 7 | import android.support.test.espresso.ViewInteraction; 8 | import android.support.test.rule.ActivityTestRule; 9 | import android.support.test.runner.AndroidJUnit4; 10 | import android.test.suitebuilder.annotation.LargeTest; 11 | import android.view.View; 12 | import android.view.ViewGroup; 13 | import android.view.ViewParent; 14 | 15 | import org.hamcrest.Description; 16 | import org.hamcrest.Matcher; 17 | import org.hamcrest.TypeSafeMatcher; 18 | import org.junit.After; 19 | import org.junit.Before; 20 | import org.junit.Rule; 21 | import org.junit.Test; 22 | import org.junit.runner.RunWith; 23 | 24 | import java.util.UUID; 25 | 26 | import static android.support.test.espresso.Espresso.onData; 27 | import static android.support.test.espresso.Espresso.onView; 28 | import static android.support.test.espresso.Espresso.pressBack; 29 | import static android.support.test.espresso.action.ViewActions.click; 30 | import static android.support.test.espresso.action.ViewActions.closeSoftKeyboard; 31 | import static android.support.test.espresso.action.ViewActions.longClick; 32 | import static android.support.test.espresso.action.ViewActions.replaceText; 33 | import static android.support.test.espresso.action.ViewActions.scrollTo; 34 | import static android.support.test.espresso.assertion.ViewAssertions.doesNotExist; 35 | import static android.support.test.espresso.assertion.ViewAssertions.matches; 36 | import static android.support.test.espresso.contrib.RecyclerViewActions.actionOnItemAtPosition; 37 | import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed; 38 | import static android.support.test.espresso.matcher.ViewMatchers.withContentDescription; 39 | import static android.support.test.espresso.matcher.ViewMatchers.withId; 40 | import static android.support.test.espresso.matcher.ViewMatchers.withParent; 41 | import static android.support.test.espresso.matcher.ViewMatchers.withText; 42 | import static org.hamcrest.Matchers.allOf; 43 | 44 | @LargeTest 45 | @RunWith(AndroidJUnit4.class) 46 | public class OffineSyncTest { 47 | 48 | @Rule 49 | public ActivityTestRule mActivityTestRule = new ActivityTestRule<>(LoginActivity.class); 50 | 51 | static String email = UUID.randomUUID().toString(); 52 | static boolean signedup = false; 53 | 54 | public void signupin() { 55 | mActivityTestRule.launchActivity(new Intent()); 56 | IdlingResource idlingResource = new ProgressIdlingResource(mActivityTestRule.getActivity()); 57 | Espresso.registerIdlingResources(idlingResource); 58 | 59 | if (!SApplication.Companion.getInstance().getValueStore().getServer().contains("staging")) { 60 | throw new RuntimeException("These tests add lots of test users - don't run against a live server."); 61 | } 62 | 63 | ViewInteraction appCompatAutoCompleteTextView = onView( 64 | withId(R.id.email)); 65 | appCompatAutoCompleteTextView.perform(scrollTo(), click()); 66 | 67 | ViewInteraction appCompatAutoCompleteTextView3 = onView( 68 | withId(R.id.email)); 69 | appCompatAutoCompleteTextView3.perform(scrollTo(), replaceText(email), closeSoftKeyboard()); 70 | 71 | ViewInteraction appCompatEditText = onView( 72 | withId(R.id.password)); 73 | appCompatEditText.perform(scrollTo(), replaceText("aaa"), closeSoftKeyboard()); 74 | 75 | if (!signedup) { 76 | ViewInteraction appCompatButton = onView( 77 | allOf(withText("Register"), 78 | withParent(allOf(withId(R.id.email_login_form), 79 | withParent(withId(R.id.login_form)))))); 80 | appCompatButton.perform(scrollTo(), click()); 81 | 82 | ViewInteraction appCompatEditText2 = onView( 83 | allOf(withId(R.id.confirm_password), isDisplayed())); 84 | appCompatEditText2.perform(replaceText("aaa"), closeSoftKeyboard()); 85 | 86 | ViewInteraction appCompatButton2 = onView( 87 | allOf(withId(android.R.id.button1), withText("OK"))); 88 | appCompatButton2.perform(scrollTo(), click()); 89 | signedup = true; 90 | } else { 91 | onView( 92 | allOf(withText("Sign in"))).perform(click()); 93 | } 94 | 95 | Espresso.unregisterIdlingResources(idlingResource); 96 | } 97 | 98 | 99 | public void logout() { 100 | ViewInteraction actionMenuItemView = onView( 101 | allOf(withId(R.id.settings), withContentDescription("Settings"), isDisplayed())); 102 | actionMenuItemView.perform(click()); 103 | 104 | ViewInteraction appCompatButton = onView( 105 | allOf(withId(R.id.logout))); 106 | appCompatButton.perform(scrollTo(), click()); 107 | onView( 108 | allOf(withId(android.R.id.button1), withText("Delete"))).perform(click()); 109 | } 110 | 111 | @Test 112 | public void createNote() { 113 | 114 | signupin(); 115 | ViewInteraction floatingActionButton = onView( 116 | allOf(withId(R.id.fab), 117 | withParent(allOf(withId(R.id.rootView), 118 | withParent(withId(R.id.drawer_layout)))), 119 | isDisplayed())); 120 | floatingActionButton.perform(click()); 121 | 122 | ViewInteraction appCompatEditText = onView( 123 | withId(R.id.titleEdit)); 124 | appCompatEditText.perform(scrollTo(), click()); 125 | 126 | ViewInteraction appCompatEditText2 = onView( 127 | withId(R.id.titleEdit)); 128 | appCompatEditText2.perform(scrollTo(), replaceText("Title1"), closeSoftKeyboard()); 129 | 130 | ViewInteraction appCompatEditText14 = onView( 131 | allOf(withId(R.id.bodyEdit))); 132 | appCompatEditText14.perform(scrollTo(), replaceText("body1"), closeSoftKeyboard()); 133 | 134 | ViewInteraction upButton = onView( 135 | allOf(withContentDescription("Navigate up"), 136 | withParent(withId(R.id.toolbar)), 137 | isDisplayed())); 138 | upButton.perform(click()); 139 | 140 | logout(); 141 | 142 | floatingActionButton.perform(click()); 143 | appCompatEditText.perform(scrollTo(), click()); 144 | appCompatEditText2.perform(scrollTo(), replaceText("Title offline"), closeSoftKeyboard()); 145 | appCompatEditText14.perform(scrollTo(), replaceText("body offline"), closeSoftKeyboard()); 146 | 147 | signupin(); 148 | logout(); 149 | signupin(); 150 | 151 | onView(withText("body1")).check(matches(isDisplayed())); 152 | onView(withText("body offline")).check(matches(isDisplayed())); 153 | 154 | logout(); 155 | 156 | } 157 | 158 | 159 | private static Matcher childAtPosition( 160 | final Matcher parentMatcher, final int position) { 161 | 162 | return new TypeSafeMatcher() { 163 | @Override 164 | public void describeTo(Description description) { 165 | description.appendText("Child at position " + position + " in parent "); 166 | parentMatcher.describeTo(description); 167 | } 168 | 169 | @Override 170 | public boolean matchesSafely(View view) { 171 | ViewParent parent = view.getParent(); 172 | return parent instanceof ViewGroup && parentMatcher.matches(parent) 173 | && view.equals(((ViewGroup) parent).getChildAt(position)); 174 | } 175 | }; 176 | } 177 | 178 | } 179 | -------------------------------------------------------------------------------- /app/src/androidTest/java/org/standardnotes/notes/ProgressIdlingResource.java: -------------------------------------------------------------------------------- 1 | package org.standardnotes.notes; 2 | 3 | import android.support.test.espresso.IdlingResource; 4 | 5 | public class ProgressIdlingResource implements IdlingResource { 6 | 7 | private IdlingResource.ResourceCallback resourceCallback; 8 | private LoginActivity loginActivity; 9 | private LoginActivity.ProgressListener progressListener; 10 | 11 | public ProgressIdlingResource(LoginActivity activity){ 12 | loginActivity = activity; 13 | 14 | progressListener = new LoginActivity.ProgressListener() { 15 | 16 | @Override 17 | public void onProgressShown() { 18 | 19 | } 20 | 21 | @Override 22 | public void onProgressDismissed() { 23 | if (resourceCallback == null){ 24 | return ; 25 | } 26 | resourceCallback.onTransitionToIdle(); 27 | } 28 | }; 29 | 30 | loginActivity.setProgressListener(progressListener); 31 | } 32 | 33 | @Override 34 | public String getName() { 35 | return "My idling resource"; 36 | } 37 | 38 | @Override 39 | public boolean isIdleNow() { 40 | return !loginActivity.isInProgress(); 41 | } 42 | 43 | @Override 44 | public void registerIdleTransitionCallback(IdlingResource.ResourceCallback resourceCallback) { 45 | this.resourceCallback = resourceCallback; 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 32 | 36 | 40 | 43 | 47 | 50 | 51 | 56 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 69 | 70 | 71 | 75 | 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /app/src/main/java/org/standardnotes/notes/BaseActivity.kt: -------------------------------------------------------------------------------- 1 | package org.standardnotes.notes 2 | 3 | import android.content.SharedPreferences 4 | import android.os.Bundle 5 | import android.preference.PreferenceManager 6 | import android.support.v7.app.AppCompatActivity 7 | import android.view.WindowManager 8 | import android.widget.Toast 9 | 10 | open class BaseActivity : AppCompatActivity(), SharedPreferences.OnSharedPreferenceChangeListener { 11 | 12 | private val KEY_ENABLE_SCREENSHOT = "enable_screenshots" 13 | lateinit var prefs: SharedPreferences 14 | 15 | protected val app: SApplication = SApplication.instance 16 | 17 | override fun onCreate(savedInstanceState: Bundle?) { 18 | super.onCreate(savedInstanceState) 19 | 20 | prefs = PreferenceManager.getDefaultSharedPreferences(this) 21 | 22 | if (!isScreenshottingEnabled()) { 23 | window.setFlags(WindowManager.LayoutParams.FLAG_SECURE, 24 | WindowManager.LayoutParams.FLAG_SECURE) 25 | } 26 | } 27 | 28 | override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { 29 | when(key) { 30 | KEY_ENABLE_SCREENSHOT -> 31 | 32 | if (isScreenshottingEnabled()) enableScreenshots() else disableScreenshots() 33 | } 34 | } 35 | 36 | override fun onResume() { 37 | super.onResume() 38 | prefs.registerOnSharedPreferenceChangeListener(this) 39 | } 40 | 41 | override fun onPause() { 42 | super.onPause() 43 | prefs.unregisterOnSharedPreferenceChangeListener(this) 44 | } 45 | 46 | private fun disableScreenshots() { 47 | window.setFlags(WindowManager.LayoutParams.FLAG_SECURE, 48 | WindowManager.LayoutParams.FLAG_SECURE) 49 | Toast.makeText(this, R.string.toast_screenshots_disabled, Toast.LENGTH_SHORT).show() 50 | } 51 | 52 | private fun enableScreenshots() { 53 | window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE) 54 | Toast.makeText(this, R.string.toast_screenshots_enabled, Toast.LENGTH_SHORT).show() 55 | } 56 | 57 | private fun isScreenshottingEnabled(): Boolean { 58 | return prefs.getBoolean(KEY_ENABLE_SCREENSHOT, false) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /app/src/main/java/org/standardnotes/notes/DebugActivity.kt: -------------------------------------------------------------------------------- 1 | package org.standardnotes.notes 2 | 3 | import android.os.Bundle 4 | import kotlinx.android.synthetic.main.activity_debug.* 5 | import org.joda.time.DateTime 6 | import org.standardnotes.notes.comms.data.ContentType 7 | import org.standardnotes.notes.comms.data.Note 8 | import org.standardnotes.notes.comms.data.Reference 9 | import org.standardnotes.notes.comms.data.Tag 10 | import java.util.* 11 | 12 | /** 13 | * Created by carl on 22/02/17. 14 | */ 15 | class DebugActivity : BaseActivity() { 16 | 17 | override fun onCreate(savedInstanceState: Bundle?) { 18 | super.onCreate(savedInstanceState) 19 | 20 | setContentView(R.layout.activity_debug) 21 | 22 | create_large_data.setOnClickListener { 23 | 24 | fun createString(length: Int): String { 25 | val chars = "abcdefghijklmnopqrstuvwxyz ".toCharArray() 26 | val sb = StringBuilder(length) 27 | val random = Random() 28 | for (i in 0..length) { 29 | val c = chars[random.nextInt(chars.size)] 30 | sb.append(c) 31 | } 32 | return sb.toString() 33 | } 34 | 35 | fun createLargeNote(tagId: String?): Note { 36 | val ns = SApplication.instance.noteStore 37 | val n = Note() 38 | n.text = createString(50000) 39 | n.encItemKey = "123" 40 | n.uuid = UUID.randomUUID().toString() 41 | n.title = n.uuid 42 | val time = DateTime.now() 43 | n.createdAt = time 44 | n.updatedAt = time 45 | n.references = ArrayList() 46 | if (tagId != null) { 47 | val ref = Reference() 48 | ref.contentType = ContentType.Tag.toString() 49 | ref.uuid = tagId 50 | n.references.add(ref) 51 | } 52 | ns.putNote(n.uuid, n) 53 | 54 | return n 55 | } 56 | 57 | fun createTag(): Tag { 58 | val ns = SApplication.instance.noteStore 59 | val t = Tag() 60 | t.uuid = UUID.randomUUID().toString() 61 | t.title = t.uuid 62 | val time = DateTime.now() 63 | t.createdAt = time 64 | t.updatedAt = time 65 | t.references = ArrayList() 66 | ns.putTag(t.uuid, t) 67 | return t 68 | } 69 | 70 | if (packageName != "org.standardnotes.notes.buildfortest") { 71 | throw Exception("only run this when connected to a staging server") 72 | } 73 | 74 | // val ns = SApplication.instance.noteStore 75 | val tagIds = ArrayList() 76 | for (i in 1..20) { 77 | tagIds.add(createTag().uuid) 78 | } 79 | 80 | val rand = Random() 81 | for (i in 1..200) { 82 | createLargeNote(tagIds[rand.nextInt(tagIds.size)]) 83 | } 84 | 85 | 86 | } 87 | 88 | clear.setOnClickListener { 89 | SApplication.instance.noteStore.deleteAll() 90 | } 91 | } 92 | 93 | } -------------------------------------------------------------------------------- /app/src/main/java/org/standardnotes/notes/LoginActivity.kt: -------------------------------------------------------------------------------- 1 | package org.standardnotes.notes 2 | 3 | import android.content.Intent 4 | import android.content.pm.ActivityInfo 5 | import android.graphics.Paint 6 | import android.os.Bundle 7 | import android.support.transition.TransitionManager 8 | import android.support.v7.app.AlertDialog 9 | import android.support.v7.app.AppCompatActivity 10 | import android.text.Editable 11 | import android.text.TextWatcher 12 | import android.view.View 13 | import android.view.ViewGroup 14 | import android.view.WindowManager 15 | import android.view.inputmethod.EditorInfo 16 | import android.widget.TextView.OnEditorActionListener 17 | import android.widget.Toast 18 | import kotlinx.android.synthetic.main.activity_login.* 19 | import kotlinx.android.synthetic.main.view_password_confirm.view.* 20 | import org.standardnotes.notes.comms.Crypt 21 | import org.standardnotes.notes.comms.data.AuthParamsResponse 22 | import org.standardnotes.notes.comms.data.SigninResponse 23 | import org.standardnotes.notes.store.ValueStore 24 | import retrofit2.Call 25 | import retrofit2.Callback 26 | import retrofit2.Response 27 | 28 | class LoginActivity : AppCompatActivity() { 29 | 30 | var progressListener: ProgressListener? = null 31 | 32 | interface ProgressListener { 33 | fun onProgressShown() 34 | fun onProgressDismissed() 35 | } 36 | 37 | private fun notifyListener() { 38 | if (isInProgress()) progressListener?.onProgressShown() else progressListener?.onProgressDismissed() 39 | } 40 | 41 | fun isInProgress(): Boolean { 42 | return login_progress.visibility == View.VISIBLE 43 | } 44 | 45 | override fun onCreate(savedInstanceState: Bundle?) { 46 | super.onCreate(savedInstanceState) 47 | setContentView(R.layout.activity_login) 48 | 49 | val signInCallback: Callback = object : Callback { 50 | override fun onResponse(call: Call, response: Response) { 51 | if (response.isSuccessful) { 52 | startActivity(Intent(this@LoginActivity, MainActivity::class.java)) 53 | finish() 54 | } else { 55 | Toast.makeText(this@LoginActivity, getString(R.string.error_login), Toast.LENGTH_LONG).show() 56 | } 57 | hideProgress() 58 | } 59 | 60 | override fun onFailure(call: Call?, t: Throwable?) { 61 | Toast.makeText(this@LoginActivity, getString(R.string.error_login), Toast.LENGTH_LONG).show() 62 | hideProgress() 63 | } 64 | } 65 | 66 | email_sign_in_button.setOnClickListener { 67 | try { 68 | showProgress() 69 | SApplication.instance.valueStore.server = server.text.toString() 70 | SApplication.instance.resetComms() 71 | SApplication.instance.comms.api.getAuthParamsForEmail(email.text.toString()).enqueue(object : Callback { 72 | override fun onResponse(call: Call, response: Response) { 73 | try { 74 | val params = response.body() 75 | Crypt.doLogin(this@LoginActivity, email.text.toString(), password.text.toString(), params, signInCallback) 76 | ValueStore(this@LoginActivity).authParams = params 77 | } catch (e: Exception) { 78 | Toast.makeText(this@LoginActivity, getString(R.string.error_login), Toast.LENGTH_LONG).show() 79 | e.printStackTrace() 80 | } 81 | 82 | } 83 | 84 | override fun onFailure(call: Call?, t: Throwable?) { 85 | Toast.makeText(this@LoginActivity, getString(R.string.error_login), Toast.LENGTH_LONG).show() 86 | hideProgress() 87 | } 88 | }) 89 | } catch (e: Exception) { 90 | e.printStackTrace() 91 | Toast.makeText(this@LoginActivity, getString(R.string.error_login), Toast.LENGTH_LONG).show() 92 | hideProgress() 93 | } 94 | } 95 | sign_up.setOnClickListener { 96 | try { 97 | showSignUpDialog(View.inflate(this, R.layout.view_password_confirm, null), signInCallback) 98 | } catch (e: Exception) { 99 | e.printStackTrace() 100 | Toast.makeText(this@LoginActivity, getString(R.string.error_login), Toast.LENGTH_LONG).show() 101 | hideProgress() 102 | } 103 | } 104 | 105 | password.setOnEditorActionListener(OnEditorActionListener { v, actionId, event -> 106 | if (actionId == EditorInfo.IME_ACTION_DONE && email_sign_in_button.isEnabled) { 107 | email_sign_in_button.performClick() 108 | return@OnEditorActionListener true 109 | } 110 | false 111 | }) 112 | 113 | advancedPanel.setOnClickListener( { 114 | TransitionManager.beginDelayedTransition(advancedChild.parent as ViewGroup) 115 | advancedChild.visibility = if (advancedChild.visibility == View.GONE) View.VISIBLE else View.GONE 116 | }) 117 | advancedPanel.paintFlags = advancedPanel.paintFlags or Paint.UNDERLINE_TEXT_FLAG 118 | 119 | val textWatcher = object : TextWatcher { 120 | override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) { 121 | // 122 | } 123 | 124 | override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { 125 | checkValidInput() 126 | } 127 | 128 | override fun afterTextChanged(s: Editable?) { 129 | // 130 | } 131 | } 132 | server.addTextChangedListener(textWatcher) 133 | email.addTextChangedListener(textWatcher) 134 | password.addTextChangedListener(textWatcher) 135 | 136 | server.setText(ValueStore(this).server) 137 | email.requestFocus() 138 | } 139 | 140 | private fun showSignUpDialog(view : View, signInCallback : Callback) { 141 | SApplication.instance.valueStore.server = server.text.toString() 142 | SApplication.instance.resetComms() 143 | 144 | val dialog = AlertDialog.Builder(this) 145 | .setView(view) 146 | .setMessage(R.string.registration_confirmation) 147 | .setTitle(R.string.prompt_confirm_password) 148 | .setNegativeButton(R.string.action_cancel, null) 149 | .setPositiveButton(R.string.action_ok, { dialogInterface, i -> 150 | if (view.confirm_password.text.toString() == password.text.toString()) { 151 | showProgress() 152 | Crypt.doRegister(email.text.toString(), password.text.toString(), signInCallback) 153 | } else { 154 | Toast.makeText(this@LoginActivity, R.string.error_passwords_mismatch, Toast.LENGTH_LONG).show() 155 | } 156 | }) 157 | .show() 158 | dialog.window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE) 159 | val okbutton = dialog.getButton(AlertDialog.BUTTON_POSITIVE) 160 | view.confirm_password.addTextChangedListener(object : TextWatcher { 161 | override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) { 162 | // 163 | } 164 | 165 | override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { 166 | okbutton.isEnabled = !view.confirm_password.text.isBlank() 167 | } 168 | 169 | override fun afterTextChanged(s: Editable?) { 170 | // 171 | } 172 | }) 173 | view.confirm_password.setOnEditorActionListener(OnEditorActionListener { v, actionId, event -> 174 | if (actionId == EditorInfo.IME_ACTION_DONE && okbutton.isEnabled) { 175 | okbutton.performClick() 176 | return@OnEditorActionListener true 177 | } 178 | false 179 | }) 180 | okbutton.isEnabled = false 181 | } 182 | 183 | private fun checkValidInput() { 184 | val valid: Boolean = !server.text.isBlank() && !email.text.isBlank() && !password.text.isBlank() 185 | email_sign_in_button.isEnabled = valid 186 | sign_up.isEnabled = valid 187 | } 188 | 189 | private fun showProgress() { 190 | // HACK to prevent activity restarts and mess up the UI state when logging in 191 | requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LOCKED 192 | server.isEnabled = false 193 | email.isEnabled = false 194 | password.isEnabled = false 195 | email_sign_in_button.isEnabled = false 196 | sign_up.isEnabled = false 197 | login_progress.visibility = View.VISIBLE 198 | notifyListener() 199 | } 200 | 201 | private fun hideProgress() { 202 | requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED 203 | server.isEnabled = true 204 | email.isEnabled = true 205 | password.isEnabled = true 206 | checkValidInput() 207 | login_progress.visibility = View.GONE 208 | notifyListener() 209 | } 210 | 211 | } 212 | 213 | -------------------------------------------------------------------------------- /app/src/main/java/org/standardnotes/notes/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package org.standardnotes.notes 2 | 3 | import android.content.Intent 4 | import android.content.res.Configuration 5 | import android.os.Bundle 6 | import android.support.v7.app.ActionBarDrawerToggle 7 | import android.view.Menu 8 | import android.view.MenuItem 9 | import android.view.MotionEvent 10 | import kotlinx.android.synthetic.main.activity_main.* 11 | import kotlinx.android.synthetic.main.view_navigation_header.view.* 12 | import org.standardnotes.notes.comms.SyncManager 13 | import org.standardnotes.notes.frag.NoteListFragment 14 | 15 | class MainActivity : BaseActivity(), SyncManager.SyncListener { 16 | 17 | override fun onSyncStarted() { 18 | } 19 | 20 | override fun onSyncFailed() { 21 | onSyncCompleted() 22 | } 23 | 24 | override fun onSyncCompleted() { 25 | updateTagsMenu() // Update tags list 26 | noteListFragment().refreshNotesForTag(selectedTagId) // Update notes in fragment 27 | } 28 | 29 | override fun onSaveInstanceState(outState: Bundle?) { 30 | super.onSaveInstanceState(outState) 31 | outState!!.putString("tag", selectedTagId) 32 | } 33 | 34 | 35 | private lateinit var drawerToggle: ActionBarDrawerToggle 36 | private var selectedTagId = "" 37 | 38 | override fun onCreate(savedInstanceState: Bundle?) { 39 | super.onCreate(savedInstanceState) 40 | if (savedInstanceState != null && savedInstanceState.containsKey("tag")) { 41 | selectedTagId = savedInstanceState.getString("tag") 42 | } 43 | 44 | setContentView(R.layout.activity_main) 45 | setSupportActionBar(toolbar) 46 | 47 | drawerToggle = ActionBarDrawerToggle(this, drawer_layout, R.string.app_name, R.string.app_name) 48 | drawer_layout.addDrawerListener(drawerToggle) 49 | drawerToggle.isDrawerIndicatorEnabled = true 50 | supportActionBar?.setDisplayHomeAsUpEnabled(true) 51 | supportActionBar?.setHomeButtonEnabled(true) 52 | val header = drawer.inflateHeaderView(R.layout.view_navigation_header) 53 | val values = SApplication.instance.valueStore 54 | if (values.token != null) { 55 | header.main_account_server.text = values.server 56 | header.main_account_email.text = values.email 57 | header.main_account_title.setOnClickListener { 58 | startActivity(Intent(this, SettingsActivity::class.java)) 59 | } 60 | } else { 61 | header.main_account_server.text = getText(R.string.not_logged_in) 62 | header.main_account_email.text = "" 63 | header.main_account_title.setOnClickListener { 64 | startActivity(Intent(this, LoginActivity::class.java)) 65 | } 66 | } 67 | 68 | title = getString(R.string.app_name) 69 | 70 | var lastX: Int? = null 71 | var lastY: Int? = null 72 | fab.setOnTouchListener({ v, event -> 73 | if (event.action == MotionEvent.ACTION_UP) { 74 | lastX = event.rawX.toInt() 75 | lastY = event.rawY.toInt() 76 | } 77 | false 78 | }) 79 | fab.setOnClickListener { view -> 80 | noteListFragment().startNewNote(lastX!!, lastY!!, selectedTagId) 81 | } 82 | 83 | } 84 | 85 | fun noteListFragment(): NoteListFragment { 86 | return supportFragmentManager.findFragmentById(R.id.noteListFrag) as NoteListFragment 87 | } 88 | 89 | fun updateTagsMenu() { 90 | fun tagMenuItem(it: MenuItem, uuid: String) { 91 | it.setIcon(R.drawable.ic_tag) 92 | it.setOnMenuItemClickListener { 93 | drawer_layout.closeDrawers() 94 | selectedTagId = uuid 95 | updateTagsMenu() 96 | noteListFragment().refreshNotesForTag(selectedTagId) 97 | return@setOnMenuItemClickListener true 98 | } 99 | } 100 | drawer.menu.clear() 101 | drawer.inflateMenu(R.menu.drawer_tags) 102 | val tags = SApplication.instance.noteStore.getAllTags(false) 103 | val menu = drawer.menu.findItem(R.id.menu_account_tags).subMenu 104 | var allNotes = menu.add(getString(R.string.drawer_all_notes)) 105 | var selectedId = "" 106 | tagMenuItem(allNotes, "") 107 | var selected: MenuItem = allNotes 108 | for (tag in tags) { 109 | val item = menu.add(tag.title) 110 | if (selectedTagId.equals(tag.uuid)) { 111 | selected = item 112 | selectedId = tag.uuid 113 | } 114 | tagMenuItem(item, tag.uuid) 115 | } 116 | selected.isChecked = true 117 | toolbar.subtitle = selected.title 118 | selectedTagId = selectedId // In case selected tag wasn't found in list 119 | } 120 | 121 | override fun onResume() { 122 | super.onResume() 123 | SyncManager.startSyncTimer() 124 | SyncManager.subscribe(this) 125 | updateTagsMenu() 126 | } 127 | 128 | override fun onPause() { 129 | super.onPause() 130 | SyncManager.stopSyncTimer() 131 | SyncManager.unsubscribe(this) 132 | } 133 | 134 | override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { 135 | super.onActivityResult(requestCode, resultCode, data) 136 | } 137 | 138 | override fun onCreateOptionsMenu(menu: Menu): Boolean { 139 | super.onCreateOptionsMenu(menu) 140 | menuInflater.inflate(R.menu.logged_in, menu) 141 | if (BuildConfig.DEBUG) { 142 | menu.findItem(R.id.debug).isVisible = true 143 | } 144 | return true 145 | } 146 | 147 | override fun onOptionsItemSelected(item: MenuItem?): Boolean { 148 | if (drawerToggle.onOptionsItemSelected(item)) 149 | return true 150 | else 151 | when (item?.itemId) { 152 | R.id.settings -> startActivity(Intent(this, SettingsActivity::class.java)) 153 | R.id.search-> startActivity(Intent(this, SearchActivity::class.java)) 154 | R.id.debug -> startActivity(Intent(this, DebugActivity::class.java)) 155 | } 156 | return true 157 | } 158 | 159 | 160 | override fun onConfigurationChanged(newConfig: Configuration?) { 161 | super.onConfigurationChanged(newConfig) 162 | drawerToggle.syncState() 163 | } 164 | 165 | override fun onPostCreate(savedInstanceState: Bundle?) { 166 | super.onPostCreate(savedInstanceState) 167 | drawerToggle.syncState() 168 | } 169 | 170 | } 171 | -------------------------------------------------------------------------------- /app/src/main/java/org/standardnotes/notes/NoteActivity.kt: -------------------------------------------------------------------------------- 1 | package org.standardnotes.notes 2 | 3 | import android.animation.Animator 4 | import android.content.Intent 5 | import android.os.Bundle 6 | import android.support.v4.app.NavUtils 7 | import android.view.* 8 | import org.standardnotes.notes.frag.NoteFragment 9 | import org.standardnotes.notes.frag.NoteListFragment.Companion.EXTRA_NOTE_ID 10 | import org.standardnotes.notes.frag.NoteListFragment.Companion.EXTRA_X_COOR 11 | import org.standardnotes.notes.frag.NoteListFragment.Companion.EXTRA_Y_COOR 12 | 13 | class NoteActivity : BaseActivity() { 14 | 15 | val REVEAL_ANIM_DURATION = 200L 16 | var revealX: Int = 0 17 | var revealY: Int = 0 18 | 19 | override fun onCreate(savedInstanceState: Bundle?) { 20 | super.onCreate(savedInstanceState) 21 | overridePendingTransition(0, 0) 22 | 23 | revealX = intent.getIntExtra(EXTRA_X_COOR, 0) 24 | revealY = intent.getIntExtra(EXTRA_Y_COOR, 0) 25 | if (savedInstanceState == null) { 26 | val frag: NoteFragment = NoteFragment() 27 | frag.arguments = intent.extras 28 | 29 | supportFragmentManager.beginTransaction().replace(android.R.id.content, frag).commit() 30 | 31 | if (revealX != 0) { 32 | 33 | val rootView = findViewById(android.R.id.content) 34 | val viewTreeObserver = rootView.viewTreeObserver 35 | if (viewTreeObserver.isAlive) { 36 | rootView.visibility = View.INVISIBLE 37 | viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener { 38 | override fun onGlobalLayout() { 39 | circularReveal() 40 | rootView.viewTreeObserver.removeOnGlobalLayoutListener(this) 41 | } 42 | }) 43 | } 44 | } 45 | } 46 | if (intent.extras.getString(EXTRA_NOTE_ID) == null) { 47 | window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE) 48 | } 49 | } 50 | 51 | override fun onBackPressed() { 52 | overridePendingTransition(0, 0) 53 | if (revealX != 0) { 54 | circularHide() 55 | } else { 56 | super.onBackPressed() 57 | } 58 | } 59 | 60 | private fun circularReveal() { 61 | val rootView = findViewById(android.R.id.content) 62 | val circularReveal = ViewAnimationUtils.createCircularReveal(rootView, revealX, revealY, 0f, Math.max(rootView.width, rootView.height).toFloat()) 63 | circularReveal.duration = REVEAL_ANIM_DURATION 64 | rootView.visibility = View.VISIBLE 65 | circularReveal.start() 66 | } 67 | 68 | private fun circularHide() { 69 | val rootView = findViewById(android.R.id.content) 70 | val circularHide = ViewAnimationUtils.createCircularReveal(rootView, revealX, revealY, Math.max(rootView.width, rootView.height).toFloat(), 0f) 71 | circularHide.duration = REVEAL_ANIM_DURATION 72 | circularHide.addListener(object : Animator.AnimatorListener { 73 | override fun onAnimationStart(animation: Animator) { 74 | // 75 | } 76 | 77 | override fun onAnimationRepeat(animation: Animator) { 78 | // 79 | } 80 | 81 | override fun onAnimationEnd(animation: Animator) { 82 | rootView.visibility = View.GONE 83 | overridePendingTransition(0, 0) 84 | finish() 85 | } 86 | 87 | override fun onAnimationCancel(animation: Animator) { 88 | // 89 | } 90 | }) 91 | circularHide.start() 92 | } 93 | 94 | override fun onOptionsItemSelected(item: MenuItem): Boolean { 95 | if (item.itemId == android.R.id.home) { 96 | if (NavUtils.getParentActivityIntent(this) != null) { 97 | NavUtils.navigateUpTo(this, NavUtils.getParentActivityIntent(this).addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)) 98 | } else { 99 | onBackPressed() 100 | } 101 | return true 102 | } 103 | return super.onOptionsItemSelected(item) 104 | } 105 | } -------------------------------------------------------------------------------- /app/src/main/java/org/standardnotes/notes/SApplication.kt: -------------------------------------------------------------------------------- 1 | package org.standardnotes.notes 2 | 3 | import android.app.Application 4 | import android.content.Context 5 | import com.google.gson.Gson 6 | import com.google.gson.GsonBuilder 7 | import net.danlew.android.joda.JodaTimeAndroid 8 | import org.acra.ACRA 9 | import org.acra.ReportingInteractionMode 10 | import org.acra.annotation.ReportsCrashes 11 | import org.acra.sender.HttpSender 12 | import org.joda.time.DateTime 13 | import org.standardnotes.notes.comms.CommsManager 14 | import org.standardnotes.notes.store.NoteStore 15 | import org.standardnotes.notes.store.ValueStore 16 | 17 | 18 | @ReportsCrashes( 19 | httpMethod = HttpSender.Method.POST, 20 | reportType = HttpSender.Type.JSON, 21 | formUri = "https://deftelf.cloudant.com/acra-notes/_design/acra-storage/_update/report", 22 | formUriBasicAuthLogin = "mrsuesendittlavessightio", 23 | formUriBasicAuthPassword = "9ff4e02277b4ae4be2102c475d6299f852260277", 24 | sendReportsInDevMode = false, 25 | mode = ReportingInteractionMode.SILENT 26 | ) 27 | class SApplication : Application() { 28 | private var commsActual: CommsManager? = null 29 | 30 | val comms: CommsManager 31 | get() { 32 | if (commsActual == null) { 33 | val server = valueStore.server 34 | if (server != null) 35 | commsActual = CommsManager(server) 36 | } 37 | return commsActual!! // Should not be called before there's a valueStore.server 38 | } 39 | val valueStore: ValueStore by lazy { ValueStore(this) } 40 | val gson: Gson by lazy { GsonBuilder().registerTypeAdapter(DateTime::class.java, CommsManager.DateTimeDeserializer()).setPrettyPrinting().create() } 41 | val noteStore: NoteStore by lazy { NoteStore() } 42 | 43 | fun resetComms() { 44 | commsActual = null 45 | } 46 | 47 | override fun onCreate() { 48 | super.onCreate() 49 | instance = this 50 | JodaTimeAndroid.init(this) 51 | } 52 | 53 | fun clearData() { 54 | valueStore.clear() 55 | noteStore.deleteAll() 56 | } 57 | 58 | override fun attachBaseContext(base: Context?) { 59 | super.attachBaseContext(base) 60 | ACRA.init(this) 61 | } 62 | 63 | companion object { 64 | 65 | lateinit var instance: SApplication 66 | private set 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /app/src/main/java/org/standardnotes/notes/SearchActivity.kt: -------------------------------------------------------------------------------- 1 | package org.standardnotes.notes 2 | 3 | import android.content.Intent 4 | import android.os.Bundle 5 | import android.support.v4.app.NavUtils 6 | import android.support.v7.app.AppCompatActivity 7 | import android.view.Menu 8 | import android.view.MenuItem 9 | import android.widget.SearchView 10 | import org.standardnotes.notes.frag.NoteListFragment 11 | 12 | 13 | class SearchActivity : AppCompatActivity() { 14 | 15 | var query: String = "" 16 | 17 | override fun onCreate(savedInstanceState: Bundle?) { 18 | super.onCreate(savedInstanceState) 19 | setContentView(R.layout.activity_search) 20 | 21 | title = "" 22 | supportActionBar?.setDisplayHomeAsUpEnabled(true) 23 | query = savedInstanceState?.getString("query", "") ?: "" 24 | noteListFragment().refreshNotesForSearch(query) 25 | } 26 | 27 | override fun onSaveInstanceState(outState: Bundle) { 28 | super.onSaveInstanceState(outState) 29 | outState.putString("query", query) 30 | } 31 | 32 | override fun onCreateOptionsMenu(menu: Menu): Boolean { 33 | val inflater = menuInflater 34 | inflater.inflate(R.menu.search, menu) 35 | val searchView = menu.findItem(R.id.search).actionView as SearchView 36 | searchView.setQuery(query, false) 37 | searchView.queryHint = getText(R.string.search) 38 | searchView.maxWidth = Int.MAX_VALUE 39 | searchView.setIconifiedByDefault(false) 40 | searchView.requestFocus() 41 | searchView.setOnQueryTextListener(object: SearchView.OnQueryTextListener { 42 | override fun onQueryTextSubmit(newText: String): Boolean { 43 | query = newText 44 | noteListFragment().refreshNotesForSearch(query) 45 | return true 46 | } 47 | 48 | override fun onQueryTextChange(newText: String): Boolean { 49 | query = newText 50 | noteListFragment().refreshNotesForSearch(newText) 51 | return true 52 | } 53 | 54 | }) 55 | return true 56 | } 57 | 58 | override fun onOptionsItemSelected(item: MenuItem): Boolean { 59 | if (item.itemId == android.R.id.home) { 60 | if (NavUtils.getParentActivityIntent(this) != null) { 61 | NavUtils.navigateUpTo(this, NavUtils.getParentActivityIntent(this).addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)) 62 | } else { 63 | onBackPressed() 64 | } 65 | return true 66 | } 67 | return super.onOptionsItemSelected(item) 68 | } 69 | 70 | fun noteListFragment(): NoteListFragment { 71 | return supportFragmentManager.findFragmentById(R.id.noteListFrag) as NoteListFragment 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /app/src/main/java/org/standardnotes/notes/SettingsActivity.kt: -------------------------------------------------------------------------------- 1 | package org.standardnotes.notes 2 | 3 | import android.content.Intent 4 | import android.net.Uri 5 | import android.os.Bundle 6 | import android.support.design.widget.Snackbar 7 | import android.support.v4.app.NavUtils 8 | import android.support.v7.app.AlertDialog 9 | import android.support.v7.preference.PreferenceFragmentCompat 10 | import android.view.MenuItem 11 | import android.view.View 12 | import android.widget.Toast 13 | import kotlinx.android.synthetic.main.activity_settings.* 14 | import org.standardnotes.notes.comms.ExportUtil 15 | import org.standardnotes.notes.comms.SyncManager 16 | 17 | class SettingsActivity : BaseActivity() { 18 | 19 | override fun onCreate(savedInstanceState: Bundle?) { 20 | super.onCreate(savedInstanceState) 21 | 22 | setContentView(R.layout.activity_settings) 23 | supportActionBar?.setDisplayHomeAsUpEnabled(true) 24 | 25 | supportFragmentManager.beginTransaction().replace(R.id.list, 26 | SettingsFragment()).commit() 27 | export.setOnClickListener { exportData() } 28 | feedback.setOnClickListener { startFeedbackIntent() } 29 | learnMore.setOnClickListener { 30 | val url = "https://standardnotes.org/" 31 | val intent = Intent(Intent.ACTION_VIEW) 32 | intent.data = Uri.parse(url) 33 | startActivity(intent) 34 | } 35 | logout.setOnClickListener { 36 | AlertDialog.Builder(this) 37 | .setTitle(R.string.action_logout) 38 | .setMessage(R.string.prompt_are_you_sure) 39 | .setPositiveButton(R.string.action_delete, { dialogInterface, i -> 40 | logout() 41 | }) 42 | .setNegativeButton(R.string.action_cancel, null) 43 | .show() 44 | } 45 | login.visibility = if (app.valueStore.token == null) View.VISIBLE else View.GONE 46 | login.setOnClickListener { startActivity(Intent(this, LoginActivity::class.java)) } 47 | version.text = "v" + packageManager.getPackageInfo(packageName, 0).versionName 48 | } 49 | 50 | 51 | override fun onOptionsItemSelected(item: MenuItem): Boolean { 52 | when (item.itemId) { 53 | android.R.id.home -> { 54 | NavUtils.navigateUpFromSameTask(this) 55 | return true 56 | } 57 | } 58 | return super.onOptionsItemSelected(item) 59 | } 60 | 61 | override fun onDestroy() { 62 | super.onDestroy() 63 | // TODO does this make sense here? 64 | ExportUtil.clearExports(this) 65 | } 66 | 67 | private fun exportData() { 68 | val listener: ExportUtil.ExportListener = object : ExportUtil.ExportListener { 69 | override fun onExportFailed() { 70 | Snackbar.make(root, R.string.error_fail_export, Snackbar.LENGTH_SHORT).show() 71 | } 72 | } 73 | if (radio_dec.isChecked) 74 | ExportUtil.exportDecrypted(this, listener) 75 | else 76 | ExportUtil.exportEncrypted(this, listener) 77 | } 78 | 79 | fun startFeedbackIntent() { 80 | val intent = Intent(Intent.ACTION_SENDTO) 81 | intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK 82 | intent.data = Uri.parse("mailto:") // only email apps should handle this 83 | intent.putExtra(Intent.EXTRA_EMAIL, arrayOf(getString(R.string.feedback_email))) 84 | intent.putExtra(Intent.EXTRA_SUBJECT, getString(R.string.feedback_subject)) 85 | if (intent.resolveActivity(packageManager) != null) { 86 | startActivity(intent) 87 | } else { 88 | Toast.makeText(this, R.string.toast_no_email, Toast.LENGTH_LONG).show() 89 | } 90 | 91 | } 92 | 93 | private fun logout() { 94 | SApplication.instance.clearData() 95 | SyncManager.stopSyncTimer() 96 | startActivity(Intent(this, StarterActivity::class.java)) 97 | finishAffinity() 98 | } 99 | 100 | 101 | class SettingsFragment : PreferenceFragmentCompat() { 102 | 103 | override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { 104 | addPreferencesFromResource(R.xml.settings) 105 | } 106 | } 107 | } -------------------------------------------------------------------------------- /app/src/main/java/org/standardnotes/notes/StarterActivity.kt: -------------------------------------------------------------------------------- 1 | package org.standardnotes.notes 2 | 3 | import android.content.Intent 4 | import android.os.Bundle 5 | import android.support.v7.app.AppCompatActivity 6 | import org.standardnotes.notes.frag.EXTRA_TEXT 7 | 8 | class StarterActivity : AppCompatActivity() { 9 | 10 | override fun onCreate(savedInstanceState: Bundle?) { 11 | super.onCreate(savedInstanceState) 12 | 13 | val mainIntent = Intent(this, MainActivity::class.java) 14 | mainIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) 15 | startActivity(mainIntent) 16 | if (intent.action == Intent.ACTION_SEND && intent.type == "text/plain") { 17 | var text = intent.getStringExtra(Intent.EXTRA_TEXT) 18 | if (text != null) { 19 | val intent = Intent(this, NoteActivity::class.java) 20 | intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) 21 | intent.putExtra(EXTRA_TEXT, text) 22 | startActivity(intent) 23 | } 24 | } 25 | finish() 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/src/main/java/org/standardnotes/notes/TagListActivity.kt: -------------------------------------------------------------------------------- 1 | package org.standardnotes.notes 2 | 3 | import android.app.Activity 4 | import android.content.Intent 5 | import android.os.Bundle 6 | import android.support.v7.app.AlertDialog 7 | import android.support.v7.widget.LinearLayoutManager 8 | import android.support.v7.widget.PopupMenu 9 | import android.support.v7.widget.RecyclerView 10 | import android.text.Editable 11 | import android.text.TextWatcher 12 | import android.view.LayoutInflater 13 | import android.view.View 14 | import android.view.ViewGroup 15 | import android.view.WindowManager 16 | import android.view.inputmethod.EditorInfo 17 | import android.widget.CheckBox 18 | import android.widget.EditText 19 | import android.widget.TextView 20 | import com.google.gson.reflect.TypeToken 21 | import kotlinx.android.synthetic.main.activity_tags.* 22 | import org.joda.time.DateTime 23 | import org.standardnotes.notes.comms.Crypt 24 | import org.standardnotes.notes.comms.SyncManager 25 | import org.standardnotes.notes.comms.data.Tag 26 | import java.util.* 27 | 28 | const val EXTRA_TAGS: String = "refTags" 29 | 30 | class TagListActivity : BaseActivity() { 31 | 32 | lateinit var selectedTags: Set 33 | lateinit var tags: List 34 | 35 | override fun onCreate(savedInstanceState: Bundle?) { 36 | super.onCreate(savedInstanceState) 37 | setContentView(R.layout.activity_tags) 38 | 39 | val listType = object : TypeToken>() {}.type 40 | val selectedTagsList: List = app.gson.fromJson( 41 | if (savedInstanceState == null) intent.getStringExtra(EXTRA_TAGS) else savedInstanceState.getString(EXTRA_TAGS), 42 | listType) 43 | selectedTags = selectedTagsList.toSet() 44 | tags = getUndeletedTags() 45 | 46 | list.adapter = Adapter() 47 | list.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false) 48 | 49 | fab.setOnClickListener { 50 | val layout = LayoutInflater.from(this).inflate(R.layout.view_new_tag, null, false) 51 | val input = layout.findViewById(R.id.tag) as EditText 52 | val dialog = AlertDialog.Builder(this).setTitle(R.string.prompt_new_tag) 53 | .setNegativeButton(R.string.action_cancel, null) 54 | .setPositiveButton(R.string.action_ok, { dialogInterface, i -> 55 | val newTag = newTag() 56 | newTag.title = input.text.toString() 57 | newTag.dirty = true 58 | app.noteStore.putTag(newTag.uuid, newTag) 59 | SyncManager.sync() 60 | tags = getUndeletedTags() 61 | list.adapter.notifyDataSetChanged() 62 | }) 63 | .setView(layout) 64 | .show() 65 | dialog.window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE) 66 | val okbutton = dialog.getButton(AlertDialog.BUTTON_POSITIVE) 67 | input.addTextChangedListener(object : TextWatcher { 68 | override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) { 69 | // 70 | } 71 | 72 | override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { 73 | okbutton.isEnabled = !input.text.isBlank() 74 | } 75 | 76 | override fun afterTextChanged(s: Editable?) { 77 | // 78 | } 79 | }) 80 | input.setOnEditorActionListener(TextView.OnEditorActionListener { v, actionId, event -> 81 | if (actionId == EditorInfo.IME_ACTION_DONE && okbutton.isEnabled) { 82 | okbutton.performClick() 83 | return@OnEditorActionListener true 84 | } 85 | false 86 | }) 87 | okbutton.isEnabled = false 88 | } 89 | } 90 | 91 | override fun onSaveInstanceState(outState: Bundle) { 92 | super.onSaveInstanceState(outState) 93 | outState.putString(EXTRA_TAGS, app.gson.toJson(selectedTags.toList())) 94 | } 95 | 96 | override fun finish() { 97 | val data = Intent() 98 | data.putExtra(EXTRA_TAGS, app.gson.toJson(selectedTags.toList())) 99 | setResult(Activity.RESULT_OK, data) 100 | super.finish() 101 | } 102 | 103 | inner class TagHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { 104 | 105 | var tag: Tag? = null 106 | get 107 | set(value) { 108 | field = value 109 | title.text = tag?.title 110 | title.setOnCheckedChangeListener(null) 111 | title.isChecked = selectedTags.filter { it.uuid == tag?.uuid }.isNotEmpty() 112 | title.setOnCheckedChangeListener { compoundButton, b -> 113 | if (compoundButton.isChecked) { 114 | selectedTags += tag!! 115 | } else { 116 | selectedTags = selectedTags.filterNot { it.uuid == tag?.uuid }.toSet() 117 | } 118 | } 119 | } 120 | private val title: CheckBox = itemView.findViewById(R.id.title) as CheckBox 121 | 122 | init { 123 | itemView.setOnLongClickListener { 124 | val popup = PopupMenu(this@TagListActivity, itemView) 125 | popup.menu.add(getString(R.string.action_delete)) 126 | val tagIdToDelete = tag!!.uuid // possible for view's assigned note to change while popup is displayed if sync happens! 127 | popup.setOnMenuItemClickListener { 128 | AlertDialog.Builder(this@TagListActivity) 129 | .setTitle(R.string.title_delete_confirm) 130 | .setMessage(R.string.prompt_are_you_sure) 131 | .setPositiveButton(R.string.action_delete, { dialogInterface, i -> 132 | SApplication.instance.noteStore.deleteItem(tagIdToDelete) 133 | tags = getUndeletedTags() 134 | val remainingTagIds = tags.map { it.uuid } 135 | selectedTags = selectedTags.filter { remainingTagIds.contains(it.uuid) }.toSet() // remove any selected and deleted tags 136 | list.adapter.notifyDataSetChanged() 137 | SyncManager.sync() 138 | }) 139 | .setNegativeButton(R.string.action_cancel, null) 140 | .show() 141 | true 142 | } 143 | popup.show() 144 | true 145 | } 146 | } 147 | 148 | } 149 | 150 | private fun getUndeletedTags() = app.noteStore.getAllTags(false).filter { !it.deleted } 151 | 152 | inner class Adapter : RecyclerView.Adapter() { 153 | 154 | override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): TagHolder { 155 | return TagHolder(LayoutInflater.from(this@TagListActivity).inflate(R.layout.item_tag, parent, false)) 156 | } 157 | 158 | override fun getItemCount(): Int { 159 | return tags.count() 160 | } 161 | 162 | override fun onBindViewHolder(holder: TagHolder, position: Int) { 163 | val tag = tags[position] 164 | holder.tag = tag 165 | } 166 | 167 | } 168 | 169 | fun newTag(): Tag { 170 | // Move to a factory 171 | val tag = Tag() 172 | tag.uuid = UUID.randomUUID().toString() 173 | tag.createdAt = DateTime.now() 174 | tag.updatedAt = tag.createdAt 175 | return tag 176 | } 177 | 178 | } -------------------------------------------------------------------------------- /app/src/main/java/org/standardnotes/notes/UiUtil.kt: -------------------------------------------------------------------------------- 1 | package org.standardnotes.notes 2 | 3 | import android.app.Activity 4 | import android.content.Context 5 | import android.support.v7.app.AlertDialog 6 | import android.util.TypedValue 7 | import android.view.inputmethod.InputMethodManager 8 | import android.widget.EditText 9 | 10 | fun Int.dpToPixels(): Int { 11 | val metrics = SApplication.instance.resources.displayMetrics 12 | return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, this.toFloat(), metrics).toInt() 13 | } 14 | 15 | fun EditText.showKeyboard() { 16 | (this.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager).showSoftInput(this, InputMethodManager.SHOW_IMPLICIT) 17 | } 18 | 19 | fun Activity.showErrorDialog(title: CharSequence, message: CharSequence) { 20 | AlertDialog.Builder(this) 21 | .setIcon(android.R.drawable.stat_notify_error) 22 | .setMessage(message) 23 | .setTitle(title) 24 | .setPositiveButton(R.string.action_ok, null) 25 | .show() 26 | } -------------------------------------------------------------------------------- /app/src/main/java/org/standardnotes/notes/comms/CommsManager.kt: -------------------------------------------------------------------------------- 1 | package org.standardnotes.notes.comms 2 | 3 | import com.google.gson.* 4 | import okhttp3.OkHttpClient 5 | import okhttp3.logging.HttpLoggingInterceptor 6 | import org.joda.time.DateTime 7 | import org.joda.time.DateTimeZone 8 | import org.joda.time.format.ISODateTimeFormat 9 | import org.standardnotes.notes.SApplication 10 | import retrofit2.Retrofit 11 | import retrofit2.converter.gson.GsonConverterFactory 12 | import java.lang.reflect.Type 13 | 14 | class CommsManager(serverBaseUrl: String) { 15 | 16 | private val retrofit: Retrofit 17 | private val okHttpClient: OkHttpClient 18 | val api: ServerApi 19 | 20 | init { 21 | val logger = HttpLoggingInterceptor() 22 | logger.level = HttpLoggingInterceptor.Level.BODY 23 | okHttpClient = OkHttpClient.Builder() 24 | .addNetworkInterceptor(logger) 25 | .addInterceptor { chain -> 26 | // Add auth to header if we have a token 27 | val original = chain.request() 28 | val requestBuilder = original.newBuilder() 29 | if (SApplication.instance.valueStore.token != null) { 30 | requestBuilder.header("Authorization", "Bearer " + SApplication.instance.valueStore.token) 31 | } 32 | val request = requestBuilder.build() 33 | chain.proceed(request) 34 | } 35 | .build() 36 | retrofit = Retrofit.Builder() 37 | .baseUrl(serverBaseUrl) 38 | .client(okHttpClient) 39 | .addConverterFactory(GsonConverterFactory.create(SApplication.instance.gson)) 40 | .build() 41 | api = retrofit.create(ServerApi::class.java) 42 | } 43 | 44 | class DateTimeDeserializer : JsonDeserializer, JsonSerializer { 45 | 46 | @Throws(JsonParseException::class) 47 | override fun deserialize(je: JsonElement, type: Type, 48 | jdc: JsonDeserializationContext): DateTime? { 49 | if (je.asString.isEmpty()) { 50 | return null 51 | } else { 52 | return DATE_TIME_FORMATTER.parseDateTime(je.asString) 53 | } 54 | } 55 | 56 | override fun serialize(src: DateTime?, typeOfSrc: Type, 57 | context: JsonSerializationContext): JsonElement { 58 | return JsonPrimitive(if (src == null) "" else DATE_TIME_FORMATTER.print(src)) 59 | } 60 | 61 | companion object { 62 | val DATE_TIME_FORMATTER: org.joda.time.format.DateTimeFormatter = ISODateTimeFormat.dateTime().withZone(DateTimeZone.UTC) 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /app/src/main/java/org/standardnotes/notes/comms/ExportUtil.kt: -------------------------------------------------------------------------------- 1 | package org.standardnotes.notes.comms 2 | 3 | import android.app.Activity 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.support.v4.app.ShareCompat 7 | import android.support.v4.content.FileProvider 8 | import org.standardnotes.notes.R 9 | import org.standardnotes.notes.SApplication 10 | import org.standardnotes.notes.comms.data.* 11 | import org.standardnotes.notes.store.ValueStore 12 | import java.io.File 13 | import java.io.FileOutputStream 14 | import java.io.IOException 15 | import java.text.SimpleDateFormat 16 | import java.util.* 17 | 18 | 19 | object ExportUtil { 20 | 21 | interface ExportListener { 22 | fun onExportFailed() 23 | } 24 | 25 | fun exportEncrypted(activity: Activity, listener: ExportListener?) { 26 | val encryptionVersion = SApplication.instance.valueStore.protocolVersion 27 | val exportItems = ExportItems() 28 | SApplication.instance.noteStore.notesList.map { Crypt.encrypt(it, encryptionVersion) }.forEach { exportItems.items.add(it) } 29 | SApplication.instance.noteStore.getAllTags(true).map { Crypt.encrypt(it, encryptionVersion) }.forEach { exportItems.items.add(it) } 30 | exportItems.authParams = ValueStore(activity).authParams 31 | 32 | export(activity, exportItems, listener) 33 | } 34 | 35 | fun exportDecrypted(activity: Activity, listener: ExportListener?) { 36 | val exportItems = ExportItems() 37 | SApplication.instance.noteStore.notesList.map { getPlaintextItem(it, ContentType.Note) }.forEach { exportItems.items.add(it) } 38 | SApplication.instance.noteStore.getAllTags(true).map { getPlaintextItem(it, ContentType.Tag) }.forEach { exportItems.items.add(it) } 39 | 40 | export(activity, exportItems, listener) 41 | } 42 | 43 | fun clearExports(context: Context) { 44 | val directory = File(context.filesDir.toString() + "/export") 45 | if (directory.isDirectory) { 46 | val children = directory.list() 47 | children.indices 48 | .map { File(directory, children[it]) } 49 | .forEach { it.delete() } 50 | } 51 | } 52 | 53 | private fun export(activity: Activity, exportItems: ExportItems, listener: ExportListener?) { 54 | val jsonString = SApplication.instance.gson.toJson(exportItems) 55 | val path = writeToFile(activity, jsonString) 56 | if (path != null) { 57 | shareFile(activity, path) 58 | } else { 59 | listener?.onExportFailed() 60 | } 61 | } 62 | 63 | private fun getPlaintextItem(source: EncryptableItem, contentType: ContentType): PlaintextItem { 64 | val content: EncryptableItem 65 | if (contentType == ContentType.Note) { 66 | content = Note() 67 | content.title = (source as Note).title 68 | content.text = source.text 69 | } else { 70 | content = Tag() 71 | content.title = (source as Tag).title 72 | } 73 | content.references = source.references 74 | content.dirty = null 75 | content.deleted = null 76 | 77 | val plaintextItem = PlaintextItem() 78 | plaintextItem.content = content 79 | plaintextItem.contentType = contentType.name 80 | plaintextItem.uuid = source.uuid 81 | plaintextItem.createdAt = source.createdAt 82 | plaintextItem.updatedAt = source.updatedAt 83 | 84 | return plaintextItem 85 | } 86 | 87 | private fun writeToFile(activity: Activity, data: String): String? { 88 | val path: String 89 | try { 90 | val filename = "SN Archive - " + SimpleDateFormat("EEE MMM d yyyy HH-mm-ss Z (z)", Locale.getDefault()).format(System.currentTimeMillis()) + ".txt" 91 | path = activity.filesDir.toString() + "/export/" + filename 92 | val file = File(path) 93 | file.parentFile.mkdirs() 94 | if (!file.exists()) { 95 | file.createNewFile() 96 | } 97 | val fos = FileOutputStream(file) 98 | fos.write(data.toByteArray()) 99 | fos.close() 100 | } catch (e: IOException) { 101 | return null 102 | } 103 | return path 104 | } 105 | 106 | private fun shareFile(activity: Activity, path: String) { 107 | val fileUri = FileProvider.getUriForFile(activity, "org.standardnotes.notes.fileprovider", File(path)) 108 | val shareIntent = ShareCompat.IntentBuilder.from(activity).setStream(fileUri).intent 109 | shareIntent.data = fileUri 110 | shareIntent.type = "message/rfc822" 111 | shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) 112 | activity.startActivity(Intent.createChooser(shareIntent, activity.getString(R.string.title_share_file))) 113 | } 114 | 115 | } 116 | -------------------------------------------------------------------------------- /app/src/main/java/org/standardnotes/notes/comms/ServerApi.java: -------------------------------------------------------------------------------- 1 | package org.standardnotes.notes.comms; 2 | 3 | import org.standardnotes.notes.comms.data.AuthParamsResponse; 4 | import org.standardnotes.notes.comms.data.SigninResponse; 5 | import org.standardnotes.notes.comms.data.SyncItems; 6 | import org.standardnotes.notes.comms.data.UploadSyncItems; 7 | 8 | import retrofit2.Call; 9 | import retrofit2.http.Body; 10 | import retrofit2.http.Field; 11 | import retrofit2.http.FormUrlEncoded; 12 | import retrofit2.http.GET; 13 | import retrofit2.http.POST; 14 | import retrofit2.http.Query; 15 | 16 | public interface ServerApi { 17 | 18 | @GET("/api/auth/params/") 19 | Call getAuthParamsForEmail(@Query("email") String email); 20 | 21 | @FormUrlEncoded 22 | @POST("/api/auth/sign_in/") 23 | Call signin(@Field("email") String email, @Field("password") String hashedPassword); 24 | 25 | @FormUrlEncoded 26 | @POST("/api/auth/") 27 | Call register(@Field("email") String email, @Field("password") String hashedPassword, 28 | @Field("pw_salt") String pwSalt, @Field("version") String version, 29 | @Field("pw_cost") Integer pwCost); 30 | 31 | @POST("/api/items/sync/") 32 | Call sync(@Body UploadSyncItems data); 33 | } 34 | -------------------------------------------------------------------------------- /app/src/main/java/org/standardnotes/notes/comms/SyncManager.kt: -------------------------------------------------------------------------------- 1 | package org.standardnotes.notes.comms 2 | 3 | import android.os.Handler 4 | import android.util.Log 5 | import org.acra.ACRA 6 | import org.standardnotes.notes.SApplication 7 | import org.standardnotes.notes.comms.data.AuthParamsResponse 8 | import org.standardnotes.notes.comms.data.Note 9 | import org.standardnotes.notes.comms.data.SyncItems 10 | import org.standardnotes.notes.comms.data.UploadSyncItems 11 | import retrofit2.Call 12 | import retrofit2.Callback 13 | import retrofit2.Response 14 | import java.lang.ref.WeakReference 15 | 16 | object SyncManager { 17 | 18 | interface SyncListener { 19 | fun onSyncStarted() 20 | 21 | fun onSyncCompleted() 22 | 23 | fun onSyncFailed() 24 | } 25 | 26 | val TAG = SyncManager.javaClass.simpleName 27 | 28 | private val SYNC_INTERVAL = 30000L 29 | private val syncHandler: Handler = Handler() 30 | private val syncRunnable: Runnable = object : Runnable { 31 | override fun run() { 32 | SyncManager.sync() 33 | syncHandler.postDelayed(this, SYNC_INTERVAL) 34 | } 35 | } 36 | 37 | private var syncCall: Call? = null 38 | private val syncListeners: MutableList> = mutableListOf() 39 | 40 | fun subscribe(listener: SyncListener) { 41 | syncListeners 42 | .filter { it.get() == listener } 43 | .forEach { return } 44 | syncListeners.add(WeakReference(listener)) 45 | } 46 | 47 | fun unsubscribe(listener: SyncListener) { 48 | val thisListeners = syncListeners 49 | .filter { listener == it.get() } 50 | if (thisListeners.isEmpty()) { 51 | Log.w(TAG, "Cannot unsusbscribe, $listener is not subscribed") 52 | } else { 53 | thisListeners 54 | .forEach { syncListeners.remove(it) } 55 | } 56 | } 57 | 58 | @Synchronized fun sync() { 59 | 60 | val existingCall = syncCall 61 | if (existingCall != null && !existingCall.isCanceled) { 62 | existingCall.cancel() 63 | } 64 | 65 | if (SApplication.instance.valueStore.token == null) 66 | return // Not logged in, sync impossible 67 | 68 | var iter = syncListeners.iterator() 69 | while (iter.hasNext()) { 70 | val listening = iter.next() 71 | if (listening.get() == null) { 72 | iter.remove() 73 | Log.w(TAG, "SyncListener is null, you may be missing a call to unsubscribe()") 74 | } else { 75 | listening.get()!!.onSyncStarted() 76 | } 77 | } 78 | 79 | val uploadSyncItems = UploadSyncItems() 80 | uploadSyncItems.syncToken = SApplication.instance.valueStore.syncToken 81 | 82 | val encryptionVersion = SApplication.instance.valueStore.protocolVersion 83 | 84 | val dirtyItems = SApplication.instance.noteStore.toSave 85 | dirtyItems.map { Crypt.encrypt(it, encryptionVersion) }.forEach { uploadSyncItems.items.add(it) } 86 | 87 | syncCall = SApplication.instance.comms.api.sync(uploadSyncItems) 88 | syncCall?.enqueue(object : Callback { 89 | override fun onResponse(call: Call, response: Response) { 90 | 91 | if (response.isSuccessful) { 92 | val putItemErrors = SApplication.instance.noteStore.putItems(response.body()) 93 | 94 | if (putItemErrors.isEmpty()) { 95 | iter = syncListeners.iterator() 96 | while (iter.hasNext()) { 97 | val listening = iter.next() 98 | if (listening.get() == null) { 99 | iter.remove() 100 | Log.w(TAG, "SyncListener is null, you may be missing a call to unsubscribe()") 101 | } else { 102 | listening.get()!!.onSyncCompleted() 103 | } 104 | } 105 | 106 | syncCall = null 107 | } else { 108 | putItemErrors.forEach { ACRA.getErrorReporter().handleException(it) } 109 | onFailure(call, Exception("Sync error")) 110 | } 111 | } else { 112 | val ex = Exception("sync failed " + response.errorBody().string()) 113 | ACRA.getErrorReporter().handleException(ex) 114 | onFailure(call, ex) 115 | } 116 | } 117 | 118 | override fun onFailure(call: Call, t: Throwable) { 119 | iter = syncListeners.iterator() 120 | while (iter.hasNext()) { 121 | val listening = iter.next() 122 | if (listening.get() == null) { 123 | iter.remove() 124 | Log.w(TAG, "SyncListener is null, you may be missing a call to unsubscribe()") 125 | } else { 126 | listening.get()!!.onSyncFailed() 127 | } 128 | } 129 | 130 | syncCall = null 131 | } 132 | }) 133 | } 134 | 135 | fun startSyncTimer() { 136 | syncHandler.removeCallbacks(syncRunnable) 137 | syncHandler.postDelayed(syncRunnable, SYNC_INTERVAL) 138 | } 139 | 140 | fun stopSyncTimer() { 141 | syncHandler.removeCallbacks(syncRunnable) 142 | } 143 | 144 | } -------------------------------------------------------------------------------- /app/src/main/java/org/standardnotes/notes/comms/data/AuthParamsResponse.java: -------------------------------------------------------------------------------- 1 | 2 | package org.standardnotes.notes.comms.data; 3 | 4 | import javax.annotation.Generated; 5 | import com.google.gson.annotations.Expose; 6 | import com.google.gson.annotations.SerializedName; 7 | 8 | 9 | /** 10 | * 11 | *

12 | * 13 | * 14 | */ 15 | @Generated("org.jsonschema2pojo") 16 | public class AuthParamsResponse { 17 | 18 | @SerializedName("pw_cost") 19 | @Expose 20 | private Integer pwCost; 21 | @SerializedName("pw_salt") 22 | @Expose 23 | private String pwSalt; 24 | @SerializedName("version") 25 | @Expose 26 | private String version; 27 | 28 | /** 29 | * 30 | * @return 31 | * The pwCost 32 | */ 33 | public Integer getPwCost() { 34 | return pwCost; 35 | } 36 | 37 | /** 38 | * 39 | * @param pwCost 40 | * The pw_cost 41 | */ 42 | public void setPwCost(Integer pwCost) { 43 | this.pwCost = pwCost; 44 | } 45 | 46 | /** 47 | * 48 | * @return 49 | * The pwSalt 50 | */ 51 | public String getPwSalt() { 52 | return pwSalt; 53 | } 54 | 55 | /** 56 | * 57 | * @param pwSalt 58 | * The pw_salt 59 | */ 60 | public void setPwSalt(String pwSalt) { 61 | this.pwSalt = pwSalt; 62 | } 63 | 64 | /** 65 | * 66 | * @return 67 | * The version 68 | */ 69 | public String getVersion() { 70 | return version; 71 | } 72 | 73 | /** 74 | * 75 | * @param version 76 | * The version 77 | */ 78 | public void setVersion(String version) { 79 | this.version = version; 80 | } 81 | 82 | } 83 | -------------------------------------------------------------------------------- /app/src/main/java/org/standardnotes/notes/comms/data/ContentType.kt: -------------------------------------------------------------------------------- 1 | package org.standardnotes.notes.comms.data 2 | 3 | enum class ContentType { 4 | Note, Tag; 5 | 6 | override fun toString(): String { 7 | return if (this == Note) "Note" else "Tag" 8 | } 9 | } 10 | 11 | fun contentTypeFromString(str: String?): ContentType? { 12 | if (str == "Note") return ContentType.Note 13 | if (str == "Tag") return ContentType.Tag 14 | return null 15 | } -------------------------------------------------------------------------------- /app/src/main/java/org/standardnotes/notes/comms/data/EncryptableItem.java: -------------------------------------------------------------------------------- 1 | 2 | package org.standardnotes.notes.comms.data; 3 | 4 | import java.util.List; 5 | import javax.annotation.Generated; 6 | import com.google.gson.annotations.Expose; 7 | import com.google.gson.annotations.SerializedName; 8 | import org.joda.time.DateTime; 9 | 10 | 11 | /** 12 | * 13 | *

14 | * 15 | * 16 | */ 17 | @Generated("org.jsonschema2pojo") 18 | public class EncryptableItem { 19 | 20 | @SerializedName("uuid") 21 | @Expose 22 | private String uuid; 23 | @SerializedName("created_at") 24 | @Expose 25 | private DateTime createdAt; 26 | @SerializedName("updated_at") 27 | @Expose 28 | private DateTime updatedAt; 29 | @SerializedName("enc_item_key") 30 | @Expose 31 | private String encItemKey; 32 | @SerializedName("presentation_name") 33 | @Expose 34 | private String presentationName = null; 35 | @SerializedName("deleted") 36 | @Expose 37 | private Boolean deleted = false; 38 | @SerializedName("dirty") 39 | @Expose 40 | private Boolean dirty = false; 41 | @SerializedName("references") 42 | @Expose 43 | private List references = null; 44 | 45 | /** 46 | * 47 | * @return 48 | * The uuid 49 | */ 50 | public String getUuid() { 51 | return uuid; 52 | } 53 | 54 | /** 55 | * 56 | * @param uuid 57 | * The uuid 58 | */ 59 | public void setUuid(String uuid) { 60 | this.uuid = uuid; 61 | } 62 | 63 | /** 64 | * 65 | * @return 66 | * The createdAt 67 | */ 68 | public DateTime getCreatedAt() { 69 | return createdAt; 70 | } 71 | 72 | /** 73 | * 74 | * @param createdAt 75 | * The created_at 76 | */ 77 | public void setCreatedAt(DateTime createdAt) { 78 | this.createdAt = createdAt; 79 | } 80 | 81 | /** 82 | * 83 | * @return 84 | * The updatedAt 85 | */ 86 | public DateTime getUpdatedAt() { 87 | return updatedAt; 88 | } 89 | 90 | /** 91 | * 92 | * @param updatedAt 93 | * The updated_at 94 | */ 95 | public void setUpdatedAt(DateTime updatedAt) { 96 | this.updatedAt = updatedAt; 97 | } 98 | 99 | /** 100 | * 101 | * @return 102 | * The encItemKey 103 | */ 104 | public String getEncItemKey() { 105 | return encItemKey; 106 | } 107 | 108 | /** 109 | * 110 | * @param encItemKey 111 | * The enc_item_key 112 | */ 113 | public void setEncItemKey(String encItemKey) { 114 | this.encItemKey = encItemKey; 115 | } 116 | 117 | /** 118 | * 119 | * @return 120 | * The presentationName 121 | */ 122 | public String getPresentationName() { 123 | return presentationName; 124 | } 125 | 126 | /** 127 | * 128 | * @param presentationName 129 | * The presentation_name 130 | */ 131 | public void setPresentationName(String presentationName) { 132 | this.presentationName = presentationName; 133 | } 134 | 135 | /** 136 | * 137 | * @return 138 | * The deleted 139 | */ 140 | public Boolean getDeleted() { 141 | return deleted; 142 | } 143 | 144 | /** 145 | * 146 | * @param deleted 147 | * The deleted 148 | */ 149 | public void setDeleted(Boolean deleted) { 150 | this.deleted = deleted; 151 | } 152 | 153 | /** 154 | * 155 | * @return 156 | * The dirty 157 | */ 158 | public Boolean getDirty() { 159 | return dirty; 160 | } 161 | 162 | /** 163 | * 164 | * @param dirty 165 | * The dirty 166 | */ 167 | public void setDirty(Boolean dirty) { 168 | this.dirty = dirty; 169 | } 170 | 171 | /** 172 | * 173 | * @return 174 | * The references 175 | */ 176 | public List getReferences() { 177 | return references; 178 | } 179 | 180 | /** 181 | * 182 | * @param references 183 | * The references 184 | */ 185 | public void setReferences(List references) { 186 | this.references = references; 187 | } 188 | 189 | } 190 | -------------------------------------------------------------------------------- /app/src/main/java/org/standardnotes/notes/comms/data/EncryptedItem.java: -------------------------------------------------------------------------------- 1 | 2 | package org.standardnotes.notes.comms.data; 3 | 4 | import javax.annotation.Generated; 5 | import com.google.gson.annotations.Expose; 6 | import com.google.gson.annotations.SerializedName; 7 | 8 | 9 | /** 10 | * 11 | *

12 | * 13 | * 14 | */ 15 | @Generated("org.jsonschema2pojo") 16 | public class EncryptedItem 17 | extends EncryptableItem 18 | { 19 | 20 | @SerializedName("content") 21 | @Expose 22 | private String content; 23 | @SerializedName("content_type") 24 | @Expose 25 | private String contentType; 26 | @SerializedName("auth_hash") 27 | @Expose 28 | private String authHash; 29 | 30 | /** 31 | * 32 | * @return 33 | * The content 34 | */ 35 | public String getContent() { 36 | return content; 37 | } 38 | 39 | /** 40 | * 41 | * @param content 42 | * The content 43 | */ 44 | public void setContent(String content) { 45 | this.content = content; 46 | } 47 | 48 | /** 49 | * 50 | * @return 51 | * The contentType 52 | */ 53 | public String getContentType() { 54 | return contentType; 55 | } 56 | 57 | /** 58 | * 59 | * @param contentType 60 | * The content_type 61 | */ 62 | public void setContentType(String contentType) { 63 | this.contentType = contentType; 64 | } 65 | 66 | /** 67 | * 68 | * @return 69 | * The authHash 70 | */ 71 | public String getAuthHash() { 72 | return authHash; 73 | } 74 | 75 | /** 76 | * 77 | * @param authHash 78 | * The auth_hash 79 | */ 80 | public void setAuthHash(String authHash) { 81 | this.authHash = authHash; 82 | } 83 | 84 | } 85 | -------------------------------------------------------------------------------- /app/src/main/java/org/standardnotes/notes/comms/data/ExportItems.java: -------------------------------------------------------------------------------- 1 | package org.standardnotes.notes.comms.data; 2 | 3 | import com.google.gson.annotations.Expose; 4 | import com.google.gson.annotations.SerializedName; 5 | 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | 9 | public class ExportItems { 10 | 11 | @SerializedName("auth_params") 12 | @Expose 13 | private AuthParamsResponse authParams; 14 | 15 | @SerializedName("items") 16 | @Expose 17 | private List items = new ArrayList(); 18 | 19 | public AuthParamsResponse getAuthParams() { 20 | return authParams; 21 | } 22 | 23 | public void setAuthParams(AuthParamsResponse authParams) { 24 | this.authParams = authParams; 25 | } 26 | 27 | public List getItems() { 28 | return items; 29 | } 30 | 31 | public void setItems(List items) { 32 | this.items = items; 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /app/src/main/java/org/standardnotes/notes/comms/data/Note.java: -------------------------------------------------------------------------------- 1 | 2 | package org.standardnotes.notes.comms.data; 3 | 4 | import javax.annotation.Generated; 5 | import com.google.gson.annotations.Expose; 6 | import com.google.gson.annotations.SerializedName; 7 | 8 | 9 | /** 10 | * 11 | *

12 | * 13 | * 14 | */ 15 | @Generated("org.jsonschema2pojo") 16 | public class Note 17 | extends EncryptableItem 18 | { 19 | 20 | @SerializedName("title") 21 | @Expose 22 | private String title = ""; 23 | @SerializedName("text") 24 | @Expose 25 | private String text = ""; 26 | 27 | /** 28 | * 29 | * @return 30 | * The title 31 | */ 32 | public String getTitle() { 33 | return title; 34 | } 35 | 36 | /** 37 | * 38 | * @param title 39 | * The title 40 | */ 41 | public void setTitle(String title) { 42 | this.title = title; 43 | } 44 | 45 | /** 46 | * 47 | * @return 48 | * The text 49 | */ 50 | public String getText() { 51 | return text; 52 | } 53 | 54 | /** 55 | * 56 | * @param text 57 | * The text 58 | */ 59 | public void setText(String text) { 60 | this.text = text; 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /app/src/main/java/org/standardnotes/notes/comms/data/NoteContent.java: -------------------------------------------------------------------------------- 1 | 2 | package org.standardnotes.notes.comms.data; 3 | 4 | import java.util.ArrayList; 5 | import java.util.List; 6 | import javax.annotation.Generated; 7 | import com.google.gson.annotations.Expose; 8 | import com.google.gson.annotations.SerializedName; 9 | 10 | 11 | /** 12 | * 13 | *

14 | * 15 | * 16 | */ 17 | @Generated("org.jsonschema2pojo") 18 | public class NoteContent { 19 | 20 | @SerializedName("title") 21 | @Expose 22 | private String title = ""; 23 | @SerializedName("text") 24 | @Expose 25 | private String text = ""; 26 | @SerializedName("references") 27 | @Expose 28 | private List references = new ArrayList(); 29 | 30 | /** 31 | * 32 | * @return 33 | * The title 34 | */ 35 | public String getTitle() { 36 | return title; 37 | } 38 | 39 | /** 40 | * 41 | * @param title 42 | * The title 43 | */ 44 | public void setTitle(String title) { 45 | this.title = title; 46 | } 47 | 48 | /** 49 | * 50 | * @return 51 | * The text 52 | */ 53 | public String getText() { 54 | return text; 55 | } 56 | 57 | /** 58 | * 59 | * @param text 60 | * The text 61 | */ 62 | public void setText(String text) { 63 | this.text = text; 64 | } 65 | 66 | /** 67 | * 68 | * @return 69 | * The references 70 | */ 71 | public List getReferences() { 72 | return references; 73 | } 74 | 75 | /** 76 | * 77 | * @param references 78 | * The references 79 | */ 80 | public void setReferences(List references) { 81 | this.references = references; 82 | } 83 | 84 | } 85 | -------------------------------------------------------------------------------- /app/src/main/java/org/standardnotes/notes/comms/data/PlaintextItem.java: -------------------------------------------------------------------------------- 1 | package org.standardnotes.notes.comms.data; 2 | 3 | import com.google.gson.annotations.Expose; 4 | import com.google.gson.annotations.SerializedName; 5 | 6 | public class PlaintextItem extends EncryptableItem { 7 | 8 | @SerializedName("content") 9 | @Expose 10 | private EncryptableItem content; 11 | @SerializedName("content_type") 12 | @Expose 13 | private String contentType; 14 | 15 | public PlaintextItem() { 16 | setDirty(null); 17 | setDeleted(null); 18 | setReferences(null); 19 | } 20 | 21 | /** 22 | * @return The content 23 | */ 24 | public EncryptableItem getContent() { 25 | return content; 26 | } 27 | 28 | /** 29 | * @param content The content 30 | */ 31 | public void setContent(EncryptableItem content) { 32 | this.content = content; 33 | } 34 | 35 | /** 36 | * @return The contentType 37 | */ 38 | public String getContentType() { 39 | return contentType; 40 | } 41 | 42 | /** 43 | * @param contentType The content_type 44 | */ 45 | public void setContentType(String contentType) { 46 | this.contentType = contentType; 47 | } 48 | 49 | } -------------------------------------------------------------------------------- /app/src/main/java/org/standardnotes/notes/comms/data/Reference.java: -------------------------------------------------------------------------------- 1 | 2 | package org.standardnotes.notes.comms.data; 3 | 4 | import javax.annotation.Generated; 5 | import com.google.gson.annotations.Expose; 6 | import com.google.gson.annotations.SerializedName; 7 | 8 | 9 | /** 10 | * 11 | *

12 | * 13 | * 14 | */ 15 | @Generated("org.jsonschema2pojo") 16 | public class Reference { 17 | 18 | @SerializedName("uuid") 19 | @Expose 20 | private String uuid; 21 | @SerializedName("content_type") 22 | @Expose 23 | private String contentType; 24 | 25 | /** 26 | * 27 | * @return 28 | * The uuid 29 | */ 30 | public String getUuid() { 31 | return uuid; 32 | } 33 | 34 | /** 35 | * 36 | * @param uuid 37 | * The uuid 38 | */ 39 | public void setUuid(String uuid) { 40 | this.uuid = uuid; 41 | } 42 | 43 | /** 44 | * 45 | * @return 46 | * The contentType 47 | */ 48 | public String getContentType() { 49 | return contentType; 50 | } 51 | 52 | /** 53 | * 54 | * @param contentType 55 | * The content_type 56 | */ 57 | public void setContentType(String contentType) { 58 | this.contentType = contentType; 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /app/src/main/java/org/standardnotes/notes/comms/data/SigninResponse.java: -------------------------------------------------------------------------------- 1 | 2 | package org.standardnotes.notes.comms.data; 3 | 4 | import javax.annotation.Generated; 5 | import com.google.gson.annotations.Expose; 6 | import com.google.gson.annotations.SerializedName; 7 | 8 | 9 | /** 10 | * 11 | *

12 | * 13 | * 14 | */ 15 | @Generated("org.jsonschema2pojo") 16 | public class SigninResponse { 17 | 18 | @SerializedName("token") 19 | @Expose 20 | private String token; 21 | 22 | /** 23 | * 24 | * @return 25 | * The token 26 | */ 27 | public String getToken() { 28 | return token; 29 | } 30 | 31 | /** 32 | * 33 | * @param token 34 | * The token 35 | */ 36 | public void setToken(String token) { 37 | this.token = token; 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /app/src/main/java/org/standardnotes/notes/comms/data/SyncItems.java: -------------------------------------------------------------------------------- 1 | 2 | package org.standardnotes.notes.comms.data; 3 | 4 | import java.util.ArrayList; 5 | import java.util.List; 6 | import javax.annotation.Generated; 7 | import com.google.gson.annotations.Expose; 8 | import com.google.gson.annotations.SerializedName; 9 | 10 | 11 | /** 12 | * 13 | *

14 | * 15 | * 16 | */ 17 | @Generated("org.jsonschema2pojo") 18 | public class SyncItems { 19 | 20 | @SerializedName("sync_token") 21 | @Expose 22 | private String syncToken; 23 | @SerializedName("retrieved_items") 24 | @Expose 25 | private List retrievedItems = new ArrayList(); 26 | @SerializedName("saved_items") 27 | @Expose 28 | private List savedItems = new ArrayList(); 29 | @SerializedName("unsaved") 30 | @Expose 31 | private List unsaved = new ArrayList(); 32 | 33 | /** 34 | * 35 | * @return 36 | * The syncToken 37 | */ 38 | public String getSyncToken() { 39 | return syncToken; 40 | } 41 | 42 | /** 43 | * 44 | * @param syncToken 45 | * The sync_token 46 | */ 47 | public void setSyncToken(String syncToken) { 48 | this.syncToken = syncToken; 49 | } 50 | 51 | /** 52 | * 53 | * @return 54 | * The retrievedItems 55 | */ 56 | public List getRetrievedItems() { 57 | return retrievedItems; 58 | } 59 | 60 | /** 61 | * 62 | * @param retrievedItems 63 | * The retrieved_items 64 | */ 65 | public void setRetrievedItems(List retrievedItems) { 66 | this.retrievedItems = retrievedItems; 67 | } 68 | 69 | /** 70 | * 71 | * @return 72 | * The savedItems 73 | */ 74 | public List getSavedItems() { 75 | return savedItems; 76 | } 77 | 78 | /** 79 | * 80 | * @param savedItems 81 | * The saved_items 82 | */ 83 | public void setSavedItems(List savedItems) { 84 | this.savedItems = savedItems; 85 | } 86 | 87 | /** 88 | * 89 | * @return 90 | * The unsaved 91 | */ 92 | public List getUnsaved() { 93 | return unsaved; 94 | } 95 | 96 | /** 97 | * 98 | * @param unsaved 99 | * The unsaved 100 | */ 101 | public void setUnsaved(List unsaved) { 102 | this.unsaved = unsaved; 103 | } 104 | 105 | } 106 | -------------------------------------------------------------------------------- /app/src/main/java/org/standardnotes/notes/comms/data/Tag.java: -------------------------------------------------------------------------------- 1 | 2 | package org.standardnotes.notes.comms.data; 3 | 4 | import javax.annotation.Generated; 5 | import com.google.gson.annotations.Expose; 6 | import com.google.gson.annotations.SerializedName; 7 | 8 | 9 | /** 10 | * 11 | *

12 | * 13 | * 14 | */ 15 | @Generated("org.jsonschema2pojo") 16 | public class Tag 17 | extends EncryptableItem 18 | { 19 | 20 | @SerializedName("title") 21 | @Expose 22 | private String title = ""; 23 | 24 | /** 25 | * 26 | * @return 27 | * The title 28 | */ 29 | public String getTitle() { 30 | return title; 31 | } 32 | 33 | /** 34 | * 35 | * @param title 36 | * The title 37 | */ 38 | public void setTitle(String title) { 39 | this.title = title; 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /app/src/main/java/org/standardnotes/notes/comms/data/TagContent.java: -------------------------------------------------------------------------------- 1 | 2 | package org.standardnotes.notes.comms.data; 3 | 4 | import java.util.ArrayList; 5 | import java.util.List; 6 | import javax.annotation.Generated; 7 | import com.google.gson.annotations.Expose; 8 | import com.google.gson.annotations.SerializedName; 9 | 10 | 11 | /** 12 | * 13 | *

14 | * 15 | * 16 | */ 17 | @Generated("org.jsonschema2pojo") 18 | public class TagContent { 19 | 20 | @SerializedName("title") 21 | @Expose 22 | private String title = ""; 23 | @SerializedName("references") 24 | @Expose 25 | private List references = new ArrayList(); 26 | 27 | /** 28 | * 29 | * @return 30 | * The title 31 | */ 32 | public String getTitle() { 33 | return title; 34 | } 35 | 36 | /** 37 | * 38 | * @param title 39 | * The title 40 | */ 41 | public void setTitle(String title) { 42 | this.title = title; 43 | } 44 | 45 | /** 46 | * 47 | * @return 48 | * The references 49 | */ 50 | public List getReferences() { 51 | return references; 52 | } 53 | 54 | /** 55 | * 56 | * @param references 57 | * The references 58 | */ 59 | public void setReferences(List references) { 60 | this.references = references; 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /app/src/main/java/org/standardnotes/notes/comms/data/UnsavedItem.java: -------------------------------------------------------------------------------- 1 | 2 | package org.standardnotes.notes.comms.data; 3 | 4 | import javax.annotation.Generated; 5 | import com.google.gson.annotations.Expose; 6 | import com.google.gson.annotations.SerializedName; 7 | 8 | 9 | /** 10 | * 11 | *

12 | * 13 | * 14 | */ 15 | @Generated("org.jsonschema2pojo") 16 | public class UnsavedItem { 17 | 18 | /** 19 | * 20 | *

21 | * 22 | * 23 | */ 24 | @SerializedName("item") 25 | @Expose 26 | private EncryptedItem item; 27 | /** 28 | * 29 | *

30 | * 31 | * 32 | */ 33 | @SerializedName("error") 34 | @Expose 35 | private UnsavedItemError error; 36 | 37 | /** 38 | * 39 | *

40 | * 41 | * 42 | * @return 43 | * The item 44 | */ 45 | public EncryptedItem getItem() { 46 | return item; 47 | } 48 | 49 | /** 50 | * 51 | *

52 | * 53 | * 54 | * @param item 55 | * The item 56 | */ 57 | public void setItem(EncryptedItem item) { 58 | this.item = item; 59 | } 60 | 61 | /** 62 | * 63 | *

64 | * 65 | * 66 | * @return 67 | * The error 68 | */ 69 | public UnsavedItemError getError() { 70 | return error; 71 | } 72 | 73 | /** 74 | * 75 | *

76 | * 77 | * 78 | * @param error 79 | * The error 80 | */ 81 | public void setError(UnsavedItemError error) { 82 | this.error = error; 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /app/src/main/java/org/standardnotes/notes/comms/data/UnsavedItemError.java: -------------------------------------------------------------------------------- 1 | 2 | package org.standardnotes.notes.comms.data; 3 | 4 | import javax.annotation.Generated; 5 | import com.google.gson.annotations.Expose; 6 | import com.google.gson.annotations.SerializedName; 7 | 8 | 9 | /** 10 | * 11 | *

12 | * 13 | * 14 | */ 15 | @Generated("org.jsonschema2pojo") 16 | public class UnsavedItemError { 17 | 18 | @SerializedName("tag") 19 | @Expose 20 | private String tag; 21 | 22 | /** 23 | * 24 | * @return 25 | * The tag 26 | */ 27 | public String getTag() { 28 | return tag; 29 | } 30 | 31 | /** 32 | * 33 | * @param tag 34 | * The tag 35 | */ 36 | public void setTag(String tag) { 37 | this.tag = tag; 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /app/src/main/java/org/standardnotes/notes/comms/data/UploadSyncItems.java: -------------------------------------------------------------------------------- 1 | 2 | package org.standardnotes.notes.comms.data; 3 | 4 | import java.util.ArrayList; 5 | import java.util.List; 6 | import javax.annotation.Generated; 7 | import com.google.gson.annotations.Expose; 8 | import com.google.gson.annotations.SerializedName; 9 | 10 | 11 | /** 12 | * 13 | *

14 | * 15 | * 16 | */ 17 | @Generated("org.jsonschema2pojo") 18 | public class UploadSyncItems { 19 | 20 | @SerializedName("sync_token") 21 | @Expose 22 | private String syncToken; 23 | @SerializedName("items") 24 | @Expose 25 | private List items = new ArrayList(); 26 | 27 | /** 28 | * 29 | * @return 30 | * The syncToken 31 | */ 32 | public String getSyncToken() { 33 | return syncToken; 34 | } 35 | 36 | /** 37 | * 38 | * @param syncToken 39 | * The sync_token 40 | */ 41 | public void setSyncToken(String syncToken) { 42 | this.syncToken = syncToken; 43 | } 44 | 45 | /** 46 | * 47 | * @return 48 | * The items 49 | */ 50 | public List getItems() { 51 | return items; 52 | } 53 | 54 | /** 55 | * 56 | * @param items 57 | * The items 58 | */ 59 | public void setItems(List items) { 60 | this.items = items; 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /app/src/main/java/org/standardnotes/notes/frag/NoteListFragment.kt: -------------------------------------------------------------------------------- 1 | package org.standardnotes.notes.frag 2 | 3 | import android.content.Intent 4 | import android.os.Bundle 5 | import android.support.design.widget.Snackbar 6 | import android.support.v4.app.Fragment 7 | import android.support.v7.app.AlertDialog 8 | import android.support.v7.widget.DividerItemDecoration 9 | import android.support.v7.widget.LinearLayoutManager 10 | import android.support.v7.widget.PopupMenu 11 | import android.support.v7.widget.RecyclerView 12 | import android.view.LayoutInflater 13 | import android.view.MotionEvent 14 | import android.view.View 15 | import android.view.ViewGroup 16 | import kotlinx.android.synthetic.main.activity_main.* 17 | import kotlinx.android.synthetic.main.frag_note_list.* 18 | import kotlinx.android.synthetic.main.item_note.view.* 19 | import org.joda.time.format.DateTimeFormat 20 | import org.standardnotes.notes.NoteActivity 21 | import org.standardnotes.notes.R 22 | import org.standardnotes.notes.SApplication 23 | import org.standardnotes.notes.comms.SyncManager 24 | import org.standardnotes.notes.comms.data.Note 25 | import java.util.* 26 | 27 | 28 | class NoteListFragment : Fragment(), SyncManager.SyncListener { 29 | 30 | val adapter: Adapter by lazy { Adapter() } 31 | 32 | var notes = ArrayList() 33 | var tagId: String? = "" 34 | var searchText: String? = null 35 | 36 | var currentSnackbar: Snackbar? = null 37 | 38 | var lastTouchedX: Int? = null 39 | var lastTouchedY: Int? = null 40 | 41 | companion object { 42 | const val EXTRA_NOTE_ID = "noteId" 43 | const val EXTRA_TAG_ID = "tagId" 44 | const val EXTRA_X_COOR = "xCoor" 45 | const val EXTRA_Y_COOR = "yCoor" 46 | } 47 | 48 | override fun onCreate(savedInstanceState: Bundle?) { 49 | super.onCreate(savedInstanceState) 50 | if (savedInstanceState != null) { 51 | tagId = savedInstanceState.getString("tagId") 52 | searchText = savedInstanceState.getString("search") 53 | } 54 | } 55 | 56 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, 57 | savedInstanceState: Bundle?): View? { 58 | val view = inflater.inflate(R.layout.frag_note_list, container, false) 59 | return view 60 | } 61 | 62 | override fun onViewCreated(view: View?, savedInstanceState: Bundle?) { 63 | super.onViewCreated(view, savedInstanceState) 64 | SyncManager.subscribe(this) 65 | list.layoutManager = LinearLayoutManager(activity, LinearLayoutManager.VERTICAL, false) 66 | list.addItemDecoration(DividerItemDecoration(activity, LinearLayoutManager.VERTICAL)) 67 | swipeRefreshLayout.setColorSchemeResources( 68 | R.color.colorPrimary, 69 | R.color.colorAccent) 70 | swipeRefreshLayout.setOnRefreshListener { SyncManager.sync() } 71 | SyncManager.sync() 72 | list.adapter = adapter 73 | } 74 | 75 | override fun onDestroyView() { 76 | super.onDestroyView() 77 | SyncManager.unsubscribe(this) 78 | } 79 | 80 | override fun onResume() { 81 | super.onResume() 82 | refreshNotes() // This is too often and slow for large datasets, but necessary until we have an event to trigger refresh 83 | } 84 | 85 | override fun onSaveInstanceState(outState: Bundle) { 86 | super.onSaveInstanceState(outState) 87 | outState.putString("tagId", tagId) 88 | outState.putString("search", searchText) 89 | } 90 | 91 | override fun onSyncStarted() { 92 | swipeRefreshLayout.isRefreshing = true 93 | currentSnackbar?.dismiss() 94 | } 95 | 96 | override fun onSyncCompleted() { 97 | swipeRefreshLayout.isRefreshing = false 98 | currentSnackbar?.dismiss() 99 | } 100 | 101 | fun refreshNotesForSearch(search: String?) { 102 | tagId = null 103 | searchText = search 104 | if (search.isNullOrEmpty()) { // Don't show anything without search term 105 | notes = ArrayList() 106 | } else { 107 | notes = ArrayList(SApplication.instance.noteStore.getNotesWithText(search!!)) 108 | } 109 | adapter.notifyDataSetChanged() 110 | } 111 | 112 | fun refreshNotesForTag(uuid: String? = null) { 113 | searchText = null 114 | val noteList = if (uuid.isNullOrEmpty()) 115 | SApplication.instance.noteStore.getAllNotes() 116 | else 117 | SApplication.instance.noteStore.getNotesForTag(uuid!!) 118 | notes = ArrayList(noteList) 119 | tagId = uuid 120 | adapter.notifyDataSetChanged() 121 | } 122 | 123 | fun refreshNotes() { 124 | if (tagId != null) 125 | refreshNotesForTag(tagId) 126 | else 127 | refreshNotesForSearch(searchText) 128 | } 129 | 130 | 131 | override fun onSyncFailed() { 132 | swipeRefreshLayout.isRefreshing = false 133 | // TODO this always assumes it's a network error, but the server may have errored or the local store may have failed 134 | currentSnackbar = Snackbar.make(activity.rootView, R.string.error_fail_sync, Snackbar.LENGTH_INDEFINITE) 135 | .setAction(R.string.sync_retry, { 136 | SyncManager.sync() 137 | }) 138 | currentSnackbar!!.show() 139 | } 140 | 141 | fun startNewNote(x: Int, y: Int, uuid: String) { 142 | val intent: Intent = Intent(activity, NoteActivity::class.java) 143 | intent.putExtra(EXTRA_X_COOR, x) 144 | intent.putExtra(EXTRA_Y_COOR, y) 145 | intent.putExtra(EXTRA_TAG_ID, uuid) 146 | startActivity(intent) 147 | } 148 | 149 | inner class NoteHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { 150 | 151 | var note: Note? = null 152 | get 153 | set(value) { 154 | field = value 155 | itemView.title.text = note?.title 156 | itemView.date.text = DateTimeFormat.shortDateTime().print(note?.updatedAt) 157 | var noteText = note?.text ?: "" 158 | noteText = noteText.substring(0, Math.min(256, noteText.length)) 159 | noteText.replace('\n', ' ') 160 | itemView.text.text = noteText 161 | itemView.synced.visibility = if (note?.dirty == true) View.VISIBLE else View.INVISIBLE 162 | } 163 | 164 | init { 165 | itemView.setOnTouchListener({ v, event -> 166 | if (event.action == MotionEvent.ACTION_UP) { 167 | lastTouchedX = event.rawX.toInt() 168 | lastTouchedY = event.rawY.toInt() 169 | } 170 | false 171 | }) 172 | itemView.setOnClickListener { 173 | val intent: Intent = Intent(activity, NoteActivity::class.java) 174 | intent.putExtra(EXTRA_NOTE_ID, note?.uuid) 175 | intent.putExtra(EXTRA_X_COOR, lastTouchedX) 176 | intent.putExtra(EXTRA_Y_COOR, lastTouchedY) 177 | startActivity(intent) 178 | } 179 | itemView.setOnLongClickListener { 180 | val popup = PopupMenu(activity, itemView) 181 | popup.menu.add(activity.getString(R.string.action_delete)) 182 | val noteIdToDelete = note!!.uuid // possible for view's assigned note to change while popup is displayed if sync happens! 183 | popup.setOnMenuItemClickListener { 184 | AlertDialog.Builder(activity) 185 | .setTitle(R.string.title_delete_confirm) 186 | .setMessage(R.string.prompt_are_you_sure) 187 | .setPositiveButton(R.string.action_delete, { dialogInterface, i -> 188 | SApplication.instance.noteStore.deleteItem(noteIdToDelete) 189 | refreshNotes() 190 | SyncManager.sync() 191 | }) 192 | .setNegativeButton(R.string.action_cancel, null) 193 | .show() 194 | true 195 | } 196 | popup.show() 197 | true 198 | } 199 | } 200 | } 201 | 202 | inner class Adapter : RecyclerView.Adapter() { 203 | 204 | override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): NoteHolder { 205 | return NoteHolder(LayoutInflater.from(activity).inflate(R.layout.item_note, parent, false)) 206 | } 207 | 208 | override fun getItemCount(): Int { 209 | return notes.count() 210 | } 211 | 212 | override fun onBindViewHolder(holder: NoteHolder, position: Int) { 213 | val note: Note = notes[position] 214 | holder.note = note 215 | } 216 | 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /app/src/main/java/org/standardnotes/notes/store/ValueStore.kt: -------------------------------------------------------------------------------- 1 | package org.standardnotes.notes.store 2 | 3 | import android.content.Context 4 | import android.content.SharedPreferences 5 | import com.google.gson.reflect.TypeToken 6 | import org.standardnotes.notes.BuildConfig 7 | import org.standardnotes.notes.SApplication 8 | import org.standardnotes.notes.comms.data.AuthParamsResponse 9 | 10 | class ValueStore(context: Context) { 11 | 12 | private val prefs: SharedPreferences = context.getSharedPreferences("values", Context.MODE_PRIVATE) 13 | 14 | fun setTokenAndMasterKey(token: String?, mk: String?, ak: String?) { 15 | prefs.edit().putString("masterKey", mk).putString("authKey", ak).putString("token", token).apply() 16 | } 17 | 18 | val masterKey: String? 19 | get() = prefs.getString("masterKey", null) 20 | 21 | val authKey: String? 22 | get() = prefs.getString("authKey", null) 23 | 24 | val token: String? 25 | get() = prefs.getString("token", null) 26 | 27 | var server: String? 28 | get() = prefs.getString("server", BuildConfig.SERVER_DEFAULT) 29 | set(value) { prefs.edit().putString("server", value).apply() } 30 | 31 | var email: String? 32 | get() = prefs.getString("email", "") // Set on login/registration 33 | set(value) { prefs.edit().putString("email", value).apply() } 34 | 35 | var syncToken: String? 36 | get() = prefs.getString("syncToken", null) 37 | set(token) { prefs.edit().putString("syncToken", token).apply() } 38 | 39 | var protocolVersion: String? 40 | get() { 41 | var savedvalue = prefs.getString("protocolVersion", null) 42 | if(savedvalue == null) { 43 | var params = authParams 44 | if(params != null && params.version != null) { 45 | savedvalue = params.version 46 | } 47 | 48 | if(authKey != null) {savedvalue = "002"} 49 | else { savedvalue = "001" } 50 | 51 | protocolVersion = savedvalue 52 | } 53 | 54 | return savedvalue 55 | } 56 | set(value) { prefs.edit().putString("protocolVersion", value).apply() } 57 | 58 | var authParams: AuthParamsResponse? 59 | get() { return SApplication.instance.gson.fromJson(prefs.getString("authParams", null), object : TypeToken() {}.type) } 60 | set(value) { prefs.edit().putString("authParams", SApplication.instance.gson.toJson(value)).apply() } 61 | 62 | fun clear() { 63 | prefs.edit().clear().apply() 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /app/src/main/java/org/standardnotes/notes/widget/AppWidgetRefreshService.java: -------------------------------------------------------------------------------- 1 | package org.standardnotes.notes.widget; 2 | 3 | import android.appwidget.AppWidgetManager; 4 | import android.content.Context; 5 | import android.content.Intent; 6 | import android.os.Bundle; 7 | import android.view.View; 8 | import android.widget.RemoteViews; 9 | import android.widget.RemoteViewsService; 10 | 11 | import org.joda.time.format.DateTimeFormat; 12 | import org.standardnotes.notes.R; 13 | import org.standardnotes.notes.comms.data.Note; 14 | import org.standardnotes.notes.frag.NoteListFragment; 15 | import org.standardnotes.notes.store.NoteStore; 16 | 17 | import java.util.ArrayList; 18 | import java.util.List; 19 | 20 | import static org.standardnotes.notes.frag.NoteListFragment.EXTRA_X_COOR; 21 | import static org.standardnotes.notes.frag.NoteListFragment.EXTRA_Y_COOR; 22 | 23 | public class AppWidgetRefreshService extends RemoteViewsService { 24 | @Override 25 | public RemoteViewsFactory onGetViewFactory(Intent intent) { 26 | int appWidgetId = intent.getIntExtra( 27 | AppWidgetManager.EXTRA_APPWIDGET_ID, 28 | AppWidgetManager.INVALID_APPWIDGET_ID); 29 | 30 | return (new ListProvider(this.getApplicationContext(), intent)); 31 | } 32 | 33 | 34 | public class ListProvider implements RemoteViewsFactory { 35 | private ArrayList noteList = new ArrayList(); 36 | private Context context = null; 37 | private int appWidgetId; 38 | 39 | public ListProvider(Context context, Intent intent) { 40 | this.context = context; 41 | appWidgetId = intent.getIntExtra( 42 | AppWidgetManager.EXTRA_APPWIDGET_ID, 43 | AppWidgetManager.INVALID_APPWIDGET_ID); 44 | 45 | loadNotes(); 46 | } 47 | 48 | private void loadNotes() { 49 | noteList.clear(); 50 | NoteStore ns = new NoteStore(); 51 | List allNotes = ns.getAllNotes(); 52 | 53 | int limit = 0; 54 | //number of notes to display. should be changeable in settings. 55 | int maxlimit = 10; 56 | for (Note n : allNotes) { 57 | noteList.add(n); 58 | if (++limit >= maxlimit) 59 | return; 60 | } 61 | } 62 | 63 | @Override 64 | public void onCreate() { 65 | } 66 | 67 | @Override 68 | public void onDataSetChanged() { 69 | loadNotes(); 70 | } 71 | 72 | @Override 73 | public void onDestroy() { 74 | } 75 | 76 | @Override 77 | public int getCount() { 78 | return noteList.size(); 79 | } 80 | 81 | @Override 82 | public long getItemId(int position) { 83 | return position; 84 | } 85 | 86 | @Override 87 | public boolean hasStableIds() { 88 | return false; 89 | } 90 | 91 | @Override 92 | public RemoteViews getViewAt(int position) { 93 | final RemoteViews remoteView = new RemoteViews(context.getPackageName(), R.layout.item_note); 94 | Note listItem = noteList.get(position); 95 | 96 | //sett all values for the note 97 | String title = listItem.getTitle(); 98 | String Text = abbreviate(listItem.getText(), 256); 99 | String UUID = listItem.getUuid(); 100 | remoteView.setTextViewText(R.id.title, title); 101 | remoteView.setTextViewText(R.id.text, Text); 102 | remoteView.setTextViewText(R.id.date, DateTimeFormat.shortDateTime().print(listItem.getUpdatedAt())); 103 | remoteView.setViewVisibility(R.id.synced, (listItem.getDirty() ? View.VISIBLE : View.INVISIBLE)); 104 | 105 | //start App with clicked Note 106 | Intent fillInIntent = new Intent(); 107 | Bundle bundle = new Bundle(); 108 | bundle.putString(NoteListFragment.EXTRA_NOTE_ID, UUID); 109 | bundle.putInt(NoteListFragment.EXTRA_X_COOR, 0); 110 | bundle.putInt(NoteListFragment.EXTRA_Y_COOR, 0); 111 | fillInIntent.putExtras(bundle); 112 | remoteView.setOnClickFillInIntent(R.id.item_note_row, fillInIntent); 113 | 114 | return remoteView; 115 | } 116 | 117 | private String abbreviate(String str, int lenght) { 118 | //reduce lenght of string 119 | str = str.substring(0, Math.min(lenght, str.length())); 120 | str.replace('\n', ' '); 121 | return str; 122 | } 123 | 124 | @Override 125 | public RemoteViews getLoadingView() { 126 | return null; 127 | } 128 | 129 | @Override 130 | public int getViewTypeCount() { 131 | return 1; 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /app/src/main/java/org/standardnotes/notes/widget/NoteListWidget.java: -------------------------------------------------------------------------------- 1 | package org.standardnotes.notes.widget; 2 | 3 | import android.app.PendingIntent; 4 | import android.appwidget.AppWidgetManager; 5 | import android.appwidget.AppWidgetProvider; 6 | import android.content.Context; 7 | import android.content.Intent; 8 | import android.net.Uri; 9 | import android.os.Bundle; 10 | import android.widget.RemoteViews; 11 | 12 | import org.standardnotes.notes.NoteActivity; 13 | import org.standardnotes.notes.R; 14 | import org.standardnotes.notes.StarterActivity; 15 | 16 | 17 | public class NoteListWidget extends AppWidgetProvider { 18 | 19 | static RemoteViews updateAppWidget(Context context, 20 | int appWidgetId) { 21 | 22 | //which layout to show on widget 23 | RemoteViews remoteViews = new RemoteViews( 24 | context.getPackageName(),R.layout.widget_note_list); 25 | 26 | //RemoteViews Service needed to provide adapter for ListView 27 | Intent svcIntent = new Intent(context.getApplicationContext(), AppWidgetRefreshService.class); 28 | //passing app widget id to that RemoteViews Service 29 | svcIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId); 30 | //setting a unique Uri to the intent 31 | //don't know its purpose to me right now 32 | svcIntent.setData(Uri.parse( 33 | svcIntent.toUri(Intent.URI_INTENT_SCHEME))); 34 | //setting adapter to listview of the widget 35 | remoteViews.setRemoteAdapter( R.id.noteListView, 36 | svcIntent); 37 | //setting an empty view in case of no data 38 | remoteViews.setEmptyView(R.id.noteListView, R.id.empty_view); 39 | 40 | //intent for App Start Button 41 | Intent intentstart = new Intent(context, StarterActivity.class); 42 | PendingIntent pendingIntentstart = PendingIntent.getActivity(context, 0, intentstart, 0); 43 | remoteViews.setOnClickPendingIntent(R.id.start_app, pendingIntentstart); 44 | 45 | //Intent for new Note Button 46 | Intent intentnew = new Intent(context, NoteActivity.class); 47 | intentnew.setAction(Intent.ACTION_SEND); 48 | intentnew.setType("text/plain"); 49 | Bundle bundle = new Bundle(); 50 | intentnew.putExtras(bundle); 51 | int iUniqueId = (int) (System.currentTimeMillis() & 0xfffffff); 52 | PendingIntent pendingIntentnew = PendingIntent.getActivity(context,iUniqueId, intentnew, PendingIntent.FLAG_UPDATE_CURRENT); 53 | remoteViews.setOnClickPendingIntent(R.id.new_note, pendingIntentnew); 54 | 55 | //Template Intent for each entry in list 56 | Intent startActivityIntent = new Intent(context, NoteActivity.class); 57 | startActivityIntent.setAction(Intent.ACTION_SEND); 58 | startActivityIntent.setType("text/plain"); 59 | startActivityIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); // Close previous note activity if already open 60 | PendingIntent startActivityPendingIntent = PendingIntent.getActivity(context, iUniqueId+10, startActivityIntent, PendingIntent.FLAG_UPDATE_CURRENT); 61 | remoteViews.setPendingIntentTemplate(R.id.noteListView, startActivityPendingIntent); 62 | 63 | return remoteViews; 64 | } 65 | 66 | @Override 67 | public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { 68 | final int N = appWidgetIds.length; 69 | for (int i = 0; i < N; ++i) { 70 | RemoteViews remoteViews = updateAppWidget(context, appWidgetIds[i]); 71 | appWidgetManager.updateAppWidget(appWidgetIds[i], remoteViews); 72 | } 73 | //update entrys 74 | appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetIds, R.id.noteListView); 75 | super.onUpdate(context, appWidgetManager, appWidgetIds); 76 | } 77 | 78 | @Override 79 | public void onEnabled(Context context) { 80 | // Enter relevant functionality for when the first widget is created 81 | } 82 | 83 | @Override 84 | public void onDisabled(Context context) { 85 | // Enter relevant functionality for when the last widget is disabled 86 | } 87 | } 88 | 89 | -------------------------------------------------------------------------------- /app/src/main/res/color/drawer_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-nodpi/appwidget_preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/standardnotes/android-classic/c86b80e19189ea33242ceba0ec130bd44fb6bfd1/app/src/main/res/drawable-nodpi/appwidget_preview.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/divider.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/fab_add.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_magnify.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_settings_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_share.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_tag.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/tag_bg.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_debug.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 |