diff --git a/.gitignore b/.gitignore index e2f5000..c838db8 100644 --- a/.gitignore +++ b/.gitignore @@ -5,13 +5,9 @@ /.idea/libraries .DS_Store /build -IAP5Helper/build /captures .externalNativeBuild .idea/ -app/standard -app/samsung *~ *.log -app/acra.properties keystore.properties diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..3c11563 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,38 @@ +image: openjdk:8-jdk + +variables: + ANDROID_COMPILE_SDK: "28" + ANDROID_BUILD_TOOLS: "28.0.3" + ANDROID_SDK_TOOLS: "4333796" + +before_script: + - export GRADLE_USER_HOME=cache/.gradle + - apt-get --quiet update --yes + - apt-get --quiet install --yes wget tar unzip lib32stdc++6 lib32z1 + - wget --quiet --output-document=android-sdk.zip https://dl.google.com/android/repository/sdk-tools-linux-${ANDROID_SDK_TOOLS}.zip + - unzip -d android-sdk-linux android-sdk.zip + - echo y | android-sdk-linux/tools/bin/sdkmanager "platforms;android-${ANDROID_COMPILE_SDK}" >/dev/null + - echo y | android-sdk-linux/tools/bin/sdkmanager "platform-tools" >/dev/null + - echo y | android-sdk-linux/tools/bin/sdkmanager "build-tools;${ANDROID_BUILD_TOOLS}" >/dev/null + - export ANDROID_HOME=$PWD/android-sdk-linux + - export PATH=$PATH:$PWD/android-sdk-linux/platform-tools/ + - chmod +x ./gradlew + # temporarily disable checking for EPIPE error and use yes to accept all licenses + - set +o pipefail + - yes | android-sdk-linux/tools/bin/sdkmanager --licenses + - set -o pipefail + +stages: + - test + +test: + stage: test + script: + - ./gradlew --stacktrace --console=plain :app:lintDebug jacocoTestReport + - cat app/build/reports/jacoco/jacocoTestReport/html/index.html + artifacts: + reports: + junit: app/build/test-results/testDebugUnitTest/TEST-*.xml + paths: + - app/build/reports/jacoco/jacocoTestReport/ + - app/build/outputs/ diff --git a/README.md b/README.md index 28221cc..36b0f59 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # [Simple Markdown](https://wbrawner.com/portfolio/simple-markdown/) -[![Build Status](https://ci.wbrawner.com/job/Simple%20Markdown/badge/icon)](https://ci.wbrawner.com/job/Simple%20Markdown/) +[![pipeline status](https://gitlab.com/billybrawner/SimpleMarkdown/badges/master/pipeline.svg)](https://gitlab.com/billybrawner/SimpleMarkdown/commits/master) +[![coverage report](https://gitlab.com/billybrawner/SimpleMarkdown/badges/master/coverage.svg)](https://gitlab.com/billybrawner/SimpleMarkdown/commits/master) Simple Markdown is simply a Markdown editor :) I wrote it to offer up an open source alternative to the other Markdown editors available on the Play Store. I also wanted to get some practice in @@ -27,8 +28,7 @@ Using Android Studio is the preferred way to build the project. To build from th ### Crashlytics SimpleMarkdown makes use of Firebase Crashlytics for error reporting. You'll need to follow the -[Get started with Firebase Crashlytics](https://firebase.google -.com/docs/crashlytics/get-started?platform=android) guide in order to build the project. +[Get started with Firebase Crashlytics](https://firebase.google.com/docs/crashlytics/get-started?platform=android) guide in order to build the project. ## Contributing diff --git a/app/.gitignore b/app/.gitignore index 284a226..cfabd20 100644 --- a/app/.gitignore +++ b/app/.gitignore @@ -1,4 +1,2 @@ /build -crashlytics.properties *.apk -google-services.json \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 5600a9e..4f46a15 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -3,10 +3,20 @@ apply plugin: 'kotlin-android-extensions' apply plugin: 'kotlin-android' apply plugin: 'kotlin-kapt' apply plugin: 'io.fabric' +apply plugin: 'jacoco' -def keystorePropertiesFile = rootProject.file("keystore.properties") def keystoreProperties = new Properties() -keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) +try { + def keystorePropertiesFile = rootProject.file("keystore.properties") + keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) +} catch (FileNotFoundException e) { + logger.warn("Unable to load keystore properties. Automatic signing won't be available") + keystoreProperties['keyAlias'] = "" + keystoreProperties['keyPassword'] = "" + keystoreProperties['storeFile'] = File.createTempFile("temp", ".tmp").absolutePath + keystoreProperties['storePassword'] = "" +} + android { configurations.all { @@ -44,6 +54,9 @@ android { } } buildTypes { + debug { + testCoverageEnabled true + } release { minifyEnabled true proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' @@ -96,9 +109,36 @@ dependencies { implementation "androidx.core:core-ktx:1.0.2" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0' + implementation 'eu.crydee:syllable-counter:4.0.2' } apply plugin: 'com.google.gms.google-services' repositories { mavenCentral() } + +jacoco { + toolVersion = '0.8.0' +} + +tasks.withType(Test) { + jacoco.includeNoLocationClasses = true +} + +task jacocoTestReport(type: JacocoReport, dependsOn: ['testDebugUnitTest']) { + reports { + xml.enabled = true + html.enabled = true + } + + def fileFilter = [ '**/R.class', '**/R$*.class', '**/BuildConfig.*', '**/Manifest*.*', '**/*Test*.*', 'android/**/*.*' ] + def javaDebugTree = fileTree(dir: "$project.buildDir/intermediates/javac/debug/compileDebugJavaWithJavac/classes", excludes: fileFilter) + def kotlinDebugTree = fileTree(dir: "$project.buildDir/tmp/kotlin-classes/debug", excludes: fileFilter) + def mainSrc = "$project.projectDir/src/main/java" + + sourceDirectories = files([mainSrc]) + classDirectories = files([javaDebugTree, kotlinDebugTree]) + executionData = fileTree(dir: project.buildDir, includes: [ + 'jacoco/testDebugUnitTest.exec', 'outputs/code-coverage/connected/*coverage.ec' + ]) +} diff --git a/app/google-services.json b/app/google-services.json new file mode 100644 index 0000000..169bd73 --- /dev/null +++ b/app/google-services.json @@ -0,0 +1,78 @@ +{ + "project_info": { + "project_number": "318641233555", + "firebase_url": "https://simplemarkdown.firebaseio.com", + "project_id": "simplemarkdown", + "storage_bucket": "simplemarkdown.appspot.com" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:318641233555:android:09d865cad87e3b5f", + "android_client_info": { + "package_name": "com.wbrawner.simplemarkdown" + } + }, + "oauth_client": [ + { + "client_id": "318641233555-83n2k1mqhokf0b7lhccqiva9pspgripq.apps.googleusercontent.com", + "client_type": 1, + "android_info": { + "package_name": "com.wbrawner.simplemarkdown", + "certificate_hash": "710e37c230689bc259fc6e2e032e2f56956f8d33" + } + }, + { + "client_id": "318641233555-a5rm2cqqf66jc9j5nmg3ttepdt2iaeno.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyBDMcXg-10NsXLDKJRtj5WnXoHrwg3m9Os" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "318641233555-a5rm2cqqf66jc9j5nmg3ttepdt2iaeno.apps.googleusercontent.com", + "client_type": 3 + } + ] + } + }, + "admob_app_id": "ca-app-pub-3319579963502409~4576405307" + }, + { + "client_info": { + "mobilesdk_app_id": "1:318641233555:android:5dfb62206717437e", + "android_client_info": { + "package_name": "com.wbrawner.simplemarkdown.samsung" + } + }, + "oauth_client": [ + { + "client_id": "318641233555-a5rm2cqqf66jc9j5nmg3ttepdt2iaeno.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyBDMcXg-10NsXLDKJRtj5WnXoHrwg3m9Os" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "318641233555-a5rm2cqqf66jc9j5nmg3ttepdt2iaeno.apps.googleusercontent.com", + "client_type": 3 + } + ] + } + } + } + ], + "configuration_version": "1" +} \ No newline at end of file diff --git a/app/src/main/java/com/wbrawner/simplemarkdown/model/Readability.java b/app/src/main/java/com/wbrawner/simplemarkdown/model/Readability.java new file mode 100644 index 0000000..53d6ca3 --- /dev/null +++ b/app/src/main/java/com/wbrawner/simplemarkdown/model/Readability.java @@ -0,0 +1,35 @@ +package com.wbrawner.simplemarkdown.model; + +import java.util.ArrayList; +import java.util.List; + +public class Readability { + private String content; + private static final String DELIMS = ".!?\n"; + + public Readability(String content) { + this.content = content; + } + + public List sentences() { + + ArrayList list = new ArrayList<>(); + + int startOfSentance = 0; + StringBuilder lineBuilder = new StringBuilder(); + for (int i = 0; i < content.length(); i++) { + String c = content.charAt(i) + ""; + if (DELIMS.contains(c)) { + list.add(new Sentence(content, startOfSentance, i)); + startOfSentance = i + 1; + lineBuilder = new StringBuilder(); + } else { + lineBuilder.append(c); + } + } + String line = lineBuilder.toString(); + if (!line.isEmpty()) list.add(new Sentence(content, startOfSentance, content.length())); + + return list; + } +} diff --git a/app/src/main/java/com/wbrawner/simplemarkdown/model/Sentence.java b/app/src/main/java/com/wbrawner/simplemarkdown/model/Sentence.java new file mode 100644 index 0000000..fe5a2f6 --- /dev/null +++ b/app/src/main/java/com/wbrawner/simplemarkdown/model/Sentence.java @@ -0,0 +1,48 @@ +package com.wbrawner.simplemarkdown.model; + +import eu.crydee.syllablecounter.SyllableCounter; + +public class Sentence { + + private String sentence = ""; + private int start = 0; + private int end = 0; + + private final static SyllableCounter sc = new SyllableCounter(); + + public Sentence(String content, int start, int end){ + this.start = start; + this.end = end; + this.sentence = content.substring(start, end); + + trimStart(); + } + + public Sentence(String sentence){ + this.sentence = sentence; + } + + private void trimStart() { + while(sentence.startsWith(" ")){ + this.start++; + sentence = sentence.substring(1); + } + } + + @Override + public String toString() { + return sentence; + } + + public int start(){ + return start; + } + + public int end(){ + return end; + } + + public int syllableCount(){ + return sc.count(sentence); + } +} diff --git a/app/src/main/java/com/wbrawner/simplemarkdown/utility/ReadabilityObserver.java b/app/src/main/java/com/wbrawner/simplemarkdown/utility/ReadabilityObserver.java new file mode 100644 index 0000000..01629be --- /dev/null +++ b/app/src/main/java/com/wbrawner/simplemarkdown/utility/ReadabilityObserver.java @@ -0,0 +1,59 @@ +package com.wbrawner.simplemarkdown.utility; + +import android.graphics.Color; +import android.text.SpannableString; +import android.text.style.BackgroundColorSpan; +import android.util.Log; +import android.widget.EditText; +import android.widget.TextView; + +import com.wbrawner.simplemarkdown.model.Readability; +import com.wbrawner.simplemarkdown.model.Sentence; + +import io.reactivex.Observer; +import io.reactivex.disposables.Disposable; + +public class ReadabilityObserver implements Observer { + private EditText text; + private String previousValue = ""; + + public ReadabilityObserver(EditText text) { + this.text = text; + } + + @Override + public void onSubscribe(Disposable d) { + + } + + @Override + public void onNext(String markdown) { + long start = System.currentTimeMillis(); + if (markdown.length() < 1) return; + if (previousValue.equals(markdown)) return; + Readability readability = new Readability(markdown); + SpannableString span = new SpannableString(markdown); + for (Sentence sentence : readability.sentences()) { + int color = Color.TRANSPARENT; + if (sentence.syllableCount() > 25) color = Color.argb(100, 229, 232, 42); + if (sentence.syllableCount() > 35) color = Color.argb(100, 193, 66, 66); + span.setSpan(new BackgroundColorSpan(color), sentence.start(), sentence.end(), 0); + } + text.setTextKeepState(span, TextView.BufferType.SPANNABLE); + previousValue = markdown; + long timeTakenMs = System.currentTimeMillis() - start; + Log.d("SimpleMarkdown", "Handled markdown in " + timeTakenMs + "ms"); + } + + @Override + public void onError(Throwable e) { + System.err.println("An error occurred while handling the markdown"); + e.printStackTrace(); + // TODO: report this? + } + + @Override + public void onComplete() { + + } +} diff --git a/app/src/main/java/com/wbrawner/simplemarkdown/view/fragment/EditFragment.kt b/app/src/main/java/com/wbrawner/simplemarkdown/view/fragment/EditFragment.kt index 3cf90b5..c583d4b 100644 --- a/app/src/main/java/com/wbrawner/simplemarkdown/view/fragment/EditFragment.kt +++ b/app/src/main/java/com/wbrawner/simplemarkdown/view/fragment/EditFragment.kt @@ -3,6 +3,7 @@ package com.wbrawner.simplemarkdown.view.fragment import android.annotation.SuppressLint import android.content.Context import android.os.Bundle +import android.preference.PreferenceManager import android.view.LayoutInflater import android.view.MotionEvent import android.view.View @@ -17,6 +18,7 @@ import com.wbrawner.simplemarkdown.MarkdownApplication import com.wbrawner.simplemarkdown.R import com.wbrawner.simplemarkdown.presentation.MarkdownPresenter import com.wbrawner.simplemarkdown.utility.MarkdownObserver +import com.wbrawner.simplemarkdown.utility.ReadabilityObserver import com.wbrawner.simplemarkdown.view.MarkdownEditView import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.schedulers.Schedulers @@ -48,6 +50,16 @@ class EditFragment : Fragment(), MarkdownEditView { .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) obs.subscribe(MarkdownObserver(presenter, obs)) + val sharedPrefs = PreferenceManager.getDefaultSharedPreferences(requireContext()) + val enableReadability = sharedPrefs.getBoolean(getString(R.string.readability_enabled), false) + if (enableReadability) { + val readabilityObserver = RxTextView.textChanges(markdownEditor!!) + .debounce(250, TimeUnit.MILLISECONDS) + .map { it.toString() } + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + readabilityObserver.subscribe(ReadabilityObserver(markdownEditor)) + } return view } @@ -89,7 +101,7 @@ class EditFragment : Fragment(), MarkdownEditView { } override fun setMarkdown(markdown: String) { - markdownEditor!!.setText(markdown) + markdownEditor?.setText(markdown) } override fun setTitle(title: String) { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2cdca66..9b40a96 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -40,6 +40,10 @@ Enable automated error reports Error reports will not be sent Error reports will be sent + readability.enable + Enable readability highlighting (experimental) + Readability highlighting is off + Readability highlighting is on Files will automatically save Files will not be automatically saved pref.custom_css diff --git a/app/src/main/res/xml/pref_general.xml b/app/src/main/res/xml/pref_general.xml index 0aff4b7..8f392cd 100644 --- a/app/src/main/res/xml/pref_general.xml +++ b/app/src/main/res/xml/pref_general.xml @@ -23,5 +23,11 @@ android:summaryOff="@string/pref_error_reports_off" android:summaryOn="@string/pref_error_reports_on" android:title="@string/pref_title_error_reports" /> + diff --git a/app/src/test/java/com/wbrawner/simplemarkdown/ReadabilityTest.java b/app/src/test/java/com/wbrawner/simplemarkdown/ReadabilityTest.java new file mode 100644 index 0000000..3f73e04 --- /dev/null +++ b/app/src/test/java/com/wbrawner/simplemarkdown/ReadabilityTest.java @@ -0,0 +1,52 @@ +package com.wbrawner.simplemarkdown; + +import com.wbrawner.simplemarkdown.model.Readability; +import com.wbrawner.simplemarkdown.model.Sentence; +import eu.crydee.syllablecounter.SyllableCounter; +import org.junit.Test; + +import java.util.List; + +import static org.junit.Assert.assertEquals; + +public class ReadabilityTest { + + @Test + public void break_content_into_sentances() { + SyllableCounter sc = new SyllableCounter(); + assertEquals(4, sc.count("facility")); + } + + @Test + public void can_break_text_into_sentences_with_indexes(){ + String content = "Hop on pop. I am a fish. This is a test."; + Readability readability = new Readability(content); + List sentenceList = readability.sentences(); + + assertEquals(3, sentenceList.size()); + + Sentence hopOnPop = sentenceList.get(0); + assertEquals(hopOnPop.toString(), "Hop on pop"); + assertEquals(0, hopOnPop.start()); + assertEquals(10, hopOnPop.end()); + + Sentence iAmAFish = sentenceList.get(1); + assertEquals(iAmAFish.toString(), "I am a fish"); + assertEquals(12, iAmAFish.start()); + assertEquals(23, iAmAFish.end()); + + Sentence thisIsATest = sentenceList.get(2); + assertEquals(thisIsATest.toString(), "This is a test"); + assertEquals(25, thisIsATest.start()); + assertEquals(39, thisIsATest.end()); + } + + + @Test + public void get_syllable_count_for_sentence(){ + assertEquals(8, new Sentence("This is the song that never ends").syllableCount()); + assertEquals(10, new Sentence("facility facility downing").syllableCount()); + } + + +} diff --git a/app/src/test/java/com/wbrawner/simplemarkdown/UtilsTest.java b/app/src/test/java/com/wbrawner/simplemarkdown/UtilsTest.java index e3236d5..1ddce0a 100644 --- a/app/src/test/java/com/wbrawner/simplemarkdown/UtilsTest.java +++ b/app/src/test/java/com/wbrawner/simplemarkdown/UtilsTest.java @@ -15,9 +15,7 @@ import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; import java.io.File; -import java.io.IOException; -import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; @@ -42,46 +40,6 @@ public class UtilsTest { rmdir(new File(rootDir)); } - @Test - public void getDocsPath() throws Exception { - sharedPreferences.edit().putString(Constants.KEY_DOCS_PATH, rootDir).apply(); - assertEquals(rootDir, Utils.getDocsPath(context)); - } - - @Test - public void getDefaultFileName() throws Exception { - sharedPreferences.edit().putString(Constants.KEY_DOCS_PATH, rootDir).apply(); - - new File(rootDir, "dummy.md").createNewFile(); - new File(rootDir, "dummy1.md").createNewFile(); - new File(rootDir, "Untitled-a.md").createNewFile(); - - String firstDefaultName = Utils.getDefaultFileName(context); - assertEquals("Untitled.md", firstDefaultName); - File firstFile = new File(rootDir, firstDefaultName); - firstFile.createNewFile(); - - String secondDefaultName = Utils.getDefaultFileName(context); - assertEquals("Untitled-1.md", secondDefaultName); - File secondFile = new File(rootDir, secondDefaultName); - secondFile.createNewFile(); - - String thirdDefaultName = Utils.getDefaultFileName(context); - assertEquals("Untitled-2.md", thirdDefaultName); - } - - @Test - public void getDefaultFileNameDoubleDigitTest() throws IOException { - sharedPreferences.edit().putString(Constants.KEY_DOCS_PATH, rootDir).apply(); - - for (int i = 0; i < 11; i++) { - new File(rootDir, "Untitled-" + i + ".md").createNewFile(); - } - assertTrue(new File(rootDir, "Untitled-10.md").exists()); - String defaultName = Utils.getDefaultFileName(context); - assertEquals("Untitled-11.md", defaultName); - } - @Test public void isAutosaveEnabled() throws Exception { assertTrue(Utils.isAutosaveEnabled(context));