diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 00000000..b467fb40 --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,42 @@ +version: 2 +jobs: + build: + working_directory: ~/code + docker: + - image: circleci/android:api-29-node + environment: + JVM_OPTS: -Xmx3200m + steps: + - checkout + - restore_cache: + key: jars-{{ checksum "build.gradle" }}-{{ checksum "app/build.gradle" }} +# - run: +# name: Chmod permissions #if permission for Gradlew Dependencies fail, use this. +# command: sudo chmod +x ./gradlew + - run: + name: Download Dependencies + command: ./gradlew androidDependencies + - save_cache: + paths: + - ~/.gradle + key: jars-{{ checksum "build.gradle" }}-{{ checksum "app/build.gradle" }} + - run: + name: Run Tests + command: ./gradlew lint test + - run: + name: Build Debug Apk + command: ./gradlew assembleDebug + - run: + name: Install AppCenter CLI + command: sudo npm install -g appcenter-cli + - run: + name: Publish to AppCenter + command: | + appcenter login --token ${APP_CENTER_TOKEN} + export apk="$(find ./app/build/outputs/apk/debug -regex '.*.apk')" + appcenter distribute release -f ${apk} -g ${APP_CENTER_DISTRIBUTION_LIST} --app ${APP_CENTER_APP_NAME} -r "Android" --silent + - store_artifacts: # for display in Artifacts: https://circleci.com/docs/2.0/artifacts/ + path: app/build/reports + destination: reports + - store_test_results: # for display in Test Summary: https://circleci.com/docs/2.0/collect-test-data/ + path: app/build/test-results \ No newline at end of file diff --git a/.gitignore b/.gitignore index 6214df79..d5dbb8c5 100644 --- a/.gitignore +++ b/.gitignore @@ -57,3 +57,7 @@ fastlane/Preview.html fastlane/screenshots fastlane/test_output fastlane/readme.md + +app/keys/ +app/release/ +app/.DS_Store diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..707f74bc --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,28 @@ +# Vocable Contributing Guidelines + +🥳🎉 Thank you for contributing to Vocable! 🎉🥳 + +Contributions from the open-source community are crucial in helping us meet make the best AAC app possible. + +# How to Contribute + +### Reporting Bugs +- Check the existing issues to make sure the bug has not already been reported. +- Open an issue describing the bug. + - Be sure to use a descriptive title and include detailed steps on how to reproduce the issue. + - Explain the expected behavior vs. the actual behavior. + - Include the version number. + - Tag the issue with the `bug` label. + + +### Suggesting Enhancements +- Open an issue clearly describing the behavior you would like to see. +- If applicable, provide a few examples of how the feature would work and what purpose it would serve. +- Tag the issue with the `enhancement` label. + +### Contributing Code by Opening a Pull Request +To contribute to Vocable, please fork the GitHub repo and submit a pull request to the `develop` branch. + +Our code owners will review your work and merge once any issues have been addressed. If the PR gets too outdated we may ask you to rebase and force push to update the PR. + +That's it! Our team will merge your PR, and your changes will be included in the next app version. Thanks for your contribution! 💯 \ No newline at end of file diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 00000000..d55b0456 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 WillowTree, LLC + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 86f624d2..acd9941d 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,55 @@ -# eyespeak-android +# Vocable AAC for Android +![Platform Android](https://img.shields.io/badge/Platform-Android-blue.svg) +![license MIT](https://img.shields.io/badge/license-MIT-brightgreen.svg) -#### Workflow: [Git Feature Branch Workflow](https://www.atlassian.com/git/tutorials/comparing-workflows/feature-branch-workflow) +> Empowering people to communicate with care takers and loved ones. + +[![Watch the video](marketing_assets/vocable_vimeo_still.gif)](https://player.vimeo.com/video/394212430) + +[![Play Store Link](marketing_assets/google-play-badge.svg)](https://play.google.com/store/apps/details?id=com.willowtree.vocable) + +## Contents +- [What is Vocable?](#what-is-vocable) +- [Features](#features) +- [Roadmap](#roadmap) +- [Contributing](#contributing) +- [Requirements](#requirements) +- [Credits](#credits) +- [License](#license) + +## What is Vocable? +Vocable AAC allows those with conditions such as MS, stroke, ALS, or spinal cord injuries to communicate using an app that tracks head movements, without the need to spend tens of thousands of dollars on technology to do so. + +## Features + +### Multimodal User Interface + +Vocable uses ARCore to track the user's head movements and understand where the user is looking at on the screen. This allows the app to be used completely hands-free: users can look around the screen and make selections by lingering their gaze at a particular element. + +For users with more mobility, the app can be operated by touch. + +### Saved Phrases +Use a list of common phrases provided by speech language pathologists, or create and save your own. + +### Full QWERTY Keyboard +Type with your head or your hands. + +## Roadmap +For the current progress on features, please visit the [project board](https://github.com/willowtreeapps/vocable-android/projects/1). + +For a high-level roadmap, see the [Vocable Roadmap](./ROADMAP.md) + +## Contributing +We love contributions! To get started, please see our [Contributing Guidelines](./CONTRIBUTING.md). + +## Device Requirements +- [Android devices with ARCore](https://developers.google.com/ar/discover/supported-devices) + +## Credits +Matt Kubota, Kyle Ohanian, Duncan Lewis, Ameir Al-Zoubi, and many more from [WillowTree](https://willowtreeapps.com/) 💙. + +## License +vocable-android is released under the MIT license. See [LICENSE](LICENSE) for details. + +## Other Variants +vocable-ios is available on [Apple Play Store](https://apps.apple.com/us/app/vocable-aac/id1497040547) and is also [open-source](https://github.com/willowtreeapps/vocable-ios). \ No newline at end of file diff --git a/ROADMAP.md b/ROADMAP.md new file mode 100644 index 00000000..ac68b773 --- /dev/null +++ b/ROADMAP.md @@ -0,0 +1,16 @@ +# Vocable Roadmap + +- [ ] Handset Support +- [ ] Edit and delete customized presets +- [ ] Custom Categories: create, edit, and delete +- [ ] Custom category/phrase sorting and UI placement +- [ ] History: recently-spoken phrases +- [ ] Head tracking calibration +- [ ] Audio feedback for navigation +- [ ] Potential Voice integration +- [ ] Alternative modalities / input actions (e.g. touch, joystick method, scanning) +- [ ] Light / Dark Mode +- [ ] Localization of app strings +- [ ] Localization of keyboard(s) +- [ ] Photo upload / pictoral buttons +- [ ] Translate selected phrase ↔️ spoken phrase \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 87f7e880..1120ae74 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -4,40 +4,88 @@ apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' +apply plugin: 'kotlin-kapt' + android { - compileSdkVersion 28 + compileSdkVersion 29 defaultConfig { - applicationId "com.example.eyespeak" + applicationId "com.willowtree.vocable" minSdkVersion 24 - targetSdkVersion 28 - versionCode 1 - versionName "1.0" + targetSdkVersion 29 + versionCode 11 + versionName "1.3.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + javaCompileOptions { + annotationProcessorOptions { + arguments = ["room.schemaLocation": "$projectDir/schemas".toString()] + } + } } buildTypes { release { - minifyEnabled false + minifyEnabled true proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } + testOptions { + execution 'ANDROIDX_TEST_ORCHESTRATOR' + } compileOptions { - sourceCompatibility 1.8 - targetCompatibility 1.8 + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = JavaVersion.VERSION_1_8.toString() + } + viewBinding { + enabled = true } + sourceSets { + androidTest.assets.srcDirs += files("$projectDir/schemas".toString()) + } + useLibrary 'android.test.runner' } dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - implementation 'androidx.appcompat:appcompat:1.0.2' - implementation 'androidx.core:core-ktx:1.0.2' + implementation 'androidx.appcompat:appcompat:1.1.0' + implementation 'androidx.core:core-ktx:1.2.0' + implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.2.0' implementation 'androidx.constraintlayout:constraintlayout:1.1.3' + implementation 'androidx.fragment:fragment:1.2.3' + // Provides ARCore Session and related resources. + implementation 'com.google.ar:core:1.16.0' + implementation 'com.google.ar.sceneform.ux:sceneform-ux:1.15.0' - testImplementation 'junit:junit:4.12' - androidTestImplementation 'androidx.test:runner:1.1.1' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.0' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.0' + implementation 'androidx.viewpager2:viewpager2:1.0.0' - // Provides ARCore Session and related resources. - implementation 'com.google.ar:core:1.9.0' - implementation 'com.google.ar.sceneform.ux:sceneform-ux:1.9.0' + implementation 'io.github.inflationx:calligraphy3:3.1.1' + implementation 'io.github.inflationx:viewpump:2.0.3' + + implementation 'android.arch.lifecycle:extensions:1.1.1' + implementation 'android.arch.lifecycle:viewmodel:1.1.1' + + implementation 'org.koin:koin-core:2.0.1' + implementation 'org.koin:koin-android:2.0.1' + + implementation "androidx.room:room-runtime:2.2.5" + kapt "androidx.room:room-compiler:2.2.5" + implementation "androidx.room:room-ktx:2.2.5" + + implementation 'androidx.security:security-crypto:1.0.0-beta01' + + // Moshi + implementation 'com.squareup.moshi:moshi-kotlin:1.9.2' + + androidTestUtil 'androidx.test:orchestrator:1.2.0' + + testImplementation 'junit:junit:4.12' + androidTestImplementation "androidx.room:room-testing:2.2.5" + androidTestImplementation 'androidx.test:runner:1.2.0' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' + androidTestImplementation 'androidx.test.ext:junit:1.1.1' + androidTestImplementation 'androidx.test:core:1.2.0' } diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index f1b42451..87f0b14b 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -19,3 +19,10 @@ # If you keep the line number information, uncomment this to # hide the original source file name. #-renamesourcefileattribute SourceFile +# JSR 305 annotations are for embedding nullability information. + +# Keep room model class members +-keepclassmembers class com.willowtree.vocable.room.models** { + (...); + ; +} \ No newline at end of file diff --git a/app/schemas/com.willowtree.vocable.room.VocableDatabase/2.json b/app/schemas/com.willowtree.vocable.room.VocableDatabase/2.json new file mode 100644 index 00000000..a0f94b39 --- /dev/null +++ b/app/schemas/com.willowtree.vocable.room.VocableDatabase/2.json @@ -0,0 +1,102 @@ +{ + "formatVersion": 1, + "database": { + "version": 2, + "identityHash": "e7b7fd6008db7fdeb34913b820f0c188", + "entities": [ + { + "tableName": "Category", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`identifier` INTEGER NOT NULL, `creation_date` INTEGER NOT NULL, `is_user_generated` INTEGER NOT NULL, `name` TEXT NOT NULL, PRIMARY KEY(`identifier`))", + "fields": [ + { + "fieldPath": "identifier", + "columnName": "identifier", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "creationDate", + "columnName": "creation_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isUserGenerated", + "columnName": "is_user_generated", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "identifier" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Phrase", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`identifier` INTEGER NOT NULL, `creation_date` INTEGER NOT NULL, `is_user_generated` INTEGER NOT NULL, `last_spoken_date` INTEGER NOT NULL, `utterance` TEXT NOT NULL, `category_id` INTEGER NOT NULL, PRIMARY KEY(`identifier`))", + "fields": [ + { + "fieldPath": "identifier", + "columnName": "identifier", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "creationDate", + "columnName": "creation_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isUserGenerated", + "columnName": "is_user_generated", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastSpokenDate", + "columnName": "last_spoken_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "utterance", + "columnName": "utterance", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "categoryId", + "columnName": "category_id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "identifier" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'e7b7fd6008db7fdeb34913b820f0c188')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.willowtree.vocable.room.VocableDatabase/3.json b/app/schemas/com.willowtree.vocable.room.VocableDatabase/3.json new file mode 100644 index 00000000..129863db --- /dev/null +++ b/app/schemas/com.willowtree.vocable.room.VocableDatabase/3.json @@ -0,0 +1,141 @@ +{ + "formatVersion": 1, + "database": { + "version": 3, + "identityHash": "4865923e2e1bd91ddd4fe607b90ea952", + "entities": [ + { + "tableName": "Category", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`category_id` TEXT NOT NULL, `creation_date` INTEGER NOT NULL, `is_user_generated` INTEGER NOT NULL, `localized_name` TEXT NOT NULL, `hidden` INTEGER NOT NULL, `sort_order` INTEGER NOT NULL, PRIMARY KEY(`category_id`))", + "fields": [ + { + "fieldPath": "categoryId", + "columnName": "category_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "creationDate", + "columnName": "creation_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isUserGenerated", + "columnName": "is_user_generated", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "localizedName", + "columnName": "localized_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hidden", + "columnName": "hidden", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sortOrder", + "columnName": "sort_order", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "category_id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Phrase", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`phrase_id` TEXT NOT NULL, `creation_date` INTEGER NOT NULL, `is_user_generated` INTEGER NOT NULL, `last_spoken_date` INTEGER NOT NULL, `localized_utterance` TEXT NOT NULL, `sort_order` INTEGER NOT NULL, PRIMARY KEY(`phrase_id`))", + "fields": [ + { + "fieldPath": "phraseId", + "columnName": "phrase_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "creationDate", + "columnName": "creation_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isUserGenerated", + "columnName": "is_user_generated", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastSpokenDate", + "columnName": "last_spoken_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "localizedUtterance", + "columnName": "localized_utterance", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sortOrder", + "columnName": "sort_order", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "phrase_id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "CategoryPhraseCrossRef", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`category_id` TEXT NOT NULL, `phrase_id` TEXT NOT NULL, PRIMARY KEY(`category_id`, `phrase_id`))", + "fields": [ + { + "fieldPath": "categoryId", + "columnName": "category_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "phraseId", + "columnName": "phrase_id", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "category_id", + "phrase_id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '4865923e2e1bd91ddd4fe607b90ea952')" + ] + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/example/eyespeak/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/willowtree/vocable/ExampleInstrumentedTest.kt similarity index 95% rename from app/src/androidTest/java/com/example/eyespeak/ExampleInstrumentedTest.kt rename to app/src/androidTest/java/com/willowtree/vocable/ExampleInstrumentedTest.kt index 687ea6b5..239bf7db 100644 --- a/app/src/androidTest/java/com/example/eyespeak/ExampleInstrumentedTest.kt +++ b/app/src/androidTest/java/com/willowtree/vocable/ExampleInstrumentedTest.kt @@ -1,4 +1,4 @@ -package com.example.eyespeak +package com.willowtree.vocable import androidx.test.InstrumentationRegistry import androidx.test.runner.AndroidJUnit4 diff --git a/app/src/androidTest/java/com/willowtree/vocable/room/MigrationTest.kt b/app/src/androidTest/java/com/willowtree/vocable/room/MigrationTest.kt new file mode 100644 index 00000000..9de3bc37 --- /dev/null +++ b/app/src/androidTest/java/com/willowtree/vocable/room/MigrationTest.kt @@ -0,0 +1,64 @@ +package com.willowtree.vocable.room + +import androidx.room.testing.MigrationTestHelper +import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import com.willowtree.vocable.utils.VocableSharedPreferences +import org.junit.Assert +import org.junit.Test +import org.junit.runner.RunWith +import java.io.IOException + +@RunWith(AndroidJUnit4::class) +class MigrationTest { + + companion object { + private const val TEST_DB = "migration-test" + } + + private val helper: MigrationTestHelper = MigrationTestHelper( + InstrumentationRegistry.getInstrumentation(), + VocableDatabase::class.java.canonicalName, + FrameworkSQLiteOpenHelperFactory() + ) + + @Test + @Throws(IOException::class) + fun migrate2To3() { + val mySayingsCategoryNameV2 = "My Sayings" + val mySayingTestPhraseV2 = "Test Phrase" + + helper.createDatabase(TEST_DB, 2).apply { + // Create mock My Sayings category + val catId = System.currentTimeMillis() + execSQL("INSERT INTO Category (identifier, creation_date, is_user_generated, name) VALUES ($catId, ${System.currentTimeMillis()}, 0, '$mySayingsCategoryNameV2')") + + // Add a custom phrase to My Sayings category + val phraseId = System.currentTimeMillis() + execSQL("INSERT INTO Phrase (identifier, creation_date, is_user_generated, last_spoken_date, utterance, category_id) VALUES ($phraseId, ${System.currentTimeMillis()}, 0, ${System.currentTimeMillis()}, '$mySayingTestPhraseV2', $catId)") + + close() + } + + helper.runMigrationsAndValidate(TEST_DB, 3, true, VocableDatabaseMigrations.MIGRATION_2_3) + .apply { + // Verify that V2 custom phrase was saved in SharedPreferences + val sharedPrefs = VocableSharedPreferences() + val v2MySayings = sharedPrefs.getMySayings() + Assert.assertEquals(1, v2MySayings.size) + Assert.assertEquals(mySayingTestPhraseV2, v2MySayings.first()) + + // Verify that new schema is as expected + val categoryIdV3 = "Category_V3" + val categoryNameV3 = "Category_Name_V3" + execSQL("INSERT INTO Category (category_id, creation_date, is_user_generated, localized_name, hidden, sort_order) VALUES ('$categoryIdV3', ${System.currentTimeMillis()}, 0, '$categoryNameV3', 0, 0)") + + val phraseIdV3 = "Phrase_V3" + val phraseUtteranceV3 = "Phrase_Utterance_V3" + execSQL("INSERT INTO Phrase (phrase_id, creation_date, is_user_generated, last_spoken_date, localized_utterance, sort_order) VALUES ('$phraseIdV3', ${System.currentTimeMillis()}, 0, ${System.currentTimeMillis()}, '$phraseUtteranceV3', 0)") + + close() + } + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 8c815994..f46255a1 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,21 +1,35 @@ + package="com.willowtree.vocable"> + + + + - + android:name=".VocableApp" + android:allowBackup="false" + android:icon="@mipmap/ic_launcher" + android:label="@string/app_name" + android:roundIcon="@mipmap/ic_launcher_round" + android:supportsRtl="true" + android:theme="@style/AppTheme"> + + + - + + - + + + \ No newline at end of file diff --git a/app/src/main/assets/fonts/MaterialIcons-Regular.ttf b/app/src/main/assets/fonts/MaterialIcons-Regular.ttf new file mode 100644 index 00000000..7015564a Binary files /dev/null and b/app/src/main/assets/fonts/MaterialIcons-Regular.ttf differ diff --git a/app/src/main/assets/json/presets.json b/app/src/main/assets/json/presets.json new file mode 100644 index 00000000..b85ba3aa --- /dev/null +++ b/app/src/main/assets/json/presets.json @@ -0,0 +1,683 @@ +{ + "phrases" : [ + { + "id" : "preset_7ACA0926-DB7F-4B9E-872C-AE9690AD79E7", + "categoryIds" : [ + "preset_F8F5D1C9-0AA6-4152-BF7C-0851ACD1406B" + ], + "localizedUtterance" : { + "de" : "Ich muss aufs Klo", + "en" : "I need to go to the restroom" + } + }, + { + "id" : "preset_9DEB32B5-8606-4689-8F6A-0B251F4DB377", + "categoryIds" : [ + "preset_F8F5D1C9-0AA6-4152-BF7C-0851ACD1406B" + ], + "localizedUtterance" : { + "de" : "Ich bin durstig", + "en" : "I am thirsty" + } + }, + { + "id" : "preset_E012D902-E8BA-4BEA-92C7-7107ECF8051C", + "categoryIds" : [ + "preset_F8F5D1C9-0AA6-4152-BF7C-0851ACD1406B" + ], + "localizedUtterance" : { + "de" : "Ich habe Hunger", + "en" : "I am hungry" + } + }, + { + "id" : "preset_8C4D0099-9BA8-4914-A6B5-730FFDC8C499", + "categoryIds" : [ + "preset_F8F5D1C9-0AA6-4152-BF7C-0851ACD1406B" + ], + "localizedUtterance" : { + "de" : "Mir ist kalt", + "en" : "I am cold" + } + }, + { + "id" : "preset_6AD93FB9-437C-455F-A4AF-C96033BCBCAE", + "categoryIds" : [ + "preset_F8F5D1C9-0AA6-4152-BF7C-0851ACD1406B" + ], + "localizedUtterance" : { + "de" : "Mir ist heiß", + "en" : "I am hot" + } + }, + { + "id" : "preset_F6F03BBD-B9CD-41F5-B26A-FCC13CCBB199", + "categoryIds" : [ + "preset_F8F5D1C9-0AA6-4152-BF7C-0851ACD1406B" + ], + "localizedUtterance" : { + "de" : "Ich bin müde", + "en" : "I am tired" + } + }, + { + "id" : "preset_ADDD57C6-D11E-4B32-A2B3-05AA43D9AC8C", + "categoryIds" : [ + "preset_F8F5D1C9-0AA6-4152-BF7C-0851ACD1406B" + ], + "localizedUtterance" : { + "de" : "Mir geht es gut", + "en" : "I am fine" + } + }, + { + "id" : "preset_C6C14627-E48E-4EF3-B81D-64C191F2EC75", + "categoryIds" : [ + "preset_F8F5D1C9-0AA6-4152-BF7C-0851ACD1406B" + ], + "localizedUtterance" : { + "de" : "Das reicht, danke", + "en" : "I am good" + } + }, + { + "id" : "preset_C6E6B6E7-BEB6-466D-822A-F8CE2D092B9E", + "categoryIds" : [ + "preset_F8F5D1C9-0AA6-4152-BF7C-0851ACD1406B" + ], + "localizedUtterance" : { + "de" : "Ich fühle mich unwohl", + "en" : "I am uncomfortable" + } + }, + { + "id" : "preset_5542BE2F-419E-41A5-BBF2-5B82E0A49834", + "categoryIds" : [ + "preset_F8F5D1C9-0AA6-4152-BF7C-0851ACD1406B" + ], + "localizedUtterance" : { + "de" : "Ich habe Schmerzen", + "en" : "I am in pain" + } + }, + { + "id" : "preset_28A8F6B6-E196-4981-B762-652C288A0C29", + "categoryIds" : [ + "preset_F8F5D1C9-0AA6-4152-BF7C-0851ACD1406B" + ], + "localizedUtterance" : { + "de" : "Ich bin fertig", + "en" : "I am finished" + } + }, + { + "id" : "preset_0B491C3E-1A7F-4A94-A523-6DE329BF9E72", + "categoryIds" : [ + "preset_F8F5D1C9-0AA6-4152-BF7C-0851ACD1406B" + ], + "localizedUtterance" : { + "de" : "Ich möchte mich hinlegen", + "en" : "I want to lie down" + } + }, + { + "id" : "preset_BBE8BABC-CCBF-49BF-87D4-057016AADBC5", + "categoryIds" : [ + "preset_F8F5D1C9-0AA6-4152-BF7C-0851ACD1406B" + ], + "localizedUtterance" : { + "de" : "Ich möchte mich aufsetzen", + "en" : "I want to sit up" + } + }, + { + "id" : "preset_3EF1FBFA-E07A-47B5-BDF6-3AE7A684B834", + "categoryIds" : [ + "preset_EB7A9732-E28E-4440-A88B-BA2A1ACFBD76" + ], + "localizedUtterance" : { + "de" : "Hallo", + "en" : "Hello" + } + }, + { + "id" : "preset_9929E83B-997D-40AA-841C-130709F5115B", + "categoryIds" : [ + "preset_EB7A9732-E28E-4440-A88B-BA2A1ACFBD76" + ], + "localizedUtterance" : { + "de" : "Guten Morgen", + "en" : "Good morning" + } + }, + { + "id" : "preset_D6A67E02-3D07-4D44-8B04-2C1B9B8B167E", + "categoryIds" : [ + "preset_EB7A9732-E28E-4440-A88B-BA2A1ACFBD76" + ], + "localizedUtterance" : { + "de" : "Guten Abend", + "en" : "Good evening" + } + }, + { + "id" : "preset_0EE14D06-92D7-4A26-A15F-253528F14A69", + "categoryIds" : [ + "preset_EB7A9732-E28E-4440-A88B-BA2A1ACFBD76" + ], + "localizedUtterance" : { + "de" : "Freut mich dich zu sehen", + "en" : "Pleased to meet you" + } + }, + { + "id" : "preset_0EF33A3F-41E8-4E47-9B2C-69FC86B957FC", + "categoryIds" : [ + "preset_EB7A9732-E28E-4440-A88B-BA2A1ACFBD76" + ], + "localizedUtterance" : { + "de" : "Wie läuft dein Tag?", + "en" : "How is your day?" + } + }, + { + "id" : "preset_1B634A96-563E-4275-875A-2B705A5D1178", + "categoryIds" : [ + "preset_EB7A9732-E28E-4440-A88B-BA2A1ACFBD76" + ], + "localizedUtterance" : { + "de" : "Wie geht es dir?", + "en" : "How are you?" + } + }, + { + "id" : "preset_31E8760F-E728-4261-A82E-B50AB40C73FF", + "categoryIds" : [ + "preset_EB7A9732-E28E-4440-A88B-BA2A1ACFBD76" + ], + "localizedUtterance" : { + "de" : "Wie läuft's?", + "en" : "How's it going?" + } + }, + { + "id" : "preset_24B48F19-EA3A-49A1-88C1-AACB67FE7278", + "categoryIds" : [ + "preset_EB7A9732-E28E-4440-A88B-BA2A1ACFBD76" + ], + "localizedUtterance" : { + "de" : "Wie war dein Wochenende?", + "en" : "How was your weekend?" + } + }, + { + "id" : "preset_B2442AA0-6004-4636-9B39-2DB97ADD1DA1", + "categoryIds" : [ + "preset_EB7A9732-E28E-4440-A88B-BA2A1ACFBD76" + ], + "localizedUtterance" : { + "de" : "Tschüss", + "en" : "Goodbye" + } + }, + { + "id" : "preset_045EF309-3EB7-46B9-8AB7-31BC8482E3DC", + "categoryIds" : [ + "preset_EB7A9732-E28E-4440-A88B-BA2A1ACFBD76" + ], + "localizedUtterance" : { + "de" : "OK", + "en" : "Okay" + } + }, + { + "id" : "preset_573DD827-7201-49F1-8BE8-0BC18028A8CB", + "categoryIds" : [ + "preset_EB7A9732-E28E-4440-A88B-BA2A1ACFBD76" + ], + "localizedUtterance" : { + "de" : "Schlecht", + "en" : "Bad" + } + }, + { + "id" : "preset_D64EE532-14D3-4434-B671-8F4368EC0A8D", + "categoryIds" : [ + "preset_EB7A9732-E28E-4440-A88B-BA2A1ACFBD76" + ], + "localizedUtterance" : { + "de" : "Gut", + "en" : "Good" + } + }, + { + "id" : "preset_6B463AF1-C884-45CD-9194-90806E75FD70", + "categoryIds" : [ + "preset_EB7A9732-E28E-4440-A88B-BA2A1ACFBD76" + ], + "localizedUtterance" : { + "de" : "Das macht Sinn", + "en" : "That makes sense" + } + }, + { + "id" : "preset_A436EAAF-BD8A-4D8D-81EA-DC6D3C849D4A", + "categoryIds" : [ + "preset_EB7A9732-E28E-4440-A88B-BA2A1ACFBD76" + ], + "localizedUtterance" : { + "de" : "Das mag ich", + "en" : "I like it" + } + }, + { + "id" : "preset_A02D6604-5C7C-4BB0-8C4A-E1C5E0D1A869", + "categoryIds" : [ + "preset_EB7A9732-E28E-4440-A88B-BA2A1ACFBD76" + ], + "localizedUtterance" : { + "de" : "Bitte aufhören", + "en" : "Please stop" + } + }, + { + "id" : "preset_30153E11-D48C-47C8-9186-47988D0A5B7A", + "categoryIds" : [ + "preset_EB7A9732-E28E-4440-A88B-BA2A1ACFBD76" + ], + "localizedUtterance" : { + "de" : "Ich stimme nicht zu", + "en" : "I do not agree" + } + }, + { + "id" : "preset_A74483A8-4069-40D7-992C-01295934E97C", + "categoryIds" : [ + "preset_EB7A9732-E28E-4440-A88B-BA2A1ACFBD76" + ], + "localizedUtterance" : { + "de" : "Wie bitte?", + "en" : "Please repeat what you said" + } + }, + { + "id" : "preset_33B3F4B7-2438-439B-A21B-46D2599C8840", + "categoryIds" : [ + "preset_C0B2A1A8-8333-4121-B4A8-FFFA185EB5D2" + ], + "localizedUtterance" : { + "de" : "Bitte", + "en" : "Please" + } + }, + { + "id" : "preset_72633C5C-47A2-4D5E-9615-E1408234478F", + "categoryIds" : [ + "preset_C0B2A1A8-8333-4121-B4A8-FFFA185EB5D2" + ], + "localizedUtterance" : { + "de" : "Danke", + "en" : "Thank you" + } + }, + { + "id" : "preset_8BCBACFB-D5ED-46CB-B3E2-21FB31D8D6AD", + "categoryIds" : [ + "preset_C0B2A1A8-8333-4121-B4A8-FFFA185EB5D2" + ], + "localizedUtterance" : { + "de" : "Ja", + "en" : "Yes" + } + }, + { + "id" : "preset_FABE749C-B54D-4031-B8E7-62777B34D273", + "categoryIds" : [ + "preset_C0B2A1A8-8333-4121-B4A8-FFFA185EB5D2" + ], + "localizedUtterance" : { + "de" : "Nein", + "en" : "No" + } + }, + { + "id" : "preset_DFB79E0D-CF93-4744-B9EE-12738E1864E2", + "categoryIds" : [ + "preset_C0B2A1A8-8333-4121-B4A8-FFFA185EB5D2" + ], + "localizedUtterance" : { + "de" : "Vielleicht", + "en" : "Maybe" + } + }, + { + "id" : "preset_7FF4503F-F838-48BE-B529-758B3531093C", + "categoryIds" : [ + "preset_C0B2A1A8-8333-4121-B4A8-FFFA185EB5D2" + ], + "localizedUtterance" : { + "de" : "Einen Moment, bitte", + "en" : "Please wait" + } + }, + { + "id" : "preset_6CBD6D3E-42A7-435C-B661-2ABD669DC6BE", + "categoryIds" : [ + "preset_C0B2A1A8-8333-4121-B4A8-FFFA185EB5D2" + ], + "localizedUtterance" : { + "de" : "Das weiß ich nicht", + "en" : "I don't know" + } + }, + { + "id" : "preset_BEE6FF86-CE30-4096-9E37-C469F63630B7", + "categoryIds" : [ + "preset_C0B2A1A8-8333-4121-B4A8-FFFA185EB5D2" + ], + "localizedUtterance" : { + "de" : "Das wollte ich nicht sagen", + "en" : "I didn't mean to say that" + } + }, + { + "id" : "preset_63980C93-EE77-40DE-BDE7-BF8F97EC0304", + "categoryIds" : [ + "preset_C0B2A1A8-8333-4121-B4A8-FFFA185EB5D2" + ], + "localizedUtterance" : { + "de" : "Hab bitte Geduld", + "en" : "Please be patient" + } + }, + { + "id" : "preset_9E3230D7-1172-491A-8A00-E734B1661DC1", + "categoryIds" : [ + "preset_52CA4E71-4A8C-4EA8-8EA8-C4B18AA16EC8" + ], + "localizedUtterance" : { + "de" : "Bitte das Licht anschalten", + "en" : "Please turn the lights on" + } + }, + { + "id" : "preset_46EF7041-50F7-4B64-A0F6-879A8A3D7532", + "categoryIds" : [ + "preset_52CA4E71-4A8C-4EA8-8EA8-C4B18AA16EC8" + ], + "localizedUtterance" : { + "de" : "Bitte das Licht ausschalten", + "en" : "Please turn the lights off" + } + }, + { + "id" : "preset_9A808275-0D67-49E4-8BF8-6F22A0C7E169", + "categoryIds" : [ + "preset_52CA4E71-4A8C-4EA8-8EA8-C4B18AA16EC8" + ], + "localizedUtterance" : { + "de" : "Kein Besuch bitte", + "en" : "No visitors please" + } + }, + { + "id" : "preset_E7D11E13-BC4F-4FE5-9C8A-BE2DE31AEE7F", + "categoryIds" : [ + "preset_52CA4E71-4A8C-4EA8-8EA8-C4B18AA16EC8" + ], + "localizedUtterance" : { + "de" : "Ich hätte gerne Besuch", + "en" : "I would like visitors" + } + }, + { + "id" : "preset_30474911-619F-4FBF-B7DA-F64039F556F2", + "categoryIds" : [ + "preset_52CA4E71-4A8C-4EA8-8EA8-C4B18AA16EC8" + ], + "localizedUtterance" : { + "de" : "Bitte leise sein", + "en" : "Please be quiet" + } + }, + { + "id" : "preset_0167AA43-587D-436F-91A9-9CEEAF1CCCF1", + "categoryIds" : [ + "preset_52CA4E71-4A8C-4EA8-8EA8-C4B18AA16EC8" + ], + "localizedUtterance" : { + "de" : "Ich möchte reden", + "en" : "I would like to talk" + } + }, + { + "id" : "preset_FB19C160-BC96-4580-9542-6F54AA1B2D70", + "categoryIds" : [ + "preset_52CA4E71-4A8C-4EA8-8EA8-C4B18AA16EC8" + ], + "localizedUtterance" : { + "de" : "Bitte den Fernseher anschalten", + "en" : "Please turn the TV on" + } + }, + { + "id" : "preset_9EE3A781-E84E-4728-976F-8EF993B8174C", + "categoryIds" : [ + "preset_52CA4E71-4A8C-4EA8-8EA8-C4B18AA16EC8" + ], + "localizedUtterance" : { + "de" : "Bitte den Fernseher ausschalten", + "en" : "Please turn the TV off" + } + }, + { + "id" : "preset_DC942BB6-B2BF-4ABB-8931-719BD2A504E6", + "categoryIds" : [ + "preset_52CA4E71-4A8C-4EA8-8EA8-C4B18AA16EC8" + ], + "localizedUtterance" : { + "de" : "Bitte lauter", + "en" : "Please turn the volume up" + } + }, + { + "id" : "preset_702D3BAD-6C56-4DC6-8290-8E0693B63152", + "categoryIds" : [ + "preset_52CA4E71-4A8C-4EA8-8EA8-C4B18AA16EC8" + ], + "localizedUtterance" : { + "de" : "Bitte leiser", + "en" : "Please turn the volume down" + } + }, + { + "id" : "preset_42DC7273-92F9-4149-8166-966C3C201862", + "categoryIds" : [ + "preset_52CA4E71-4A8C-4EA8-8EA8-C4B18AA16EC8" + ], + "localizedUtterance" : { + "de" : "Bitte die Vorhänge öffnen", + "en" : "Please open the blinds" + } + }, + { + "id" : "preset_88556B8E-FE61-401D-888C-0D79568715A0", + "categoryIds" : [ + "preset_52CA4E71-4A8C-4EA8-8EA8-C4B18AA16EC8" + ], + "localizedUtterance" : { + "de" : "Bitte die Vorhänge schließen", + "en" : "Please close the blinds" + } + }, + { + "id" : "preset_2BF1A363-CE24-4F26-9727-C5EAD1FEC27C", + "categoryIds" : [ + "preset_52CA4E71-4A8C-4EA8-8EA8-C4B18AA16EC8" + ], + "localizedUtterance" : { + "de" : "Bitte das Fenster öffnen", + "en" : "Please open the window" + } + }, + { + "id" : "preset_C865BE96-40B5-406C-9BAA-0A0D3280E377", + "categoryIds" : [ + "preset_52CA4E71-4A8C-4EA8-8EA8-C4B18AA16EC8" + ], + "localizedUtterance" : { + "de" : "Bitte das Fenster schließen", + "en" : "Please close the window" + } + }, + { + "id" : "preset_1244394F-2793-47EF-BCAC-DB8BBDAB78C6", + "categoryIds" : [ + "preset_E7ADBE88-2722-4DE7-BDC1-994F07EA294B" + ], + "localizedUtterance" : { + "de" : "Ich brauche meine Medikamente", + "en" : "I need my medication" + } + }, + { + "id" : "preset_64348AD1-9F63-4ADA-B391-E1742913C0E6", + "categoryIds" : [ + "preset_E7ADBE88-2722-4DE7-BDC1-994F07EA294B" + ], + "localizedUtterance" : { + "de" : "Ich möchte baden", + "en" : "I need a bath" + } + }, + { + "id" : "preset_81B473E3-2FC9-40F0-AE59-3C658EC6CD91", + "categoryIds" : [ + "preset_E7ADBE88-2722-4DE7-BDC1-994F07EA294B" + ], + "localizedUtterance" : { + "de" : "Ich möchte duschen", + "en" : "I need a shower" + } + }, + { + "id" : "preset_73B654AA-C999-4864-BF5A-77379DAA991C", + "categoryIds" : [ + "preset_E7ADBE88-2722-4DE7-BDC1-994F07EA294B" + ], + "localizedUtterance" : { + "de" : "Ich möchte mein Gesicht waschen", + "en" : "I need to wash my face" + } + }, + { + "id" : "preset_1BAD392A-C26C-4080-B0F2-FC544E08349C", + "categoryIds" : [ + "preset_E7ADBE88-2722-4DE7-BDC1-994F07EA294B" + ], + "localizedUtterance" : { + "de" : "Ich möchte mein Haar kämmen", + "en" : "I need to brush my hair" + } + }, + { + "id" : "preset_4F776A4A-4154-4DFE-89AC-E603C2A578EE", + "categoryIds" : [ + "preset_E7ADBE88-2722-4DE7-BDC1-994F07EA294B" + ], + "localizedUtterance" : { + "de" : "Bitte mein Kissen aufschütteln", + "en" : "Please fix my pillow" + } + }, + { + "id" : "preset_8359B09D-7259-40CC-B258-02895ECC4DB7", + "categoryIds" : [ + "preset_E7ADBE88-2722-4DE7-BDC1-994F07EA294B" + ], + "localizedUtterance" : { + "de" : "Ich muss spucken", + "en" : "I need to spit" + } + }, + { + "id" : "preset_939B4829-D8B4-42F1-B8A2-0E956AAE1FEE", + "categoryIds" : [ + "preset_E7ADBE88-2722-4DE7-BDC1-994F07EA294B" + ], + "localizedUtterance" : { + "de" : "Ich habe Atembeschwerden", + "en" : "I am having trouble breathing" + } + }, + { + "id" : "preset_05754D0C-962D-48C3-8554-E4F7D5E86D93", + "categoryIds" : [ + "preset_E7ADBE88-2722-4DE7-BDC1-994F07EA294B" + ], + "localizedUtterance" : { + "de" : "Ich brauchen eine Jacke", + "en" : "I need a jacket" + } + } + ], + "schemaVersion" : 1, + "categories" : [ + { + "id" : "preset_C0B2A1A8-8333-4121-B4A8-FFFA185EB5D2", + "localizedName" : { + "de" : "Allgemeines", + "en" : "General" + }, + "hidden" : false + }, + { + "id" : "preset_F8F5D1C9-0AA6-4152-BF7C-0851ACD1406B", + "localizedName" : { + "de" : "Bedürfnisse", + "en" : "Basic Needs" + }, + "hidden" : false + }, + { + "id" : "preset_E7ADBE88-2722-4DE7-BDC1-994F07EA294B", + "localizedName" : { + "de" : "Hygiene", + "en" : "Personal Care" + }, + "hidden" : false + }, + { + "id" : "preset_EB7A9732-E28E-4440-A88B-BA2A1ACFBD76", + "localizedName" : { + "de" : "Unterhaltung", + "en" : "Conversation" + }, + "hidden" : false + }, + { + "id" : "preset_52CA4E71-4A8C-4EA8-8EA8-C4B18AA16EC8", + "localizedName" : { + "de" : "Umgebung", + "en" : "Environment" + }, + "hidden" : false + }, + { + "id" : "preset_user_keypad", + "localizedName" : { + "de" : "123", + "en" : "123" + }, + "hidden" : false + }, + { + "id" : "preset_user_favorites", + "localizedName" : { + "de" : "Meine Redewendung", + "en" : "My Sayings" + }, + "hidden" : false + } + ] +} \ No newline at end of file diff --git a/app/src/main/java/com/example/eyespeak/MainActivity.kt b/app/src/main/java/com/example/eyespeak/MainActivity.kt deleted file mode 100644 index b734f863..00000000 --- a/app/src/main/java/com/example/eyespeak/MainActivity.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.example.eyespeak - -import android.graphics.PointF -import android.os.Bundle -import androidx.appcompat.app.AppCompatActivity -import kotlinx.android.synthetic.main.activity_main.* - -class MainActivity : AppCompatActivity() { - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_main) - } - - override fun onStart() { - super.onStart() - pointer_view.updatePointerPositionUnitInternal(PointF(0f, -1f)) - } - -} diff --git a/app/src/main/java/com/example/eyespeak/PointerView.kt b/app/src/main/java/com/example/eyespeak/PointerView.kt deleted file mode 100644 index d6443f22..00000000 --- a/app/src/main/java/com/example/eyespeak/PointerView.kt +++ /dev/null @@ -1,52 +0,0 @@ -package com.example.eyespeak - -import android.content.Context -import android.graphics.Canvas -import android.graphics.Color -import android.graphics.Paint -import android.graphics.PointF -import android.util.AttributeSet -import android.util.Log -import android.view.MotionEvent -import android.view.View - -const val POINTER_COLOR: Int = 0xFFFFFFFF.toInt() -const val POINTER_RADIUS: Float = 100f - -class PointerView(context: Context, attrs: AttributeSet) : View(context, attrs) { - - private val paint: Paint = Paint().apply { color = POINTER_COLOR } - - private val pointerPosition = PointF(0f, 0f) - private val pointerPositionUnit = PointF(0f, 0f) - - override fun onDraw(canvas: Canvas?) { - super.onDraw(canvas) - canvas?.drawCircle(pointerPosition.x, pointerPosition.y, POINTER_RADIUS, paint) - } - - private fun updatePointerPosition(position: PointF) { - this.pointerPosition.set(position) - invalidate() - } - - //Expect values satisfying [-1,1] - fun updatePointerPositionUnitInternal(positionUnit: PointF) { - pointerPositionUnit.set(positionUnit) - - val widthHalf: Float = width.toFloat()/2 - val heightHalf: Float = height.toFloat()/2 - - val pointerXNew = (widthHalf * pointerPositionUnit.x) + widthHalf - val pointerYNew = (heightHalf * pointerPositionUnit.y) + heightHalf - - updatePointerPosition(PointF(pointerXNew, pointerYNew)) - } - - override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { - super.onSizeChanged(w, h, oldw, oldh) - updatePointerPositionUnitInternal(pointerPositionUnit) - invalidate() - } - -} diff --git a/app/src/main/java/com/willowtree/vocable/AppKoinModule.kt b/app/src/main/java/com/willowtree/vocable/AppKoinModule.kt new file mode 100644 index 00000000..905d82c4 --- /dev/null +++ b/app/src/main/java/com/willowtree/vocable/AppKoinModule.kt @@ -0,0 +1,16 @@ +package com.willowtree.vocable + +import com.squareup.moshi.Moshi +import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory +import com.willowtree.vocable.presets.PresetsRepository +import com.willowtree.vocable.utils.VocableSharedPreferences +import org.koin.dsl.module + +object AppKoinModule { + + fun getModule() = module { + single { VocableSharedPreferences() } + single { PresetsRepository(get()) } + single { Moshi.Builder().add(KotlinJsonAdapterFactory()).build() } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/willowtree/vocable/BaseActivity.kt b/app/src/main/java/com/willowtree/vocable/BaseActivity.kt new file mode 100644 index 00000000..ae06e320 --- /dev/null +++ b/app/src/main/java/com/willowtree/vocable/BaseActivity.kt @@ -0,0 +1,232 @@ +package com.willowtree.vocable + +import android.app.ActivityManager +import android.content.Context +import android.graphics.Rect +import android.hardware.display.DisplayManager +import android.os.Bundle +import android.util.DisplayMetrics +import android.util.Log +import android.view.Surface +import android.view.View +import android.widget.Toast +import androidx.annotation.CallSuper +import androidx.annotation.LayoutRes +import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.isVisible +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProviders +import com.google.ar.core.ArCoreApk +import com.willowtree.vocable.customviews.PointerListener +import com.willowtree.vocable.customviews.PointerView +import com.willowtree.vocable.facetracking.FaceTrackFragment +import com.willowtree.vocable.facetracking.FaceTrackingViewModel +import com.willowtree.vocable.utils.VocableSharedPreferences +import org.koin.android.ext.android.inject + +abstract class BaseActivity : AppCompatActivity() { + + private val minOpenGlVersion = 3.0 + + private val displayMetrics = DisplayMetrics() + + private var currentView: View? = null + + private lateinit var viewModel: FaceTrackingViewModel + + private val sharedPrefs: VocableSharedPreferences by inject() + + private var paused = false + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + if (!checkIsSupportedDeviceOrFinish()) { + return + } + windowManager.defaultDisplay.getMetrics(displayMetrics) + viewModel = ViewModelProviders.of(this).get(FaceTrackingViewModel::class.java) + subscribeToViewModel() + + val displayListener = object : DisplayManager.DisplayListener { + + private var orientation = windowManager.defaultDisplay.rotation + + override fun onDisplayChanged(displayId: Int) { + val newOrientation = windowManager.defaultDisplay.rotation + // Only reset FaceTrackFragment if device is rotated 180 degrees + when (orientation) { + Surface.ROTATION_0 -> { + if (newOrientation == Surface.ROTATION_180) { + resetFaceTrackFragment("${Surface.ROTATION_180}") + } + } + Surface.ROTATION_90 -> { + if (newOrientation == Surface.ROTATION_270) { + resetFaceTrackFragment("${Surface.ROTATION_270}") + } + } + Surface.ROTATION_180 -> { + if (newOrientation == Surface.ROTATION_0) { + resetFaceTrackFragment("${Surface.ROTATION_0}") + } + } + Surface.ROTATION_270 -> { + if (newOrientation == Surface.ROTATION_90) { + resetFaceTrackFragment("${Surface.ROTATION_90}") + } + } + } + orientation = newOrientation + } + + override fun onDisplayAdded(displayId: Int) {} + + override fun onDisplayRemoved(displayId: Int) {} + } + val displayManager = getSystemService(Context.DISPLAY_SERVICE) as DisplayManager + displayManager.registerDisplayListener(displayListener, null) + } + + /** + * If the device rotates 180 degrees (portrait to portrait/landscape to landscape), the + * activity won't be destroyed and recreated. This means that the FaceTrackFragment will not + * reset its camera positioning. The only way to reset it currently is to create a new + * instance of the fragment and add it to the activity. + * @param tag The tag to use for the FaceTrackFragment, should be unique to the orientation + */ + private fun resetFaceTrackFragment(tag: String) { + if (!supportFragmentManager.isDestroyed && supportFragmentManager.findFragmentByTag(tag) == null) { + supportFragmentManager + .beginTransaction() + .replace(R.id.face_fragment, FaceTrackFragment(), tag) + .commit() + } + } + + protected abstract fun getPointerView(): PointerView + + protected abstract fun getErrorView(): View + + protected abstract fun getAllViews(): List + + @LayoutRes + protected abstract fun getLayout(): Int + + @CallSuper + protected open fun subscribeToViewModel() { + viewModel.showError.observe(this, Observer { + if (!sharedPrefs.getHeadTrackingEnabled()) { + getPointerView().isVisible = false + getErrorView().isVisible = false + return@Observer + } + it?.let { + if (it) { + (currentView as? PointerListener)?.onPointerExit() + } + getErrorView().isVisible = it + getPointerView().isVisible = !it + } + }) + viewModel.pointerLocation.observe(this, Observer { + it.let { + updatePointer(it.x, it.y) + } + }) + } + + private fun updatePointer(x: Float, y: Float) { + var newX = x + var newY = y + if (x < 0) { + newX = 0f + } else if (x > displayMetrics.widthPixels) { + newX = displayMetrics.widthPixels.toFloat() + } + + if (y < 0) { + newY = 0f + } else if (y > displayMetrics.heightPixels) { + newY = displayMetrics.heightPixels.toFloat() + } + getPointerView().updatePointerPosition(newX, newY) + getPointerView().bringToFront() + + if (currentView == null) { + findIntersectingView() + } else { + if (!viewIntersects(currentView!!, getPointerView())) { + (currentView as? PointerListener)?.onPointerExit() + findIntersectingView() + } + } + } + + private fun findIntersectingView() { + currentView = null + if (!paused) { + getAllViews().forEach { + if (viewIntersects(it, getPointerView())) { + if (it.isEnabled && it.isVisible) { + currentView = it + (currentView as PointerListener).onPointerEnter() + return + } + } + } + } + } + + + private fun viewIntersects(view1: View, view2: View): Boolean { + val coords = IntArray(2) + view1.getLocationOnScreen(coords) + val rect = Rect( + coords[0], + coords[1], + coords[0] + view1.measuredWidth, + coords[1] + view1.measuredHeight + ) + + val view2Coords = IntArray(2) + view2.getLocationOnScreen(view2Coords) + val view2Rect = Rect( + view2Coords[0], + view2Coords[1], + view2Coords[0] + view2.measuredWidth, + view2Coords[1] + view2.measuredHeight + ) + return rect.contains(view2Rect.centerX(), view2Rect.centerY()) + } + + /** + * Returns false and displays an error message if Sceneform can not run, true if Sceneform can run + * on this device. + * + * + * Sceneform requires Android N on the device as well as OpenGL 3.0 capabilities. + * + * + * Finishes the activity if Sceneform can not run + */ + private fun checkIsSupportedDeviceOrFinish(): Boolean { + if (ArCoreApk.getInstance().checkAvailability(this) === ArCoreApk.Availability.UNSUPPORTED_DEVICE_NOT_CAPABLE) { + Log.e("TAG", "Augmented Faces requires ARCore.") + Toast.makeText(this, "Augmented Faces requires ARCore", Toast.LENGTH_LONG).show() + finish() + return false + } + val openGlVersionString = + (getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager) + .deviceConfigurationInfo + .glEsVersion + if (java.lang.Double.parseDouble(openGlVersionString) < minOpenGlVersion) { + Log.e("TAG", "Sceneform requires OpenGL ES 3.0 later") + Toast.makeText(this, "Sceneform requires OpenGL ES 3.0 or later", Toast.LENGTH_LONG) + .show() + finish() + return false + } + return true + } +} \ No newline at end of file diff --git a/app/src/main/java/com/willowtree/vocable/BaseFragment.kt b/app/src/main/java/com/willowtree/vocable/BaseFragment.kt new file mode 100644 index 00000000..30a017d8 --- /dev/null +++ b/app/src/main/java/com/willowtree/vocable/BaseFragment.kt @@ -0,0 +1,61 @@ +package com.willowtree.vocable + +import android.os.Bundle +import android.text.SpannableStringBuilder +import android.text.Spanned +import android.text.style.DynamicDrawableSpan +import android.text.style.ImageSpan +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.annotation.DrawableRes +import androidx.fragment.app.Fragment +import androidx.viewbinding.ViewBinding + +abstract class BaseFragment : Fragment() { + + @Suppress("UNCHECKED_CAST") + protected val binding: T + get() = _binding ?: throw Exception("View binding has been accessed after the view has been destroyed") + + private var _binding: T? = null + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + _binding = bindingInflater(inflater, container, false) + return binding.root + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + + protected abstract val bindingInflater: BindingInflater + + abstract fun getAllViews(): List + + protected fun buildTextWithIcon( + vararg strings: String, + iconCharStart: Int, + iconCharEnd: Int, + view: TextView, + @DrawableRes icon: Int + ) { + val sBuilder = SpannableStringBuilder() + + for (item in strings) { + sBuilder.append(item) + } + val imageSpan = ImageSpan( + requireContext(), + icon, + DynamicDrawableSpan.ALIGN_BASELINE + ) + sBuilder.setSpan(imageSpan, iconCharStart, iconCharEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) + + view.setText(sBuilder, TextView.BufferType.SPANNABLE) + } +} + +typealias BindingInflater = (LayoutInflater, ViewGroup?, Boolean) -> B \ No newline at end of file diff --git a/app/src/main/java/com/willowtree/vocable/BaseViewModel.kt b/app/src/main/java/com/willowtree/vocable/BaseViewModel.kt new file mode 100644 index 00000000..278e28a6 --- /dev/null +++ b/app/src/main/java/com/willowtree/vocable/BaseViewModel.kt @@ -0,0 +1,24 @@ +package com.willowtree.vocable + +import androidx.annotation.CallSuper +import androidx.lifecycle.ViewModel +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import org.koin.core.KoinComponent + +open class BaseViewModel( + protected val numbersCategoryId: String, + protected val mySayingsCategoryId: String +) : ViewModel(), KoinComponent { + + private val viewModelJob = SupervisorJob() + protected val backgroundScope = CoroutineScope(viewModelJob + Dispatchers.IO) + protected val uiScope = CoroutineScope(viewModelJob + Dispatchers.Main) + + @CallSuper + override fun onCleared() { + super.onCleared() + viewModelJob.cancel() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/willowtree/vocable/BaseViewModelFactory.kt b/app/src/main/java/com/willowtree/vocable/BaseViewModelFactory.kt new file mode 100644 index 00000000..03b4a6cb --- /dev/null +++ b/app/src/main/java/com/willowtree/vocable/BaseViewModelFactory.kt @@ -0,0 +1,41 @@ +package com.willowtree.vocable + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import com.willowtree.vocable.keyboard.KeyboardViewModel +import com.willowtree.vocable.presets.PresetsViewModel +import com.willowtree.vocable.settings.EditCategoriesViewModel +import com.willowtree.vocable.settings.EditPhrasesViewModel +import com.willowtree.vocable.settings.SettingsViewModel +import com.willowtree.vocable.splash.SplashViewModel + +class BaseViewModelFactory( + private val numbersCategoryId: String, + private val mySayingsCategoryId: String +) : ViewModelProvider.Factory { + + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T { + when { + modelClass.isAssignableFrom(SplashViewModel::class.java) -> { + return SplashViewModel(numbersCategoryId, mySayingsCategoryId) as T + } + modelClass.isAssignableFrom(PresetsViewModel::class.java) -> { + return PresetsViewModel(numbersCategoryId, mySayingsCategoryId) as T + } + modelClass.isAssignableFrom(SettingsViewModel::class.java) -> { + return SettingsViewModel(numbersCategoryId, mySayingsCategoryId) as T + } + modelClass.isAssignableFrom(KeyboardViewModel::class.java) -> { + return KeyboardViewModel(numbersCategoryId, mySayingsCategoryId) as T + } + modelClass.isAssignableFrom(EditPhrasesViewModel::class.java) -> { + return EditPhrasesViewModel(numbersCategoryId, mySayingsCategoryId) as T + } + modelClass.isAssignableFrom(EditCategoriesViewModel::class.java) -> { + return EditCategoriesViewModel(numbersCategoryId, mySayingsCategoryId) as T + } + else -> throw IllegalArgumentException("Unknown class: ${modelClass::class.java.canonicalName}") + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/willowtree/vocable/MainActivity.kt b/app/src/main/java/com/willowtree/vocable/MainActivity.kt new file mode 100644 index 00000000..e7b680c5 --- /dev/null +++ b/app/src/main/java/com/willowtree/vocable/MainActivity.kt @@ -0,0 +1,86 @@ +package com.willowtree.vocable + +import android.content.Context +import android.os.Bundle +import android.view.View +import android.view.ViewGroup +import androidx.core.view.children +import com.willowtree.vocable.customviews.PointerListener +import com.willowtree.vocable.customviews.PointerView +import com.willowtree.vocable.databinding.ActivityMainBinding +import com.willowtree.vocable.presets.PresetsFragment +import com.willowtree.vocable.utils.VocableTextToSpeech +import io.github.inflationx.viewpump.ViewPumpContextWrapper + + +class MainActivity : BaseActivity() { + + private lateinit var binding: ActivityMainBinding + private val allViews = mutableListOf() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + binding = ActivityMainBinding.inflate(layoutInflater) + setContentView(binding.root) + + supportActionBar?.hide() + VocableTextToSpeech.initialize(this) + + if (supportFragmentManager.findFragmentById(R.id.fragment_container) == null) { + supportFragmentManager + .beginTransaction() + .replace(R.id.fragment_container, PresetsFragment()) + .commit() + } + + binding.fragmentContainer.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ -> + allViews.clear() + } + } + + override fun attachBaseContext(newBase: Context) { + super.attachBaseContext(ViewPumpContextWrapper.wrap(newBase)) + } + + override fun onDestroy() { + super.onDestroy() + VocableTextToSpeech.shutdown() + } + + override fun getErrorView(): View = binding.errorView.root + + override fun getPointerView(): PointerView = binding.pointerView + + override fun getAllViews(): List { + if (allViews.isEmpty()) { + getAllChildViews(binding.parentLayout) + getAllFragmentViews() + } + return allViews + } + + fun resetAllViews() { + allViews.clear() + } + + override fun getLayout(): Int = R.layout.activity_main + + private fun getAllChildViews(viewGroup: ViewGroup) { + viewGroup.children.forEach { + if (it is PointerListener) { + allViews.add(it) + } else if (it is ViewGroup) { + getAllChildViews(it) + } + } + } + + private fun getAllFragmentViews() { + supportFragmentManager.fragments.forEach { + if (it is BaseFragment<*>) { + allViews.addAll(it.getAllViews()) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/willowtree/vocable/VocableApp.kt b/app/src/main/java/com/willowtree/vocable/VocableApp.kt new file mode 100644 index 00000000..80258317 --- /dev/null +++ b/app/src/main/java/com/willowtree/vocable/VocableApp.kt @@ -0,0 +1,35 @@ +package com.willowtree.vocable + +import android.app.Application +import io.github.inflationx.calligraphy3.CalligraphyConfig +import io.github.inflationx.calligraphy3.CalligraphyInterceptor +import io.github.inflationx.viewpump.ViewPump +import org.koin.android.ext.koin.androidContext +import org.koin.core.context.startKoin + + +class VocableApp : Application() { + + override fun onCreate() { + super.onCreate() + + ViewPump.init( + ViewPump.builder() + .addInterceptor( + CalligraphyInterceptor( + CalligraphyConfig.Builder() + .setDefaultFontPath("fonts/Roboto-RobotoRegular.ttf") + .setFontAttrId(R.attr.fontPath) + .build() + ) + ) + .build() + ) + + startKoin { + androidContext(this@VocableApp) + + modules(listOf(AppKoinModule.getModule())) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/willowtree/vocable/customviews/ActionButton.kt b/app/src/main/java/com/willowtree/vocable/customviews/ActionButton.kt new file mode 100644 index 00000000..89ca27b3 --- /dev/null +++ b/app/src/main/java/com/willowtree/vocable/customviews/ActionButton.kt @@ -0,0 +1,28 @@ +package com.willowtree.vocable.customviews + +import android.content.Context +import android.speech.tts.TextToSpeech +import android.util.AttributeSet +import com.willowtree.vocable.utils.VocableTextToSpeech + +/** + * A subclass of VocableButton that allows a caller to define a custom action + */ +open class ActionButton @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyle: Int = 0 +) : VocableButton(context, attrs, defStyle) { + + var action: (() -> Unit)? = null + + override fun performAction() { + action?.invoke() + } + + override fun sayText(text: CharSequence?) { + if (text?.isNotBlank() == true) { + VocableTextToSpeech.speak(locale, text.toString()) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/willowtree/vocable/customviews/CategoryButton.kt b/app/src/main/java/com/willowtree/vocable/customviews/CategoryButton.kt new file mode 100644 index 00000000..2c4f2484 --- /dev/null +++ b/app/src/main/java/com/willowtree/vocable/customviews/CategoryButton.kt @@ -0,0 +1,58 @@ +package com.willowtree.vocable.customviews + +import android.content.Context +import android.util.AttributeSet +import kotlinx.coroutines.* + +/** + * A subclass of AppCompatRadioButton that represents a category on the main screen + */ +class CategoryButton @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyle: Int = 0 +) : ActionButton(context, attrs, defStyle), + PointerListener { + + private var buttonJob: Job? = null + private val backgroundScope = CoroutineScope(Dispatchers.IO) + private val uiScope = CoroutineScope(Dispatchers.Main) + + init { + isEnabled = false + setOnClickListener { + isSelected = true + sayText(text) + performAction() + } + } + + override fun onPointerEnter() { + if (isSelected) { + return + } + buttonJob = backgroundScope.launch { + uiScope.launch { + isPressed = true + } + + delay(dwellTime) + + uiScope.launch { + isPressed = false + isSelected = true + sayText(text) + performAction() + } + } + } + + override fun onPointerExit() { + isPressed = false + buttonJob?.cancel() + } + + override fun sayText(text: CharSequence?) { + // No-op + } +} \ No newline at end of file diff --git a/app/src/main/java/com/willowtree/vocable/customviews/KeyboardButton.kt b/app/src/main/java/com/willowtree/vocable/customviews/KeyboardButton.kt new file mode 100644 index 00000000..2f0b45e9 --- /dev/null +++ b/app/src/main/java/com/willowtree/vocable/customviews/KeyboardButton.kt @@ -0,0 +1,15 @@ +package com.willowtree.vocable.customviews + +import android.content.Context +import android.util.AttributeSet + +class KeyboardButton @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyle: Int = 0 +) : ActionButton(context, attrs, defStyle) { + + override fun sayText(text: CharSequence?) { + //no-op + } +} \ No newline at end of file diff --git a/app/src/main/java/com/willowtree/vocable/customviews/PointerListener.kt b/app/src/main/java/com/willowtree/vocable/customviews/PointerListener.kt new file mode 100644 index 00000000..25e58e8c --- /dev/null +++ b/app/src/main/java/com/willowtree/vocable/customviews/PointerListener.kt @@ -0,0 +1,8 @@ +package com.willowtree.vocable.customviews + +interface PointerListener { + + fun onPointerEnter() + + fun onPointerExit() +} \ No newline at end of file diff --git a/app/src/main/java/com/willowtree/vocable/customviews/PointerView.kt b/app/src/main/java/com/willowtree/vocable/customviews/PointerView.kt new file mode 100644 index 00000000..ecad24d2 --- /dev/null +++ b/app/src/main/java/com/willowtree/vocable/customviews/PointerView.kt @@ -0,0 +1,23 @@ +package com.willowtree.vocable.customviews + +import android.content.Context +import android.util.AttributeSet +import android.view.View +import androidx.constraintlayout.widget.ConstraintLayout + +class PointerView(context: Context, attrs: AttributeSet) : View(context, attrs) { + + private var xAdjusted: Float = 0f + private var yAdjusted: Float = 0f + + fun updatePointerPosition(x: Float, y: Float) { + this.xAdjusted = x + this.yAdjusted = y + val params = layoutParams as ConstraintLayout.LayoutParams + layoutParams = params.apply { + marginStart = x.toInt() + topMargin = y.toInt() + } + invalidate() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/willowtree/vocable/customviews/SettingsButton.kt b/app/src/main/java/com/willowtree/vocable/customviews/SettingsButton.kt new file mode 100644 index 00000000..f867029b --- /dev/null +++ b/app/src/main/java/com/willowtree/vocable/customviews/SettingsButton.kt @@ -0,0 +1,20 @@ +package com.willowtree.vocable.customviews + +import android.content.Context +import android.content.Intent +import android.util.AttributeSet +import com.willowtree.vocable.settings.SettingsActivity + +/** + * A subclass of VocableButton that will open SettingsActivity when interacted with + */ +class SettingsButton @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyle: Int = 0 +) : ActionButton(context, attrs, defStyle) { + + override fun sayText(text: CharSequence?) { + //no-op + } +} \ No newline at end of file diff --git a/app/src/main/java/com/willowtree/vocable/customviews/VocableButton.kt b/app/src/main/java/com/willowtree/vocable/customviews/VocableButton.kt new file mode 100644 index 00000000..051fd321 --- /dev/null +++ b/app/src/main/java/com/willowtree/vocable/customviews/VocableButton.kt @@ -0,0 +1,132 @@ +package com.willowtree.vocable.customviews + +import android.content.Context +import android.content.SharedPreferences +import android.text.SpannableStringBuilder +import android.text.Spanned +import android.text.style.DynamicDrawableSpan +import android.text.style.ImageSpan +import android.util.AttributeSet +import androidx.annotation.DrawableRes +import androidx.appcompat.widget.AppCompatButton +import com.willowtree.vocable.utils.SpokenText +import com.willowtree.vocable.utils.VocableSharedPreferences +import com.willowtree.vocable.utils.VocableTextToSpeech +import kotlinx.coroutines.* +import org.koin.core.KoinComponent +import org.koin.core.inject +import java.util.* + +/** + * A custom AppCompatButton that will delay for two seconds when a pointer enters and then will call + * VocableTextToSpeech to say the button text aloud and then perform an action based on the + * subclass's implementation + */ +open class VocableButton @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyle: Int = 0 +) : AppCompatButton(context, attrs, defStyle), + PointerListener, + SharedPreferences.OnSharedPreferenceChangeListener, + KoinComponent { + + companion object { + private const val SINGLE_SPACE = " " + private const val NO_TEXT_START = 0 + private const val NO_TEXT_END = 1 + } + + private var buttonJob: Job? = null + private val backgroundScope = CoroutineScope(Dispatchers.IO) + private val uiScope = CoroutineScope(Dispatchers.Main) + + private val sharedPrefs: VocableSharedPreferences by inject() + protected var dwellTime: Long + protected var locale: Locale = Locale.ENGLISH + + init { + dwellTime = sharedPrefs.getDwellTime() + setOnClickListener { + sayText(text) + performAction() + } + } + + override fun onPointerEnter() { + buttonJob = backgroundScope.launch { + uiScope.launch { + isSelected = true + } + + delay(dwellTime) + + uiScope.launch { + isSelected = false + isPressed = true + sayText(text) + performAction() + } + } + } + + fun setIconWithNoText(@DrawableRes icon: Int) { + val imageSpan = ImageSpan( + context, + icon, + DynamicDrawableSpan.ALIGN_BOTTOM + ) + val stringBuilder = SpannableStringBuilder().apply { + append(SINGLE_SPACE) + setSpan(imageSpan, NO_TEXT_START, NO_TEXT_END, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) + } + setText(stringBuilder, BufferType.SPANNABLE) + } + + /** + * Sets the text and the language of the text to use for the text-to-speech engine + * + * @param text The user-visible text to set on the button + * @param locale The locale to be used for text-to-speech + */ + fun setText(text: String, locale: Locale) { + this.locale = locale + setText(text) + } + + protected open fun sayText(text: CharSequence?) { + text?.let { + VocableTextToSpeech.speak(locale, it.toString()) + SpokenText.postValue(it.toString()) + } + } + + protected open fun performAction() { + // No-op in the base class + } + + override fun onPointerExit() { + isPressed = false + isSelected = false + buttonJob?.cancel() + } + + override fun onAttachedToWindow() { + super.onAttachedToWindow() + sharedPrefs.registerOnSharedPreferenceChangeListener(this) + } + + override fun onDetachedFromWindow() { + buttonJob?.cancel() + sharedPrefs.unregisterOnSharedPreferenceChangeListener(this) + super.onDetachedFromWindow() + } + + override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { + when (key) { + VocableSharedPreferences.KEY_DWELL_TIME -> { + dwellTime = sharedPrefs.getDwellTime() + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/willowtree/vocable/customviews/VocableConstraintLayout.kt b/app/src/main/java/com/willowtree/vocable/customviews/VocableConstraintLayout.kt new file mode 100644 index 00000000..42bbbc32 --- /dev/null +++ b/app/src/main/java/com/willowtree/vocable/customviews/VocableConstraintLayout.kt @@ -0,0 +1,75 @@ +package com.willowtree.vocable.customviews + +import android.content.Context +import android.content.SharedPreferences +import android.util.AttributeSet +import androidx.constraintlayout.widget.ConstraintLayout +import com.willowtree.vocable.utils.VocableSharedPreferences +import kotlinx.coroutines.* +import org.koin.core.KoinComponent +import org.koin.core.inject + +class VocableConstraintLayout @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyle: Int = 0 +) : ConstraintLayout(context, attrs, defStyle), + PointerListener, + SharedPreferences.OnSharedPreferenceChangeListener, + KoinComponent { + + private var buttonJob: Job? = null + private val backgroundScope = CoroutineScope(Dispatchers.IO) + private val uiScope = CoroutineScope(Dispatchers.Main) + + private val sharedPrefs: VocableSharedPreferences by inject() + private var dwellTime: Long + + var action: (() -> Unit)? = null + + init { + dwellTime = sharedPrefs.getDwellTime() + setOnClickListener { + action?.invoke() + } + } + + override fun onPointerEnter() { + buttonJob = backgroundScope.launch { + uiScope.launch { + isSelected = true + } + + delay(dwellTime) + + uiScope.launch { + isSelected = false + action?.invoke() + } + } + } + + override fun onPointerExit() { + isSelected = false + buttonJob?.cancel() + } + + override fun onAttachedToWindow() { + super.onAttachedToWindow() + sharedPrefs.registerOnSharedPreferenceChangeListener(this) + } + + override fun onDetachedFromWindow() { + buttonJob?.cancel() + sharedPrefs.unregisterOnSharedPreferenceChangeListener(this) + super.onDetachedFromWindow() + } + + override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { + when (key) { + VocableSharedPreferences.KEY_DWELL_TIME -> { + dwellTime = sharedPrefs.getDwellTime() + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/willowtree/vocable/customviews/VocableImageButton.kt b/app/src/main/java/com/willowtree/vocable/customviews/VocableImageButton.kt new file mode 100644 index 00000000..bf98bde8 --- /dev/null +++ b/app/src/main/java/com/willowtree/vocable/customviews/VocableImageButton.kt @@ -0,0 +1,81 @@ +package com.willowtree.vocable.customviews + +import android.content.Context +import android.content.SharedPreferences +import android.util.AttributeSet +import androidx.appcompat.widget.AppCompatImageView +import com.willowtree.vocable.utils.VocableSharedPreferences +import kotlinx.coroutines.* +import org.koin.core.KoinComponent +import org.koin.core.inject + +class VocableImageButton @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyle: Int = 0 +) : AppCompatImageView(context, attrs, defStyle), + PointerListener, + SharedPreferences.OnSharedPreferenceChangeListener, + KoinComponent { + + private var buttonJob: Job? = null + private val backgroundScope = CoroutineScope(Dispatchers.IO) + private val uiScope = CoroutineScope(Dispatchers.Main) + + private val sharedPrefs: VocableSharedPreferences by inject() + private var dwellTime: Long + + var action: (() -> Unit)? = null + + init { + dwellTime = sharedPrefs.getDwellTime() + setOnClickListener { + performAction() + } + } + + override fun onPointerEnter() { + buttonJob = backgroundScope.launch { + uiScope.launch { + isSelected = true + } + + delay(dwellTime) + + uiScope.launch { + isSelected = false + isPressed = true + performAction() + } + } + } + + override fun onPointerExit() { + isPressed = false + isSelected = false + buttonJob?.cancel() + } + + private fun performAction() { + action?.invoke() + } + + override fun onAttachedToWindow() { + super.onAttachedToWindow() + sharedPrefs.registerOnSharedPreferenceChangeListener(this) + } + + override fun onDetachedFromWindow() { + buttonJob?.cancel() + sharedPrefs.unregisterOnSharedPreferenceChangeListener(this) + super.onDetachedFromWindow() + } + + override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { + when (key) { + VocableSharedPreferences.KEY_DWELL_TIME -> { + dwellTime = sharedPrefs.getDwellTime() + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/willowtree/vocable/facetracking/FaceTrackFragment.kt b/app/src/main/java/com/willowtree/vocable/facetracking/FaceTrackFragment.kt new file mode 100644 index 00000000..ca0a9a1a --- /dev/null +++ b/app/src/main/java/com/willowtree/vocable/facetracking/FaceTrackFragment.kt @@ -0,0 +1,70 @@ +package com.willowtree.vocable.facetracking + +import android.os.Bundle +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProviders +import com.google.ar.core.AugmentedFace +import com.google.ar.core.Config +import com.google.ar.core.Session +import com.google.ar.sceneform.ux.ArFragment +import com.willowtree.vocable.utils.VocableSharedPreferences +import org.koin.android.ext.android.inject +import java.util.* + + +class FaceTrackFragment : ArFragment() { + + private lateinit var viewModel: FaceTrackingViewModel + + private val sharedPrefs: VocableSharedPreferences by inject() + + override fun getSessionConfiguration(session: Session): Config { + val config = Config(session) + config.augmentedFaceMode = Config.AugmentedFaceMode.MESH3D + return config + } + + override fun getSessionFeatures(): Set { + return EnumSet.of(Session.Feature.FRONT_CAMERA) + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + with(planeDiscoveryController) { + hide() + setInstructionView(null) + } + viewModel = ViewModelProviders.of(requireActivity()).get(FaceTrackingViewModel::class.java) + subscribeToViewModel() + attachFaceTracker() + } + + override fun onResume() { + super.onResume() + enableFaceTracking(sharedPrefs.getHeadTrackingEnabled()) + } + + private fun subscribeToViewModel() { + viewModel.adjustedVector.observe(viewLifecycleOwner, Observer { + it?.let { + viewModel.onScreenPointAvailable(arSceneView.scene.camera.worldToScreenPoint(it)) + } + }) + } + + private fun attachFaceTracker() { + val scene = arSceneView.scene + scene.addOnUpdateListener { + viewModel.onFaceDetected(arSceneView.session?.getAllTrackables(AugmentedFace::class.java)) + } + } + + fun enableFaceTracking(enable: Boolean) { + if (enable) { + arSceneView.resume() + } else { + arSceneView.pause() + viewModel.onFaceDetected(null) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/willowtree/vocable/facetracking/FaceTrackingViewModel.kt b/app/src/main/java/com/willowtree/vocable/facetracking/FaceTrackingViewModel.kt new file mode 100644 index 00000000..1c4a3ef6 --- /dev/null +++ b/app/src/main/java/com/willowtree/vocable/facetracking/FaceTrackingViewModel.kt @@ -0,0 +1,107 @@ +package com.willowtree.vocable.facetracking + +import android.content.SharedPreferences +import android.content.Context +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import com.google.ar.core.AugmentedFace +import com.google.ar.sceneform.math.Vector3 +import com.willowtree.vocable.R +import com.willowtree.vocable.utils.VocableSharedPreferences +import kotlinx.coroutines.* +import org.koin.core.KoinComponent +import org.koin.core.get +import org.koin.core.inject + +class FaceTrackingViewModel : ViewModel(), KoinComponent { + + private val viewModelJob = SupervisorJob() + private val backgroundScope = CoroutineScope(viewModelJob + Dispatchers.IO) + + private val sharedPrefs: VocableSharedPreferences by inject() + private var sensitivity = VocableSharedPreferences.DEFAULT_SENSITIVITY + private var headTrackingEnabled = true + private val sharedPrefsListener = + SharedPreferences.OnSharedPreferenceChangeListener { _, key -> + when (key) { + VocableSharedPreferences.KEY_SENSITIVITY -> { + sensitivity = sharedPrefs.getSensitivity() + } + VocableSharedPreferences.KEY_HEAD_TRACKING_ENABLED -> { + headTrackingEnabled = sharedPrefs.getHeadTrackingEnabled() + } + } + } + + private var faceTrackingJob: Job? = null + + private val liveAdjustedVector = MutableLiveData() + val adjustedVector: LiveData = liveAdjustedVector + + private val livePointerLocation = MutableLiveData() + val pointerLocation: LiveData = livePointerLocation + + private val liveShowError = MutableLiveData() + val showError: LiveData = liveShowError + + private var isTablet = false + + private var oldVector: Vector3? = null + + init { + sharedPrefs.registerOnSharedPreferenceChangeListener(sharedPrefsListener) + isTablet = get().resources.getBoolean(R.bool.is_tablet) + } + + fun onFaceDetected(augmentedFaces: Collection?) { + if (!headTrackingEnabled) { + liveShowError.postValue(false) + return + } + if (augmentedFaces?.firstOrNull() == null) { + liveShowError.postValue(true) + return + } + if (liveShowError.value == true) { + liveShowError.postValue(false) + } + + if (faceTrackingJob != null && faceTrackingJob?.isActive == true) { + return + } + augmentedFaces.firstOrNull()?.let { augmentedFace -> + faceTrackingJob = backgroundScope.launch { + val pose = augmentedFace.getRegionPose(AugmentedFace.RegionType.NOSE_TIP) + val zAxis = pose.zAxis + val x = zAxis[0] + var y = zAxis[1] + val z = -zAxis[2] + + when (oldVector) { + null -> { + oldVector = Vector3(x, y, z) + liveAdjustedVector.postValue(oldVector) + } + else -> { + if (!isTablet) { + y *= 2F + } + val adjustedVector = Vector3.lerp(oldVector, Vector3(x, y, z), sensitivity) + liveAdjustedVector.postValue(adjustedVector) + oldVector = adjustedVector + } + } + } + } + } + + fun onScreenPointAvailable(screenPoint: Vector3) { + livePointerLocation.postValue(screenPoint) + } + + override fun onCleared() { + viewModelJob.cancel() + sharedPrefs.unregisterOnSharedPreferenceChangeListener(sharedPrefsListener) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/willowtree/vocable/keyboard/KeyboardFragment.kt b/app/src/main/java/com/willowtree/vocable/keyboard/KeyboardFragment.kt new file mode 100644 index 00000000..0192e2b7 --- /dev/null +++ b/app/src/main/java/com/willowtree/vocable/keyboard/KeyboardFragment.kt @@ -0,0 +1,174 @@ +package com.willowtree.vocable.keyboard + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.core.view.children +import androidx.core.view.isVisible +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProviders +import com.willowtree.vocable.BaseFragment +import com.willowtree.vocable.BaseViewModelFactory +import com.willowtree.vocable.BindingInflater +import com.willowtree.vocable.R +import com.willowtree.vocable.customviews.ActionButton +import com.willowtree.vocable.customviews.PointerListener +import com.willowtree.vocable.databinding.FragmentKeyboardBinding +import com.willowtree.vocable.databinding.KeyboardKeyLayoutBinding +import com.willowtree.vocable.presets.PresetsFragment +import com.willowtree.vocable.settings.SettingsActivity +import com.willowtree.vocable.utils.VocableTextToSpeech +import org.koin.android.ext.android.get +import java.util.* + +class KeyboardFragment : BaseFragment() { + + override val bindingInflater: BindingInflater = FragmentKeyboardBinding::inflate + private lateinit var viewModel: KeyboardViewModel + private lateinit var keys: Array + private val currentLocale = get().resources.configuration?.locales?.get(0) + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + super.onCreateView(inflater, container, savedInstanceState) + keys = resources.getStringArray(R.array.keyboard_keys) + populateKeys() + return binding.root + } + + private fun populateKeys() { + keys.withIndex().forEach { + with( + KeyboardKeyLayoutBinding.inflate( + layoutInflater, + binding.keyboardKeyHolder, + true + ).root as ActionButton + ) { + text = it.value + action = { + //This action mimics sentence capitalization + //Example: "This is what's going on in here. Do you get it? Some letters are capitalized." + val currentText = binding.keyboardInput.text?.toString() ?: "" + if (isDefaultTextVisible()) { + binding.keyboardInput.text = null + binding.keyboardInput.append(text?.toString()) + } else if (currentText.endsWith(". ") || currentText.endsWith("? ")) { + binding.keyboardInput.append(text?.toString()) + } else { + binding.keyboardInput.append( + text?.toString()?.toLowerCase(Locale.getDefault()) + ) + } + } + } + } + } + + private fun isDefaultTextVisible(): Boolean { + return binding.keyboardInput.text.toString() == getString(R.string.keyboard_select_letters) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + VocableTextToSpeech.isSpeaking.observe(viewLifecycleOwner, Observer { + binding.speakerIcon.isVisible = it + }) + + binding.actionButtonContainer.presetsButton.action = { + parentFragmentManager + .beginTransaction() + .replace(R.id.fragment_container, PresetsFragment()) + .commit() + } + + binding.actionButtonContainer.settingsButton.action = { + val intent = Intent(activity, SettingsActivity::class.java) + startActivity(intent) + } + + binding.actionButtonContainer.saveButton.action = { + if (!isDefaultTextVisible()) { + binding.keyboardInput.text?.let { text -> + if (text.isNotBlank()) { + viewModel.addNewPhrase(text.toString()) + } + } + } + } + + binding.keyboardClearButton.action = { + binding.keyboardInput.setText(R.string.keyboard_select_letters) + } + + binding.keyboardSpaceButton.action = { + if (!isDefaultTextVisible() && binding.keyboardInput.text?.endsWith(' ') == false) { + binding.keyboardInput.append(" ") + } + } + + binding.keyboardBackspaceButton.action = { + if (!isDefaultTextVisible()) { + binding.keyboardInput.apply { + setText(text.toString().dropLast(1)) + if (text.isNullOrEmpty()) { + setText(R.string.keyboard_select_letters) + } + } + } + } + + binding.keyboardSpeakButton.action = { + if (!isDefaultTextVisible()) { + VocableTextToSpeech.speak( + Locale.getDefault(), + binding.keyboardInput.text?.toString() ?: "" + ) + } + } + + (binding.phraseSavedView.root as TextView).setText(R.string.saved_successfully) + + viewModel = ViewModelProviders.of( + this, + BaseViewModelFactory( + getString(R.string.category_123_id), + getString(R.string.category_my_sayings_id) + ) + ).get(KeyboardViewModel::class.java) + subscribeToViewModel() + } + + private fun subscribeToViewModel() { + viewModel.showPhraseAdded.observe(viewLifecycleOwner, Observer { + binding.phraseSavedView.root.isVisible = it + }) + } + + private val allViews = mutableListOf() + + override fun getAllViews(): List { + if (allViews.isEmpty()) { + getAllChildViews(binding.keyboardParent) + } + return allViews + } + + private fun getAllChildViews(viewGroup: ViewGroup?) { + viewGroup?.children?.forEach { + if (it is PointerListener) { + allViews.add(it) + } else if (it is ViewGroup) { + getAllChildViews(it) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/willowtree/vocable/keyboard/KeyboardViewModel.kt b/app/src/main/java/com/willowtree/vocable/keyboard/KeyboardViewModel.kt new file mode 100644 index 00000000..1483fcce --- /dev/null +++ b/app/src/main/java/com/willowtree/vocable/keyboard/KeyboardViewModel.kt @@ -0,0 +1,56 @@ +package com.willowtree.vocable.keyboard + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import com.willowtree.vocable.BaseViewModel +import com.willowtree.vocable.presets.PresetsRepository +import com.willowtree.vocable.room.CategoryPhraseCrossRef +import com.willowtree.vocable.room.Phrase +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import org.koin.core.inject +import java.util.* + +class KeyboardViewModel(numbersCategoryId: String, mySayingsCategoryId: String) : + BaseViewModel(numbersCategoryId, mySayingsCategoryId) { + + companion object { + private const val PHRASE_ADDED_DELAY = 2000L + } + + private val presetsRepository: PresetsRepository by inject() + + private val liveShowPhraseAdded = MutableLiveData() + val showPhraseAdded: LiveData = liveShowPhraseAdded + + fun addNewPhrase(phraseStr: String) { + backgroundScope.launch { + val mySayingsCategory = + presetsRepository.getCategoryById(mySayingsCategoryId) + val phraseId = UUID.randomUUID().toString() + val mySayingsPhrases = + presetsRepository.getPhrasesForCategory(mySayingsCategoryId) + with(presetsRepository) { + addPhrase( + Phrase( + phraseId, + System.currentTimeMillis(), + true, + System.currentTimeMillis(), + mapOf(Pair(Locale.getDefault().toString(), phraseStr)), + mySayingsPhrases.size + ) + ) + addCrossRef( + CategoryPhraseCrossRef( + mySayingsCategory.categoryId, + phraseId + ) + ) + } + liveShowPhraseAdded.postValue(true) + delay(PHRASE_ADDED_DELAY) + liveShowPhraseAdded.postValue(false) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/willowtree/vocable/presets/CategoriesFragment.kt b/app/src/main/java/com/willowtree/vocable/presets/CategoriesFragment.kt new file mode 100644 index 00000000..8dc56983 --- /dev/null +++ b/app/src/main/java/com/willowtree/vocable/presets/CategoriesFragment.kt @@ -0,0 +1,141 @@ +package com.willowtree.vocable.presets + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.LinearLayout +import androidx.core.view.children +import androidx.core.view.isInvisible +import androidx.core.view.isVisible +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProviders +import com.willowtree.vocable.BaseFragment +import com.willowtree.vocable.BaseViewModelFactory +import com.willowtree.vocable.BindingInflater +import com.willowtree.vocable.R +import com.willowtree.vocable.customviews.CategoryButton +import com.willowtree.vocable.customviews.PointerListener +import com.willowtree.vocable.databinding.CategoriesFragmentBinding +import com.willowtree.vocable.databinding.CategoryButtonBinding +import com.willowtree.vocable.room.Category + +class CategoriesFragment : BaseFragment() { + + companion object { + const val KEY_CATEGORIES = "KEY_CATEGORIES" + + fun newInstance(categories: List): CategoriesFragment { + return CategoriesFragment().apply { + arguments = Bundle().apply { + putParcelableArrayList(KEY_CATEGORIES, ArrayList(categories)) + } + } + } + } + + override val bindingInflater: BindingInflater = CategoriesFragmentBinding::inflate + + private lateinit var viewModel: PresetsViewModel + private val allViews = mutableListOf() + private var maxCategories = 1 + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + super.onCreateView(inflater, container, savedInstanceState) + maxCategories = resources.getInteger(R.integer.max_categories) + val isTablet = resources.getBoolean(R.bool.is_tablet) + + val categories = arguments?.getParcelableArrayList(KEY_CATEGORIES) + categories?.forEachIndexed { index, category -> + val categoryButton = + CategoryButtonBinding.inflate(inflater, binding.categoryButtonContainer, false) + with(categoryButton.root as CategoryButton) { + tag = category + text = category.getLocalizedText() + action = { + viewModel.onCategorySelected(category) + } + if (!isTablet && index > 0 && index + 1 == maxCategories) { + layoutParams = (layoutParams as LinearLayout.LayoutParams).apply { + marginStart = 0 + } + } + } + binding.categoryButtonContainer.addView(categoryButton.root) + } + categories?.let { + for (i in 0 until maxCategories - it.size) { + val hiddenButton = + CategoryButtonBinding.inflate(inflater, binding.categoryButtonContainer, false) + binding.categoryButtonContainer.addView(hiddenButton.root.apply { + isEnabled = false + isInvisible = true + }) + } + } + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + viewModel = ViewModelProviders.of( + requireActivity(), + BaseViewModelFactory( + getString(R.string.category_123_id), + getString(R.string.category_my_sayings_id) + ) + ).get(PresetsViewModel::class.java) + subscribeToViewModel() + } + + override fun onResume() { + super.onResume() + binding.categoryButtonContainer.children.forEach { + if (it is CategoryButton && it.isVisible) { + it.isEnabled = true + } + } + } + + override fun onPause() { + super.onPause() + binding.categoryButtonContainer.children.forEach { + if (it is CategoryButton) { + it.isEnabled = false + } + } + } + + private fun subscribeToViewModel() { + viewModel.selectedCategory.observe(viewLifecycleOwner, Observer { category -> + category?.let { + binding.categoryButtonContainer.children.forEach { + if (it is CategoryButton) { + it.isSelected = (it.tag as? Category)?.categoryId == category.categoryId + } + } + } + }) + } + + override fun getAllViews(): List { + if (allViews.isEmpty()) { + getAllChildViews(binding.categoryButtonContainer) + } + return allViews + } + + private fun getAllChildViews(viewGroup: ViewGroup?) { + viewGroup?.children?.forEach { + if (it is PointerListener) { + allViews.add(it) + } else if (it is ViewGroup) { + getAllChildViews(it) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/willowtree/vocable/presets/MySayingsEmptyFragment.kt b/app/src/main/java/com/willowtree/vocable/presets/MySayingsEmptyFragment.kt new file mode 100644 index 00000000..17ca22df --- /dev/null +++ b/app/src/main/java/com/willowtree/vocable/presets/MySayingsEmptyFragment.kt @@ -0,0 +1,61 @@ +package com.willowtree.vocable.presets + +import android.content.res.Configuration +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.view.isVisible +import com.willowtree.vocable.BaseFragment +import com.willowtree.vocable.BindingInflater +import com.willowtree.vocable.R +import com.willowtree.vocable.databinding.MySayingsEmptyLayoutBinding + +class MySayingsEmptyFragment : BaseFragment() { + + override val bindingInflater: BindingInflater = MySayingsEmptyLayoutBinding::inflate + + companion object { + private const val KEY_IS_SETTINGS = "KEY_IS_SETTINGS" + + fun newInstance(isSettings: Boolean): MySayingsEmptyFragment { + return MySayingsEmptyFragment().apply { + arguments = Bundle().apply { + putBoolean(KEY_IS_SETTINGS, isSettings) + } + } + } + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + super.onCreateView(inflater, container, savedInstanceState) + + arguments?.getBoolean(KEY_IS_SETTINGS)?.let { isSettings -> + when { + isSettings -> { + binding.star.isVisible = true + binding.emptyMySayingsText.setText(R.string.my_sayings_empty_settings) + } + !isSettings && resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE && !resources.getBoolean(R.bool.is_tablet) -> { + val param = binding.emptyMySayingsText.layoutParams as ConstraintLayout.LayoutParams + param.setMargins(0,0,0,0) + binding.emptyMySayingsText.layoutParams = param + } + else -> { + // no-op + } + } + } + + return binding.root + } + + override fun getAllViews(): List { + return emptyList() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/willowtree/vocable/presets/NumberPadFragment.kt b/app/src/main/java/com/willowtree/vocable/presets/NumberPadFragment.kt new file mode 100644 index 00000000..a8f1471c --- /dev/null +++ b/app/src/main/java/com/willowtree/vocable/presets/NumberPadFragment.kt @@ -0,0 +1,68 @@ +package com.willowtree.vocable.presets + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.GridLayout +import androidx.core.os.bundleOf +import androidx.core.view.updateMargins +import com.willowtree.vocable.BaseFragment +import com.willowtree.vocable.BindingInflater +import com.willowtree.vocable.R +import com.willowtree.vocable.customviews.VocableButton +import com.willowtree.vocable.databinding.FragmentNumberPadBinding +import com.willowtree.vocable.databinding.PhraseButtonBinding +import com.willowtree.vocable.room.Phrase +import java.util.* +import kotlin.collections.ArrayList + +class NumberPadFragment : BaseFragment() { + + companion object { + private const val KEY_PHRASES = "KEY_PHRASES" + const val MAX_PHRASES = 12 + + fun newInstance(phrases: List) = NumberPadFragment().apply { + arguments = bundleOf(KEY_PHRASES to ArrayList(phrases)) + } + } + + override val bindingInflater: BindingInflater = FragmentNumberPadBinding::inflate + private var numColumns = 1 + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + super.onCreateView(inflater, container, savedInstanceState) + numColumns = resources.getInteger(R.integer.number_pad_columns) + + val phrases = arguments?.getParcelableArrayList(KEY_PHRASES) + phrases?.forEachIndexed { index, phrase -> + val phraseButton = + PhraseButtonBinding.inflate(inflater, binding.phrasesContainer, false) + with(phraseButton.root as VocableButton) { + val pair = phrase.getLocalizedPair() + setText(pair.first, Locale.getDefault()) + // Remove end margin on last column + if (index % numColumns == numColumns - 1) { + layoutParams = (layoutParams as GridLayout.LayoutParams).apply { + marginEnd = 0 + } + } + if (index >= MAX_PHRASES - numColumns) { + layoutParams = (layoutParams as GridLayout.LayoutParams).apply { + updateMargins(bottom = 0) + } + } + } + binding.phrasesContainer.addView(phraseButton.root) + } + + return binding.root + } + + override fun getAllViews() = emptyList() +} \ No newline at end of file diff --git a/app/src/main/java/com/willowtree/vocable/presets/PhrasesFragment.kt b/app/src/main/java/com/willowtree/vocable/presets/PhrasesFragment.kt new file mode 100644 index 00000000..e4bd57f4 --- /dev/null +++ b/app/src/main/java/com/willowtree/vocable/presets/PhrasesFragment.kt @@ -0,0 +1,99 @@ +package com.willowtree.vocable.presets + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.GridLayout +import androidx.core.view.children +import androidx.core.view.updateMargins +import com.willowtree.vocable.BaseFragment +import com.willowtree.vocable.BindingInflater +import com.willowtree.vocable.R +import com.willowtree.vocable.customviews.PointerListener +import com.willowtree.vocable.customviews.VocableButton +import com.willowtree.vocable.databinding.FragmentPhrasesBinding +import com.willowtree.vocable.databinding.PhraseButtonBinding +import com.willowtree.vocable.room.Phrase + +class PhrasesFragment : BaseFragment() { + + companion object { + private const val KEY_PHRASES = "KEY_PHRASES" + + fun newInstance(phrases: List): PhrasesFragment { + return PhrasesFragment().apply { + arguments = Bundle().apply { + putParcelableArrayList(KEY_PHRASES, ArrayList(phrases)) + } + } + } + } + + override val bindingInflater: BindingInflater = FragmentPhrasesBinding::inflate + private val allViews = mutableListOf() + private var maxPhrases = 1 + private var numColumns = 1 + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + super.onCreateView(inflater, container, savedInstanceState) + maxPhrases = resources.getInteger(R.integer.max_phrases) + numColumns = resources.getInteger(R.integer.phrases_columns) + + val phrases = arguments?.getParcelableArrayList(KEY_PHRASES) + phrases?.forEachIndexed { index, phrase -> + val phraseButton = + PhraseButtonBinding.inflate(inflater, binding.phrasesContainer, false) + with(phraseButton.root as VocableButton) { + val (phraseStr, locale) = phrase.getLocalizedPair() + setText(phraseStr, locale) + // Remove end margin on last column + if (index % numColumns == numColumns - 1) { + layoutParams = (layoutParams as GridLayout.LayoutParams).apply { + marginEnd = 0 + } + } + if (index >= maxPhrases - numColumns) { + layoutParams = (layoutParams as GridLayout.LayoutParams).apply { + updateMargins(bottom = 0) + } + } + } + binding.phrasesContainer.addView(phraseButton.root) + } + phrases?.let { + // Add invisible views to fill out the rest of the space + for (i in 0 until maxPhrases - it.size) { + val hiddenButton = + PhraseButtonBinding.inflate(inflater, binding.phrasesContainer, false) + binding.phrasesContainer.addView(hiddenButton.root.apply { + isEnabled = false + visibility = View.INVISIBLE + }) + } + } + + return binding.root + } + + override fun getAllViews(): List { + if (allViews.isEmpty()) { + getAllChildViews(binding.phrasesContainer) + } + return allViews + } + + private fun getAllChildViews(viewGroup: ViewGroup?) { + viewGroup?.children?.forEach { + if (it is PointerListener) { + allViews.add(it) + } else if (it is ViewGroup) { + getAllChildViews(it) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/willowtree/vocable/presets/PresetsFragment.kt b/app/src/main/java/com/willowtree/vocable/presets/PresetsFragment.kt new file mode 100644 index 00000000..b66e74d3 --- /dev/null +++ b/app/src/main/java/com/willowtree/vocable/presets/PresetsFragment.kt @@ -0,0 +1,284 @@ +package com.willowtree.vocable.presets + +import android.content.Intent +import android.os.Bundle +import android.view.View +import android.view.ViewGroup +import androidx.core.view.children +import androidx.core.view.isVisible +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentManager +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProviders +import androidx.lifecycle.observe +import androidx.viewpager2.widget.ViewPager2 +import com.willowtree.vocable.* +import com.willowtree.vocable.customviews.PointerListener +import com.willowtree.vocable.databinding.FragmentPresetsBinding +import com.willowtree.vocable.keyboard.KeyboardFragment +import com.willowtree.vocable.room.Category +import com.willowtree.vocable.room.Phrase +import com.willowtree.vocable.settings.SettingsActivity +import com.willowtree.vocable.utils.SpokenText +import com.willowtree.vocable.utils.VocableFragmentStateAdapter +import com.willowtree.vocable.utils.VocableTextToSpeech + +class PresetsFragment : BaseFragment() { + + override val bindingInflater: BindingInflater = FragmentPresetsBinding::inflate + private val allViews = mutableListOf() + + private var maxCategories = 1 + private var maxPhrases = 1 + + private lateinit var presetsViewModel: PresetsViewModel + private lateinit var categoriesAdapter: CategoriesPagerAdapter + private lateinit var phrasesAdapter: PhrasesPagerAdapter + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + maxCategories = resources.getInteger(R.integer.max_categories) + + binding.categoryForwardButton.action = { + when (val currentPosition = binding.categoryView.currentItem) { + categoriesAdapter.itemCount - 1 -> { + binding.categoryView.setCurrentItem(0, true) + } + else -> { + binding.categoryView.setCurrentItem(currentPosition + 1, true) + } + } + } + + binding.categoryBackButton.action = { + when (val currentPosition = binding.categoryView.currentItem) { + 0 -> { + binding.categoryView.setCurrentItem(categoriesAdapter.itemCount - 1, true) + } + else -> { + binding.categoryView.setCurrentItem(currentPosition - 1, true) + } + } + } + + binding.phrasesForwardButton.action = { + when (val currentPosition = binding.phrasesView.currentItem) { + phrasesAdapter.itemCount - 1 -> { + binding.phrasesView.setCurrentItem(0, true) + } + else -> { + binding.phrasesView.setCurrentItem(currentPosition + 1, true) + } + } + } + + binding.phrasesBackButton.action = { + when (val currentPosition = binding.phrasesView.currentItem) { + 0 -> { + binding.phrasesView.setCurrentItem(phrasesAdapter.itemCount - 1, true) + } + else -> { + binding.phrasesView.setCurrentItem(currentPosition - 1, true) + } + } + } + + binding.actionButtonContainer.keyboardButton.action = { + parentFragmentManager + .beginTransaction() + .replace(R.id.fragment_container, KeyboardFragment()) + .addToBackStack(null) + .commit() + } + + binding.actionButtonContainer.settingsButton.action = { + val intent = Intent(activity, SettingsActivity::class.java) + startActivity(intent) + } + + categoriesAdapter = CategoriesPagerAdapter(childFragmentManager) + phrasesAdapter = PhrasesPagerAdapter(childFragmentManager) + + binding.categoryView.registerOnPageChangeCallback(object : + ViewPager2.OnPageChangeCallback() { + override fun onPageSelected(position: Int) { + activity?.let { activity -> + allViews.clear() + if (activity is MainActivity) { + activity.resetAllViews() + } + } + } + }) + + binding.phrasesView.registerOnPageChangeCallback(object : + ViewPager2.OnPageChangeCallback() { + override fun onPageSelected(position: Int) { + val pageNum = position % phrasesAdapter.numPages + 1 + binding.phrasesPageNumber.text = getString( + R.string.phrases_page_number, + pageNum, + phrasesAdapter.numPages + ) + + activity?.let { activity -> + allViews.clear() + if (activity is MainActivity) { + activity.resetAllViews() + } + } + } + }) + + SpokenText.postValue(null) + + presetsViewModel = + ViewModelProviders.of( + requireActivity(), + BaseViewModelFactory( + getString(R.string.category_123_id), + getString(R.string.category_my_sayings_id) + ) + ).get(PresetsViewModel::class.java) + subscribeToViewModel() + } + + override fun onResume() { + super.onResume() + presetsViewModel.populateCategories() + } + + private fun subscribeToViewModel() { + SpokenText.observe(viewLifecycleOwner, Observer { + binding.currentText.text = if (it.isNullOrBlank()) { + getString(R.string.select_something) + } else { + it + } + }) + + VocableTextToSpeech.isSpeaking.observe(viewLifecycleOwner, Observer { + binding.speakerIcon.isVisible = it + }) + + presetsViewModel.apply { + categoryList.observe(viewLifecycleOwner, ::handleCategories) + currentPhrases.observe(viewLifecycleOwner, ::handlePhrases) + } + } + + override fun getAllViews(): List { + if (allViews.isEmpty()) { + getAllChildViews(binding.presetsParent) + } + return allViews + } + + private fun getAllChildViews(viewGroup: ViewGroup?) { + viewGroup?.children?.forEach { + if (it is PointerListener) { + allViews.add(it) + } else if (it is ViewGroup) { + getAllChildViews(it) + } + } + } + + private fun handleCategories(categories: List) { + binding.categoryView.apply { + isSaveEnabled = false + adapter = categoriesAdapter + categoriesAdapter.setItems(categories) + + // The categories ViewPager position will initially be set to the middle so that the + // user can scroll in both directions. Upon subsequent config changes, the position + // will be set to the closest page to the middle which contains the selected category. + var targetPosition = categoriesAdapter.itemCount / 2 + + if (targetPosition % categoriesAdapter.numPages != 0) { + targetPosition %= categoriesAdapter.numPages + } + + presetsViewModel.selectedCategory.value?.let { selectedCategory -> + for (i in targetPosition until targetPosition + categoriesAdapter.numPages) { + val pageCategories = categoriesAdapter.getItemsByPosition(i) + + if (pageCategories.find { it.categoryId == selectedCategory.categoryId } != null) { + targetPosition = i + break + } + } + } + + setCurrentItem(targetPosition, false) + } + } + + private fun handlePhrases(phrases: List) { + binding.phrasesView.apply { + isSaveEnabled = false + adapter = phrasesAdapter + + maxPhrases = + if (presetsViewModel.selectedCategory.value?.categoryId == getString(R.string.category_123_id)) { + NumberPadFragment.MAX_PHRASES + } else { + resources.getInteger(R.integer.max_phrases) + } + + phrasesAdapter.setItems(phrases) + // Move adapter to middle so user can scroll both directions + val middle = phrasesAdapter.itemCount / 2 + if (middle % phrasesAdapter.numPages == 0) { + setCurrentItem(middle, false) + } else { + val mod = middle % phrasesAdapter.numPages + setCurrentItem( + middle + (phrasesAdapter.numPages - mod), + false + ) + } + } + } + + inner class CategoriesPagerAdapter(fm: FragmentManager) : + VocableFragmentStateAdapter(fm, viewLifecycleOwner.lifecycle) { + + override fun getMaxItemsPerPage(): Int = maxCategories + + override fun createFragment(position: Int) = CategoriesFragment.newInstance(getItemsByPosition(position)) + } + + inner class PhrasesPagerAdapter(fm: FragmentManager) : + VocableFragmentStateAdapter(fm, viewLifecycleOwner.lifecycle) { + + override fun setItems(items: List) { + super.setItems(items) + setPagingButtonsEnabled(phrasesAdapter.numPages > 1) + } + + private fun setPagingButtonsEnabled(enable: Boolean) { + binding.apply { + phrasesForwardButton.isEnabled = enable + phrasesBackButton.isEnabled = enable + phrasesView.isUserInputEnabled = enable + } + } + + override fun getMaxItemsPerPage(): Int = maxPhrases + + override fun createFragment(position: Int): Fragment { + val phrases = getItemsByPosition(position) + + return if (presetsViewModel.selectedCategory.value?.categoryId == getString(R.string.category_123_id)) { + NumberPadFragment.newInstance(phrases) + } else if (presetsViewModel.selectedCategory.value?.categoryId == getString(R.string.category_my_sayings_id) && items.isEmpty()) { + MySayingsEmptyFragment.newInstance(false) + } else { + PhrasesFragment.newInstance(phrases) + } + } + + } +} \ No newline at end of file diff --git a/app/src/main/java/com/willowtree/vocable/presets/PresetsRepository.kt b/app/src/main/java/com/willowtree/vocable/presets/PresetsRepository.kt new file mode 100644 index 00000000..efc3297a --- /dev/null +++ b/app/src/main/java/com/willowtree/vocable/presets/PresetsRepository.kt @@ -0,0 +1,192 @@ +package com.willowtree.vocable.presets + +import android.content.Context +import android.util.Log +import com.squareup.moshi.Moshi +import com.willowtree.vocable.R +import com.willowtree.vocable.room.* +import com.willowtree.vocable.room.models.PresetsObject +import com.willowtree.vocable.utils.VocableSharedPreferences +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.koin.core.KoinComponent +import org.koin.core.get +import org.koin.core.inject +import java.nio.charset.Charset +import java.util.* + +class PresetsRepository(context: Context) : KoinComponent { + + private val database = VocableDatabase.getVocableDatabase(context) + private val sharedPrefs: VocableSharedPreferences by inject() + private val moshi: Moshi by inject() + + suspend fun getAllCategories(): List { + return database.categoryDao().getAllCategories() + } + + suspend fun getPhrasesForCategory(categoryId: String): List { + val categoriesList = database.categoryDao().getCategoryWithPhrases(categoryId) + return categoriesList.firstOrNull()?.phrases ?: listOf() + } + + suspend fun addPhrase(phrase: Phrase) { + database.phraseDao().insertPhrase(phrase) + } + + suspend fun addCategory(category: Category) { + database.categoryDao().insertCategory(category) + } + + suspend fun populateCategories(categories: List) { + database.categoryDao().insertCategories(*categories.toTypedArray()) + } + + suspend fun populatePhrases(phrases: List) { + database.phraseDao().insertPhrases(*phrases.toTypedArray()) + } + + suspend fun populateCrossRefs(crossRefs: List) { + database.categoryPhraseCrossRefDao() + .insertCategoryPhraseCrossRefs(*crossRefs.toTypedArray()) + } + + suspend fun addCrossRef(crossRef: CategoryPhraseCrossRef) { + database.categoryPhraseCrossRefDao().insertCategoryPhraseCrossRef(crossRef) + } + + suspend fun deleteCrossRef(crossRef: CategoryPhraseCrossRef) { + database.categoryPhraseCrossRefDao().deleteCategoryPhraseCrossRefDao(crossRef) + } + + suspend fun deletePhrase(phrase: Phrase) { + database.phraseDao().deletePhrase(phrase) + } + + suspend fun deleteCategory(category: Category) { + database.categoryDao().deleteCategory(category) + } + + suspend fun updatePhrase(phrase: Phrase) { + database.phraseDao().updatePhrase(phrase) + } + + suspend fun updateCategory(category: Category) { + database.categoryDao().updateCategory(category) + } + + suspend fun updateCategories(categories: List) { + database.categoryDao().updateCategories(*categories.toTypedArray()) + } + + suspend fun getCategoryById(categoryId: String): Category { + return database.categoryDao().getCategoryById(categoryId) + } + + suspend fun populateDatabase(numbersCategoryId: String, mySayingsCategoryId: String) { + val categories = getAllCategories() + if (categories.isNotEmpty()) { + return + } + val presets = withContext(Dispatchers.IO) { + var json = "" + try { + val inputStream = get().assets.open("json/presets.json") + val size = inputStream.available() + val buffer = ByteArray(size) + inputStream.read(buffer) + inputStream.close() + json = String(buffer, Charset.forName("UTF-8")) + } catch (e: Exception) { + Log.e("populateDatabase", e.message ?: "Error reading JSON") + } + + var presetsObject: PresetsObject? = null + try { + presetsObject = moshi.adapter(PresetsObject::class.java).fromJson(json) + } catch (e: Exception) { + Log.e("populateDatabase", e.message ?: "Error parsing JSON") + } + return@withContext presetsObject + } + + val categoryObjects = mutableListOf() + val phraseObjects = mutableListOf() + val crossRefObjects = mutableListOf() + + // Populate the presets from JSON + presets?.categories?.forEach { + categoryObjects.add( + Category( + it.id, + System.currentTimeMillis(), + false, + it.localizedName, + it.hidden, + categoryObjects.size + ) + ) + } + + presets?.phrases?.forEach { presetPhrase -> + phraseObjects.add( + Phrase( + presetPhrase.id, + System.currentTimeMillis(), + false, + System.currentTimeMillis(), + presetPhrase.localizedUtterance, + phraseObjects.size + ) + ) + presetPhrase.categoryIds.forEach { categoryId -> + crossRefObjects.add(CategoryPhraseCrossRef(categoryId, presetPhrase.id)) + } + } + + // Populate the numbers category from arrays.xml + get().resources.getStringArray(R.array.category_123).forEach { + val phraseId = UUID.randomUUID().toString() + phraseObjects.add( + Phrase( + phraseId, + System.currentTimeMillis(), + false, + System.currentTimeMillis(), + mapOf(Pair(Locale.US.language, it)), + phraseObjects.size + ) + ) + crossRefObjects.add(CategoryPhraseCrossRef(numbersCategoryId, phraseId)) + } + + // Create My Sayings category + val mySayingsCategory = + categoryObjects.first { it.categoryId == mySayingsCategoryId } + val mySayings = sharedPrefs.getMySayings() + mySayings.forEach { + val phraseId = UUID.randomUUID().toString() + phraseObjects.add( + Phrase( + phraseId, + System.currentTimeMillis(), + true, + System.currentTimeMillis(), + mapOf(Pair(Locale.US.language, it)), + phraseObjects.size + ) + ) + crossRefObjects.add( + CategoryPhraseCrossRef( + mySayingsCategory.categoryId, + phraseId + ) + ) + sharedPrefs.setMySayings(emptySet()) + } + + populateCategories(categoryObjects) + populatePhrases(phraseObjects) + populateCrossRefs(crossRefObjects) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/willowtree/vocable/presets/PresetsViewModel.kt b/app/src/main/java/com/willowtree/vocable/presets/PresetsViewModel.kt new file mode 100644 index 00000000..6927c3c6 --- /dev/null +++ b/app/src/main/java/com/willowtree/vocable/presets/PresetsViewModel.kt @@ -0,0 +1,46 @@ +package com.willowtree.vocable.presets + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import com.willowtree.vocable.BaseViewModel +import com.willowtree.vocable.room.Category +import com.willowtree.vocable.room.Phrase +import kotlinx.coroutines.launch +import org.koin.core.inject + +class PresetsViewModel(numbersCategoryId: String, mySayingsCategoryId: String) : + BaseViewModel(numbersCategoryId, mySayingsCategoryId) { + + private val presetsRepository: PresetsRepository by inject() + + private val liveCategoryList = MutableLiveData>() + val categoryList: LiveData> = liveCategoryList + + private val liveSelectedCategory = MutableLiveData() + val selectedCategory: LiveData = liveSelectedCategory + + private val liveCurrentPhrases = MutableLiveData>() + val currentPhrases: LiveData> = liveCurrentPhrases + + init { + populateCategories() + } + + fun populateCategories() { + backgroundScope.launch { + val categories = presetsRepository.getAllCategories().filter { !it.hidden } + liveCategoryList.postValue(categories) + val currentCategory = liveSelectedCategory.value ?: categories.first() + onCategorySelected(currentCategory) + } + } + + fun onCategorySelected(category: Category) { + liveSelectedCategory.postValue(category) + backgroundScope.launch { + val phrases = presetsRepository.getPhrasesForCategory(category.categoryId) + .sortedBy { it.sortOrder } + liveCurrentPhrases.postValue(phrases) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/willowtree/vocable/room/Category.kt b/app/src/main/java/com/willowtree/vocable/room/Category.kt new file mode 100644 index 00000000..0ead5faa --- /dev/null +++ b/app/src/main/java/com/willowtree/vocable/room/Category.kt @@ -0,0 +1,24 @@ +package com.willowtree.vocable.room + +import android.os.Parcelable +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey +import com.willowtree.vocable.utils.LocaleUtils +import kotlinx.android.parcel.Parcelize + +@Entity +@Parcelize +data class Category( + @PrimaryKey @ColumnInfo(name = "category_id") val categoryId: String, + @ColumnInfo(name = "creation_date") val creationDate: Long, + @ColumnInfo(name = "is_user_generated") val isUserGenerated: Boolean, + @ColumnInfo(name = "localized_name") var localizedName: Map, + var hidden: Boolean, + @ColumnInfo(name = "sort_order") var sortOrder: Int +) : Parcelable { + + fun getLocalizedText(): String { + return LocaleUtils.getTextForLocale(localizedName) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/willowtree/vocable/room/CategoryDao.kt b/app/src/main/java/com/willowtree/vocable/room/CategoryDao.kt new file mode 100644 index 00000000..f4cdf93d --- /dev/null +++ b/app/src/main/java/com/willowtree/vocable/room/CategoryDao.kt @@ -0,0 +1,32 @@ +package com.willowtree.vocable.room + +import androidx.room.* + +@Dao +interface CategoryDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertCategories(vararg categories: Category) + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertCategory(category: Category) + + @Query("SELECT * FROM Category ORDER BY sort_order ASC") + suspend fun getAllCategories(): List + + @Query("SELECT * FROM Category WHERE category_id = :categoryId") + suspend fun getCategoryById(categoryId: String): Category + + @Delete + suspend fun deleteCategory(category: Category) + + @Update + suspend fun updateCategory(category: Category) + + @Update + suspend fun updateCategories(vararg categories: Category) + + @Transaction + @Query("SELECT * FROM Category WHERE category_id == :categoryId") + suspend fun getCategoryWithPhrases(categoryId: String): List +} \ No newline at end of file diff --git a/app/src/main/java/com/willowtree/vocable/room/CategoryPhraseCrossRef.kt b/app/src/main/java/com/willowtree/vocable/room/CategoryPhraseCrossRef.kt new file mode 100644 index 00000000..843f71c0 --- /dev/null +++ b/app/src/main/java/com/willowtree/vocable/room/CategoryPhraseCrossRef.kt @@ -0,0 +1,10 @@ +package com.willowtree.vocable.room + +import androidx.room.ColumnInfo +import androidx.room.Entity + +@Entity(primaryKeys = ["category_id", "phrase_id"]) +data class CategoryPhraseCrossRef( + @ColumnInfo(name = "category_id") val categoryId: String, + @ColumnInfo(name = "phrase_id") val phraseId: String +) \ No newline at end of file diff --git a/app/src/main/java/com/willowtree/vocable/room/CategoryPhraseCrossRefDao.kt b/app/src/main/java/com/willowtree/vocable/room/CategoryPhraseCrossRefDao.kt new file mode 100644 index 00000000..9de9ce0a --- /dev/null +++ b/app/src/main/java/com/willowtree/vocable/room/CategoryPhraseCrossRefDao.kt @@ -0,0 +1,19 @@ +package com.willowtree.vocable.room + +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.OnConflictStrategy + +@Dao +interface CategoryPhraseCrossRefDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertCategoryPhraseCrossRef(categoryPhraseCrossRef: CategoryPhraseCrossRef) + + @Delete + suspend fun deleteCategoryPhraseCrossRefDao(categoryPhraseCrossRef: CategoryPhraseCrossRef) + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertCategoryPhraseCrossRefs(vararg crossRefs: CategoryPhraseCrossRef) +} \ No newline at end of file diff --git a/app/src/main/java/com/willowtree/vocable/room/CategoryWithPhrases.kt b/app/src/main/java/com/willowtree/vocable/room/CategoryWithPhrases.kt new file mode 100644 index 00000000..2906df9b --- /dev/null +++ b/app/src/main/java/com/willowtree/vocable/room/CategoryWithPhrases.kt @@ -0,0 +1,15 @@ +package com.willowtree.vocable.room + +import androidx.room.Embedded +import androidx.room.Junction +import androidx.room.Relation + +data class CategoryWithPhrases( + @Embedded val category: Category, + @Relation( + parentColumn = "category_id", + entityColumn = "phrase_id", + associateBy = Junction(CategoryPhraseCrossRef::class) + ) + val phrases: List +) \ No newline at end of file diff --git a/app/src/main/java/com/willowtree/vocable/room/Converters.kt b/app/src/main/java/com/willowtree/vocable/room/Converters.kt new file mode 100644 index 00000000..f859170a --- /dev/null +++ b/app/src/main/java/com/willowtree/vocable/room/Converters.kt @@ -0,0 +1,31 @@ +package com.willowtree.vocable.room + +import androidx.room.TypeConverter +import com.squareup.moshi.JsonAdapter +import com.squareup.moshi.Moshi +import com.squareup.moshi.Types +import org.koin.core.KoinComponent +import org.koin.core.inject + +object Converters : KoinComponent { + + private val moshi: Moshi by inject() + + @TypeConverter + @JvmStatic + fun stringMapToJson(stringMap: Map?): String { + val type = + Types.newParameterizedType(Map::class.java, String::class.java, String::class.java) + val adapter: JsonAdapter> = moshi.adapter(type) + return adapter.toJson(stringMap) + } + + @TypeConverter + @JvmStatic + fun jsonToStringMap(json: String): Map? { + val type = + Types.newParameterizedType(Map::class.java, String::class.java, String::class.java) + val adapter: JsonAdapter> = moshi.adapter(type) + return adapter.fromJson(json) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/willowtree/vocable/room/Phrase.kt b/app/src/main/java/com/willowtree/vocable/room/Phrase.kt new file mode 100644 index 00000000..0f2ab7be --- /dev/null +++ b/app/src/main/java/com/willowtree/vocable/room/Phrase.kt @@ -0,0 +1,29 @@ +package com.willowtree.vocable.room + +import android.os.Parcelable +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey +import com.willowtree.vocable.utils.LocaleUtils +import kotlinx.android.parcel.Parcelize +import java.util.* + +@Entity +@Parcelize +data class Phrase( + @PrimaryKey @ColumnInfo(name = "phrase_id") val phraseId: String, + @ColumnInfo(name = "creation_date") val creationDate: Long, + @ColumnInfo(name = "is_user_generated") val isUserGenerated: Boolean, + @ColumnInfo(name = "last_spoken_date") val lastSpokenDate: Long, + @ColumnInfo(name = "localized_utterance") var localizedUtterance: Map, + @ColumnInfo(name = "sort_order") var sortOrder: Int +) : Parcelable { + + fun getLocalizedText(): String { + return LocaleUtils.getTextForLocale(localizedUtterance) + } + + fun getLocalizedPair(): Pair { + return LocaleUtils.getLocalizedPair(localizedUtterance) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/willowtree/vocable/room/PhraseDao.kt b/app/src/main/java/com/willowtree/vocable/room/PhraseDao.kt new file mode 100644 index 00000000..8d59e040 --- /dev/null +++ b/app/src/main/java/com/willowtree/vocable/room/PhraseDao.kt @@ -0,0 +1,19 @@ +package com.willowtree.vocable.room + +import androidx.room.* + +@Dao +interface PhraseDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertPhrase(phrase: Phrase) + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertPhrases(vararg phrases: Phrase) + + @Delete + suspend fun deletePhrase(phrase: Phrase) + + @Update + suspend fun updatePhrase(phrase: Phrase) +} \ No newline at end of file diff --git a/app/src/main/java/com/willowtree/vocable/room/VocableDatabase.kt b/app/src/main/java/com/willowtree/vocable/room/VocableDatabase.kt new file mode 100644 index 00000000..442f3e2e --- /dev/null +++ b/app/src/main/java/com/willowtree/vocable/room/VocableDatabase.kt @@ -0,0 +1,37 @@ +package com.willowtree.vocable.room + +import android.content.Context +import androidx.room.Database +import androidx.room.Room +import androidx.room.RoomDatabase +import androidx.room.TypeConverters + +@Database(entities = [Category::class, Phrase::class, CategoryPhraseCrossRef::class], version = 3) +@TypeConverters(Converters::class) +abstract class VocableDatabase : RoomDatabase() { + + companion object { + private var vocableDatabase: VocableDatabase? = null + private const val DATABASE_NAME = "VocableDatabase" + + fun getVocableDatabase(context: Context): VocableDatabase { + if (vocableDatabase == null) { + vocableDatabase = + Room.databaseBuilder(context, VocableDatabase::class.java, DATABASE_NAME) + .addMigrations( + VocableDatabaseMigrations.MIGRATION_1_2, + VocableDatabaseMigrations.MIGRATION_2_3 + ) + .build() + } + return vocableDatabase as VocableDatabase + } + } + + abstract fun categoryDao(): CategoryDao + + abstract fun phraseDao(): PhraseDao + + abstract fun categoryPhraseCrossRefDao(): CategoryPhraseCrossRefDao +} + diff --git a/app/src/main/java/com/willowtree/vocable/room/VocableDatabaseMigrations.kt b/app/src/main/java/com/willowtree/vocable/room/VocableDatabaseMigrations.kt new file mode 100644 index 00000000..1a288d52 --- /dev/null +++ b/app/src/main/java/com/willowtree/vocable/room/VocableDatabaseMigrations.kt @@ -0,0 +1,56 @@ +package com.willowtree.vocable.room + +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase +import com.willowtree.vocable.utils.VocableSharedPreferences + +object VocableDatabaseMigrations { + + val MIGRATION_1_2: Migration = object : Migration(1, 2) { + override fun migrate(database: SupportSQLiteDatabase) { + // Phrases no longer have to have unique utterances + database.execSQL("DROP INDEX index_Phrase_utterance") + } + } + + val MIGRATION_2_3: Migration = object : Migration(2, 3) { + // Moving to new JSON schema + override fun migrate(database: SupportSQLiteDatabase) { + //Create Category-Phrase relation + database.execSQL("CREATE TABLE CategoryPhraseCrossRef (category_id TEXT NOT NULL, phrase_id TEXT NOT NULL, PRIMARY KEY(category_id, phrase_id))") + + // Create new Category and Phrase tables + database.execSQL("CREATE TABLE Category_New (category_id TEXT NOT NULL, creation_date INTEGER NOT NULL, is_user_generated INTEGER NOT NULL, localized_name TEXT NOT NULL, hidden INTEGER NOT NULL, sort_order INTEGER NOT NULL, PRIMARY KEY(category_id))") + database.execSQL("CREATE TABLE Phrase_New (phrase_id TEXT NOT NULL, creation_date INTEGER NOT NULL, is_user_generated INTEGER NOT NULL, last_spoken_date INTEGER NOT NULL, localized_utterance TEXT NOT NULL, sort_order INTEGER NOT NULL, PRIMARY KEY(phrase_id))") + + // Get id of My Sayings category + val categoryCursor = + database.query("SELECT identifier FROM Category WHERE name = 'My Sayings'") + var categoryId = -1L + while (categoryCursor.moveToNext()) { + categoryId = categoryCursor.getLong(categoryCursor.getColumnIndex("identifier")) + } + categoryCursor.close() + + // Get My Sayings + val phraseCursor = + database.query("SELECT utterance FROM Phrase WHERE category_id = $categoryId ORDER BY creation_date ASC") + val mySayings = LinkedHashSet() + while (phraseCursor.moveToNext()) { + val saying = phraseCursor.getString(phraseCursor.getColumnIndex("utterance")) + mySayings.add(saying) + } + phraseCursor.close() + + // Save My Sayings to Shared Prefs + VocableSharedPreferences().setMySayings(mySayings) + + // Delete old tables and rename new ones to match old names + database.execSQL("DROP TABLE Category") + database.execSQL("ALTER TABLE Category_New RENAME TO Category") + + database.execSQL("DROP TABLE Phrase") + database.execSQL("ALTER TABLE Phrase_New RENAME TO Phrase") + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/willowtree/vocable/room/models/PresetCategory.kt b/app/src/main/java/com/willowtree/vocable/room/models/PresetCategory.kt new file mode 100644 index 00000000..888ff24d --- /dev/null +++ b/app/src/main/java/com/willowtree/vocable/room/models/PresetCategory.kt @@ -0,0 +1,7 @@ +package com.willowtree.vocable.room.models + +data class PresetCategory( + val id: String, + val localizedName: Map, + val hidden: Boolean +) \ No newline at end of file diff --git a/app/src/main/java/com/willowtree/vocable/room/models/PresetPhrase.kt b/app/src/main/java/com/willowtree/vocable/room/models/PresetPhrase.kt new file mode 100644 index 00000000..984cc234 --- /dev/null +++ b/app/src/main/java/com/willowtree/vocable/room/models/PresetPhrase.kt @@ -0,0 +1,7 @@ +package com.willowtree.vocable.room.models + +data class PresetPhrase( + val id: String, + val categoryIds: List, + val localizedUtterance: Map +) \ No newline at end of file diff --git a/app/src/main/java/com/willowtree/vocable/room/models/PresetsObject.kt b/app/src/main/java/com/willowtree/vocable/room/models/PresetsObject.kt new file mode 100644 index 00000000..9dc17104 --- /dev/null +++ b/app/src/main/java/com/willowtree/vocable/room/models/PresetsObject.kt @@ -0,0 +1,7 @@ +package com.willowtree.vocable.room.models + +data class PresetsObject( + val schemaVersion: Int, + val phrases: List, + val categories: List +) \ No newline at end of file diff --git a/app/src/main/java/com/willowtree/vocable/settings/EditCategoriesFragment.kt b/app/src/main/java/com/willowtree/vocable/settings/EditCategoriesFragment.kt new file mode 100644 index 00000000..1992b242 --- /dev/null +++ b/app/src/main/java/com/willowtree/vocable/settings/EditCategoriesFragment.kt @@ -0,0 +1,174 @@ +package com.willowtree.vocable.settings + +import android.os.Bundle +import android.view.View +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentManager +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProviders +import androidx.viewpager2.widget.ViewPager2 +import com.willowtree.vocable.* +import com.willowtree.vocable.databinding.FragmentEditCategoriesBinding +import com.willowtree.vocable.room.Category +import com.willowtree.vocable.utils.VocableFragmentStateAdapter +import kotlin.math.min + +class EditCategoriesFragment : BaseFragment() { + + override val bindingInflater: BindingInflater = FragmentEditCategoriesBinding::inflate + + private lateinit var categoriesAdapter: CategoriesPagerAdapter + private lateinit var editCategoriesViewModel: EditCategoriesViewModel + + private val allViews = mutableListOf() + private var maxEditCategories = 1 + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + binding.categoryBackButton.action = { + when (val currentPosition = binding.editCategoriesViewPager.currentItem) { + null -> { + // No-op + } + 0 -> { + binding.editCategoriesViewPager.setCurrentItem( + categoriesAdapter.itemCount - 1, + true + ) + } + else -> { + binding.editCategoriesViewPager.setCurrentItem(currentPosition - 1, true) + } + } + } + + binding.categoryForwardButton.action = { + when (val currentPosition = binding.editCategoriesViewPager.currentItem) { + categoriesAdapter.itemCount - 1 -> { + binding.editCategoriesViewPager.setCurrentItem(0, true) + } + else -> { + binding.editCategoriesViewPager.setCurrentItem(currentPosition + 1, true) + } + } + } + categoriesAdapter = CategoriesPagerAdapter(childFragmentManager) + + maxEditCategories = resources.getInteger(R.integer.max_edit_categories) + + binding.editCategoriesViewPager.registerOnPageChangeCallback(object : + ViewPager2.OnPageChangeCallback() { + override fun onPageSelected(position: Int) { + val pageNum = position % categoriesAdapter.numPages + 1 + binding.categoryPageNumber.text = getString( + R.string.phrases_page_number, + pageNum, + categoriesAdapter.numPages + ) + + activity?.let { activity -> + allViews.clear() + if (activity is MainActivity) { + activity.resetAllViews() + } + } + } + }) + + binding.backButton.action = { + parentFragmentManager.popBackStack() + } + + binding.addCategoryButton.action = { + parentFragmentManager + .beginTransaction() + .replace( + R.id.settings_fragment_container, + EditKeyboardFragment.newCreateCategoryInstance() + ) + .addToBackStack(null) + .commit() + } + + editCategoriesViewModel = + ViewModelProviders.of( + requireActivity(), + BaseViewModelFactory( + getString(R.string.category_123_id), + getString(R.string.category_my_sayings_id) + ) + ).get(EditCategoriesViewModel::class.java) + subscribeToViewModel() + } + + override fun getAllViews(): List { + return emptyList() + } + + private fun subscribeToViewModel() { + editCategoriesViewModel.addRemoveCategoryList.observe(requireActivity(), Observer { + it?.let { categories -> + binding.editCategoriesViewPager.apply { + isSaveEnabled = false + adapter = categoriesAdapter + categoriesAdapter.setItems(categories) + // Move adapter to middle so user can scroll both directions + val middle = categoriesAdapter.itemCount / 2 + if (middle % categoriesAdapter.numPages == 0) { + setCurrentItem(middle, false) + } else { + val mod = middle % categoriesAdapter.numPages + setCurrentItem( + middle + (categoriesAdapter.numPages - mod), + false + ) + } + } + } + }) + + editCategoriesViewModel.lastViewedIndex.observe(requireActivity(), Observer { + it?.let { index -> + val pageNum = index / maxEditCategories + val middle = categoriesAdapter.itemCount / 2 + val toScrollTo = if (middle % categoriesAdapter.numPages == 0) { + middle + pageNum + } else { + val mod = middle % categoriesAdapter.numPages + middle + (categoriesAdapter.numPages - mod) + pageNum + } + if (binding.editCategoriesViewPager.currentItem != toScrollTo) { + binding.editCategoriesViewPager.setCurrentItem(toScrollTo, false) + } + } + }) + } + + inner class CategoriesPagerAdapter(fm: FragmentManager) : + VocableFragmentStateAdapter(fm, viewLifecycleOwner.lifecycle) { + + override fun setItems(items: List) { + super.setItems(items) + setPagingButtonsEnabled(categoriesAdapter.numPages > 1) + } + + override fun getMaxItemsPerPage(): Int = maxEditCategories + + override fun createFragment(position: Int): Fragment { + val startPosition = (position % numPages) * maxEditCategories + val endPosition = min(items.size, startPosition + maxEditCategories) + + return EditCategoriesListFragment.newInstance(startPosition, endPosition) + } + } + + private fun setPagingButtonsEnabled(enable: Boolean) { + binding.apply { + categoryForwardButton.isEnabled = enable + categoryBackButton.isEnabled = enable + editCategoriesViewPager.isUserInputEnabled = enable + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/willowtree/vocable/settings/EditCategoriesListFragment.kt b/app/src/main/java/com/willowtree/vocable/settings/EditCategoriesListFragment.kt new file mode 100644 index 00000000..d0c3c4a3 --- /dev/null +++ b/app/src/main/java/com/willowtree/vocable/settings/EditCategoriesListFragment.kt @@ -0,0 +1,177 @@ +package com.willowtree.vocable.settings + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.view.isVisible +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProviders +import com.willowtree.vocable.BaseFragment +import com.willowtree.vocable.BaseViewModelFactory +import com.willowtree.vocable.BindingInflater +import com.willowtree.vocable.R +import com.willowtree.vocable.databinding.CategoryEditButtonBinding +import com.willowtree.vocable.databinding.FragmentEditCategoriesListBinding +import com.willowtree.vocable.room.Category + +class EditCategoriesListFragment : BaseFragment() { + + companion object { + private const val KEY_START_POSITION = "KEY_START_POSITION" + private const val KEY_END_POSITION = "KEY_END_POSITION" + + fun newInstance( + startPosition: Int, + endPosition: Int + ): EditCategoriesListFragment { + return EditCategoriesListFragment().apply { + arguments = Bundle().apply { + putInt(KEY_START_POSITION, startPosition) + putInt(KEY_END_POSITION, endPosition) + } + } + } + } + + override val bindingInflater: BindingInflater = FragmentEditCategoriesListBinding::inflate + private lateinit var editCategoriesViewModel: EditCategoriesViewModel + private var maxEditCategories = 1 + + private var startPosition = 0 + private var endPosition = 0 + + private val editButtonList = mutableListOf() + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + super.onCreateView(inflater, container, savedInstanceState) + maxEditCategories = resources.getInteger(R.integer.max_edit_categories) + + startPosition = arguments?.getInt(KEY_START_POSITION, 0) ?: 0 + endPosition = arguments?.getInt(KEY_END_POSITION, 0) ?: 0 + + val numberOfButtons = endPosition - startPosition + + for (i in 0 until numberOfButtons) { + val categoryView = + CategoryEditButtonBinding.inflate( + inflater, + binding.categoryEditButtonContainer, + false + ) + binding.categoryEditButtonContainer.addView(categoryView.root) + + editButtonList.add(categoryView) + } + + // Add invisible views to fill out the rest of the space + for (i in 0 until maxEditCategories - numberOfButtons) { + val hiddenButton = + CategoryEditButtonBinding.inflate( + inflater, + binding.categoryEditButtonContainer, + false + ) + binding.categoryEditButtonContainer.addView(hiddenButton.root.apply { + isEnabled = false + visibility = View.INVISIBLE + }) + } + + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + editCategoriesViewModel = + ViewModelProviders.of( + requireActivity(), + BaseViewModelFactory( + getString(R.string.category_123_id), + getString(R.string.category_my_sayings_id) + ) + ).get(EditCategoriesViewModel::class.java) + subscribeToViewModel() + } + + override fun getAllViews(): List { + return emptyList() + } + + private fun subscribeToViewModel() { + editCategoriesViewModel.orderCategoryList.observe(viewLifecycleOwner, Observer { + it?.let { overallList -> + if (startPosition >= 0 && endPosition <= overallList.size) { + var firstHiddenIndex = overallList.indexOfFirst { category -> category.hidden } + // Just default to list size if no categories are hidden + if (firstHiddenIndex == -1) { + firstHiddenIndex = overallList.size + } + + overallList.subList(startPosition, endPosition) + .forEachIndexed { index, category -> + bindCategoryEditButton( + editButtonList[index], + category, + startPosition + index, + firstHiddenIndex + ) + } + } + } + }) + } + + private fun bindCategoryEditButton( + editButtonBinding: CategoryEditButtonBinding, + category: Category, + overallIndex: Int, + firstHiddenIndex: Int + ) { + with(editButtonBinding) { + if (category.hidden) { + categoryName.text = category.getLocalizedText() + } else { + categoryName.text = getString( + R.string.edit_categories_button_number, + overallIndex + 1, + category.getLocalizedText() + ) + } + + hiddenView.isVisible = category.hidden + + moveCategoryUpButton.isEnabled = !category.hidden && overallIndex > 0 + moveCategoryDownButton.isEnabled = + !category.hidden && overallIndex + 1 < firstHiddenIndex + + moveCategoryUpButton.action = { + editCategoriesViewModel.moveCategoryUp(category) + } + moveCategoryDownButton.action = { + editCategoriesViewModel.moveCategoryDown(category) + } + + if (category.categoryId == getString(R.string.category_my_sayings_id)) { + editCategorySelectButton.isEnabled = false + } + + editCategorySelectButton.action = { + requireActivity().supportFragmentManager + .beginTransaction() + .replace( + R.id.settings_fragment_container, + EditCategoryOptionsFragment.newInstance(category) + ) + .addToBackStack(null) + .commit() + editCategoriesViewModel.onCategorySelected(category) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/willowtree/vocable/settings/EditCategoriesViewModel.kt b/app/src/main/java/com/willowtree/vocable/settings/EditCategoriesViewModel.kt new file mode 100644 index 00000000..350e1b90 --- /dev/null +++ b/app/src/main/java/com/willowtree/vocable/settings/EditCategoriesViewModel.kt @@ -0,0 +1,192 @@ +package com.willowtree.vocable.settings + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import com.willowtree.vocable.BaseViewModel +import com.willowtree.vocable.presets.PresetsRepository +import com.willowtree.vocable.room.Category +import kotlinx.coroutines.launch +import org.koin.core.inject +import java.util.* + +class EditCategoriesViewModel(numbersCategoryId: String, mySayingsCategoryId: String) : + BaseViewModel(numbersCategoryId, mySayingsCategoryId) { + + companion object { + private const val CATEGORY_UPDATED_DELAY = 2000L + } + + private val presetsRepository: PresetsRepository by inject() + + private val liveCategoryList = MutableLiveData>() + val categoryList: LiveData> = liveCategoryList + + private val liveOrderCategoryList = MutableLiveData>() + val orderCategoryList: LiveData> = liveOrderCategoryList + + private val liveAddRemoveCategoryList = MutableLiveData>() + val addRemoveCategoryList: LiveData> = liveAddRemoveCategoryList + + private val liveShowCategoryAdded = MutableLiveData() + val showCategoryAdded: LiveData = liveShowCategoryAdded + + private val liveLastViewedIndex = MutableLiveData() + val lastViewedIndex: LiveData = liveLastViewedIndex + + private var overallCategories = listOf() + + init { + populateCategories() + } + + private fun populateCategories() { + backgroundScope.launch { + + overallCategories = presetsRepository.getAllCategories() + + liveOrderCategoryList.postValue(overallCategories) + liveAddRemoveCategoryList.postValue(overallCategories) + } + } + + fun deleteCategory(category: Category) { + backgroundScope.launch { + presetsRepository.deleteCategory(category) + populateCategories() + } + } + + fun updateCategory(category: Category) { + backgroundScope.launch { + presetsRepository.updateCategory(category) + } + } + + fun onCategorySelected(category: Category) { + val index = overallCategories.indexOf(category) + if (index > -1) { + liveLastViewedIndex.postValue(index) + } + } + + fun hideShowCategory(category: Category, hide: Boolean) { + backgroundScope.launch { + if (hide) { + hideCategory(category) + } else { + showCategory(category) + } + } + } + + private suspend fun hideCategory(category: Category) { + val catIndex = overallCategories.indexOf(category) + if (catIndex > -1) { + val listToUpdate = overallCategories.filter { it.sortOrder >= category.sortOrder } + listToUpdate.forEach { + if (it.categoryId == category.categoryId) { + it.sortOrder = overallCategories.size - 1 + it.hidden = true + } else { + it.sortOrder-- + } + } + + overallCategories = overallCategories.sortedBy { it.sortOrder } + liveOrderCategoryList.postValue(overallCategories) + + presetsRepository.updateCategories(listToUpdate) + } + } + + private suspend fun showCategory(category: Category) { + val catIndex = overallCategories.indexOf(category) + if (catIndex > -1) { + var firstHiddenIndex = overallCategories.indexOfFirst { it.hidden } + if (firstHiddenIndex == -1) { + firstHiddenIndex = overallCategories.size - 1 + } + val listToUpdate = overallCategories.filter { it.hidden } + listToUpdate.forEach { + if (it.categoryId == category.categoryId) { + it.sortOrder = firstHiddenIndex + it.hidden = false + } else { + it.sortOrder++ + } + } + + overallCategories = overallCategories.sortedBy { it.sortOrder } + liveOrderCategoryList.postValue(overallCategories) + + presetsRepository.updateCategories(listToUpdate) + } + } + + fun moveCategoryUp(category: Category) { + backgroundScope.launch { + val catIndex = overallCategories.indexOf(category) + if (catIndex > 0) { + val previousCat = overallCategories[catIndex - 1] + category.sortOrder-- + previousCat.sortOrder++ + + overallCategories = overallCategories.sortedBy { it.sortOrder } + liveOrderCategoryList.postValue(overallCategories) + + presetsRepository.updateCategories(listOf(category, previousCat)) + } + } + } + + fun moveCategoryDown(category: Category) { + backgroundScope.launch { + val catIndex = overallCategories.indexOf(category) + if (catIndex > -1) { + val nextCat = overallCategories[catIndex + 1] + category.sortOrder++ + nextCat.sortOrder-- + + overallCategories = overallCategories.sortedBy { it.sortOrder } + liveOrderCategoryList.postValue(overallCategories) + + presetsRepository.updateCategories(listOf(category, nextCat)) + } + } + } + + fun addNewCategory(categoryStr: String) { + backgroundScope.launch { + var firstHiddenIndex = overallCategories.indexOfFirst { it.hidden } + if (firstHiddenIndex == -1) { + firstHiddenIndex = overallCategories.size - 1 + } + val listToUpdate = overallCategories.filter { it.hidden } + listToUpdate.forEach { + it.sortOrder++ + } + val newCategory = Category( + UUID.randomUUID().toString(), + System.currentTimeMillis(), + true, + mapOf(Pair(Locale.getDefault().toString(), categoryStr)), + false, + firstHiddenIndex + ) + + overallCategories = overallCategories + .toMutableList() + .apply { add(newCategory) } + .sortedBy { it.sortOrder } + liveAddRemoveCategoryList.postValue(overallCategories) + liveLastViewedIndex.postValue(firstHiddenIndex) + liveOrderCategoryList.postValue(overallCategories) + + with(presetsRepository) { + addCategory(newCategory) + updateCategories(listToUpdate) + } + } + } + +} diff --git a/app/src/main/java/com/willowtree/vocable/settings/EditCategoryOptionsFragment.kt b/app/src/main/java/com/willowtree/vocable/settings/EditCategoryOptionsFragment.kt new file mode 100644 index 00000000..e91be64f --- /dev/null +++ b/app/src/main/java/com/willowtree/vocable/settings/EditCategoryOptionsFragment.kt @@ -0,0 +1,126 @@ +package com.willowtree.vocable.settings + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.view.isInvisible +import androidx.core.view.isVisible +import androidx.lifecycle.ViewModelProviders +import androidx.viewbinding.ViewBinding +import com.willowtree.vocable.BaseFragment +import com.willowtree.vocable.BaseViewModelFactory +import com.willowtree.vocable.BindingInflater +import com.willowtree.vocable.R +import com.willowtree.vocable.databinding.FragmentEditCategoryOptionsBinding +import com.willowtree.vocable.room.Category + +class EditCategoryOptionsFragment : BaseFragment() { + + companion object { + private const val KEY_CATEGORY = "KEY_CATEGORY" + + fun newInstance(category: Category): EditCategoryOptionsFragment { + return EditCategoryOptionsFragment().apply { + arguments = Bundle().apply { + putParcelable(KEY_CATEGORY, category) + } + } + } + } + + override val bindingInflater: BindingInflater = FragmentEditCategoryOptionsBinding::inflate + private lateinit var editCategoriesViewModel: EditCategoriesViewModel + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + val category = arguments?.getParcelable(KEY_CATEGORY) + + if (category?.isUserGenerated == true) { + binding.removeCategoryButton.isInvisible = false + binding.editOptionsButton?.isInvisible = false + } + + binding.categoryTitle.text = category?.getLocalizedText() + + category?.let { + binding.editOptionsButton?.action = { + parentFragmentManager + .beginTransaction() + .replace( + R.id.settings_fragment_container, + EditKeyboardFragment.newInstance(category) + ).addToBackStack(null) + .commit() + } + } + + binding.editOptionsBackButton.action = { + parentFragmentManager.popBackStack() + } + + binding.showCategorySwitch.action = { + binding.categoryShowSwitch.isChecked = !binding.categoryShowSwitch.isChecked + } + + binding.categoryShowSwitch.apply { + isChecked = category?.hidden?.not() ?: false + setOnCheckedChangeListener { _, isChecked -> + category?.let { category -> + editCategoriesViewModel.hideShowCategory(category, !isChecked) + } + } + } + + binding.removeCategoryButton.action = { + setEditButtonsEnabled(false) + toggleDialogVisibility(true) + binding.confirmationDialog.apply { + dialogTitle.text = resources.getString(R.string.are_you_sure) + dialogMessage.text = getString(R.string.removed_cant_be_restored) + dialogPositiveButton.text = + resources.getString(R.string.settings_dialog_continue) + dialogPositiveButton.action = { + category?.let { + editCategoriesViewModel.deleteCategory(category) + } + + parentFragmentManager.popBackStack() + } + dialogNegativeButton.text = resources.getString(R.string.settings_dialog_cancel) + dialogNegativeButton.action = { + toggleDialogVisibility(false) + setEditButtonsEnabled(true) + } + } + } + + + editCategoriesViewModel = ViewModelProviders.of( + requireActivity(), + BaseViewModelFactory( + getString(R.string.category_123_id), + getString(R.string.category_my_sayings_id) + ) + ).get(EditCategoriesViewModel::class.java) + } + + private fun toggleDialogVisibility(visible: Boolean) { + binding.confirmationDialog.root.isVisible = visible + } + + private fun setEditButtonsEnabled(enabled: Boolean) { + binding.apply { + showCategorySwitch.isEnabled = enabled + editOptionsButton?.isEnabled = enabled + editOptionsBackButton.isEnabled = enabled + removeCategoryButton.isEnabled = enabled + categoryShowSwitch.isEnabled = enabled + } + } + + override fun getAllViews(): List { + return emptyList() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/willowtree/vocable/settings/EditKeyboardFragment.kt b/app/src/main/java/com/willowtree/vocable/settings/EditKeyboardFragment.kt new file mode 100644 index 00000000..e77af712 --- /dev/null +++ b/app/src/main/java/com/willowtree/vocable/settings/EditKeyboardFragment.kt @@ -0,0 +1,320 @@ +package com.willowtree.vocable.settings + +import android.os.Bundle +import android.text.Editable +import android.text.TextWatcher +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.core.os.bundleOf +import androidx.core.view.children +import androidx.core.view.isVisible +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProviders +import com.willowtree.vocable.BaseFragment +import com.willowtree.vocable.BaseViewModelFactory +import com.willowtree.vocable.BindingInflater +import com.willowtree.vocable.R +import com.willowtree.vocable.customviews.ActionButton +import com.willowtree.vocable.databinding.FragmentEditKeyboardBinding +import com.willowtree.vocable.databinding.KeyboardKeyLayoutBinding +import com.willowtree.vocable.room.Category +import com.willowtree.vocable.room.Phrase +import java.util.* + +class EditKeyboardFragment : BaseFragment() { + + companion object { + private const val KEY_PHRASE = "KEY_PHRASE" + private const val KEY_IS_EDITING = "KEY_IS_EDITING" + private const val KEY_CATEGORY = "KEY_CATEGORY" + private const val KEY_IS_CATEGORY = "KEY_IS_CATEGORY" + private const val KEY_USER_INPUT = "KEY_USER_INPUT" + + fun newInstance(phrase: Phrase): EditKeyboardFragment { + return EditKeyboardFragment().apply { + arguments = Bundle().apply { + putParcelable(KEY_PHRASE, phrase) + putBoolean(KEY_IS_EDITING, true) + putBoolean(KEY_IS_CATEGORY, false) + } + } + } + + fun newInstance(isEditing: Boolean) = EditKeyboardFragment().apply { + arguments = bundleOf(KEY_IS_EDITING to isEditing) + } + + fun newInstance(category: Category) = EditKeyboardFragment().apply { + arguments = Bundle().apply { + putParcelable(KEY_CATEGORY, category) + putBoolean(KEY_IS_EDITING, true) + putBoolean(KEY_IS_CATEGORY, true) + } + } + + fun newCreateCategoryInstance(): EditKeyboardFragment = EditKeyboardFragment().apply { + arguments = bundleOf(KEY_IS_CATEGORY to true) + } + } + + override val bindingInflater: BindingInflater = FragmentEditKeyboardBinding::inflate + private lateinit var viewModel: EditPhrasesViewModel + private lateinit var editCategoriesViewModel: EditCategoriesViewModel + private lateinit var keys: Array + private var phrase: Phrase? = null + private var category: Category? = null + private var isCategory = false + private var addNewPhrase = false + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + super.onCreateView(inflater, container, savedInstanceState) + keys = resources.getStringArray(R.array.keyboard_keys) + arguments?.getParcelable(KEY_PHRASE)?.let { + phrase = it + } + arguments?.getParcelable(KEY_CATEGORY)?.let { + category = it + } + + isCategory = arguments?.getBoolean(KEY_IS_CATEGORY) ?: false + + keys = resources.getStringArray(R.array.keyboard_keys) + + populateKeys() + + return binding.root + } + + private fun populateKeys() { + keys.withIndex().forEach { + with( + KeyboardKeyLayoutBinding.inflate( + layoutInflater, + binding.keyboardKeyHolder, + true + ).root as ActionButton + ) { + text = it.value + action = { + //This action mimics sentence capitalization + //Example: "This is what's going on in here. Do you get it? Some letters are capitalized." + val currentText = binding.keyboardInput.text?.toString() ?: "" + if (isDefaultTextVisible()) { + binding.keyboardInput.text = null + binding.keyboardInput.append(text?.toString()) + } else if (currentText.endsWith(". ") || currentText.endsWith("? ")) { + binding.keyboardInput.append(text?.toString()) + } else { + binding.keyboardInput.append( + text?.toString()?.toLowerCase(Locale.getDefault()) + ) + } + } + } + } + } + + private fun isDefaultTextVisible(): Boolean { + return binding.keyboardInput.text.toString() == getString(R.string.keyboard_select_letters) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + binding.backButton.action = { + val isEditing = arguments?.getBoolean(KEY_IS_EDITING) ?: false + val textChanged = binding.keyboardInput.text.toString() != phrase?.getLocalizedText() + val categoryTextChanged = + binding.keyboardInput.text.toString() != category?.getLocalizedText() + if (isCategory && !categoryTextChanged || isDefaultTextVisible()) { + parentFragmentManager.popBackStack() + } else if (!textChanged || isDefaultTextVisible() || addNewPhrase) { + parentFragmentManager.popBackStack() + } else { + showConfirmationDialog() + } + } + + val isEditing = arguments?.getBoolean(KEY_IS_EDITING) ?: false + + binding.saveButton.action = { + if (isCategory && !isEditing && !isDefaultTextVisible()) { + binding.keyboardInput.text?.let { text -> + if (text.isNotBlank()) { + editCategoriesViewModel.addNewCategory(text.toString()) + parentFragmentManager.popBackStack() + } + } + } else if (isCategory && isEditing && !isDefaultTextVisible()) { + binding.keyboardInput.text.let { text -> + val categoryName = category?.localizedName?.toMutableMap()?.apply { + put(Locale.getDefault().toString(), text.toString()) + } + category?.localizedName = categoryName ?: mapOf() + category?.let { updatedCategory -> + editCategoriesViewModel.updateCategory(updatedCategory) + } ?: editCategoriesViewModel.addNewCategory(text.toString()) + } + } else if (!isDefaultTextVisible()) { + binding.keyboardInput.text.let { text -> + if (text.isNotBlank()) { + val phraseUtterance = + phrase?.localizedUtterance?.toMutableMap()?.apply { + put(Locale.getDefault().toString(), text.toString()) + } + phrase?.localizedUtterance = phraseUtterance ?: mapOf() + if (phrase == null) { + viewModel.addNewPhrase(text.toString()) + addNewPhrase = true + } else { + phrase?.let { updatedPhrase -> + viewModel.updatePhrase(updatedPhrase) + addNewPhrase = false + } + } + + } + } + } + } + + val inputText = if (!isCategory && phrase?.getLocalizedText().isNullOrEmpty()) { + getString(R.string.keyboard_select_letters) + } else if (isCategory && category?.getLocalizedText().isNullOrEmpty()) { + getString(R.string.keyboard_select_letters) + } else if (isCategory) { + category?.getLocalizedText() + } else { + phrase?.getLocalizedText() + } + + binding.keyboardInput.setText(inputText) + + binding.keyboardInput.addTextChangedListener(object : TextWatcher { + override fun afterTextChanged(s: Editable?) { + // no-op + } + + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) { + // no-op + } + + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { + binding.saveButton.isEnabled = !isDefaultTextVisible() + } + + }) + + binding.saveButton.isEnabled = false + + binding.keyboardClearButton.action = { + binding.keyboardInput.setText(R.string.keyboard_select_letters) + } + + binding.keyboardSpaceButton.action = { + if (!isDefaultTextVisible() && !binding.keyboardInput.text.endsWith(' ')) { + binding.keyboardInput.append(" ") + } + } + + binding.keyboardBackspaceButton.action = { + if (!isDefaultTextVisible()) { + binding.keyboardInput.let { keyboardInput -> + keyboardInput.setText(keyboardInput.text.toString().dropLast(1)) + if (keyboardInput.text.isNullOrEmpty()) { + keyboardInput.setText(R.string.keyboard_select_letters) + } + } + } + } + + (binding.phraseSavedView.root as TextView).apply { + if (arguments?.getBoolean(KEY_IS_EDITING) == true) { + setText(R.string.changes_saved) + } else { + setText(R.string.new_phrase_saved) + } + } + + // Restore user input on config change + savedInstanceState?.apply { binding.keyboardInput.setText(getString(KEY_USER_INPUT)) } + + if (isCategory) { + editCategoriesViewModel = ViewModelProviders.of( + requireActivity(), + BaseViewModelFactory( + getString(R.string.category_123_id), + getString(R.string.category_my_sayings_id) + ) + ).get(EditCategoriesViewModel::class.java) + } else { + viewModel = ViewModelProviders.of( + requireActivity(), + BaseViewModelFactory( + getString(R.string.category_123_id), + getString(R.string.category_my_sayings_id) + ) + ).get(EditPhrasesViewModel::class.java) + } + subscribeToViewModel() + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + outState.putString(KEY_USER_INPUT, binding.keyboardInput.text.toString()) + } + + private fun showConfirmationDialog() { + setSettingsButtonsEnabled(false) + binding.editConfirmation.dialogTitle.text = getString(R.string.are_you_sure) + binding.editConfirmation.dialogMessage.text = getString(R.string.back_warning) + binding.editConfirmation.dialogPositiveButton.apply { + text = getString(R.string.contiue_editing) + action = { + toggleDialogVisibility(false) + setSettingsButtonsEnabled(true) + } + } + binding.editConfirmation.dialogNegativeButton.apply { + text = getString(R.string.discard) + action = { + parentFragmentManager.popBackStack() + } + } + toggleDialogVisibility(true) + } + + private fun setSettingsButtonsEnabled(enable: Boolean) { + binding.apply { + backButton.isEnabled = enable + saveButton.isEnabled = enable + keyboardBackspaceButton.isEnabled = enable + keyboardSpaceButton.isEnabled = enable + keyboardClearButton.isEnabled = enable + keyboardInput.isEnabled = enable + keyboardKeyHolder.children.forEach { + it.isEnabled = enable + } + } + } + + private fun toggleDialogVisibility(visible: Boolean) { + binding.editConfirmation.root.isVisible = visible + } + + private fun subscribeToViewModel() { + if (!isCategory) { + viewModel.showPhraseAdded.observe(viewLifecycleOwner, Observer { + binding.phraseSavedView.root.isVisible = it ?: false + }) + } + } + + override fun getAllViews(): List = emptyList() +} \ No newline at end of file diff --git a/app/src/main/java/com/willowtree/vocable/settings/EditPhrasesFragment.kt b/app/src/main/java/com/willowtree/vocable/settings/EditPhrasesFragment.kt new file mode 100644 index 00000000..5e52c8dc --- /dev/null +++ b/app/src/main/java/com/willowtree/vocable/settings/EditPhrasesFragment.kt @@ -0,0 +1,170 @@ +package com.willowtree.vocable.settings + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.GridLayout +import androidx.core.view.children +import androidx.core.view.isInvisible +import androidx.core.view.isVisible +import androidx.core.view.updateMargins +import androidx.lifecycle.ViewModelProviders +import com.willowtree.vocable.BaseFragment +import com.willowtree.vocable.BaseViewModelFactory +import com.willowtree.vocable.BindingInflater +import com.willowtree.vocable.R +import com.willowtree.vocable.customviews.PointerListener +import com.willowtree.vocable.databinding.FragmentEditPhrasesBinding +import com.willowtree.vocable.databinding.PhraseEditLayoutBinding +import com.willowtree.vocable.room.Phrase + +class EditPhrasesFragment : BaseFragment() { + + companion object { + private const val KEY_PHRASES = "KEY_PHRASES" + + fun newInstance(phrases: List): EditPhrasesFragment { + return EditPhrasesFragment().apply { + arguments = Bundle().apply { + putParcelableArrayList(KEY_PHRASES, ArrayList(phrases)) + } + } + } + } + + override val bindingInflater: BindingInflater = FragmentEditPhrasesBinding::inflate + private lateinit var editPhrasesViewModel: EditPhrasesViewModel + private val allViews = mutableListOf() + private var maxPhrases = 1 + private var numColumns = 1 + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + super.onCreateView(inflater, container, savedInstanceState) + maxPhrases = resources.getInteger(R.integer.max_edit_phrases) + numColumns = resources.getInteger(R.integer.edit_phrases_columns) + + val phrases = arguments?.getParcelableArrayList(KEY_PHRASES) + phrases?.forEachIndexed { index, phrase -> + val phraseView = + PhraseEditLayoutBinding.inflate(inflater, binding.editPhrasesContainer, false) + with(phraseView) { + phraseEditText.text = phrase.getLocalizedText() + phraseEditText.tag = phrase + } + with(phraseView.root) { + // Remove end margin on last column + if (index % numColumns == numColumns - 1) { + layoutParams = (layoutParams as GridLayout.LayoutParams).apply { + marginEnd = 0 + } + } + if (index >= maxPhrases - numColumns) { + layoutParams = (layoutParams as GridLayout.LayoutParams).apply { + updateMargins(bottom = 0) + } + } + } + + phraseView.actionButtonContainer.deleteSayingsButton.action = { + showDeletePhraseDialog(phrase) + } + + phraseView.actionButtonContainer.editSayingsButton.action = { + requireActivity().supportFragmentManager + .beginTransaction() + .addToBackStack(null) + .replace( + R.id.settings_fragment_container, + EditKeyboardFragment.newInstance(phrase) + ) + .commit() + } + + binding.editPhrasesContainer.addView(phraseView.root) + } + + phrases?.let { + // Add invisible views to fill out the rest of the space + for (i in 0 until maxPhrases - it.size) { + val hiddenView = + PhraseEditLayoutBinding.inflate(inflater, binding.editPhrasesContainer, false) + binding.editPhrasesContainer.addView(hiddenView.root.apply { + isEnabled = false + isInvisible = true + }) + } + } + + return binding.root + } + + private fun showDeletePhraseDialog(phrase: Phrase) { + setSettingsButtonsEnabled(false) + + binding.deleteConfirmation.apply { + dialogTitle.text = getString(R.string.are_you_sure) + dialogMessage.text = getString(R.string.delete_warning) + dialogPositiveButton.apply { + text = getString(R.string.delete) + action = { + editPhrasesViewModel.deletePhrase(phrase) + toggleDialogVisibility(false) + setSettingsButtonsEnabled(true) + } + } + dialogNegativeButton.apply { + text = getString(R.string.settings_dialog_cancel) + action = { + toggleDialogVisibility(false) + setSettingsButtonsEnabled(true) + } + } + } + + toggleDialogVisibility(true) + } + + private fun setSettingsButtonsEnabled(enable: Boolean) { + editPhrasesViewModel.setEditButtonsEnabled(enable) + + } + + private fun toggleDialogVisibility(visible: Boolean) { + binding.deleteConfirmation.root.isVisible = visible + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + editPhrasesViewModel = + ViewModelProviders.of( + requireActivity(), + BaseViewModelFactory( + getString(R.string.category_123_id), + getString(R.string.category_my_sayings_id) + ) + ).get(EditPhrasesViewModel::class.java) + } + + override fun getAllViews(): List { + if (allViews.isEmpty()) { + getAllChildViews(binding.editPhrasesContainer) + } + return allViews + } + + private fun getAllChildViews(viewGroup: ViewGroup?) { + viewGroup?.children?.forEach { + if (it is PointerListener) { + allViews.add(it) + } else if (it is ViewGroup) { + getAllChildViews(it) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/willowtree/vocable/settings/EditPhrasesViewModel.kt b/app/src/main/java/com/willowtree/vocable/settings/EditPhrasesViewModel.kt new file mode 100644 index 00000000..f3466d59 --- /dev/null +++ b/app/src/main/java/com/willowtree/vocable/settings/EditPhrasesViewModel.kt @@ -0,0 +1,101 @@ +package com.willowtree.vocable.settings + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import com.willowtree.vocable.BaseViewModel +import com.willowtree.vocable.presets.PresetsRepository +import com.willowtree.vocable.room.CategoryPhraseCrossRef +import com.willowtree.vocable.room.Phrase +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import org.koin.core.inject +import java.util.* + +class EditPhrasesViewModel(numbersCategoryId: String, mySayingsCategoryId: String) : + BaseViewModel(numbersCategoryId, mySayingsCategoryId) { + + companion object { + private const val PHRASE_UPDATED_DELAY = 2000L + } + + private val presetsRepository: PresetsRepository by inject() + + private val liveMySayingsList = MutableLiveData>() + val mySayingsList: LiveData> = liveMySayingsList + + private val liveSetButtonsEnabled = MutableLiveData() + val setButtonEnabled: LiveData = liveSetButtonsEnabled + + private val liveShowPhraseAdded = MutableLiveData() + val showPhraseAdded: LiveData = liveShowPhraseAdded + + init { + populateMySayings() + } + + private fun populateMySayings() { + backgroundScope.launch { + + val phrases = + presetsRepository.getPhrasesForCategory(mySayingsCategoryId).sortedBy { it.sortOrder } + + liveMySayingsList.postValue(phrases) + } + } + + fun deletePhrase(phrase: Phrase) { + backgroundScope.launch { + with(presetsRepository) { + deletePhrase(phrase) + val mySayingsCategory = getCategoryById(mySayingsCategoryId) + deleteCrossRef( + CategoryPhraseCrossRef( + mySayingsCategory.categoryId, + phrase.phraseId + ) + ) + } + populateMySayings() + } + } + + fun setEditButtonsEnabled(enabled: Boolean) { + liveSetButtonsEnabled.postValue(enabled) + } + + fun updatePhrase(phrase: Phrase) { + backgroundScope.launch { + presetsRepository.updatePhrase(phrase) + populateMySayings() + + liveShowPhraseAdded.postValue(true) + delay(PHRASE_UPDATED_DELAY) + liveShowPhraseAdded.postValue(false) + } + } + + fun addNewPhrase(phraseStr: String) { + backgroundScope.launch { + val mySayingsPhrases = presetsRepository.getPhrasesForCategory(mySayingsCategoryId) + val phraseId = UUID.randomUUID().toString() + presetsRepository.addPhrase( + Phrase( + phraseId, + System.currentTimeMillis(), + true, + System.currentTimeMillis(), + mapOf(Pair(Locale.getDefault().toString(), phraseStr)), + mySayingsPhrases.size + ) + ) + presetsRepository.addCrossRef(CategoryPhraseCrossRef(mySayingsCategoryId, phraseId)) + + populateMySayings() + + liveShowPhraseAdded.postValue(true) + delay(PHRASE_UPDATED_DELAY) + liveShowPhraseAdded.postValue(false) + } + } + +} diff --git a/app/src/main/java/com/willowtree/vocable/settings/EditPresetsFragment.kt b/app/src/main/java/com/willowtree/vocable/settings/EditPresetsFragment.kt new file mode 100644 index 00000000..8a87936c --- /dev/null +++ b/app/src/main/java/com/willowtree/vocable/settings/EditPresetsFragment.kt @@ -0,0 +1,201 @@ +package com.willowtree.vocable.settings + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.view.children +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentManager +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProviders +import androidx.viewbinding.ViewBinding +import androidx.viewpager2.widget.ViewPager2 +import com.willowtree.vocable.BaseFragment +import com.willowtree.vocable.BaseViewModelFactory +import com.willowtree.vocable.BindingInflater +import com.willowtree.vocable.R +import com.willowtree.vocable.customviews.PointerListener +import com.willowtree.vocable.databinding.FragmentEditPresetsBinding +import com.willowtree.vocable.presets.MySayingsEmptyFragment +import com.willowtree.vocable.room.Phrase +import com.willowtree.vocable.utils.VocableFragmentStateAdapter + +class EditPresetsFragment : BaseFragment() { + + override val bindingInflater: BindingInflater = FragmentEditPresetsBinding::inflate + private var allViews = mutableListOf() + + private var maxPhrases = 1 + + private lateinit var editPhrasesViewModel: EditPhrasesViewModel + private lateinit var phrasesAdapter: EditPhrasesAdapter + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + maxPhrases = resources.getInteger(R.integer.max_edit_phrases) + + binding.backButton.action = { + parentFragmentManager + .beginTransaction() + .replace(R.id.settings_fragment_container, SettingsFragment()) + .commit() + } + + binding.phrasesForwardButton.action = { + when (val currentPosition = binding.editSayingsViewPager.currentItem) { + phrasesAdapter.itemCount - 1 -> { + binding.editSayingsViewPager.setCurrentItem(0, true) + } + else -> { + binding.editSayingsViewPager.setCurrentItem(currentPosition + 1, true) + } + } + } + + binding.phrasesBackButton.action = { + when (val currentPosition = binding.editSayingsViewPager.currentItem) { + 0 -> { + binding.editSayingsViewPager.setCurrentItem( + phrasesAdapter.itemCount - 1, + true + ) + } + else -> { + binding.editSayingsViewPager.setCurrentItem(currentPosition - 1, true) + } + } + } + + phrasesAdapter = EditPhrasesAdapter(childFragmentManager) + + binding.editSayingsViewPager.registerOnPageChangeCallback(object : + ViewPager2.OnPageChangeCallback() { + override fun onPageSelected(position: Int) { + binding.phrasesPageNumber.post { + val pageNum = position % phrasesAdapter.numPages + 1 + binding.phrasesPageNumber.text = getString( + R.string.phrases_page_number, + pageNum, + phrasesAdapter.numPages + ) + } + + activity?.let { activity -> + allViews.clear() + if (activity is SettingsActivity) { + activity.resetAllViews() + } + } + } + }) + + binding.addSayingsButton.action = { + parentFragmentManager + .beginTransaction() + .replace( + R.id.settings_fragment_container, + EditKeyboardFragment.newInstance(false) + ) + .addToBackStack(null) + .commit() + } + + editPhrasesViewModel = + ViewModelProviders.of( + requireActivity(), + BaseViewModelFactory( + getString(R.string.category_123_id), + getString(R.string.category_my_sayings_id) + ) + ).get(EditPhrasesViewModel::class.java) + subscribeToViewModel() + + } + + private fun subscribeToViewModel() { + editPhrasesViewModel.mySayingsList.observe(viewLifecycleOwner, Observer { + it?.let { phrases -> + binding.editSayingsViewPager.apply { + adapter = phrasesAdapter + phrasesAdapter.setItems(phrases) + // Move adapter to middle so user can scroll both directions + val middle = phrasesAdapter.itemCount / 2 + if (phrasesAdapter.numPages == 0) { + phrasesAdapter.numPages = 1 + } + if (middle % phrasesAdapter.numPages == 0) { + setCurrentItem(middle, false) + } else { + val mod = middle % phrasesAdapter.numPages + setCurrentItem( + middle + (phrasesAdapter.numPages - mod), + false + ) + } + } + } + }) + + editPhrasesViewModel.setButtonEnabled.observe(viewLifecycleOwner, Observer { enable -> + binding.apply { + backButton.isEnabled = enable + addSayingsButton.isEnabled = enable + phrasesForwardButton.isEnabled = enable + phrasesBackButton.isEnabled = enable + } + }) + } + + override fun getAllViews(): List { + if (allViews.isEmpty()) { + getAllChildViews(binding.presetsParent) + } + return allViews + } + + private fun getAllChildViews(viewGroup: ViewGroup?) { + viewGroup?.children?.forEach { + if (it is PointerListener) { + allViews.add(it) + } else if (it is ViewGroup) { + getAllChildViews(it) + } + } + } + + inner class EditPhrasesAdapter(fm: FragmentManager) : + VocableFragmentStateAdapter(fm, viewLifecycleOwner.lifecycle) { + + override fun setItems(items: List) { + super.setItems(items) + setPagingButtonsEnabled(numPages > 1) + } + + override fun getMaxItemsPerPage(): Int = maxPhrases + + private fun setPagingButtonsEnabled(enable: Boolean) { + binding.apply { + phrasesForwardButton.isEnabled = enable + phrasesBackButton.isEnabled = enable + editSayingsViewPager.isUserInputEnabled = enable + } + } + + override fun createFragment(position: Int): Fragment { + val startPosition = (position % numPages) * maxPhrases + val sublist = items.subList( + startPosition, + items.size.coerceAtMost(startPosition + maxPhrases) + ) + + return if (items.isEmpty()) { + MySayingsEmptyFragment.newInstance(true) + } else { + EditPhrasesFragment.newInstance(sublist) + } + + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/willowtree/vocable/settings/SelectionModeFragment.kt b/app/src/main/java/com/willowtree/vocable/settings/SelectionModeFragment.kt new file mode 100644 index 00000000..d4c29c78 --- /dev/null +++ b/app/src/main/java/com/willowtree/vocable/settings/SelectionModeFragment.kt @@ -0,0 +1,58 @@ +package com.willowtree.vocable.settings + +import android.os.Bundle +import android.view.View +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProviders +import com.willowtree.vocable.BaseFragment +import com.willowtree.vocable.BaseViewModelFactory +import com.willowtree.vocable.BindingInflater +import com.willowtree.vocable.R +import com.willowtree.vocable.databinding.FragmentSelectionModeBinding + +class SelectionModeFragment : BaseFragment() { + + override val bindingInflater: BindingInflater = FragmentSelectionModeBinding::inflate + private var allViews = mutableListOf() + private lateinit var viewModel: SettingsViewModel + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + binding.selectionModeBackButton.action = { + parentFragmentManager + .beginTransaction() + .replace(R.id.settings_fragment_container, SettingsFragment()) + .commit() + } + + binding.selectionModeOptions.apply { + headTrackingContainer.action = { + headTrackingSwitch.isChecked = !headTrackingSwitch.isChecked + } + } + + binding.selectionModeOptions.headTrackingSwitch.setOnCheckedChangeListener { _, isChecked -> + viewModel.onHeadTrackingChecked(isChecked) + } + + viewModel = ViewModelProviders.of( + requireActivity(), + BaseViewModelFactory( + getString(R.string.category_123_id), + getString(R.string.category_my_sayings_id) + ) + ).get(SettingsViewModel::class.java) + subscribeToViewModel() + } + + private fun subscribeToViewModel() { + viewModel.headTrackingEnabled.observe(viewLifecycleOwner, Observer { + binding.selectionModeOptions.headTrackingSwitch.isChecked = it + }) + } + + override fun getAllViews(): List { + return listOf() + } +} diff --git a/app/src/main/java/com/willowtree/vocable/settings/SensitivityFragment.kt b/app/src/main/java/com/willowtree/vocable/settings/SensitivityFragment.kt new file mode 100644 index 00000000..599ad8af --- /dev/null +++ b/app/src/main/java/com/willowtree/vocable/settings/SensitivityFragment.kt @@ -0,0 +1,152 @@ +package com.willowtree.vocable.settings + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.willowtree.vocable.BaseFragment +import com.willowtree.vocable.BindingInflater +import com.willowtree.vocable.R +import com.willowtree.vocable.databinding.FragmentTimingSensitivityBinding +import com.willowtree.vocable.utils.VocableSharedPreferences +import org.koin.android.ext.android.inject +import java.text.DecimalFormat + +class SensitivityFragment : BaseFragment() { + + companion object { + private const val LOW_SENSITIVITY = 0.05F + const val MEDIUM_SENSITIVITY = 0.1F + private const val HIGH_SENSITIVITY = 0.15F + private const val DWELL_TIME_CHANGE = 500L + const val DWELL_TIME_ONE_SECOND = 1000L + private const val MIN_DWELL_TIME = 500L + private const val MAX_DWELL_TIME = 4000L + } + + override val bindingInflater: BindingInflater = FragmentTimingSensitivityBinding::inflate + + private val sharedPrefs: VocableSharedPreferences by inject() + private var dwellTime: Long = 0 + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + super.onCreateView(inflater, container, savedInstanceState) + dwellTime = sharedPrefs.getDwellTime() + setDwellTimeText() + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + when (sharedPrefs.getSensitivity()) { + LOW_SENSITIVITY -> { + toggleSensitivityButtons(lowActivated = true) + } + + MEDIUM_SENSITIVITY -> { + toggleSensitivityButtons(mediumActivated = true) + } + + HIGH_SENSITIVITY -> { + toggleSensitivityButtons(highActivated = true) + } + } + + binding.timingSensitivityBackButton.action = { + parentFragmentManager + .beginTransaction() + .replace(R.id.settings_fragment_container, SettingsFragment()) + .commit() + } + + binding.decreaseHoverTime.action = { + setDwellTime(false) + } + + binding.increaseHoverTime.action = { + setDwellTime(true) + } + + binding.lowSensitivityButton.action = { + sharedPrefs.setSensitivity(LOW_SENSITIVITY) + toggleSensitivityButtons(lowActivated = true) + } + + binding.mediumSensitivityButton.action = { + sharedPrefs.setSensitivity(MEDIUM_SENSITIVITY) + toggleSensitivityButtons(mediumActivated = true) + } + + binding.highSensitivityButton.action = { + sharedPrefs.setSensitivity(HIGH_SENSITIVITY) + toggleSensitivityButtons(highActivated = true) + } + + } + + override fun getAllViews(): List = emptyList() + + private fun toggleSensitivityButtons( + lowActivated: Boolean = false, + mediumActivated: Boolean = false, + highActivated: Boolean = false + ) { + binding.lowSensitivityButton.apply { + isSelected = lowActivated + isEnabled = !lowActivated + } + + binding.mediumSensitivityButton.apply { + isSelected = mediumActivated + isEnabled = !mediumActivated + } + + binding.highSensitivityButton.apply { + isSelected = highActivated + isEnabled = !highActivated + } + + } + + private fun setDwellTime(increase: Boolean) { + dwellTime = if (increase) { + dwellTime + DWELL_TIME_CHANGE + } else { + dwellTime - DWELL_TIME_CHANGE + } + sharedPrefs.setDwellTime(dwellTime) + setDwellTimeText() + + when { + dwellTime >= MAX_DWELL_TIME -> { + binding.increaseHoverTime.isEnabled = false + } + dwellTime <= MIN_DWELL_TIME -> { + binding.decreaseHoverTime.isEnabled = false + } + else -> { + binding.apply { + increaseHoverTime.isEnabled = true + decreaseHoverTime.isEnabled = true + } + } + } + } + + private fun setDwellTimeText() { + if (dwellTime == DWELL_TIME_ONE_SECOND) { + binding.hoverTimeText.text = getString(R.string.hover_time_one_text) + } else { + val df = DecimalFormat("#.#") + binding.hoverTimeText.text = getString( + R.string.hover_time_amount_text, + df.format(dwellTime.toDouble() / DWELL_TIME_ONE_SECOND) + ) + } + } +} diff --git a/app/src/main/java/com/willowtree/vocable/settings/SettingsActivity.kt b/app/src/main/java/com/willowtree/vocable/settings/SettingsActivity.kt new file mode 100644 index 00000000..d6b61289 --- /dev/null +++ b/app/src/main/java/com/willowtree/vocable/settings/SettingsActivity.kt @@ -0,0 +1,91 @@ +package com.willowtree.vocable.settings + +import android.os.Bundle +import android.view.View +import android.view.ViewGroup +import androidx.core.view.children +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProviders +import com.willowtree.vocable.BaseActivity +import com.willowtree.vocable.BaseViewModelFactory +import com.willowtree.vocable.R +import com.willowtree.vocable.customviews.PointerListener +import com.willowtree.vocable.customviews.PointerView +import com.willowtree.vocable.databinding.ActivitySettingsBinding +import com.willowtree.vocable.facetracking.FaceTrackFragment + +class SettingsActivity : BaseActivity() { + + private lateinit var binding: ActivitySettingsBinding + private val allViews = mutableListOf() + private lateinit var viewModel: SettingsViewModel + + override fun onCreate(savedInstanceState: Bundle?) { + viewModel = ViewModelProviders.of( + this, + BaseViewModelFactory( + getString(R.string.category_123_id), + getString(R.string.category_my_sayings_id) + ) + ).get(SettingsViewModel::class.java) + + super.onCreate(savedInstanceState) + + binding = ActivitySettingsBinding.inflate(layoutInflater) + setContentView(binding.root) + + if (supportFragmentManager.findFragmentById(R.id.settings_fragment_container) == null) { + supportFragmentManager + .beginTransaction() + .replace(R.id.settings_fragment_container, SettingsFragment()) + .commit() + } + + binding.settingsFragmentContainer.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ -> + allViews.clear() + } + } + + override fun subscribeToViewModel() { + super.subscribeToViewModel() + viewModel.headTrackingEnabled.observe(this, Observer { + it?.let { + val faceFragment = supportFragmentManager.findFragmentById(R.id.face_fragment) + if (faceFragment is FaceTrackFragment) { + faceFragment.enableFaceTracking(it) + } + } + }) + } + + override fun getErrorView(): View = binding.errorView.root + + override fun getPointerView(): PointerView = binding.pointerView + + override fun getAllViews(): List { + return when { + allViews.isNotEmpty() -> allViews + else -> { + getAllChildViews(binding.parentLayout) + allViews + } + } + } + + fun resetAllViews() { + allViews.clear() + } + + override fun getLayout(): Int = + R.layout.activity_settings + + private fun getAllChildViews(viewGroup: ViewGroup) { + viewGroup.children.forEach { + if (it is PointerListener) { + allViews.add(it) + } else if (it is ViewGroup) { + getAllChildViews(it) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/willowtree/vocable/settings/SettingsFragment.kt b/app/src/main/java/com/willowtree/vocable/settings/SettingsFragment.kt new file mode 100644 index 00000000..7e176a4d --- /dev/null +++ b/app/src/main/java/com/willowtree/vocable/settings/SettingsFragment.kt @@ -0,0 +1,165 @@ +package com.willowtree.vocable.settings + +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.GridLayout +import androidx.core.view.children +import androidx.core.view.isVisible +import androidx.core.view.updateMargins +import androidx.lifecycle.ViewModelProviders +import com.willowtree.vocable.* +import com.willowtree.vocable.databinding.FragmentSettingsBinding + +class SettingsFragment : BaseFragment() { + + companion object { + private const val PRIVACY_POLICY = "https://vocable.app/privacy.html" + private const val MAIL_TO = + "mailto:vocable@willowtreeapps.com?subject=Feedback for Android Vocable " + private const val SETTINGS_OPTION_COUNT = 5 + } + + override val bindingInflater: BindingInflater = FragmentSettingsBinding::inflate + private lateinit var viewModel: SettingsViewModel + private var numColumns = 1 + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + super.onCreateView(inflater, container, savedInstanceState) + numColumns = resources.getInteger(R.integer.settings_options_columns) + + (binding.settingsOptionsContainer.root as GridLayout).children.forEachIndexed { index, child -> + if (index % numColumns == numColumns - 1) { + child.layoutParams = (child.layoutParams as GridLayout.LayoutParams).apply { + marginEnd = 0 + } + } + if (index > SETTINGS_OPTION_COUNT - numColumns) { + child.layoutParams = (child.layoutParams as GridLayout.LayoutParams).apply { + updateMargins(bottom = 0) + } + } + } + + return binding.root + } + + override fun getAllViews(): List { + return emptyList() + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding.version.text = getString(R.string.version, BuildConfig.VERSION_NAME) + + binding.privacyPolicyButton.action = { + showLeavingAppDialog { + startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(PRIVACY_POLICY))) + } + } + + binding.contactDevsButton.action = { + showLeavingAppDialog { + val sendEmail = Intent(Intent.ACTION_SENDTO).apply { + data = + Uri.parse("$MAIL_TO${BuildConfig.VERSION_NAME}-${BuildConfig.VERSION_CODE}") + } + startActivity(sendEmail) + } + } + + binding.settingsCloseButton.action = { + requireActivity().finish() + } + + binding.settingsOptionsContainer.editSayingsButton.action = { + parentFragmentManager + .beginTransaction() + .replace(R.id.settings_fragment_container, EditPresetsFragment()) + .addToBackStack(null) + .commit() + } + + binding.settingsOptionsContainer.timingSensitivityButton.action = { + parentFragmentManager + .beginTransaction() + .replace(R.id.settings_fragment_container, SensitivityFragment()) + .addToBackStack(null) + .commit() + } + + binding.settingsOptionsContainer.selectionModeButton.action = { + parentFragmentManager + .beginTransaction() + .replace(R.id.settings_fragment_container, SelectionModeFragment()) + .addToBackStack(null) + .commit() + } + + binding.settingsOptionsContainer.editCategoriesButton.action = { + parentFragmentManager + .beginTransaction() + .replace(R.id.settings_fragment_container, EditCategoriesFragment()) + .addToBackStack(null) + .commit() + } + + viewModel = ViewModelProviders.of( + requireActivity(), + BaseViewModelFactory( + getString(R.string.category_123_id), + getString(R.string.category_my_sayings_id) + ) + ).get(SettingsViewModel::class.java) + } + + + private fun showLeavingAppDialog(positiveAction: (() -> Unit)) { + setSettingsButtonsEnabled(false) + binding.settingsConfirmation.apply { + dialogTitle.setText(R.string.settings_dialog_title) + dialogMessage.setText(R.string.settings_dialog_message) + dialogPositiveButton.apply { + setText(R.string.settings_dialog_continue) + action = { + positiveAction.invoke() + toggleDialogVisibility(false) + + setSettingsButtonsEnabled(true) + } + } + dialogNegativeButton.apply { + setText(R.string.settings_dialog_cancel) + action = { + toggleDialogVisibility(false) + setSettingsButtonsEnabled(true) + } + } + } + toggleDialogVisibility(true) + } + + private fun setSettingsButtonsEnabled(enable: Boolean) { + binding.apply { + settingsCloseButton.isEnabled = enable + privacyPolicyButton.isEnabled = enable + contactDevsButton.isEnabled = enable + settingsOptionsContainer.editCategoriesButton.isEnabled = enable + settingsOptionsContainer.editSayingsButton.isEnabled = enable + settingsOptionsContainer.resetAppButton.isEnabled = enable + settingsOptionsContainer.selectionModeButton.isEnabled = enable + settingsOptionsContainer.timingSensitivityButton.isEnabled = enable + } + } + + private fun toggleDialogVisibility(visible: Boolean) { + binding.settingsConfirmation.root.isVisible = visible + } +} \ No newline at end of file diff --git a/app/src/main/java/com/willowtree/vocable/settings/SettingsViewModel.kt b/app/src/main/java/com/willowtree/vocable/settings/SettingsViewModel.kt new file mode 100644 index 00000000..659f286e --- /dev/null +++ b/app/src/main/java/com/willowtree/vocable/settings/SettingsViewModel.kt @@ -0,0 +1,30 @@ +package com.willowtree.vocable.settings + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import com.willowtree.vocable.BaseViewModel +import com.willowtree.vocable.presets.PresetsRepository +import com.willowtree.vocable.utils.VocableSharedPreferences +import kotlinx.coroutines.launch +import org.koin.core.KoinComponent +import org.koin.core.inject + +class SettingsViewModel(numbersCategoryId: String, mySayingsCategoryId: String) : + BaseViewModel(numbersCategoryId, mySayingsCategoryId), KoinComponent { + + private val sharedPrefs: VocableSharedPreferences by inject() + private val presetsRepository: PresetsRepository by inject() + + private val liveHeadTrackingEnabled = MutableLiveData() + val headTrackingEnabled: LiveData = liveHeadTrackingEnabled + + init { + liveHeadTrackingEnabled.postValue(sharedPrefs.getHeadTrackingEnabled()) + } + + fun onHeadTrackingChecked(isChecked: Boolean) { + sharedPrefs.setHeadTrackingEnabled(isChecked) + liveHeadTrackingEnabled.postValue(isChecked) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/willowtree/vocable/splash/SplashActivity.kt b/app/src/main/java/com/willowtree/vocable/splash/SplashActivity.kt new file mode 100644 index 00000000..52b1f042 --- /dev/null +++ b/app/src/main/java/com/willowtree/vocable/splash/SplashActivity.kt @@ -0,0 +1,32 @@ +package com.willowtree.vocable.splash + +import android.content.Intent +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProviders +import com.willowtree.vocable.BaseViewModelFactory +import com.willowtree.vocable.MainActivity +import com.willowtree.vocable.R + +class SplashActivity : AppCompatActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val viewModel = ViewModelProviders.of( + this, + BaseViewModelFactory( + getString(R.string.category_123_id), + getString(R.string.category_my_sayings_id) + ) + ).get(SplashViewModel::class.java) + + viewModel.exitSplash.observe(this, Observer { + if (it) { + startActivity(Intent(this, MainActivity::class.java)) + finish() + } + }) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/willowtree/vocable/splash/SplashViewModel.kt b/app/src/main/java/com/willowtree/vocable/splash/SplashViewModel.kt new file mode 100644 index 00000000..9c18bd30 --- /dev/null +++ b/app/src/main/java/com/willowtree/vocable/splash/SplashViewModel.kt @@ -0,0 +1,28 @@ +package com.willowtree.vocable.splash + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import com.willowtree.vocable.BaseViewModel +import com.willowtree.vocable.presets.PresetsRepository +import kotlinx.coroutines.launch +import org.koin.core.inject + +class SplashViewModel(numbersCategoryId: String, mySayingsCategoryId: String) : + BaseViewModel(numbersCategoryId, mySayingsCategoryId) { + + private val presetsRepository: PresetsRepository by inject() + + private val liveExitSplash = MutableLiveData() + val exitSplash: LiveData = liveExitSplash + + init { + populateDatabase() + } + + private fun populateDatabase() { + backgroundScope.launch { + presetsRepository.populateDatabase(numbersCategoryId, mySayingsCategoryId) + liveExitSplash.postValue(true) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/willowtree/vocable/utils/LocaleUtils.kt b/app/src/main/java/com/willowtree/vocable/utils/LocaleUtils.kt new file mode 100644 index 00000000..476b89cb --- /dev/null +++ b/app/src/main/java/com/willowtree/vocable/utils/LocaleUtils.kt @@ -0,0 +1,59 @@ +package com.willowtree.vocable.utils + +import java.util.* + +object LocaleUtils { + + private const val LOCALE_DELIMITER = "_" + + fun getTextForLocale(localizedPairs: Map): String { + return getLocalizedPair(localizedPairs).first + } + + /** + * Gets a Pair that contains the text to be displayed and/or spoken and the + * closest matching Locale for the text. This method will first try to find the string for the + * device's default locale. Then it will try to find the string that matches + * just the language of the device's default locale. It will then try to default to the English + * language locale. If no English version of the string exists, it will simply return the first + * string in the map and its corresponding locale. + * + * @param localizedPairs The map of language/locale codes and corresponding strings + * @return A Pair representing the closest matching text and locale + */ + fun getLocalizedPair(localizedPairs: Map): Pair { + var locale: Locale? = null + var text: String? = null + getLocaleList().forEach { + if (text != null) return@forEach + locale = it + text = localizedPairs[it.toString()] + } + if (text == null) { + localizedPairs.keys.firstOrNull()?.let { + locale = getLocaleForLanguage(it) + text = localizedPairs[it] + } + } + return Pair(text ?: "", locale ?: Locale.ENGLISH) + } + + private fun getLocaleList(): List { + val defaultLocale = Locale.getDefault() + return mutableListOf().apply { + // First the device's default locale + add(defaultLocale) + // Then just the language locale + add(Locale(defaultLocale.language)) + // Default to English + add(Locale.ENGLISH) + } + } + + private fun getLocaleForLanguage(language: String): Locale { + val split = language.split(LOCALE_DELIMITER) + val languageStr = split.first() + val countryStr = split.getOrNull(1) ?: "" + return Locale(languageStr, countryStr) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/willowtree/vocable/utils/SpokenText.kt b/app/src/main/java/com/willowtree/vocable/utils/SpokenText.kt new file mode 100644 index 00000000..fb6cad95 --- /dev/null +++ b/app/src/main/java/com/willowtree/vocable/utils/SpokenText.kt @@ -0,0 +1,8 @@ +package com.willowtree.vocable.utils + +import androidx.lifecycle.MutableLiveData + +/** + * A LiveData object that represents the text that was most recently spoken aloud by TTS + */ +object SpokenText : MutableLiveData() \ No newline at end of file diff --git a/app/src/main/java/com/willowtree/vocable/utils/VocableFragmentStateAdapter.kt b/app/src/main/java/com/willowtree/vocable/utils/VocableFragmentStateAdapter.kt new file mode 100644 index 00000000..73c6ee2c --- /dev/null +++ b/app/src/main/java/com/willowtree/vocable/utils/VocableFragmentStateAdapter.kt @@ -0,0 +1,74 @@ +package com.willowtree.vocable.utils + +import androidx.annotation.CallSuper +import androidx.fragment.app.FragmentManager +import androidx.lifecycle.Lifecycle +import androidx.viewpager2.adapter.FragmentStateAdapter +import kotlin.math.ceil +import kotlin.math.min + +/** + * A custom implementation of FragmentStateAdapter to solve an issue with fragments not being + * properly updated when new data was added. If the default implementations of getItemId() and + * containsItem() are left unchanged, the adapter will not update any previously created fragments + * with new data. This custom class solves this issue by giving unique ids to each fragment and + * updating the ids every time the data set is changed with setItems(). + */ +abstract class VocableFragmentStateAdapter(fm: FragmentManager, lifecycle: Lifecycle) : + FragmentStateAdapter(fm, lifecycle) { + + protected val items = mutableListOf() + private var baseId = 0L + var numPages = 0 + + override fun getItemCount(): Int { + return if (items.isEmpty() || getMaxItemsPerPage() >= items.size) { + 1 + } else { + Int.MAX_VALUE + } + } + + @CallSuper + open fun setItems(items: List) { + baseId = System.currentTimeMillis() + with(this.items) { + clear() + addAll(items) + } + + numPages = if (items.isEmpty() || getMaxItemsPerPage() >= items.size) { + 1 + } else { + ceil(items.size / getMaxItemsPerPage().toDouble()).toInt() + } + + notifyDataSetChanged() + } + + override fun getItemId(position: Int): Long { + return baseId - position + } + + override fun containsItem(itemId: Long): Boolean { + val position = baseId - itemId + return position in 0 until itemCount + } + + /** + * Returns the list of items held by the page at the provided position. + */ + fun getItemsByPosition(position: Int): List { + val pageItems = mutableListOf() + val startPosition = (position % numPages) * getMaxItemsPerPage() + + pageItems.addAll(items.subList( + startPosition, + min(items.size, startPosition + getMaxItemsPerPage()) + )) + + return pageItems + } + + abstract fun getMaxItemsPerPage(): Int +} \ No newline at end of file diff --git a/app/src/main/java/com/willowtree/vocable/utils/VocableSharedPreferences.kt b/app/src/main/java/com/willowtree/vocable/utils/VocableSharedPreferences.kt new file mode 100644 index 00000000..2673ba32 --- /dev/null +++ b/app/src/main/java/com/willowtree/vocable/utils/VocableSharedPreferences.kt @@ -0,0 +1,76 @@ +package com.willowtree.vocable.utils + +import android.content.Context +import android.content.SharedPreferences +import androidx.security.crypto.EncryptedSharedPreferences +import androidx.security.crypto.MasterKeys +import com.willowtree.vocable.settings.SensitivityFragment +import org.koin.core.KoinComponent +import org.koin.core.get + +class VocableSharedPreferences : KoinComponent { + + companion object { + private const val PREFERENCES_NAME = + "com.willowtree.vocable.utils.vocable-encrypted-preferences" + private const val KEY_MY_SAYINGS = "KEY_MY_SAYINGS" + const val KEY_HEAD_TRACKING_ENABLED = "KEY_HEAD_TRACKING_ENABLED" + const val KEY_SENSITIVITY = "KEY_SENSITIVITY" + const val DEFAULT_SENSITIVITY = SensitivityFragment.MEDIUM_SENSITIVITY + const val KEY_DWELL_TIME = "KEY_DWELL_TIME" + const val DEFAULT_DWELL_TIME = SensitivityFragment.DWELL_TIME_ONE_SECOND + } + + private val encryptedPrefs: EncryptedSharedPreferences by lazy { + val context = get() + EncryptedSharedPreferences.create( + PREFERENCES_NAME, + MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC), + context, + EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, + EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM + ) as EncryptedSharedPreferences + } + + fun registerOnSharedPreferenceChangeListener(vararg listeners: SharedPreferences.OnSharedPreferenceChangeListener) { + listeners.forEach { + encryptedPrefs.registerOnSharedPreferenceChangeListener(it) + } + } + + fun unregisterOnSharedPreferenceChangeListener(vararg listeners: SharedPreferences.OnSharedPreferenceChangeListener) { + listeners.forEach { + encryptedPrefs.unregisterOnSharedPreferenceChangeListener(it) + } + } + + fun getMySayings(): List { + encryptedPrefs.getStringSet(KEY_MY_SAYINGS, setOf())?.let { + return it.toList() + } + return listOf() + } + + fun setMySayings(mySayings: Set) { + encryptedPrefs.edit().putStringSet(KEY_MY_SAYINGS, mySayings).apply() + } + + fun getDwellTime(): Long = encryptedPrefs.getLong(KEY_DWELL_TIME, DEFAULT_DWELL_TIME) + + fun setDwellTime(time: Long) { + encryptedPrefs.edit().putLong(KEY_DWELL_TIME, time).apply() + } + + fun getSensitivity(): Float = encryptedPrefs.getFloat(KEY_SENSITIVITY, DEFAULT_SENSITIVITY) + + fun setSensitivity(sensitivity: Float) { + encryptedPrefs.edit().putFloat(KEY_SENSITIVITY, sensitivity).apply() + } + + fun setHeadTrackingEnabled(enabled: Boolean) { + encryptedPrefs.edit().putBoolean(KEY_HEAD_TRACKING_ENABLED, enabled).apply() + } + + fun getHeadTrackingEnabled(): Boolean = + encryptedPrefs.getBoolean(KEY_HEAD_TRACKING_ENABLED, true) +} \ No newline at end of file diff --git a/app/src/main/java/com/willowtree/vocable/utils/VocableTextToSpeech.kt b/app/src/main/java/com/willowtree/vocable/utils/VocableTextToSpeech.kt new file mode 100644 index 00000000..916edb68 --- /dev/null +++ b/app/src/main/java/com/willowtree/vocable/utils/VocableTextToSpeech.kt @@ -0,0 +1,53 @@ +package com.willowtree.vocable.utils + +import android.content.Context +import android.speech.tts.TextToSpeech +import android.speech.tts.UtteranceProgressListener +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import java.util.* + +object VocableTextToSpeech { + + private var textToSpeech: TextToSpeech? = null + + private val liveIsSpeaking = MutableLiveData() + val isSpeaking: LiveData = liveIsSpeaking + + fun initialize(context: Context) { + if (textToSpeech == null) { + textToSpeech = TextToSpeech(context, TextToSpeech.OnInitListener { + // No-op + }).apply { + setOnUtteranceProgressListener(object : UtteranceProgressListener() { + override fun onDone(utteranceId: String?) { + liveIsSpeaking.postValue(false) + } + + override fun onError(utteranceId: String?) { + liveIsSpeaking.postValue(false) + } + + override fun onStart(utteranceId: String?) { + liveIsSpeaking.postValue(true) + } + }) + } + } + } + + fun shutdown() { + textToSpeech?.let { + it.stop() + it.shutdown() + } + textToSpeech = null + } + + fun speak(locale: Locale, text: String) { + textToSpeech?.let { + it.language = locale + it.speak(text, TextToSpeech.QUEUE_FLUSH, null, text) + } + } +} \ No newline at end of file diff --git a/app/src/main/res/color/category_button_text_colors.xml b/app/src/main/res/color/category_button_text_colors.xml new file mode 100644 index 00000000..f0f1a86c --- /dev/null +++ b/app/src/main/res/color/category_button_text_colors.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/color/vocable_button_text_color.xml b/app/src/main/res/color/vocable_button_text_color.xml new file mode 100644 index 00000000..3714e452 --- /dev/null +++ b/app/src/main/res/color/vocable_button_text_color.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-sw600dp/ic_heart_solid_blue.xml b/app/src/main/res/drawable-sw600dp/ic_heart_solid_blue.xml new file mode 100644 index 00000000..bbc650f3 --- /dev/null +++ b/app/src/main/res/drawable-sw600dp/ic_heart_solid_blue.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml deleted file mode 100644 index 6348baae..00000000 --- a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - - - diff --git a/app/src/main/res/drawable/add_40dp.xml b/app/src/main/res/drawable/add_40dp.xml new file mode 100644 index 00000000..08d2094c --- /dev/null +++ b/app/src/main/res/drawable/add_40dp.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/add_favorite_action_button_icon.xml b/app/src/main/res/drawable/add_favorite_action_button_icon.xml new file mode 100644 index 00000000..64daa857 --- /dev/null +++ b/app/src/main/res/drawable/add_favorite_action_button_icon.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/arrow_back_40dp.xml b/app/src/main/res/drawable/arrow_back_40dp.xml new file mode 100644 index 00000000..0d35d9bd --- /dev/null +++ b/app/src/main/res/drawable/arrow_back_40dp.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/arrow_down_40dp.xml b/app/src/main/res/drawable/arrow_down_40dp.xml new file mode 100644 index 00000000..4f3a1ed8 --- /dev/null +++ b/app/src/main/res/drawable/arrow_down_40dp.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/arrow_right_32dp.xml b/app/src/main/res/drawable/arrow_right_32dp.xml new file mode 100644 index 00000000..8ac6b4b5 --- /dev/null +++ b/app/src/main/res/drawable/arrow_right_32dp.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/arrow_up_40dp.xml b/app/src/main/res/drawable/arrow_up_40dp.xml new file mode 100644 index 00000000..71b04909 --- /dev/null +++ b/app/src/main/res/drawable/arrow_up_40dp.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/button_background.xml b/app/src/main/res/drawable/button_background.xml new file mode 100644 index 00000000..988148c6 --- /dev/null +++ b/app/src/main/res/drawable/button_background.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/button_background_disabled.xml b/app/src/main/res/drawable/button_background_disabled.xml new file mode 100644 index 00000000..cb434c8a --- /dev/null +++ b/app/src/main/res/drawable/button_background_disabled.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/button_category_highlighted_background.xml b/app/src/main/res/drawable/button_category_highlighted_background.xml new file mode 100644 index 00000000..8f3670e3 --- /dev/null +++ b/app/src/main/res/drawable/button_category_highlighted_background.xml @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/button_default_background.xml b/app/src/main/res/drawable/button_default_background.xml new file mode 100644 index 00000000..aa52819c --- /dev/null +++ b/app/src/main/res/drawable/button_default_background.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/button_highlighted_background.xml b/app/src/main/res/drawable/button_highlighted_background.xml new file mode 100644 index 00000000..b2cb82bd --- /dev/null +++ b/app/src/main/res/drawable/button_highlighted_background.xml @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/button_selected_background.xml b/app/src/main/res/drawable/button_selected_background.xml new file mode 100644 index 00000000..8b968e49 --- /dev/null +++ b/app/src/main/res/drawable/button_selected_background.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/button_speaker.xml b/app/src/main/res/drawable/button_speaker.xml new file mode 100644 index 00000000..198b54aa --- /dev/null +++ b/app/src/main/res/drawable/button_speaker.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/button_speaker_background.xml b/app/src/main/res/drawable/button_speaker_background.xml new file mode 100644 index 00000000..d6afb6fd --- /dev/null +++ b/app/src/main/res/drawable/button_speaker_background.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/button_speaker_highlighted_background.xml b/app/src/main/res/drawable/button_speaker_highlighted_background.xml new file mode 100644 index 00000000..f9d22edb --- /dev/null +++ b/app/src/main/res/drawable/button_speaker_highlighted_background.xml @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/category_back_button_icon.xml b/app/src/main/res/drawable/category_back_button_icon.xml new file mode 100644 index 00000000..0dc5c7ba --- /dev/null +++ b/app/src/main/res/drawable/category_back_button_icon.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/category_button_background.xml b/app/src/main/res/drawable/category_button_background.xml new file mode 100644 index 00000000..988e7751 --- /dev/null +++ b/app/src/main/res/drawable/category_button_background.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/category_forward_button_icon.xml b/app/src/main/res/drawable/category_forward_button_icon.xml new file mode 100644 index 00000000..820814d6 --- /dev/null +++ b/app/src/main/res/drawable/category_forward_button_icon.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/category_group_background.xml b/app/src/main/res/drawable/category_group_background.xml new file mode 100644 index 00000000..57c94fb3 --- /dev/null +++ b/app/src/main/res/drawable/category_group_background.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/checkmark.xml b/app/src/main/res/drawable/checkmark.xml new file mode 100644 index 00000000..963cf9b7 --- /dev/null +++ b/app/src/main/res/drawable/checkmark.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/close_action_button_icon.xml b/app/src/main/res/drawable/close_action_button_icon.xml new file mode 100644 index 00000000..ac87074e --- /dev/null +++ b/app/src/main/res/drawable/close_action_button_icon.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/decrease_40dp.xml b/app/src/main/res/drawable/decrease_40dp.xml new file mode 100644 index 00000000..a5275fef --- /dev/null +++ b/app/src/main/res/drawable/decrease_40dp.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/edit_40dp.xml b/app/src/main/res/drawable/edit_40dp.xml new file mode 100644 index 00000000..c6c06ff5 --- /dev/null +++ b/app/src/main/res/drawable/edit_40dp.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/error_background.xml b/app/src/main/res/drawable/error_background.xml new file mode 100644 index 00000000..903aa4e2 --- /dev/null +++ b/app/src/main/res/drawable/error_background.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_add_40dp.xml b/app/src/main/res/drawable/ic_add_40dp.xml new file mode 100644 index 00000000..6f9a868f --- /dev/null +++ b/app/src/main/res/drawable/ic_add_40dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_add_dark_blue_40dp.xml b/app/src/main/res/drawable/ic_add_dark_blue_40dp.xml new file mode 100644 index 00000000..d7e7f849 --- /dev/null +++ b/app/src/main/res/drawable/ic_add_dark_blue_40dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_add_phrase.xml b/app/src/main/res/drawable/ic_add_phrase.xml new file mode 100644 index 00000000..4b76703b --- /dev/null +++ b/app/src/main/res/drawable/ic_add_phrase.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_arrow_back_40dp.xml b/app/src/main/res/drawable/ic_arrow_back_40dp.xml new file mode 100644 index 00000000..52b24800 --- /dev/null +++ b/app/src/main/res/drawable/ic_arrow_back_40dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_arrow_back_blue.xml b/app/src/main/res/drawable/ic_arrow_back_blue.xml new file mode 100644 index 00000000..63e24c64 --- /dev/null +++ b/app/src/main/res/drawable/ic_arrow_back_blue.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_arrow_back_dark_blue.xml b/app/src/main/res/drawable/ic_arrow_back_dark_blue.xml new file mode 100644 index 00000000..10bf96c7 --- /dev/null +++ b/app/src/main/res/drawable/ic_arrow_back_dark_blue.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_arrow_back_dark_blue_40dp.xml b/app/src/main/res/drawable/ic_arrow_back_dark_blue_40dp.xml new file mode 100644 index 00000000..5cb8b1b9 --- /dev/null +++ b/app/src/main/res/drawable/ic_arrow_back_dark_blue_40dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_arrow_down_40dp.xml b/app/src/main/res/drawable/ic_arrow_down_40dp.xml new file mode 100644 index 00000000..5d78ae5d --- /dev/null +++ b/app/src/main/res/drawable/ic_arrow_down_40dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_arrow_down_dark_40dp.xml b/app/src/main/res/drawable/ic_arrow_down_dark_40dp.xml new file mode 100644 index 00000000..2298da0b --- /dev/null +++ b/app/src/main/res/drawable/ic_arrow_down_dark_40dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_arrow_down_disabled_40dp.xml b/app/src/main/res/drawable/ic_arrow_down_disabled_40dp.xml new file mode 100644 index 00000000..3daa4615 --- /dev/null +++ b/app/src/main/res/drawable/ic_arrow_down_disabled_40dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_arrow_forward_blue.xml b/app/src/main/res/drawable/ic_arrow_forward_blue.xml new file mode 100644 index 00000000..3b75c7a4 --- /dev/null +++ b/app/src/main/res/drawable/ic_arrow_forward_blue.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_arrow_forward_dark_blue.xml b/app/src/main/res/drawable/ic_arrow_forward_dark_blue.xml new file mode 100644 index 00000000..a30fb8a6 --- /dev/null +++ b/app/src/main/res/drawable/ic_arrow_forward_dark_blue.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_arrow_right_32dp.xml b/app/src/main/res/drawable/ic_arrow_right_32dp.xml new file mode 100644 index 00000000..6ecba333 --- /dev/null +++ b/app/src/main/res/drawable/ic_arrow_right_32dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_arrow_right_40dp.xml b/app/src/main/res/drawable/ic_arrow_right_40dp.xml new file mode 100644 index 00000000..a305c66d --- /dev/null +++ b/app/src/main/res/drawable/ic_arrow_right_40dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_arrow_right_dark_40dp.xml b/app/src/main/res/drawable/ic_arrow_right_dark_40dp.xml new file mode 100644 index 00000000..f6adfeac --- /dev/null +++ b/app/src/main/res/drawable/ic_arrow_right_dark_40dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_arrow_right_dark_blue_32dp.xml b/app/src/main/res/drawable/ic_arrow_right_dark_blue_32dp.xml new file mode 100644 index 00000000..dbfa97ec --- /dev/null +++ b/app/src/main/res/drawable/ic_arrow_right_dark_blue_32dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_arrow_right_disabled_32dp.xml b/app/src/main/res/drawable/ic_arrow_right_disabled_32dp.xml new file mode 100644 index 00000000..0ff9a747 --- /dev/null +++ b/app/src/main/res/drawable/ic_arrow_right_disabled_32dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_arrow_right_disabled_40dp.xml b/app/src/main/res/drawable/ic_arrow_right_disabled_40dp.xml new file mode 100644 index 00000000..8d99a714 --- /dev/null +++ b/app/src/main/res/drawable/ic_arrow_right_disabled_40dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_arrow_up_40dp.xml b/app/src/main/res/drawable/ic_arrow_up_40dp.xml new file mode 100644 index 00000000..136e3666 --- /dev/null +++ b/app/src/main/res/drawable/ic_arrow_up_40dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_arrow_up_dark_40dp.xml b/app/src/main/res/drawable/ic_arrow_up_dark_40dp.xml new file mode 100644 index 00000000..8fd9b1db --- /dev/null +++ b/app/src/main/res/drawable/ic_arrow_up_dark_40dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_arrow_up_disabled_40dp.xml b/app/src/main/res/drawable/ic_arrow_up_disabled_40dp.xml new file mode 100644 index 00000000..d5ba61be --- /dev/null +++ b/app/src/main/res/drawable/ic_arrow_up_disabled_40dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_backspace.xml b/app/src/main/res/drawable/ic_backspace.xml new file mode 100644 index 00000000..e313f8bc --- /dev/null +++ b/app/src/main/res/drawable/ic_backspace.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_backspace_dark_blue.xml b/app/src/main/res/drawable/ic_backspace_dark_blue.xml new file mode 100644 index 00000000..9a1849fa --- /dev/null +++ b/app/src/main/res/drawable/ic_backspace_dark_blue.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_check_40dp.xml b/app/src/main/res/drawable/ic_check_40dp.xml new file mode 100644 index 00000000..fed2cb27 --- /dev/null +++ b/app/src/main/res/drawable/ic_check_40dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_check_dark_40dp.xml b/app/src/main/res/drawable/ic_check_dark_40dp.xml new file mode 100644 index 00000000..ca5387ba --- /dev/null +++ b/app/src/main/res/drawable/ic_check_dark_40dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_check_disabled_40dp.xml b/app/src/main/res/drawable/ic_check_disabled_40dp.xml new file mode 100644 index 00000000..b4a10ce2 --- /dev/null +++ b/app/src/main/res/drawable/ic_check_disabled_40dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_close.xml b/app/src/main/res/drawable/ic_close.xml new file mode 100644 index 00000000..c3a1d2ec --- /dev/null +++ b/app/src/main/res/drawable/ic_close.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_close_dark_blue.xml b/app/src/main/res/drawable/ic_close_dark_blue.xml new file mode 100644 index 00000000..d4e141b5 --- /dev/null +++ b/app/src/main/res/drawable/ic_close_dark_blue.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_decrease_40dp.xml b/app/src/main/res/drawable/ic_decrease_40dp.xml new file mode 100644 index 00000000..6ca0f329 --- /dev/null +++ b/app/src/main/res/drawable/ic_decrease_40dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_decrease_dark_blue_40dp.xml b/app/src/main/res/drawable/ic_decrease_dark_blue_40dp.xml new file mode 100644 index 00000000..df7de1e7 --- /dev/null +++ b/app/src/main/res/drawable/ic_decrease_dark_blue_40dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_delete.xml b/app/src/main/res/drawable/ic_delete.xml new file mode 100644 index 00000000..d578beca --- /dev/null +++ b/app/src/main/res/drawable/ic_delete.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_delete_56dp.xml b/app/src/main/res/drawable/ic_delete_56dp.xml new file mode 100644 index 00000000..7a96ddda --- /dev/null +++ b/app/src/main/res/drawable/ic_delete_56dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_delete_dark_blue.xml b/app/src/main/res/drawable/ic_delete_dark_blue.xml new file mode 100644 index 00000000..9e93372f --- /dev/null +++ b/app/src/main/res/drawable/ic_delete_dark_blue.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_edit_40dp.xml b/app/src/main/res/drawable/ic_edit_40dp.xml new file mode 100644 index 00000000..6ef2ee8e --- /dev/null +++ b/app/src/main/res/drawable/ic_edit_40dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_edit_dark_blue_40dp.xml b/app/src/main/res/drawable/ic_edit_dark_blue_40dp.xml new file mode 100644 index 00000000..7b6ea763 --- /dev/null +++ b/app/src/main/res/drawable/ic_edit_dark_blue_40dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_error.xml b/app/src/main/res/drawable/ic_error.xml new file mode 100644 index 00000000..e22d1025 --- /dev/null +++ b/app/src/main/res/drawable/ic_error.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_heart_border_blue.xml b/app/src/main/res/drawable/ic_heart_border_blue.xml new file mode 100644 index 00000000..7c37710b --- /dev/null +++ b/app/src/main/res/drawable/ic_heart_border_blue.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_heart_solid_blue.xml b/app/src/main/res/drawable/ic_heart_solid_blue.xml new file mode 100644 index 00000000..35b3f26e --- /dev/null +++ b/app/src/main/res/drawable/ic_heart_solid_blue.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_heart_solid_dark_blue.xml b/app/src/main/res/drawable/ic_heart_solid_dark_blue.xml new file mode 100644 index 00000000..e18153e8 --- /dev/null +++ b/app/src/main/res/drawable/ic_heart_solid_dark_blue.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_keyboard.xml b/app/src/main/res/drawable/ic_keyboard.xml new file mode 100644 index 00000000..49b8dd47 --- /dev/null +++ b/app/src/main/res/drawable/ic_keyboard.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_keyboard_arrow_right_dark_40dp.xml b/app/src/main/res/drawable/ic_keyboard_arrow_right_dark_40dp.xml new file mode 100644 index 00000000..f6adfeac --- /dev/null +++ b/app/src/main/res/drawable/ic_keyboard_arrow_right_dark_40dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_keyboard_dark_blue.xml b/app/src/main/res/drawable/ic_keyboard_dark_blue.xml new file mode 100644 index 00000000..c460a97f --- /dev/null +++ b/app/src/main/res/drawable/ic_keyboard_dark_blue.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_launch_32dp.xml b/app/src/main/res/drawable/ic_launch_32dp.xml new file mode 100644 index 00000000..ae7a5f85 --- /dev/null +++ b/app/src/main/res/drawable/ic_launch_32dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_launch_dark_blue_32dp.xml b/app/src/main/res/drawable/ic_launch_dark_blue_32dp.xml new file mode 100644 index 00000000..a693432f --- /dev/null +++ b/app/src/main/res/drawable/ic_launch_dark_blue_32dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_launch_disabled_32dp.xml b/app/src/main/res/drawable/ic_launch_disabled_32dp.xml new file mode 100644 index 00000000..64261003 --- /dev/null +++ b/app/src/main/res/drawable/ic_launch_disabled_32dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml index a0ad202f..4063bb12 100644 --- a/app/src/main/res/drawable/ic_launcher_background.xml +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -1,74 +1,9 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 00000000..34289a62 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_pause.xml b/app/src/main/res/drawable/ic_pause.xml new file mode 100644 index 00000000..9859d891 --- /dev/null +++ b/app/src/main/res/drawable/ic_pause.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_phrases_arrow_back_blue.xml b/app/src/main/res/drawable/ic_phrases_arrow_back_blue.xml new file mode 100644 index 00000000..dac873ed --- /dev/null +++ b/app/src/main/res/drawable/ic_phrases_arrow_back_blue.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_phrases_arrow_back_dark_blue.xml b/app/src/main/res/drawable/ic_phrases_arrow_back_dark_blue.xml new file mode 100644 index 00000000..897cd77f --- /dev/null +++ b/app/src/main/res/drawable/ic_phrases_arrow_back_dark_blue.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_phrases_arrow_back_disabled.xml b/app/src/main/res/drawable/ic_phrases_arrow_back_disabled.xml new file mode 100644 index 00000000..3bdf77c4 --- /dev/null +++ b/app/src/main/res/drawable/ic_phrases_arrow_back_disabled.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_phrases_arrow_forward_blue.xml b/app/src/main/res/drawable/ic_phrases_arrow_forward_blue.xml new file mode 100644 index 00000000..a305c66d --- /dev/null +++ b/app/src/main/res/drawable/ic_phrases_arrow_forward_blue.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_phrases_arrow_forward_dark_blue.xml b/app/src/main/res/drawable/ic_phrases_arrow_forward_dark_blue.xml new file mode 100644 index 00000000..f6adfeac --- /dev/null +++ b/app/src/main/res/drawable/ic_phrases_arrow_forward_dark_blue.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_phrases_arrow_forward_disabled.xml b/app/src/main/res/drawable/ic_phrases_arrow_forward_disabled.xml new file mode 100644 index 00000000..3bb9cad1 --- /dev/null +++ b/app/src/main/res/drawable/ic_phrases_arrow_forward_disabled.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_presets.xml b/app/src/main/res/drawable/ic_presets.xml new file mode 100644 index 00000000..96ad2f20 --- /dev/null +++ b/app/src/main/res/drawable/ic_presets.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_presets_dark_blue.xml b/app/src/main/res/drawable/ic_presets_dark_blue.xml new file mode 100644 index 00000000..adec87e9 --- /dev/null +++ b/app/src/main/res/drawable/ic_presets_dark_blue.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_repeat.xml b/app/src/main/res/drawable/ic_repeat.xml new file mode 100644 index 00000000..05e4cc19 --- /dev/null +++ b/app/src/main/res/drawable/ic_repeat.xml @@ -0,0 +1,6 @@ + + + + diff --git a/app/src/main/res/drawable/ic_resume.xml b/app/src/main/res/drawable/ic_resume.xml new file mode 100644 index 00000000..4bfe5d71 --- /dev/null +++ b/app/src/main/res/drawable/ic_resume.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_save_40dp.xml b/app/src/main/res/drawable/ic_save_40dp.xml new file mode 100644 index 00000000..b76d8722 --- /dev/null +++ b/app/src/main/res/drawable/ic_save_40dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_save_dark_blue_40dp.xml b/app/src/main/res/drawable/ic_save_dark_blue_40dp.xml new file mode 100644 index 00000000..f41876bd --- /dev/null +++ b/app/src/main/res/drawable/ic_save_dark_blue_40dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_settings_dark_blue.xml b/app/src/main/res/drawable/ic_settings_dark_blue.xml new file mode 100644 index 00000000..472447ae --- /dev/null +++ b/app/src/main/res/drawable/ic_settings_dark_blue.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_settings_light_48dp.xml b/app/src/main/res/drawable/ic_settings_light_48dp.xml new file mode 100644 index 00000000..afbb10ab --- /dev/null +++ b/app/src/main/res/drawable/ic_settings_light_48dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_space_bar_56dp.xml b/app/src/main/res/drawable/ic_space_bar_56dp.xml new file mode 100644 index 00000000..ae2383a4 --- /dev/null +++ b/app/src/main/res/drawable/ic_space_bar_56dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_space_dark_blue.xml b/app/src/main/res/drawable/ic_space_dark_blue.xml new file mode 100644 index 00000000..190b5061 --- /dev/null +++ b/app/src/main/res/drawable/ic_space_dark_blue.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_speak_40dp.xml b/app/src/main/res/drawable/ic_speak_40dp.xml new file mode 100644 index 00000000..05e4cc19 --- /dev/null +++ b/app/src/main/res/drawable/ic_speak_40dp.xml @@ -0,0 +1,6 @@ + + + + diff --git a/app/src/main/res/drawable/ic_speak_dark_blue.xml b/app/src/main/res/drawable/ic_speak_dark_blue.xml new file mode 100644 index 00000000..60ebd0a9 --- /dev/null +++ b/app/src/main/res/drawable/ic_speak_dark_blue.xml @@ -0,0 +1,6 @@ + + + + diff --git a/app/src/main/res/drawable/ic_speaker.xml b/app/src/main/res/drawable/ic_speaker.xml new file mode 100644 index 00000000..4eb8ccdd --- /dev/null +++ b/app/src/main/res/drawable/ic_speaker.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_speaker_blue.xml b/app/src/main/res/drawable/ic_speaker_blue.xml new file mode 100644 index 00000000..33b39bbc --- /dev/null +++ b/app/src/main/res/drawable/ic_speaker_blue.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_star_blue_56dp.xml b/app/src/main/res/drawable/ic_star_blue_56dp.xml new file mode 100644 index 00000000..3f33bc7d --- /dev/null +++ b/app/src/main/res/drawable/ic_star_blue_56dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_star_border_40dp.xml b/app/src/main/res/drawable/ic_star_border_40dp.xml new file mode 100644 index 00000000..7808172d --- /dev/null +++ b/app/src/main/res/drawable/ic_star_border_40dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_star_dark_40dp.xml b/app/src/main/res/drawable/ic_star_dark_40dp.xml new file mode 100644 index 00000000..76e5e168 --- /dev/null +++ b/app/src/main/res/drawable/ic_star_dark_40dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_undo.xml b/app/src/main/res/drawable/ic_undo.xml new file mode 100644 index 00000000..d228d3bd --- /dev/null +++ b/app/src/main/res/drawable/ic_undo.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_visibility_off_40dp.xml b/app/src/main/res/drawable/ic_visibility_off_40dp.xml new file mode 100644 index 00000000..1bd7eecd --- /dev/null +++ b/app/src/main/res/drawable/ic_visibility_off_40dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/keyboard_action_button_icon.xml b/app/src/main/res/drawable/keyboard_action_button_icon.xml new file mode 100644 index 00000000..e3d646c2 --- /dev/null +++ b/app/src/main/res/drawable/keyboard_action_button_icon.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/keyboard_backspace_button_icon.xml b/app/src/main/res/drawable/keyboard_backspace_button_icon.xml new file mode 100644 index 00000000..7786c604 --- /dev/null +++ b/app/src/main/res/drawable/keyboard_backspace_button_icon.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/keyboard_delete_button_icon.xml b/app/src/main/res/drawable/keyboard_delete_button_icon.xml new file mode 100644 index 00000000..d8e03c71 --- /dev/null +++ b/app/src/main/res/drawable/keyboard_delete_button_icon.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/keyboard_space_button_icon.xml b/app/src/main/res/drawable/keyboard_space_button_icon.xml new file mode 100644 index 00000000..b4cfda7f --- /dev/null +++ b/app/src/main/res/drawable/keyboard_space_button_icon.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/keyboard_speak_button_icon.xml b/app/src/main/res/drawable/keyboard_speak_button_icon.xml new file mode 100644 index 00000000..3091abc5 --- /dev/null +++ b/app/src/main/res/drawable/keyboard_speak_button_icon.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/launch_32dp.xml b/app/src/main/res/drawable/launch_32dp.xml new file mode 100644 index 00000000..e8918b09 --- /dev/null +++ b/app/src/main/res/drawable/launch_32dp.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/phrases_back_button_icon.xml b/app/src/main/res/drawable/phrases_back_button_icon.xml new file mode 100644 index 00000000..02a30e14 --- /dev/null +++ b/app/src/main/res/drawable/phrases_back_button_icon.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/phrases_forward_button_icon.xml b/app/src/main/res/drawable/phrases_forward_button_icon.xml new file mode 100644 index 00000000..664c8bc9 --- /dev/null +++ b/app/src/main/res/drawable/phrases_forward_button_icon.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/pointer_background.xml b/app/src/main/res/drawable/pointer_background.xml new file mode 100644 index 00000000..f865f248 --- /dev/null +++ b/app/src/main/res/drawable/pointer_background.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/presets_action_button_icon.xml b/app/src/main/res/drawable/presets_action_button_icon.xml new file mode 100644 index 00000000..8dd7b3d3 --- /dev/null +++ b/app/src/main/res/drawable/presets_action_button_icon.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/radio_button_background.xml b/app/src/main/res/drawable/radio_button_background.xml new file mode 100644 index 00000000..ab8b9ccc --- /dev/null +++ b/app/src/main/res/drawable/radio_button_background.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/radio_button_default_background.xml b/app/src/main/res/drawable/radio_button_default_background.xml new file mode 100644 index 00000000..57c94fb3 --- /dev/null +++ b/app/src/main/res/drawable/radio_button_default_background.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/save_40dp.xml b/app/src/main/res/drawable/save_40dp.xml new file mode 100644 index 00000000..a62764f2 --- /dev/null +++ b/app/src/main/res/drawable/save_40dp.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/saved_phrase_success_background.xml b/app/src/main/res/drawable/saved_phrase_success_background.xml new file mode 100644 index 00000000..eca0e036 --- /dev/null +++ b/app/src/main/res/drawable/saved_phrase_success_background.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/sensitivity_button_background.xml b/app/src/main/res/drawable/sensitivity_button_background.xml new file mode 100644 index 00000000..e2debd83 --- /dev/null +++ b/app/src/main/res/drawable/sensitivity_button_background.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/settings_action_button_icon.xml b/app/src/main/res/drawable/settings_action_button_icon.xml new file mode 100644 index 00000000..269d3b1b --- /dev/null +++ b/app/src/main/res/drawable/settings_action_button_icon.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/settings_group_background.xml b/app/src/main/res/drawable/settings_group_background.xml new file mode 100644 index 00000000..00087177 --- /dev/null +++ b/app/src/main/res/drawable/settings_group_background.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/settings_group_highlighted_background.xml b/app/src/main/res/drawable/settings_group_highlighted_background.xml new file mode 100644 index 00000000..aadd4b18 --- /dev/null +++ b/app/src/main/res/drawable/settings_group_highlighted_background.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/small_arrow_right_40dp.xml b/app/src/main/res/drawable/small_arrow_right_40dp.xml new file mode 100644 index 00000000..0103e222 --- /dev/null +++ b/app/src/main/res/drawable/small_arrow_right_40dp.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/splash_background.xml b/app/src/main/res/drawable/splash_background.xml new file mode 100644 index 00000000..9575290c --- /dev/null +++ b/app/src/main/res/drawable/splash_background.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/splash_icon.xml b/app/src/main/res/drawable/splash_icon.xml new file mode 100644 index 00000000..6dbfac6f --- /dev/null +++ b/app/src/main/res/drawable/splash_icon.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/vocable_dialog_button_background.xml b/app/src/main/res/drawable/vocable_dialog_button_background.xml new file mode 100644 index 00000000..df95aff2 --- /dev/null +++ b/app/src/main/res/drawable/vocable_dialog_button_background.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-land/category_edit_button.xml b/app/src/main/res/layout-land/category_edit_button.xml new file mode 100644 index 00000000..f7e3f2c8 --- /dev/null +++ b/app/src/main/res/layout-land/category_edit_button.xml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout-land/edit_phrases_action_button.xml b/app/src/main/res/layout-land/edit_phrases_action_button.xml new file mode 100644 index 00000000..6f49dbfb --- /dev/null +++ b/app/src/main/res/layout-land/edit_phrases_action_button.xml @@ -0,0 +1,30 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-land/fragment_edit_category_options.xml b/app/src/main/res/layout-land/fragment_edit_category_options.xml new file mode 100644 index 00000000..3babc698 --- /dev/null +++ b/app/src/main/res/layout-land/fragment_edit_category_options.xml @@ -0,0 +1,112 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout-land/fragment_edit_keyboard.xml b/app/src/main/res/layout-land/fragment_edit_keyboard.xml new file mode 100644 index 00000000..446c87f0 --- /dev/null +++ b/app/src/main/res/layout-land/fragment_edit_keyboard.xml @@ -0,0 +1,154 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-land/fragment_edit_presets.xml b/app/src/main/res/layout-land/fragment_edit_presets.xml new file mode 100644 index 00000000..62927bc6 --- /dev/null +++ b/app/src/main/res/layout-land/fragment_edit_presets.xml @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-land/fragment_keyboard.xml b/app/src/main/res/layout-land/fragment_keyboard.xml new file mode 100644 index 00000000..c8987d29 --- /dev/null +++ b/app/src/main/res/layout-land/fragment_keyboard.xml @@ -0,0 +1,222 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-land/fragment_presets.xml b/app/src/main/res/layout-land/fragment_presets.xml new file mode 100644 index 00000000..c38bf3f9 --- /dev/null +++ b/app/src/main/res/layout-land/fragment_presets.xml @@ -0,0 +1,143 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-land/fragment_settings.xml b/app/src/main/res/layout-land/fragment_settings.xml new file mode 100644 index 00000000..0bb48785 --- /dev/null +++ b/app/src/main/res/layout-land/fragment_settings.xml @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-land/fragment_timing_sensitivity.xml b/app/src/main/res/layout-land/fragment_timing_sensitivity.xml new file mode 100644 index 00000000..b0701e97 --- /dev/null +++ b/app/src/main/res/layout-land/fragment_timing_sensitivity.xml @@ -0,0 +1,128 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-land/keyboard_action_buttons.xml b/app/src/main/res/layout-land/keyboard_action_buttons.xml new file mode 100644 index 00000000..57736338 --- /dev/null +++ b/app/src/main/res/layout-land/keyboard_action_buttons.xml @@ -0,0 +1,27 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-land/my_sayings_empty_layout.xml b/app/src/main/res/layout-land/my_sayings_empty_layout.xml new file mode 100644 index 00000000..fcca2be2 --- /dev/null +++ b/app/src/main/res/layout-land/my_sayings_empty_layout.xml @@ -0,0 +1,28 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-land/phrase_edit_layout.xml b/app/src/main/res/layout-land/phrase_edit_layout.xml new file mode 100644 index 00000000..189d407f --- /dev/null +++ b/app/src/main/res/layout-land/phrase_edit_layout.xml @@ -0,0 +1,39 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-land/presets_action_buttons.xml b/app/src/main/res/layout-land/presets_action_buttons.xml new file mode 100644 index 00000000..12983e2e --- /dev/null +++ b/app/src/main/res/layout-land/presets_action_buttons.xml @@ -0,0 +1,20 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-sw600dp-land/fragment_edit_categories.xml b/app/src/main/res/layout-sw600dp-land/fragment_edit_categories.xml new file mode 100644 index 00000000..4ccd3c79 --- /dev/null +++ b/app/src/main/res/layout-sw600dp-land/fragment_edit_categories.xml @@ -0,0 +1,113 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-sw600dp-land/fragment_edit_keyboard.xml b/app/src/main/res/layout-sw600dp-land/fragment_edit_keyboard.xml new file mode 100644 index 00000000..49e3db86 --- /dev/null +++ b/app/src/main/res/layout-sw600dp-land/fragment_edit_keyboard.xml @@ -0,0 +1,154 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-sw600dp-land/fragment_edit_presets.xml b/app/src/main/res/layout-sw600dp-land/fragment_edit_presets.xml new file mode 100644 index 00000000..edb40010 --- /dev/null +++ b/app/src/main/res/layout-sw600dp-land/fragment_edit_presets.xml @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-sw600dp-land/fragment_keyboard.xml b/app/src/main/res/layout-sw600dp-land/fragment_keyboard.xml new file mode 100644 index 00000000..6e3152c3 --- /dev/null +++ b/app/src/main/res/layout-sw600dp-land/fragment_keyboard.xml @@ -0,0 +1,222 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-sw600dp-land/fragment_selection_mode.xml b/app/src/main/res/layout-sw600dp-land/fragment_selection_mode.xml new file mode 100644 index 00000000..79db2b44 --- /dev/null +++ b/app/src/main/res/layout-sw600dp-land/fragment_selection_mode.xml @@ -0,0 +1,45 @@ + + + + + + + + + + diff --git a/app/src/main/res/layout-sw600dp-land/fragment_settings.xml b/app/src/main/res/layout-sw600dp-land/fragment_settings.xml new file mode 100644 index 00000000..0bb48785 --- /dev/null +++ b/app/src/main/res/layout-sw600dp-land/fragment_settings.xml @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-sw600dp-land/fragment_timing_sensitivity.xml b/app/src/main/res/layout-sw600dp-land/fragment_timing_sensitivity.xml new file mode 100644 index 00000000..b0701e97 --- /dev/null +++ b/app/src/main/res/layout-sw600dp-land/fragment_timing_sensitivity.xml @@ -0,0 +1,128 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-sw600dp-land/presets_action_buttons.xml b/app/src/main/res/layout-sw600dp-land/presets_action_buttons.xml new file mode 100644 index 00000000..12983e2e --- /dev/null +++ b/app/src/main/res/layout-sw600dp-land/presets_action_buttons.xml @@ -0,0 +1,20 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-sw600dp/activity_main.xml b/app/src/main/res/layout-sw600dp/activity_main.xml new file mode 100644 index 00000000..75b810b1 --- /dev/null +++ b/app/src/main/res/layout-sw600dp/activity_main.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-sw600dp/categories_fragment.xml b/app/src/main/res/layout-sw600dp/categories_fragment.xml new file mode 100644 index 00000000..2bbf63b5 --- /dev/null +++ b/app/src/main/res/layout-sw600dp/categories_fragment.xml @@ -0,0 +1,7 @@ + + \ No newline at end of file diff --git a/app/src/main/res/layout-sw600dp/category_button.xml b/app/src/main/res/layout-sw600dp/category_button.xml new file mode 100644 index 00000000..03c26f7a --- /dev/null +++ b/app/src/main/res/layout-sw600dp/category_button.xml @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/app/src/main/res/layout-sw600dp/category_edit_button.xml b/app/src/main/res/layout-sw600dp/category_edit_button.xml new file mode 100644 index 00000000..c6564316 --- /dev/null +++ b/app/src/main/res/layout-sw600dp/category_edit_button.xml @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-sw600dp/fragment_edit_category_options.xml b/app/src/main/res/layout-sw600dp/fragment_edit_category_options.xml new file mode 100644 index 00000000..4d45b476 --- /dev/null +++ b/app/src/main/res/layout-sw600dp/fragment_edit_category_options.xml @@ -0,0 +1,113 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout-sw600dp/fragment_edit_keyboard.xml b/app/src/main/res/layout-sw600dp/fragment_edit_keyboard.xml new file mode 100644 index 00000000..8a5d617c --- /dev/null +++ b/app/src/main/res/layout-sw600dp/fragment_edit_keyboard.xml @@ -0,0 +1,139 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-sw600dp/fragment_edit_presets.xml b/app/src/main/res/layout-sw600dp/fragment_edit_presets.xml new file mode 100644 index 00000000..842a68a2 --- /dev/null +++ b/app/src/main/res/layout-sw600dp/fragment_edit_presets.xml @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-sw600dp/fragment_keyboard.xml b/app/src/main/res/layout-sw600dp/fragment_keyboard.xml new file mode 100644 index 00000000..6966cb31 --- /dev/null +++ b/app/src/main/res/layout-sw600dp/fragment_keyboard.xml @@ -0,0 +1,208 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-sw600dp/fragment_presets.xml b/app/src/main/res/layout-sw600dp/fragment_presets.xml new file mode 100644 index 00000000..0ec0f3da --- /dev/null +++ b/app/src/main/res/layout-sw600dp/fragment_presets.xml @@ -0,0 +1,143 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-sw600dp/fragment_settings.xml b/app/src/main/res/layout-sw600dp/fragment_settings.xml new file mode 100644 index 00000000..a4f69120 --- /dev/null +++ b/app/src/main/res/layout-sw600dp/fragment_settings.xml @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-sw600dp/fragment_timing_sensitivity.xml b/app/src/main/res/layout-sw600dp/fragment_timing_sensitivity.xml new file mode 100644 index 00000000..cb56b41e --- /dev/null +++ b/app/src/main/res/layout-sw600dp/fragment_timing_sensitivity.xml @@ -0,0 +1,130 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-sw600dp/keyboard_action_buttons.xml b/app/src/main/res/layout-sw600dp/keyboard_action_buttons.xml new file mode 100644 index 00000000..57736338 --- /dev/null +++ b/app/src/main/res/layout-sw600dp/keyboard_action_buttons.xml @@ -0,0 +1,27 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-sw600dp/my_sayings_empty_layout.xml b/app/src/main/res/layout-sw600dp/my_sayings_empty_layout.xml new file mode 100644 index 00000000..9cd7324f --- /dev/null +++ b/app/src/main/res/layout-sw600dp/my_sayings_empty_layout.xml @@ -0,0 +1,27 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-sw600dp/presets_action_buttons.xml b/app/src/main/res/layout-sw600dp/presets_action_buttons.xml new file mode 100644 index 00000000..12983e2e --- /dev/null +++ b/app/src/main/res/layout-sw600dp/presets_action_buttons.xml @@ -0,0 +1,20 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-sw600dp/vocable_confirmation_dialog.xml b/app/src/main/res/layout-sw600dp/vocable_confirmation_dialog.xml new file mode 100644 index 00000000..0c4ebec4 --- /dev/null +++ b/app/src/main/res/layout-sw600dp/vocable_confirmation_dialog.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index c72467ec..6c47be6d 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,17 +1,44 @@ - + + + + + android:background="@color/colorPrimaryDark" /> + + + + - + - + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_settings.xml b/app/src/main/res/layout/activity_settings.xml new file mode 100644 index 00000000..5d02cf0d --- /dev/null +++ b/app/src/main/res/layout/activity_settings.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/categories_fragment.xml b/app/src/main/res/layout/categories_fragment.xml new file mode 100644 index 00000000..2858aaac --- /dev/null +++ b/app/src/main/res/layout/categories_fragment.xml @@ -0,0 +1,6 @@ + + \ No newline at end of file diff --git a/app/src/main/res/layout/category_button.xml b/app/src/main/res/layout/category_button.xml new file mode 100644 index 00000000..c2907568 --- /dev/null +++ b/app/src/main/res/layout/category_button.xml @@ -0,0 +1,5 @@ + + diff --git a/app/src/main/res/layout/category_edit_button.xml b/app/src/main/res/layout/category_edit_button.xml new file mode 100644 index 00000000..6258fb92 --- /dev/null +++ b/app/src/main/res/layout/category_edit_button.xml @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/edit_phrases_action_button.xml b/app/src/main/res/layout/edit_phrases_action_button.xml new file mode 100644 index 00000000..90ce2269 --- /dev/null +++ b/app/src/main/res/layout/edit_phrases_action_button.xml @@ -0,0 +1,31 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/error_layout.xml b/app/src/main/res/layout/error_layout.xml new file mode 100644 index 00000000..1b7648ce --- /dev/null +++ b/app/src/main/res/layout/error_layout.xml @@ -0,0 +1,27 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_edit_categories.xml b/app/src/main/res/layout/fragment_edit_categories.xml new file mode 100644 index 00000000..0508546e --- /dev/null +++ b/app/src/main/res/layout/fragment_edit_categories.xml @@ -0,0 +1,109 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_edit_categories_list.xml b/app/src/main/res/layout/fragment_edit_categories_list.xml new file mode 100644 index 00000000..666dbc68 --- /dev/null +++ b/app/src/main/res/layout/fragment_edit_categories_list.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_edit_category_options.xml b/app/src/main/res/layout/fragment_edit_category_options.xml new file mode 100644 index 00000000..c0dbd79a --- /dev/null +++ b/app/src/main/res/layout/fragment_edit_category_options.xml @@ -0,0 +1,115 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_edit_keyboard.xml b/app/src/main/res/layout/fragment_edit_keyboard.xml new file mode 100644 index 00000000..bfc1f2d6 --- /dev/null +++ b/app/src/main/res/layout/fragment_edit_keyboard.xml @@ -0,0 +1,154 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_edit_phrases.xml b/app/src/main/res/layout/fragment_edit_phrases.xml new file mode 100644 index 00000000..fb4f2409 --- /dev/null +++ b/app/src/main/res/layout/fragment_edit_phrases.xml @@ -0,0 +1,27 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_edit_presets.xml b/app/src/main/res/layout/fragment_edit_presets.xml new file mode 100644 index 00000000..f55bee37 --- /dev/null +++ b/app/src/main/res/layout/fragment_edit_presets.xml @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_keyboard.xml b/app/src/main/res/layout/fragment_keyboard.xml new file mode 100644 index 00000000..c0f70ebd --- /dev/null +++ b/app/src/main/res/layout/fragment_keyboard.xml @@ -0,0 +1,214 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_keyboard_2_stroke_ff.xml b/app/src/main/res/layout/fragment_keyboard_2_stroke_ff.xml new file mode 100644 index 00000000..399bd200 --- /dev/null +++ b/app/src/main/res/layout/fragment_keyboard_2_stroke_ff.xml @@ -0,0 +1,255 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_number_pad.xml b/app/src/main/res/layout/fragment_number_pad.xml new file mode 100644 index 00000000..d50f7b19 --- /dev/null +++ b/app/src/main/res/layout/fragment_number_pad.xml @@ -0,0 +1,7 @@ + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_phrases.xml b/app/src/main/res/layout/fragment_phrases.xml new file mode 100644 index 00000000..30775c2c --- /dev/null +++ b/app/src/main/res/layout/fragment_phrases.xml @@ -0,0 +1,7 @@ + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_presets.xml b/app/src/main/res/layout/fragment_presets.xml new file mode 100644 index 00000000..d4234a05 --- /dev/null +++ b/app/src/main/res/layout/fragment_presets.xml @@ -0,0 +1,142 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_selection_mode.xml b/app/src/main/res/layout/fragment_selection_mode.xml new file mode 100644 index 00000000..b30caad5 --- /dev/null +++ b/app/src/main/res/layout/fragment_selection_mode.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_settings.xml b/app/src/main/res/layout/fragment_settings.xml new file mode 100644 index 00000000..ca280e17 --- /dev/null +++ b/app/src/main/res/layout/fragment_settings.xml @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_timing_sensitivity.xml b/app/src/main/res/layout/fragment_timing_sensitivity.xml new file mode 100644 index 00000000..aca0bcf8 --- /dev/null +++ b/app/src/main/res/layout/fragment_timing_sensitivity.xml @@ -0,0 +1,131 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/guidelines_settings.xml b/app/src/main/res/layout/guidelines_settings.xml new file mode 100644 index 00000000..505c4411 --- /dev/null +++ b/app/src/main/res/layout/guidelines_settings.xml @@ -0,0 +1,19 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/keyboard_action_buttons.xml b/app/src/main/res/layout/keyboard_action_buttons.xml new file mode 100644 index 00000000..e22e751b --- /dev/null +++ b/app/src/main/res/layout/keyboard_action_buttons.xml @@ -0,0 +1,40 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/keyboard_action_layout.xml b/app/src/main/res/layout/keyboard_action_layout.xml new file mode 100644 index 00000000..59e7c1ed --- /dev/null +++ b/app/src/main/res/layout/keyboard_action_layout.xml @@ -0,0 +1,13 @@ + + \ No newline at end of file diff --git a/app/src/main/res/layout/keyboard_key_layout.xml b/app/src/main/res/layout/keyboard_key_layout.xml new file mode 100644 index 00000000..d43c8d7c --- /dev/null +++ b/app/src/main/res/layout/keyboard_key_layout.xml @@ -0,0 +1,11 @@ + + \ No newline at end of file diff --git a/app/src/main/res/layout/my_sayings_empty_layout.xml b/app/src/main/res/layout/my_sayings_empty_layout.xml new file mode 100644 index 00000000..46d8cc95 --- /dev/null +++ b/app/src/main/res/layout/my_sayings_empty_layout.xml @@ -0,0 +1,28 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/phrase_button.xml b/app/src/main/res/layout/phrase_button.xml new file mode 100644 index 00000000..64438f4c --- /dev/null +++ b/app/src/main/res/layout/phrase_button.xml @@ -0,0 +1,12 @@ + + \ No newline at end of file diff --git a/app/src/main/res/layout/phrase_edit_layout.xml b/app/src/main/res/layout/phrase_edit_layout.xml new file mode 100644 index 00000000..2d9e3921 --- /dev/null +++ b/app/src/main/res/layout/phrase_edit_layout.xml @@ -0,0 +1,39 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/phrase_saved_success.xml b/app/src/main/res/layout/phrase_saved_success.xml new file mode 100644 index 00000000..83ab439b --- /dev/null +++ b/app/src/main/res/layout/phrase_saved_success.xml @@ -0,0 +1,22 @@ + + + diff --git a/app/src/main/res/layout/presets_action_buttons.xml b/app/src/main/res/layout/presets_action_buttons.xml new file mode 100644 index 00000000..181c3783 --- /dev/null +++ b/app/src/main/res/layout/presets_action_buttons.xml @@ -0,0 +1,28 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/selection_mode_options_layout.xml b/app/src/main/res/layout/selection_mode_options_layout.xml new file mode 100644 index 00000000..f5c28884 --- /dev/null +++ b/app/src/main/res/layout/selection_mode_options_layout.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/settings_options_layout.xml b/app/src/main/res/layout/settings_options_layout.xml new file mode 100644 index 00000000..19ca1348 --- /dev/null +++ b/app/src/main/res/layout/settings_options_layout.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/vocable_confirmation_dialog.xml b/app/src/main/res/layout/vocable_confirmation_dialog.xml new file mode 100644 index 00000000..1050e654 --- /dev/null +++ b/app/src/main/res/layout/vocable_confirmation_dialog.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml deleted file mode 100644 index bbd3e021..00000000 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png index 898f3ed5..3d48b6a5 100644 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher.png and b/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png index dffca360..13138440 100644 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png index 64ba76f7..e2b5f07a 100644 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher.png and b/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png index dae5e082..640d89f4 100644 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png index e5ed4659..2c2d0e73 100644 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher.png and b/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png index 14ed0af3..71b1e7af 100644 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png index b0907cac..27dec17d 100644 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png index d8ae0315..9e227873 100644 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png index 2c18de9e..7fad45bd 100644 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png index beed3cdd..c06ee4f2 100644 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/values-de-land/arrays.xml b/app/src/main/res/values-de-land/arrays.xml new file mode 100644 index 00000000..cd441c59 --- /dev/null +++ b/app/src/main/res/values-de-land/arrays.xml @@ -0,0 +1,38 @@ + + + + Q + W + E + R + T + Z + U + I + O + P + Ü + A + S + D + F + G + H + J + K + L + Ö + Ä + Y + X + C + V + B + N + M + ß + \' + . + \? + + \ No newline at end of file diff --git a/app/src/main/res/values-de-land/integers.xml b/app/src/main/res/values-de-land/integers.xml new file mode 100644 index 00000000..985cdccb --- /dev/null +++ b/app/src/main/res/values-de-land/integers.xml @@ -0,0 +1,5 @@ + + + 11 + 3 + \ No newline at end of file diff --git a/app/src/main/res/values-de-sw600dp-land/integers.xml b/app/src/main/res/values-de-sw600dp-land/integers.xml new file mode 100644 index 00000000..985cdccb --- /dev/null +++ b/app/src/main/res/values-de-sw600dp-land/integers.xml @@ -0,0 +1,5 @@ + + + 11 + 3 + \ No newline at end of file diff --git a/app/src/main/res/values-de-sw600dp/arrays.xml b/app/src/main/res/values-de-sw600dp/arrays.xml new file mode 100644 index 00000000..cd441c59 --- /dev/null +++ b/app/src/main/res/values-de-sw600dp/arrays.xml @@ -0,0 +1,38 @@ + + + + Q + W + E + R + T + Z + U + I + O + P + Ü + A + S + D + F + G + H + J + K + L + Ö + Ä + Y + X + C + V + B + N + M + ß + \' + . + \? + + \ No newline at end of file diff --git a/app/src/main/res/values-de-sw600dp/integers.xml b/app/src/main/res/values-de-sw600dp/integers.xml new file mode 100644 index 00000000..985cdccb --- /dev/null +++ b/app/src/main/res/values-de-sw600dp/integers.xml @@ -0,0 +1,5 @@ + + + 11 + 3 + \ No newline at end of file diff --git a/app/src/main/res/values-de/arrays.xml b/app/src/main/res/values-de/arrays.xml new file mode 100644 index 00000000..1ab75572 --- /dev/null +++ b/app/src/main/res/values-de/arrays.xml @@ -0,0 +1,40 @@ + + + + A + B + C + D + E + F + G + H + I + J + K + L + M + N + O + P + Q + R + S + T + U + V + W + X + Y + Z + Ä + Ö + Ü + ß + \' + , + . + ! + \? + + \ No newline at end of file diff --git a/app/src/main/res/values-de/integers.xml b/app/src/main/res/values-de/integers.xml new file mode 100644 index 00000000..aa48c633 --- /dev/null +++ b/app/src/main/res/values-de/integers.xml @@ -0,0 +1,5 @@ + + + 5 + 7 + \ No newline at end of file diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml new file mode 100644 index 00000000..ad2347a4 --- /dev/null +++ b/app/src/main/res/values-de/strings.xml @@ -0,0 +1,84 @@ + + + Vocable + + + Wähle unten etwas aus, um zu sprechen + Seite %1d von %2d + Ihre Redewendungen werden hier angezeigt.\n\nNavigieren Sie zu der Tastatur, geben Sie etwas ein und\n wählen Sie dann den \"☆\", um es zu speichern. + Ihre Redewendungen werden hier angezeigt.\n\nNavigieren Sie zu der Tastatur, geben Sie etwas ein und\n wählen Sie dann den \"☆\", um es zu speichern. + Ihre Redewendungen werden hier angezeigt.\n\nNavigieren Sie zu der Tastatur, geben Sie etwas ein und wählen Sie dann den \"☆\", um es zu speichern. + Sie haben noch keine Redewendungen gespeichert. Wählen Sie das "+" oben rechts, um etwas hinzuzufügen. + + + Einstellungen + Datenschutz + Entwickler kontaktieren + V %s + Per Kopfbewegung + App verlassen + Dieser link wir außerhalb der Vocable App geöffnet. Sie laufen möglicherweise Gefahr, nicht mehr per Kopfbewegung steuern zu können. + Weiter + Abbrechen + + + Meine Redewendung + Kategorien + Steuerungsmodus + App Einstellungen zurücksetzen + + + Steuerungsmodus + + + Pause + Resume + + + Bitte näher ans Gerät kommen. + + + Texteingabe beginnen… + + + In Meine Sätze gespeichert + + + Löschen + Gelöschte Redewendung können nicht wiederhergestellt werden. + Neue Redewendung gespeichert + Weiter editieren + Verwerfen + Bist du sicher? + Nicht gespeicherte Veränderungen werden beim Verlassen des Menüs verworfen.. + + + Änderungen gespeichert + + + Timing und Empfindlichkeit + Schwebezeit + Zeigerempfindlichkeit + %s sekunden + 1 zweite + Niedrig + Mittel + Hoch + + + preset_user_favorites + 123 + preset_user_keypad + Ja + Nein + + + Kategorien + %1d. %2s + + + Kategorien anzeigen + Kategorie entfernen + Entfernte Kategorien können nicht wiederhergestellt werden. + + \ No newline at end of file diff --git a/app/src/main/res/values-land/arrays.xml b/app/src/main/res/values-land/arrays.xml new file mode 100644 index 00000000..1ee021e9 --- /dev/null +++ b/app/src/main/res/values-land/arrays.xml @@ -0,0 +1,36 @@ + + + + Q + W + E + R + T + Y + U + I + O + P + A + S + D + F + G + H + J + K + L + \' + Z + X + C + V + B + N + M + , + . + \? + + + \ No newline at end of file diff --git a/app/src/main/res/values-land/dimens.xml b/app/src/main/res/values-land/dimens.xml new file mode 100644 index 00000000..1a607cae --- /dev/null +++ b/app/src/main/res/values-land/dimens.xml @@ -0,0 +1,70 @@ + + + + + 8dp + 24dp + 34sp + 48sp + 8dp + + + 64dp + 90dp + + + 380dp + 8dp + 40dp + 16dp + + + 37dp + 16dp + 16dp + 8dp + 96dp + 88dp + + + 16dp + 20dp + 24dp + 32dp + 19dp + + + 8dp + 0dp + + + 34sp + 90dp + 64dp + 8dp + 12dp + 104dp + 16dp + 24dp + + + 24dp + 34sp + + + 72dp + 72dp + 16dp + 24dp + 42dp + 168dp + 37dp + + + 56dp + 56dp + + + 0dp + + \ No newline at end of file diff --git a/app/src/main/res/values-land/integers.xml b/app/src/main/res/values-land/integers.xml new file mode 100644 index 00000000..be02b7de --- /dev/null +++ b/app/src/main/res/values-land/integers.xml @@ -0,0 +1,19 @@ + + + 2 + 6 + 2 + 3 + 2 + 3 + 2 + 10 + 3 + 6 + 2 + 2 + 2 + 2 + 10 + 3 + \ No newline at end of file diff --git a/app/src/main/res/values-sw600dp-land/dimens.xml b/app/src/main/res/values-sw600dp-land/dimens.xml new file mode 100644 index 00000000..4c9cd6e9 --- /dev/null +++ b/app/src/main/res/values-sw600dp-land/dimens.xml @@ -0,0 +1,76 @@ + + + + 96dp + 104dp + + + 8dp + + + 48sp + + + 16dp + + + 16dp + 44dp + + + 96dp + 112dp + 36dp + 44dp + 40dp + 65dp + + + 688dp + 276dp + + + 40sp + 34sp + 24dp + 52dp + 96dp + 44dp + + + 24dp + 24dp + 112dp + 96dp + 16dp + 24sp + 8dp + 44dp + 84dp + 48sp + 112dp + 96dp + 44dp + 20dp + 44dp + 96dp + + + 24dp + 104dp + 52dp + 44dp + + + 88dp + 88dp + + + 112dp + 96dp + 28dp + 44dp + 34dp + 84dp + + \ No newline at end of file diff --git a/app/src/main/res/values-sw600dp-land/integers.xml b/app/src/main/res/values-sw600dp-land/integers.xml new file mode 100644 index 00000000..bf6bdaff --- /dev/null +++ b/app/src/main/res/values-sw600dp-land/integers.xml @@ -0,0 +1,15 @@ + + + 4 + 9 + 6 + 3 + 3 + 2 + 3 + 2 + 2 + 5 + 10 + 3 + \ No newline at end of file diff --git a/app/src/main/res/values-sw600dp/arrays.xml b/app/src/main/res/values-sw600dp/arrays.xml new file mode 100644 index 00000000..1ee021e9 --- /dev/null +++ b/app/src/main/res/values-sw600dp/arrays.xml @@ -0,0 +1,36 @@ + + + + Q + W + E + R + T + Y + U + I + O + P + A + S + D + F + G + H + J + K + L + \' + Z + X + C + V + B + N + M + , + . + \? + + + \ No newline at end of file diff --git a/app/src/main/res/values-sw600dp/booleans.xml b/app/src/main/res/values-sw600dp/booleans.xml new file mode 100644 index 00000000..5fc3a0d0 --- /dev/null +++ b/app/src/main/res/values-sw600dp/booleans.xml @@ -0,0 +1,4 @@ + + + true + \ No newline at end of file diff --git a/app/src/main/res/values-sw600dp/dimens.xml b/app/src/main/res/values-sw600dp/dimens.xml new file mode 100644 index 00000000..ceed6dd4 --- /dev/null +++ b/app/src/main/res/values-sw600dp/dimens.xml @@ -0,0 +1,126 @@ + + + + + 112dp + 34sp + + + 72dp + 72dp + + + 104dp + 112dp + 40dp + 8dp + + + 104dp + 80dp + 48dp + 88dp + + + 16dp + 32dp + 40dp + + + 8dp + + + 34sp + 34sp + 144dp + 464dp + 80dp + 24dp + + + 48sp + + + 40dp + 48sp + 8dp + 24dp + 80dp + 64dp + 20sp + 48dp + 20dp + 36dp + 16dp + + + 560dp + 225dp + 24dp + 16dp + + + 80dp + 52dp + 44dp + 60dp + 40dp + 450dp + 60dp + 88dp + 44dp + + + 44dp + 88dp + 8dp + 0dp + + + 84dp + 44dp + 64dp + 24dp + 102dp + 64dp + 104dp + 96dp + 112dp + 96dp + + + 24dp + 104dp + 16dp + 24dp + + + 88dp + 88dp + 44dp + 44dp + 48sp + 34sp + 88dp + 44dp + + + 72dp + 80dp + 42dp + 44dp + 111dp + 105dp + 44dp + 48sp + 76dp + + + 56dp + 56dp + + + 445dp + 24dp + + \ No newline at end of file diff --git a/app/src/main/res/values-sw600dp/integers.xml b/app/src/main/res/values-sw600dp/integers.xml new file mode 100644 index 00000000..9426d6dc --- /dev/null +++ b/app/src/main/res/values-sw600dp/integers.xml @@ -0,0 +1,19 @@ + + + 2 + 10 + 4 + 2 + 5 + 3 + 2 + 10 + 3 + 3 + 4 + 2 + 2 + 9 + 10 + 3 + \ No newline at end of file diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml new file mode 100644 index 00000000..f8e87c91 --- /dev/null +++ b/app/src/main/res/values/arrays.xml @@ -0,0 +1,59 @@ + + + + A + B + C + D + E + F + G + H + I + J + K + L + M + N + O + P + Q + R + S + T + U + V + W + X + Y + Z + \' + , + . + \? + + + + @string/settings_edit_saying + @string/edit_categories_title + @string/timing_sensitivity_title + @string/settings_selection_mode + @string/settings_reset_app + + + + 0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + @string/category_123_yes + @string/category_123_no + + + \ No newline at end of file diff --git a/app/src/main/res/values/booleans.xml b/app/src/main/res/values/booleans.xml new file mode 100644 index 00000000..84d7d2f5 --- /dev/null +++ b/app/src/main/res/values/booleans.xml @@ -0,0 +1,4 @@ + + + false + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 69b22338..950dd54b 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -1,6 +1,15 @@ - #008577 - #00574B - #D81B60 + #3831A0 + #803831A0 + #201C5B + #59201C5B + #504B93 + #41201C5B + #D1EEE9 + #32D1EEE9 + #4DD9F9 + #FEA430 + #00FA9A + #AD006C diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml new file mode 100644 index 00000000..c8a67c77 --- /dev/null +++ b/app/src/main/res/values/dimens.xml @@ -0,0 +1,255 @@ + + + + 32dp + + 8dp + + + 104dp + 0dp + 8dp + 20sp + + + 24sp + 10dp + 8dp + + + 8dp + 20sp + + + 66dp + 32sp + + + 10dp + 4 + + + 8dp + 20sp + + + 16dp + 88dp + 40dp + 34sp + 8dp + 24dp + 80dp + 64dp + 20sp + 48dp + 20dp + 36dp + 16dp + + + 0dp + 120dp + 142dp + 40dp + 24sp + + + 24sp + + + 8dp + 4dp + + + 24dp + 16dp + 8dp + 48dp + 8dp + + + 64dp + 72dp + 8dp + + + 48sp + 24dp + 120dp + 16dp + 24dp + 72dp + 20dp + 52dp + 8dp + 8dp + 72dp + 34sp + + + 80dp + 72dp + 28dp + 0dp + 8dp + + + 64dp + 64dp + 32dp + 16dp + 24sp + 40dp + 8dp + + + 20sp + 16dp + 20dp + 8dp + 64dp + + + 12dp + 32dp + 24sp + + + 325dp + 225dp + 24sp + 16dp + 142dp + 44dp + 24dp + 16dp + 16dp + 20sp + 15dp + + + 24dp + 16dp + 24sp + 8dp + 44dp + 8dp + 72dp + 72dp + 0dp + 40dp + 34sp + 72dp + 72dp + 8dp + 24dp + 33dp + 33dp + 64dp + 64dp + 192dp + 33dp + 56dp + 56dp + + + 24dp + 72dp + 72dp + 24dp + 24dp + 32dp + 64dp + 24dp + 48dp + 8dp + 88dp + 8dp + 24sp + 55dp + 8dp + 8dp + 8dp + + + 0dp + 8dp + 24dp + 72dp + 24sp + + + 34sp + 16dp + 24dp + 104dp + 16dp + 24dp + 117dp + 64dp + + + 18dp + 48dp + 48dp + 32dp + 24dp + 24dp + 16dp + 8dp + 16dp + 24sp + 24sp + 24sp + 8dp + 16dp + 16dp + 48dp + 32dp + + + 24dp + 72dp + 72dp + 44dp + 24dp + 0dp + 32dp + 34sp + 30dp + 8dp + 24dp + 64dp + + + 56dp + 56dp + 16dp + 8dp + 4dp + 24sp + 2dp + + + 48sp + 48dp + 48dp + 32dp + 8dp + 24dp + 16dp + 8dp + + + 0dp + 32dp + 100dp + 24sp + 40dp + + + 16dp + 8dp + + \ No newline at end of file diff --git a/app/src/main/res/values/integers.xml b/app/src/main/res/values/integers.xml new file mode 100644 index 00000000..45d33724 --- /dev/null +++ b/app/src/main/res/values/integers.xml @@ -0,0 +1,21 @@ + + + 1 + 8 + 3 + 2 + 4 + 1 + 4 + 5 + 1 + 5 + 6 + 3 + 4 + 2 + 1 + 4 + 5 + 6 + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a7c7ead0..8dc234da 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,3 +1,83 @@ - eyespeak + Vocable + + + Select something below to speak. + Page %1d of %2d + Your sayings will appear\nhere.\n\nNavigate to the keyboard, type something\n then select the \"☆\" to save it here. + Your sayings will appear\nhere.\n\nNavigate to the keyboard, type something then select the \"☆\" to save it here. + Your sayings will appear here.\n\nNavigate to the keyboard, type something\n then select the \"☆\" to save it here. + You don’t have any sayings saved yet. Select the “+” in the top right to add something. + + + Settings + Privacy Policy + Contact Developers + V %s + Head Tracking + Leaving the app + You\'re about to be taken outside the app. You may lose head tracking control. + Continue + Cancel + + + My Sayings + Categories + Selection Mode + Reset App Settings + + + Selection Mode + + + Pause + Resume + + + Please move closer to the device. + + + Start typing… + + + Saved to My Sayings + + + Delete + Deleted phrases cannot be recovered. + New phrase saved + Continue editing + Discard + Are you sure? + Going back before saving will clear any edits made. + + + Changes Saved + + + Timing and Sensitivity + Hover Time + Cursor Sensitivity + %s seconds + 1 second + Low + Medium + High + + + preset_user_favorites + 123 + preset_user_keypad + Yes + No + + + Categories + %1d. %2s + + + Show Category + Remove Category + Removed categories can not be restored. + diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 5885930d..55836893 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -6,6 +6,300 @@ @color/colorPrimary @color/colorPrimaryDark @color/colorAccent + false + false + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/test/java/com/example/eyespeak/ExampleUnitTest.kt b/app/src/test/java/com/willowtree/vocable/ExampleUnitTest.kt similarity index 91% rename from app/src/test/java/com/example/eyespeak/ExampleUnitTest.kt rename to app/src/test/java/com/willowtree/vocable/ExampleUnitTest.kt index c13d94f3..4950faf5 100644 --- a/app/src/test/java/com/example/eyespeak/ExampleUnitTest.kt +++ b/app/src/test/java/com/willowtree/vocable/ExampleUnitTest.kt @@ -1,4 +1,4 @@ -package com.example.eyespeak +package com.willowtree.vocable import org.junit.Test diff --git a/app/src/test/java/com/willowtree/vocable/utils/LocaleUtilsTest.kt b/app/src/test/java/com/willowtree/vocable/utils/LocaleUtilsTest.kt new file mode 100644 index 00000000..72458f75 --- /dev/null +++ b/app/src/test/java/com/willowtree/vocable/utils/LocaleUtilsTest.kt @@ -0,0 +1,72 @@ +package com.willowtree.vocable.utils + +import org.junit.Assert +import org.junit.Test +import java.util.* + +class LocaleUtilsTest { + + companion object { + private const val EN_HELLO = "Hello" + private const val FR_CA_HELLO = "Salut" + private const val FR_HELLO = "Bonjour" + private const val DE_DE_HELLO = "Hallo" + + private val EN_PAIR = Pair(Locale.ENGLISH.toString(), EN_HELLO) + private val FR_CA_PAIR = Pair(Locale.CANADA_FRENCH.toString(), FR_CA_HELLO) + private val FR_PAIR = Pair(Locale.FRENCH.toString(), FR_HELLO) + private val DE_DE_PAIR = Pair(Locale.GERMANY.toString(), DE_DE_HELLO) + } + + @Test + fun `full locale pair returned for full locale`() { + val localizedPairs = mapOf(FR_CA_PAIR, FR_PAIR, EN_PAIR) + Locale.setDefault(Locale.CANADA_FRENCH) + val localizedPair = LocaleUtils.getLocalizedPair(localizedPairs) + Assert.assertEquals(FR_CA_HELLO, localizedPair.first) + Assert.assertEquals(Locale.CANADA_FRENCH, localizedPair.second) + } + + @Test + fun `language-only locale pair returned for full locale`() { + val localizedPairs = mapOf(FR_PAIR, EN_PAIR) + Locale.setDefault(Locale.CANADA_FRENCH) + val localizedPair = LocaleUtils.getLocalizedPair(localizedPairs) + Assert.assertEquals(FR_HELLO, localizedPair.first) + Assert.assertEquals(Locale.FRENCH, localizedPair.second) + } + + @Test + fun `English locale pair returned for full locale`() { + val localizedPairs = mapOf(EN_PAIR) + Locale.setDefault(Locale.CANADA_FRENCH) + val localizedPair = LocaleUtils.getLocalizedPair(localizedPairs) + Assert.assertEquals(EN_HELLO, localizedPair.first) + Assert.assertEquals(Locale.ENGLISH, localizedPair.second) + } + + @Test + fun `default to first map value if no value exists for locale`() { + val localizedPairs = mapOf(DE_DE_PAIR) + Locale.setDefault(Locale.CANADA_FRENCH) + val localizedPair = LocaleUtils.getLocalizedPair(localizedPairs) + Assert.assertEquals(DE_DE_HELLO, localizedPair.first) + Assert.assertEquals(Locale.GERMANY, localizedPair.second) + } + + @Test + fun `empty map returns default values`() { + val localizedPairs = mapOf() + val localizedPair = LocaleUtils.getLocalizedPair(localizedPairs) + Assert.assertEquals("", localizedPair.first) + Assert.assertEquals(Locale.ENGLISH, localizedPair.second) + } + + @Test + fun `getTextForLocale returns closest match`() { + val localizedPairs = mapOf(FR_CA_PAIR, FR_PAIR, EN_PAIR) + Locale.setDefault(Locale.CANADA_FRENCH) + val textForLocale = LocaleUtils.getTextForLocale(localizedPairs) + Assert.assertEquals(FR_CA_HELLO, textForLocale) + } +} \ No newline at end of file diff --git a/build.gradle b/build.gradle index b39ac102..c164a71e 100644 --- a/build.gradle +++ b/build.gradle @@ -1,14 +1,14 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - ext.kotlin_version = '1.3.31' + ext.kotlin_version = '1.3.61' repositories { google() jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:3.4.1' + classpath 'com.android.tools.build:gradle:3.6.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index d3083732..98b74919 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Fri May 24 11:24:08 EDT 2019 +#Wed Feb 26 14:44:13 EST 2020 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip diff --git a/marketing_assets/google-play-badge.svg b/marketing_assets/google-play-badge.svg new file mode 100644 index 00000000..19260737 --- /dev/null +++ b/marketing_assets/google-play-badge.svg @@ -0,0 +1,105 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/marketing_assets/vocable_vimeo_still.gif b/marketing_assets/vocable_vimeo_still.gif new file mode 100644 index 00000000..19a16488 Binary files /dev/null and b/marketing_assets/vocable_vimeo_still.gif differ