diff --git a/.github/scripts/gradlew_recursive.sh b/.github/scripts/gradlew_recursive.sh new file mode 100644 index 0000000..d575c49 --- /dev/null +++ b/.github/scripts/gradlew_recursive.sh @@ -0,0 +1,37 @@ +#!/bin/bash + +# Copyright (C) 2020 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -xe + +# Default Gradle settings are not optimal for Android builds, override them +# here to make the most out of the GitHub Actions build servers +GRADLE_OPTS="$GRADLE_OPTS -Xms4g -Xmx4g" +GRADLE_OPTS="$GRADLE_OPTS -XX:+HeapDumpOnOutOfMemoryError" +GRADLE_OPTS="$GRADLE_OPTS -Dorg.gradle.daemon=false" +GRADLE_OPTS="$GRADLE_OPTS -Dorg.gradle.workers.max=2" +GRADLE_OPTS="$GRADLE_OPTS -Dkotlin.incremental=false" +GRADLE_OPTS="$GRADLE_OPTS -Dkotlin.compiler.execution.strategy=in-process" +GRADLE_OPTS="$GRADLE_OPTS -Dfile.encoding=UTF-8" +export GRADLE_OPTS + +# Crawl all gradlew files which indicate an Android project +# You may edit this if your repo has a different project structure +for GRADLEW in `find . -name "gradlew"` ; do + SAMPLE=$(dirname "${GRADLEW}") + # Tell Gradle that this is a CI environment and disable parallel compilation + bash "$GRADLEW" -p "$SAMPLE" -Pci --no-parallel --stacktrace $@ +done + diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 863d685..59e4be4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,3 +1,17 @@ +# Copyright (C) 2020 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + name: Android CI on: @@ -7,27 +21,33 @@ on: branches: [ master ] jobs: - build: + build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - name: set up JDK 11 - uses: actions/setup-java@v2 - with: - java-version: '11' - distribution: 'adopt' - cache: gradle + - uses: actions/checkout@v2 + - name: set up JDK 11 + uses: actions/setup-java@v2 + with: + java-version: '11' + distribution: 'adopt' + cache: gradle + - name: Prepare project + run: chmod +x .github/scripts/gradlew_recursive.sh + - name: Build project + run: .github/scripts/gradlew_recursive.sh assembleDebug + - name: Zip artifacts + run: zip -r assemble.zip . -i '**/build/*.apk' '**/build/*.aab' '**/build/*.aar' '**/build/*.so' + - name: Upload artifacts + uses: actions/upload-artifact@v1 + with: + name: assemble + path: assemble.zip + - name: ktlint + uses: ScaCap/action-ktlint@master + with: + github_token: ${{ secrets.github_token }} + android: true + reporter: github-pr-review # Change reporter - - name: Grant execute permission for gradlew - run: chmod +x gradlew - - name: Build with Gradle - run: ./gradlew build - - - name: ktlint - uses: ScaCap/action-ktlint@master - with: - github_token: ${{ secrets.github_token }} - android: true - reporter: github-pr-review # Change reporter diff --git a/.gitignore b/.gitignore index 10cfdbf..82d4574 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,15 @@ .externalNativeBuild .cxx local.properties + +# keystore +keystore.properties +private_key.pepk +upload_key.jks + +# Fastlane +fastlane_secret_keys.json + +# App +/app/debug +/app/release \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 5014ce6..d182631 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -13,28 +13,36 @@ + + + - + + + - + + + + diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..adc90d9 --- /dev/null +++ b/Gemfile @@ -0,0 +1,3 @@ +source "https://rubygems.org" + +gem "fastlane" \ No newline at end of file diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..10eb92d --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,214 @@ +GEM + remote: https://rubygems.org/ + specs: + CFPropertyList (3.0.5) + rexml + addressable (2.8.0) + public_suffix (>= 2.0.2, < 5.0) + artifactory (3.0.15) + atomos (0.1.3) + aws-eventstream (1.2.0) + aws-partitions (1.543.0) + aws-sdk-core (3.125.0) + aws-eventstream (~> 1, >= 1.0.2) + aws-partitions (~> 1, >= 1.525.0) + aws-sigv4 (~> 1.1) + jmespath (~> 1.0) + aws-sdk-kms (1.53.0) + aws-sdk-core (~> 3, >= 3.125.0) + aws-sigv4 (~> 1.1) + aws-sdk-s3 (1.110.0) + aws-sdk-core (~> 3, >= 3.125.0) + aws-sdk-kms (~> 1) + aws-sigv4 (~> 1.4) + aws-sigv4 (1.4.0) + aws-eventstream (~> 1, >= 1.0.2) + babosa (1.0.4) + claide (1.0.3) + colored (1.2) + colored2 (3.1.2) + commander (4.6.0) + highline (~> 2.0.0) + declarative (0.0.20) + digest-crc (0.6.4) + rake (>= 12.0.0, < 14.0.0) + domain_name (0.5.20190701) + unf (>= 0.0.5, < 1.0.0) + dotenv (2.7.6) + emoji_regex (3.2.3) + excon (0.89.0) + faraday (1.8.0) + faraday-em_http (~> 1.0) + faraday-em_synchrony (~> 1.0) + faraday-excon (~> 1.1) + faraday-httpclient (~> 1.0.1) + faraday-net_http (~> 1.0) + faraday-net_http_persistent (~> 1.1) + faraday-patron (~> 1.0) + faraday-rack (~> 1.0) + multipart-post (>= 1.2, < 3) + ruby2_keywords (>= 0.0.4) + faraday-cookie_jar (0.0.7) + faraday (>= 0.8.0) + http-cookie (~> 1.0.0) + faraday-em_http (1.0.0) + faraday-em_synchrony (1.0.0) + faraday-excon (1.1.0) + faraday-httpclient (1.0.1) + faraday-net_http (1.0.1) + faraday-net_http_persistent (1.2.0) + faraday-patron (1.0.0) + faraday-rack (1.0.0) + faraday_middleware (1.2.0) + faraday (~> 1.0) + fastimage (2.2.6) + fastlane (2.199.0) + CFPropertyList (>= 2.3, < 4.0.0) + addressable (>= 2.8, < 3.0.0) + artifactory (~> 3.0) + aws-sdk-s3 (~> 1.0) + babosa (>= 1.0.3, < 2.0.0) + bundler (>= 1.12.0, < 3.0.0) + colored + commander (~> 4.6) + dotenv (>= 2.1.1, < 3.0.0) + emoji_regex (>= 0.1, < 4.0) + excon (>= 0.71.0, < 1.0.0) + faraday (~> 1.0) + faraday-cookie_jar (~> 0.0.6) + faraday_middleware (~> 1.0) + fastimage (>= 2.1.0, < 3.0.0) + gh_inspector (>= 1.1.2, < 2.0.0) + google-apis-androidpublisher_v3 (~> 0.3) + google-apis-playcustomapp_v1 (~> 0.1) + google-cloud-storage (~> 1.31) + highline (~> 2.0) + json (< 3.0.0) + jwt (>= 2.1.0, < 3) + mini_magick (>= 4.9.4, < 5.0.0) + multipart-post (~> 2.0.0) + naturally (~> 2.2) + optparse (~> 0.1.1) + plist (>= 3.1.0, < 4.0.0) + rubyzip (>= 2.0.0, < 3.0.0) + security (= 0.1.3) + simctl (~> 1.6.3) + terminal-notifier (>= 2.0.0, < 3.0.0) + terminal-table (>= 1.4.5, < 2.0.0) + tty-screen (>= 0.6.3, < 1.0.0) + tty-spinner (>= 0.8.0, < 1.0.0) + word_wrap (~> 1.0.0) + xcodeproj (>= 1.13.0, < 2.0.0) + xcpretty (~> 0.3.0) + xcpretty-travis-formatter (>= 0.0.3) + gh_inspector (1.1.3) + google-apis-androidpublisher_v3 (0.14.0) + google-apis-core (>= 0.4, < 2.a) + google-apis-core (0.4.1) + addressable (~> 2.5, >= 2.5.1) + googleauth (>= 0.16.2, < 2.a) + httpclient (>= 2.8.1, < 3.a) + mini_mime (~> 1.0) + representable (~> 3.0) + retriable (>= 2.0, < 4.a) + rexml + webrick + google-apis-iamcredentials_v1 (0.9.0) + google-apis-core (>= 0.4, < 2.a) + google-apis-playcustomapp_v1 (0.6.0) + google-apis-core (>= 0.4, < 2.a) + google-apis-storage_v1 (0.10.0) + google-apis-core (>= 0.4, < 2.a) + google-cloud-core (1.6.0) + google-cloud-env (~> 1.0) + google-cloud-errors (~> 1.0) + google-cloud-env (1.5.0) + faraday (>= 0.17.3, < 2.0) + google-cloud-errors (1.2.0) + google-cloud-storage (1.35.0) + addressable (~> 2.8) + digest-crc (~> 0.4) + google-apis-iamcredentials_v1 (~> 0.1) + google-apis-storage_v1 (~> 0.1) + google-cloud-core (~> 1.6) + googleauth (>= 0.16.2, < 2.a) + mini_mime (~> 1.0) + googleauth (1.1.0) + faraday (>= 0.17.3, < 2.0) + jwt (>= 1.4, < 3.0) + memoist (~> 0.16) + multi_json (~> 1.11) + os (>= 0.9, < 2.0) + signet (>= 0.16, < 2.a) + highline (2.0.3) + http-cookie (1.0.4) + domain_name (~> 0.5) + httpclient (2.8.3) + jmespath (1.4.0) + json (2.6.1) + jwt (2.3.0) + memoist (0.16.2) + mini_magick (4.11.0) + mini_mime (1.1.2) + multi_json (1.15.0) + multipart-post (2.0.0) + nanaimo (0.3.0) + naturally (2.2.1) + optparse (0.1.1) + os (1.1.4) + plist (3.6.0) + public_suffix (4.0.6) + rake (13.0.6) + representable (3.1.1) + declarative (< 0.1.0) + trailblazer-option (>= 0.1.1, < 0.2.0) + uber (< 0.2.0) + retriable (3.1.2) + rexml (3.2.5) + rouge (2.0.7) + ruby2_keywords (0.0.5) + rubyzip (2.3.2) + security (0.1.3) + signet (0.16.0) + addressable (~> 2.8) + faraday (>= 0.17.3, < 2.0) + jwt (>= 1.5, < 3.0) + multi_json (~> 1.10) + simctl (1.6.8) + CFPropertyList + naturally + terminal-notifier (2.0.0) + terminal-table (1.8.0) + unicode-display_width (~> 1.1, >= 1.1.1) + trailblazer-option (0.1.2) + tty-cursor (0.7.1) + tty-screen (0.8.1) + tty-spinner (0.9.3) + tty-cursor (~> 0.7) + uber (0.1.0) + unf (0.1.4) + unf_ext + unf_ext (0.0.8-x64-mingw32) + unicode-display_width (1.8.0) + webrick (1.7.0) + word_wrap (1.0.0) + xcodeproj (1.21.0) + CFPropertyList (>= 2.3.3, < 4.0) + atomos (~> 0.1.3) + claide (>= 1.0.2, < 2.0) + colored2 (~> 3.1) + nanaimo (~> 0.3.0) + rexml (~> 3.2.4) + xcpretty (0.3.0) + rouge (~> 2.0.7) + xcpretty-travis-formatter (1.0.1) + xcpretty (~> 0.2, >= 0.0.7) + +PLATFORMS + x64-mingw32 + +DEPENDENCIES + fastlane + +BUNDLED WITH + 2.2.32 diff --git a/app/build.gradle b/app/build.gradle index 45554c7..0ec3f07 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -9,36 +9,87 @@ plugins { } android { + + signingConfigs { + + release { + + def keystorePropertiesFile = rootProject.file("./keystore.properties") + def keystoreProperties = new Properties() + try { + keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) + } catch (FileNotFoundException ignored) { + keystoreProperties = null + } + + if (keystoreProperties != null) { + storePassword keystoreProperties["storePassword"] + keyPassword keystoreProperties["keyPassword"] + keyAlias keystoreProperties["keyAlias"] + storeFile file(keystoreProperties["storeFile"]) + } + + } + + } + compileSdk 31 defaultConfig { + // App ID applicationId "com.dzeio.openhealth" + + // Android 5 Lollipop minSdk 21 + + // Android 12 targetSdk 31 + versionCode 1 - versionName "1.0" + + // Semantic Versioning + versionName "1.0.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } buildTypes { + release { - minifyEnabled false + minifyEnabled true proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + signingConfig signingConfigs.release } + + debug { + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + applicationIdSuffix ".dev" + versionNameSuffix '-dev' + debuggable true + + // make it debuggable + renderscriptDebuggable true + + // Optimization Level + renderscriptOptimLevel 0 + } + } + compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } + kotlinOptions { jvmTarget = '1.8' } + buildFeatures { viewBinding true dataBinding true - } + } dependencies { @@ -81,6 +132,7 @@ dependencies { // Samsung Health implementation files('libs/samsung-health-data-1.5.0.aar') + implementation "com.google.code.gson:gson:2.8.9" // ROOM def room_version = "2.4.0" diff --git a/app/src/debug/ic_launcher-playstore.png b/app/src/debug/ic_launcher-playstore.png new file mode 100644 index 0000000..70f398c Binary files /dev/null and b/app/src/debug/ic_launcher-playstore.png differ diff --git a/app/src/debug/res/drawable-v24/ic_launcher_foreground.xml b/app/src/debug/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..5ec3d6a --- /dev/null +++ b/app/src/debug/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/debug/res/drawable/ic_logo_app.xml b/app/src/debug/res/drawable/ic_logo_app.xml new file mode 100644 index 0000000..357dd1c --- /dev/null +++ b/app/src/debug/res/drawable/ic_logo_app.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/src/debug/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/debug/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..7353dbd --- /dev/null +++ b/app/src/debug/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/debug/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/debug/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..7353dbd --- /dev/null +++ b/app/src/debug/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/debug/res/mipmap-hdpi/ic_launcher.png b/app/src/debug/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..1321a47 Binary files /dev/null and b/app/src/debug/res/mipmap-hdpi/ic_launcher.png differ diff --git a/app/src/debug/res/mipmap-hdpi/ic_launcher_round.png b/app/src/debug/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 0000000..c334358 Binary files /dev/null and b/app/src/debug/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/app/src/debug/res/mipmap-mdpi/ic_launcher.png b/app/src/debug/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..e962867 Binary files /dev/null and b/app/src/debug/res/mipmap-mdpi/ic_launcher.png differ diff --git a/app/src/debug/res/mipmap-mdpi/ic_launcher_round.png b/app/src/debug/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 0000000..3d45a57 Binary files /dev/null and b/app/src/debug/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/app/src/debug/res/mipmap-xhdpi/ic_launcher.png b/app/src/debug/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..9a1fb34 Binary files /dev/null and b/app/src/debug/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/app/src/debug/res/mipmap-xhdpi/ic_launcher_round.png b/app/src/debug/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000..24fde3b Binary files /dev/null and b/app/src/debug/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/app/src/debug/res/mipmap-xxhdpi/ic_launcher.png b/app/src/debug/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..b3bffc0 Binary files /dev/null and b/app/src/debug/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/app/src/debug/res/mipmap-xxhdpi/ic_launcher_round.png b/app/src/debug/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..32e2771 Binary files /dev/null and b/app/src/debug/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/app/src/debug/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/debug/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..693ce32 Binary files /dev/null and b/app/src/debug/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/app/src/debug/res/mipmap-xxxhdpi/ic_launcher_round.png b/app/src/debug/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..8c77946 Binary files /dev/null and b/app/src/debug/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/app/src/debug/res/values/ic_launcher_background.xml b/app/src/debug/res/values/ic_launcher_background.xml new file mode 100644 index 0000000..c5d5899 --- /dev/null +++ b/app/src/debug/res/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #FFFFFF + \ No newline at end of file diff --git a/app/src/debug/res/values/strings.xml b/app/src/debug/res/values/strings.xml new file mode 100644 index 0000000..5863d12 --- /dev/null +++ b/app/src/debug/res/values/strings.xml @@ -0,0 +1,4 @@ + + + OpenHealth - Debug + \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 634afa5..600b804 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -6,8 +6,8 @@ - - + + diff --git a/app/src/main/java/com/dzeio/openhealth/Application.kt b/app/src/main/java/com/dzeio/openhealth/Application.kt index d845fc2..bb2ee43 100644 --- a/app/src/main/java/com/dzeio/openhealth/Application.kt +++ b/app/src/main/java/com/dzeio/openhealth/Application.kt @@ -1,6 +1,7 @@ package com.dzeio.openhealth import android.app.Application +import com.google.android.material.color.DynamicColors import dagger.hilt.android.HiltAndroidApp @HiltAndroidApp @@ -8,4 +9,11 @@ class Application : Application() { companion object { const val TAG = "OpenHealth" } + + override fun onCreate() { + super.onCreate() + + // Android Dynamics Colors + DynamicColors.applyToActivitiesIfAvailable(this) + } } \ No newline at end of file diff --git a/app/src/main/java/com/dzeio/openhealth/MainActivity.kt b/app/src/main/java/com/dzeio/openhealth/MainActivity.kt index 977af20..e8c9d8c 100644 --- a/app/src/main/java/com/dzeio/openhealth/MainActivity.kt +++ b/app/src/main/java/com/dzeio/openhealth/MainActivity.kt @@ -10,22 +10,17 @@ import android.util.Log import android.view.LayoutInflater import android.view.Menu import android.view.MenuItem -import androidx.core.content.ContextCompat -import androidx.lifecycle.lifecycleScope import androidx.navigation.NavController import androidx.navigation.fragment.NavHostFragment -import androidx.navigation.fragment.findNavController import androidx.navigation.ui.AppBarConfiguration import androidx.navigation.ui.navigateUp import androidx.navigation.ui.setupActionBarWithNavController -import androidx.navigation.ui.setupWithNavController import androidx.work.WorkManager import com.dzeio.openhealth.core.BaseActivity import com.dzeio.openhealth.databinding.ActivityMainBinding import com.dzeio.openhealth.interfaces.NotificationChannels import com.dzeio.openhealth.services.WaterReminderService import com.dzeio.openhealth.ui.home.HomeFragmentDirections -import com.google.android.material.dialog.MaterialAlertDialogBuilder import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint @@ -45,6 +40,7 @@ class MainActivity : BaseActivity() { val navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment + navController = navHostFragment.navController appBarConfiguration = AppBarConfiguration( @@ -69,9 +65,8 @@ class MainActivity : BaseActivity() { return true } - override fun onSupportNavigateUp(): Boolean { - return navController.navigateUp(appBarConfiguration) || super.onSupportNavigateUp() - } + override fun onSupportNavigateUp(): Boolean = + navController.navigateUp(appBarConfiguration) || super.onSupportNavigateUp() override fun onRequestPermissionsResult( requestCode: Int, @@ -94,21 +89,23 @@ class MainActivity : BaseActivity() { } private fun createNotificationChannel() { - // Create the NotificationChannel, but only on API 26+ because - // the NotificationChannel class is new and not in the support library - val notificationManager: NotificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { for (channel in NotificationChannels.values()) { - notificationManager.createNotificationChannel( - NotificationChannel( - channel.id, - channel.channelName, - channel.importance + Log.d("MainActivity", channel.channelName) + try { + notificationManager.createNotificationChannel( + NotificationChannel( + channel.id, + channel.channelName, + channel.importance + ) ) - ) + } catch (e: Exception) { + Log.e("MainActivity", "Error Creating Notification Channel", e) + } } } diff --git a/app/src/main/java/com/dzeio/openhealth/adapters/ExtensionAdapter.kt b/app/src/main/java/com/dzeio/openhealth/adapters/ExtensionAdapter.kt new file mode 100644 index 0000000..9b3d94a --- /dev/null +++ b/app/src/main/java/com/dzeio/openhealth/adapters/ExtensionAdapter.kt @@ -0,0 +1,28 @@ +package com.dzeio.openhealth.adapters + +import android.view.LayoutInflater +import android.view.ViewGroup +import com.dzeio.openhealth.core.BaseAdapter +import com.dzeio.openhealth.core.BaseViewHolder +import com.dzeio.openhealth.databinding.LayoutExtensionItemBinding +import com.dzeio.openhealth.extensions.Extension + +class ExtensionAdapter() : BaseAdapter() { + + override val bindingInflater: (LayoutInflater, ViewGroup?, Boolean) -> LayoutExtensionItemBinding + get() = LayoutExtensionItemBinding::inflate + + var onItemClick: ((weight: Extension) -> Unit)? = null + + override fun onBindData( + holder: BaseViewHolder, + item: Extension, + position: Int + ) { + holder.binding.name.text = item.name + holder.binding.status.text = item.getStatus() + holder.binding.card.setOnClickListener { + onItemClick?.invoke(item) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/dzeio/openhealth/adapters/WaterAdapter.kt b/app/src/main/java/com/dzeio/openhealth/adapters/WaterAdapter.kt index bdeaf07..2669c3a 100644 --- a/app/src/main/java/com/dzeio/openhealth/adapters/WaterAdapter.kt +++ b/app/src/main/java/com/dzeio/openhealth/adapters/WaterAdapter.kt @@ -1,14 +1,11 @@ package com.dzeio.openhealth.adapters -import android.util.Log import android.view.LayoutInflater import android.view.ViewGroup import com.dzeio.openhealth.core.BaseAdapter import com.dzeio.openhealth.core.BaseViewHolder import com.dzeio.openhealth.data.water.Water -import com.dzeio.openhealth.data.weight.Weight import com.dzeio.openhealth.databinding.LayoutItemListBinding -import com.dzeio.openhealth.databinding.LayoutItemWeightBinding class WaterAdapter() : BaseAdapter() { @@ -23,7 +20,7 @@ class WaterAdapter() : BaseAdapter() { position: Int ) { holder.binding.value.text = "${item.value}ml" - holder.binding.datetime.text = item.formatTimestamp() + holder.binding.datetime.text = "${item.formatTimestamp()} ${item.timestamp}" holder.binding.edit.setOnClickListener { onItemClick?.invoke(item) } diff --git a/app/src/main/java/com/dzeio/openhealth/adapters/WeightAdapter.kt b/app/src/main/java/com/dzeio/openhealth/adapters/WeightAdapter.kt index f88f3ab..783ccba 100644 --- a/app/src/main/java/com/dzeio/openhealth/adapters/WeightAdapter.kt +++ b/app/src/main/java/com/dzeio/openhealth/adapters/WeightAdapter.kt @@ -1,26 +1,25 @@ package com.dzeio.openhealth.adapters -import android.util.Log import android.view.LayoutInflater import android.view.ViewGroup import com.dzeio.openhealth.core.BaseAdapter import com.dzeio.openhealth.core.BaseViewHolder import com.dzeio.openhealth.data.weight.Weight -import com.dzeio.openhealth.databinding.LayoutItemWeightBinding +import com.dzeio.openhealth.databinding.LayoutItemListBinding -class WeightAdapter() : BaseAdapter() { +class WeightAdapter() : BaseAdapter() { - override val bindingInflater: (LayoutInflater, ViewGroup?, Boolean) -> LayoutItemWeightBinding - get() = LayoutItemWeightBinding::inflate + override val bindingInflater: (LayoutInflater, ViewGroup?, Boolean) -> LayoutItemListBinding + get() = LayoutItemListBinding::inflate var onItemClick: ((weight: Weight) -> Unit)? = null override fun onBindData( - holder: BaseViewHolder, + holder: BaseViewHolder, item: Weight, position: Int ) { - holder.binding.weight.text = "${item.weight}kg" + holder.binding.value.text = "${item.weight}kg" holder.binding.datetime.text = item.formatTimestamp() holder.binding.edit.setOnClickListener { onItemClick?.invoke(item) diff --git a/app/src/main/java/com/dzeio/openhealth/data/water/WaterDao.kt b/app/src/main/java/com/dzeio/openhealth/data/water/WaterDao.kt index 09a361a..87995a9 100644 --- a/app/src/main/java/com/dzeio/openhealth/data/water/WaterDao.kt +++ b/app/src/main/java/com/dzeio/openhealth/data/water/WaterDao.kt @@ -1,14 +1,14 @@ package com.dzeio.openhealth.data.water -import androidx.room.* +import androidx.room.Dao +import androidx.room.Query import com.dzeio.openhealth.core.BaseDao -import dagger.Provides import kotlinx.coroutines.flow.Flow @Dao interface WaterDao : BaseDao { - @Query("SELECT * FROM Water ORDER BY timestamp") + @Query("SELECT * FROM Water ORDER BY timestamp DESC") fun getAll(): Flow> @Query("SELECT * FROM Water where id = :weightId") diff --git a/app/src/main/java/com/dzeio/openhealth/data/weight/Weight.kt b/app/src/main/java/com/dzeio/openhealth/data/weight/Weight.kt index 8199d34..032eafb 100644 --- a/app/src/main/java/com/dzeio/openhealth/data/weight/Weight.kt +++ b/app/src/main/java/com/dzeio/openhealth/data/weight/Weight.kt @@ -7,8 +7,9 @@ import java.sql.Date import java.text.DateFormat.getDateInstance @Entity() -data class Weight ( - @PrimaryKey(autoGenerate = true) var id: Long = 0, +data class Weight( + @PrimaryKey(autoGenerate = true) + var id: Long = 0, var weight: Float = 0f, @ColumnInfo(index = true) var timestamp: Long = System.currentTimeMillis(), diff --git a/app/src/main/java/com/dzeio/openhealth/extensions/DataType.kt b/app/src/main/java/com/dzeio/openhealth/extensions/DataType.kt deleted file mode 100644 index e7777d6..0000000 --- a/app/src/main/java/com/dzeio/openhealth/extensions/DataType.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.dzeio.openhealth.extensions - -enum class DataType { - WEIGHT -} - -/** - * STEP_COUNT_CUMULATIVE - * ACTIVITY_SEGMENT - * SLEEP_SEGMENT - * CALORIES_EXPENDED - * BASAL_METABOLIC_RATE - * POWER_SAMPLE - * HEART_RATE_BPM - * LOCATION_SAMPLE - */ \ No newline at end of file diff --git a/app/src/main/java/com/dzeio/openhealth/extensions/Extension.kt b/app/src/main/java/com/dzeio/openhealth/extensions/Extension.kt index a42b04a..89dff24 100644 --- a/app/src/main/java/com/dzeio/openhealth/extensions/Extension.kt +++ b/app/src/main/java/com/dzeio/openhealth/extensions/Extension.kt @@ -2,18 +2,103 @@ package com.dzeio.openhealth.extensions import android.app.Activity import android.content.Intent +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData import com.dzeio.openhealth.data.weight.Weight +/** + * Extension Schema + * + * Version: 1.0.0 + */ abstract class Extension { - enum class Data { - WEIGHT, - STEPS + data class ImportState( + val state: States = States.WIP, + val list: List = ArrayList() + ) + + enum class States { + WIP, + DONE, + CANCELLED } - abstract val sourceID: String + enum class Data { + /** + * Special case to handle basic errors from other activities + */ + NOTHING, + WEIGHT, + STEPS - open fun init(activity: Activity): Array = arrayOf() + /** + * STEP_COUNT_CUMULATIVE + * ACTIVITY_SEGMENT + * SLEEP_SEGMENT + * CALORIES_EXPENDED + * BASAL_METABOLIC_RATE + * POWER_SAMPLE + * HEART_RATE_BPM + * LOCATION_SAMPLE + */ + } + + /** + * the Source ID + * + * DO NOT CHANGE IT AFTER THE EXTENSION IS IN PRODUCTION + */ + abstract val id: String + + /** + * The Extension Display Name + */ + abstract val name: String + + /** + * Initialize hte Extension + * + * It is run Before any functions is launched and after events handlers are set + */ + abstract fun init(activity: Activity): Array + + /** + * A status shown on the extension list page + */ + open fun getStatus(): String { + return "No Status set..." + } + + /** + * Function that will check + */ + abstract fun isAvailable(): Boolean + + /** + * idk + */ + abstract fun isConnected(): Boolean + + open fun connect(): LiveData { + return MutableLiveData(States.DONE) + } + + open fun importWeight(): LiveData> { + return MutableLiveData(ImportState(States.DONE)) + } + + /** + * function run when outgoing sync is enabled and new value is added + * or manual export is launched + */ + open fun exportWeight(weight: Weight): LiveData { + return MutableLiveData(States.DONE) + } + + /** + * Activity Events + */ /** * Same as Activity/Fragment onRequestPermissionResult @@ -26,8 +111,4 @@ abstract class Extension { * Same as Activity/Fragment onActivityResult */ open fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {} - - open fun import(data: Data, cb: (item: T, end: Boolean) -> Unit) {} - - open fun importWeight(callback: (weight: Weight, end: Boolean) -> Unit) {} } \ No newline at end of file diff --git a/app/src/main/java/com/dzeio/openhealth/extensions/ExtensionFactory.kt b/app/src/main/java/com/dzeio/openhealth/extensions/ExtensionFactory.kt new file mode 100644 index 0000000..19c6f6d --- /dev/null +++ b/app/src/main/java/com/dzeio/openhealth/extensions/ExtensionFactory.kt @@ -0,0 +1,16 @@ +package com.dzeio.openhealth.extensions + +class ExtensionFactory { + companion object { + fun getExtension(extension: String): Extension? { + return when (extension) { + "GoogleFit" -> { + GoogleFit() + } + else -> { + null + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/dzeio/openhealth/extensions/GoogleFit.kt b/app/src/main/java/com/dzeio/openhealth/extensions/GoogleFit.kt index 1cbf5e4..8bce6ec 100644 --- a/app/src/main/java/com/dzeio/openhealth/extensions/GoogleFit.kt +++ b/app/src/main/java/com/dzeio/openhealth/extensions/GoogleFit.kt @@ -7,69 +7,114 @@ import android.content.pm.PackageManager import android.os.Build import android.util.Log import androidx.core.app.ActivityCompat +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData import com.dzeio.openhealth.data.weight.Weight import com.google.android.gms.auth.api.signin.GoogleSignIn import com.google.android.gms.fitness.Fitness import com.google.android.gms.fitness.FitnessOptions -import com.google.android.gms.fitness.data.DataPoint import com.google.android.gms.fitness.data.DataType import com.google.android.gms.fitness.request.DataReadRequest import java.text.DateFormat import java.util.* import java.util.concurrent.TimeUnit -class GoogleFit( - private val activity: Activity, -) : Extension() { +class GoogleFit() : Extension() { companion object { const val TAG = "GoogleFitConnector" } - override val sourceID: String = "GoogleFit" + private lateinit var activity: Activity + + override val id = "GoogleFit" + override val name = "Google Fit" + + override fun init(activity: Activity): Array { + this.activity = activity + return arrayOf( + Data.WEIGHT + ) + } + + override fun getStatus(): String { + return if (isConnected()) "Connected" else "Not Connected" + } + + override fun isAvailable(): Boolean { + return true + } + + override fun isConnected(): Boolean = + GoogleSignIn.hasPermissions(getGoogleAccount(), fitnessOptions) private val fitnessOptions = FitnessOptions.builder() .addDataType(DataType.TYPE_WEIGHT) - .addDataType(DataType.TYPE_STEP_COUNT_CUMULATIVE) +// .addDataType(DataType.TYPE_STEP_COUNT_CUMULATIVE) +// .addDataType(DataType.TYPE_CALORIES_EXPENDED) .build() - private fun checkPermissionsAndRun(data: Data) { - if (permissionApproved()) { - signIn(data) - } else { - Log.d(TAG, "Asking for permission") - // Ask for permission - ActivityCompat.requestPermissions( - activity, - arrayOf(Manifest.permission.ACCESS_FINE_LOCATION), - data.ordinal - ) - } - } +// private fun checkPermissionsAndRun(data: Data) { +// if (permissionApproved()) { +// signIn(data) +// } else { +// Log.d(TAG, "Asking for permission") +// // Ask for permission +// ActivityCompat.requestPermissions( +// activity, +// arrayOf(Manifest.permission.ACCESS_FINE_LOCATION), +// data.ordinal +// ) +// } +// } - private fun permissionApproved(): Boolean { - val approved = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + private fun permissionApproved(): Boolean = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { PackageManager.PERMISSION_GRANTED == ActivityCompat.checkSelfPermission( activity, - Manifest.permission.ACCESS_FINE_LOCATION) + Manifest.permission.ACCESS_FINE_LOCATION + ) } else { true } - return approved - } - private fun signIn(data: Data) { - if (oAuthPermissionsApproved()) { - startImport(data) + private val connectLiveData: MutableLiveData = MutableLiveData(States.WIP) + + override fun connect(): LiveData { + + if (!permissionApproved()) { + ActivityCompat.requestPermissions( + activity, + arrayOf(Manifest.permission.ACCESS_FINE_LOCATION), + 87531 + ) + return connectLiveData + } + + if (isConnected()) { + connectLiveData.value = States.DONE } else { Log.d("GoogleFitImporter", "Signing In") GoogleSignIn.requestPermissions( activity, - data.ordinal, - getGoogleAccount(), fitnessOptions) + 124887, + getGoogleAccount(), fitnessOptions + ) } + return connectLiveData } - private fun oAuthPermissionsApproved() = GoogleSignIn.hasPermissions(getGoogleAccount(), fitnessOptions) +// private fun signIn(data: Data) { +// if (isConnected()) { +// startImport(data) +// } else { +// Log.d("GoogleFitImporter", "Signing In") +// GoogleSignIn.requestPermissions( +// activity, +// data.ordinal, +// getGoogleAccount(), fitnessOptions +// ) +// } +// } private fun getGoogleAccount() = GoogleSignIn.getAccountForExtension(activity, fitnessOptions) @@ -102,85 +147,116 @@ class GoogleFit( else -> {} } - runRequest(DataReadRequest.Builder() - .read(type) - .setTimeRange(timeRange[0], timeRange[1], timeUnit) - .build(), data) + runRequest( + DataReadRequest.Builder() + .read(type) + .setTimeRange(timeRange[0], timeRange[1], timeUnit) + .build(), data + ) } private fun runRequest(request: DataReadRequest, data: Data) { - Fitness.getHistoryClient(activity, GoogleSignIn.getAccountForExtension(activity, fitnessOptions)) + Fitness.getHistoryClient( + activity, + GoogleSignIn.getAccountForExtension(activity, fitnessOptions) + ) .readData(request) .addOnSuccessListener { response -> - Log.d(TAG, "Received response! ${response.dataSets.size} ${response.buckets.size} ${response.status}") + Log.d( + TAG, + "Received response! ${response.dataSets.size} ${response.buckets.size} ${response.status}" + ) for (dataSet in response.dataSets) { - Log.i(TAG, "Data returned for Data type: ${dataSet.dataType.name} ${dataSet.dataPoints.size}") - dataSet.dataPoints.forEachIndexed { index, dp -> - val isLast = (index + 1) == dataSet.dataPoints.size + Log.i( + TAG, + "Data returned for Data type: ${dataSet.dataType.name} ${dataSet.dataPoints.size} ${dataSet.dataSource.toDebugString()}" + ) + dataSet.dataPoints.forEach { dp -> // Global - Log.i(TAG,"Importing Data point:") - Log.i(TAG,"\tType: ${dp.dataType.name}") - Log.i(TAG,"\tStart: ${dp.getStartTimeString()}") - Log.i(TAG,"\tEnd: ${dp.getEndTimeString()}") + Log.i(TAG, "Importing Data point:") + Log.i(TAG, "\tType: ${dp.dataType.name}") + Log.i( + TAG, + "\tStart: ${Date(dp.getStartTime(TimeUnit.SECONDS) * 1000L).toLocaleString()}" + ) + Log.i( + TAG, + "\tEnd: ${Date(dp.getEndTime(TimeUnit.SECONDS) * 1000L).toLocaleString()}" + ) // Field Specifics for (field in dp.dataType.fields) { - Log.i(TAG,"\tField: ${field.name} Value: ${dp.getValue(field)}") + Log.i(TAG, "\tField: ${field.name} Value: ${dp.getValue(field)}") when (data) { Data.WEIGHT -> { val weight = Weight() weight.timestamp = dp.getStartTime(TimeUnit.MILLISECONDS) weight.weight = dp.getValue(field).asFloat() - weightCallback(weight, isLast) + val list = weightLiveData.value?.list?.toMutableList() + ?: ArrayList() + list.add(weight) + weightLiveData.value = + + ImportState(States.WIP, list) } else -> {} } } } + when (data) { + Data.WEIGHT -> { + weightLiveData.value = + ImportState( + States.DONE, weightLiveData.value?.list + ?: ArrayList() + ) + } + else -> {} + } } } .addOnFailureListener { e -> - Log.e(TAG,"There was an error reading data from Google Fit", e) + Log.e(TAG, "There was an error reading data from Google Fit", e) } } - private fun DataPoint.getStartTimeString(): String = Date(this.getStartTime(TimeUnit.SECONDS) * 1000L).toLocaleString() - - private fun DataPoint.getEndTimeString(): String = Date(this.getEndTime(TimeUnit.SECONDS) * 1000L).toLocaleString() - + /** + * Currently not usable + */ override fun onRequestPermissionResult( requestCode: Int, permission: Array, grantResult: IntArray ) { - signIn(Data.values()[requestCode]) + connect() + // signIn(Data.values()[requestCode]) } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - signIn(Data.values()[requestCode]) - } - - private lateinit var weightCallback: (weight: Weight, end: Boolean) -> Unit - - override fun importWeight(callback: (weight: Weight, end: Boolean) -> Unit) { - this.weightCallback = callback - checkPermissionsAndRun(Data.WEIGHT) - } - - private lateinit var callback : (item: Any, end: Boolean) -> Unit - - override fun import(data: Data, cb: (item: T, end: Boolean) -> Unit) { - callback = cb as (item: Any, end: Boolean) -> Unit - when (data) { - Data.WEIGHT -> { - checkPermissionsAndRun(data) - } - else -> { - Log.d(TAG, "PRRRRRRRRRRRRR") - } + if (requestCode == 0) { + return } + connectLiveData.value = States.DONE + //signIn(Data.values()[requestCode]) + } + + private lateinit var weightLiveData: MutableLiveData> + + override fun importWeight(): LiveData> { + + weightLiveData = MutableLiveData( + ImportState( + States.WIP + ) + ) + + startImport(Data.WEIGHT) + +// checkPermissionsAndRun(Data.WEIGHT) + + return weightLiveData } } \ No newline at end of file diff --git a/app/src/main/java/com/dzeio/openhealth/extensions/GoogleFit.kt.old b/app/src/main/java/com/dzeio/openhealth/extensions/GoogleFit.kt.old deleted file mode 100644 index 54254cb..0000000 --- a/app/src/main/java/com/dzeio/openhealth/extensions/GoogleFit.kt.old +++ /dev/null @@ -1,374 +0,0 @@ -package com.dzeio.openhealth.extensions - -import android.Manifest -import android.app.Activity -import android.content.pm.PackageManager -import android.os.Build -import android.util.Log -import androidx.annotation.RequiresApi -import androidx.core.app.ActivityCompat -import com.dzeio.openhealth.data.weight.Weight -import com.google.android.gms.auth.api.signin.GoogleSignIn -import com.google.android.gms.fitness.Fitness -import com.google.android.gms.fitness.FitnessOptions -import com.google.android.gms.fitness.data.DataPoint -import com.google.android.gms.fitness.data.DataSet -import com.google.android.gms.fitness.data.DataSource -import com.google.android.gms.fitness.data.DataType -import com.google.android.gms.fitness.request.DataReadRequest -import com.google.android.gms.fitness.request.DataSourcesRequest -import com.google.android.gms.fitness.request.OnDataPointListener -import com.google.android.gms.fitness.request.SensorRequest -import java.text.DateFormat -import java.text.SimpleDateFormat -import java.util.* -import java.util.concurrent.TimeUnit - -enum class ActionRequestCode { - FIND_DATA_SOURCES -} - -class GoogleFit( - private val activity: Activity, -) { - companion object { - const val TAG = "GoogleFitConnector" - } -// private val fitnessOptions = FitnessOptions.builder() -// .addDataType(DataType.TYPE_ACTIVITY_SEGMENT, FitnessOptions.ACCESS_READ) -// .addDataType(DataType.TYPE_ACTIVITY_SEGMENT, FitnessOptions.ACCESS_WRITE) -// -// .addDataType(DataType.TYPE_HEART_RATE_BPM, FitnessOptions.ACCESS_READ) -// .addDataType(DataType.TYPE_HEART_RATE_BPM, FitnessOptions.ACCESS_WRITE) -// -// .addDataType(DataType.TYPE_HEIGHT, FitnessOptions.ACCESS_READ) -// .addDataType(DataType.TYPE_HEIGHT, FitnessOptions.ACCESS_WRITE) -// -// .addDataType(DataType.TYPE_WEIGHT, FitnessOptions.ACCESS_READ) -// .addDataType(DataType.TYPE_WEIGHT, FitnessOptions.ACCESS_WRITE) -// .build() - - private val fitnessOptions = FitnessOptions.builder() -// .addDataType(DataType.TYPE_STEP_COUNT_CUMULATIVE) -// .addDataType(DataType.TYPE_ACTIVITY_SEGMENT) -// .addDataType(DataType.TYPE_SLEEP_SEGMENT) -// .addDataType(DataType.TYPE_CALORIES_EXPENDED) -// .addDataType(DataType.TYPE_BASAL_METABOLIC_RATE) -// .addDataType(DataType.TYPE_POWER_SAMPLE) -// .addDataType(DataType.TYPE_HEART_RATE_BPM) -// .addDataType(DataType.TYPE_LOCATION_SAMPLE) - .addDataType(DataType.TYPE_WEIGHT) - .build() - - private val runningQOrLater = - Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q - - // [START dataPointListener_variable_reference] - // Need to hold a reference to this listener, as it's passed into the "unregister" - // method in order to stop all sensors from sending data to this listener. - private var dataPointListener: OnDataPointListener? = null - - fun import() { - checkPermissionsAndRun(ActionRequestCode.FIND_DATA_SOURCES) - } - - private fun checkPermissionsAndRun(actionRequestCode: ActionRequestCode) { - if (permissionApproved()) { - signIn(actionRequestCode) - } else { - requestRuntimePermissions(actionRequestCode) - } - } - - private fun requestRuntimePermissions(requestCode: ActionRequestCode) { - val shouldProvideRationale = - ActivityCompat.shouldShowRequestPermissionRationale(activity, Manifest.permission.ACCESS_FINE_LOCATION) - - // Provide an additional rationale to the user. This would happen if the user denied the - // request previously, but didn't check the "Don't ask again" checkbox. - requestCode.let { - if (shouldProvideRationale) { - Log.i(TAG, "Displaying permission rationale to provide additional context.") -// ProgressDialog.show(activity, "Waiting for authorization...", "") - ActivityCompat.requestPermissions(activity, - arrayOf(Manifest.permission.ACCESS_FINE_LOCATION), - requestCode.ordinal) - } else { - Log.i(TAG, "Requesting permission") - // Request permission. It's possible this can be auto answered if device policy - // sets the permission in a given state or the user denied the permission - // previously and checked "Never ask again". - ActivityCompat.requestPermissions(activity, - arrayOf(Manifest.permission.ACCESS_FINE_LOCATION), - requestCode.ordinal) - } - } - } - - private fun permissionApproved(): Boolean { - val approved = if (runningQOrLater) { - PackageManager.PERMISSION_GRANTED == ActivityCompat.checkSelfPermission( - activity, - Manifest.permission.ACCESS_FINE_LOCATION) - } else { - true - } - return approved - } - - fun signIn(requestCode: ActionRequestCode) { - if (oAuthPermissionsApproved()) { - performActionForRequestCode(requestCode) - } else { - requestCode.let { - GoogleSignIn.requestPermissions( - activity, - it.ordinal, - getGoogleAccount(), fitnessOptions) - } - } - } - - private fun oAuthPermissionsApproved() = GoogleSignIn.hasPermissions(getGoogleAccount(), fitnessOptions) - - /** - * Gets a Google account for use in creating the Fitness client. This is achieved by either - * using the last signed-in account, or if necessary, prompting the user to sign in. - * `getAccountForExtension` is recommended over `getLastSignedInAccount` as the latter can - * return `null` if there has been no sign in before. - */ - private fun getGoogleAccount() = GoogleSignIn.getAccountForExtension(activity, fitnessOptions) - - /** - * Runs the desired method, based on the specified request code. The request code is typically - * passed to the Fit sign-in flow, and returned with the success callback. This allows the - * caller to specify which method, post-sign-in, should be called. - * - * @param requestCode The code corresponding to the action to perform. - */ - fun performActionForRequestCode(requestCode: ActionRequestCode) = when (requestCode) { - ActionRequestCode.FIND_DATA_SOURCES -> findFitnessDataSources() - } - - /** Finds available data sources and attempts to register on a specific [DataType]. */ - private fun findFitnessDataSources() { // [START find_data_sources] - // Note: Fitness.SensorsApi.findDataSources() requires the ACCESS_FINE_LOCATION permission. - Fitness.getSensorsClient(activity, getGoogleAccount()) - .findDataSources( - DataSourcesRequest.Builder() - .setDataTypes(DataType.TYPE_LOCATION_SAMPLE) - .setDataSourceTypes(DataSource.TYPE_RAW) - .build()) - .addOnSuccessListener { dataSources -> - for (dataSource in dataSources) { - Log.i(TAG, "Data source found: $dataSource") - Log.i(TAG, "Data Source type: " + dataSource.dataType.name) - // Let's register a listener to receive Activity data! - if (dataSource.dataType == DataType.TYPE_LOCATION_SAMPLE && dataPointListener == null) { - Log.i(TAG, "Data source for LOCATION_SAMPLE found! Registering.") - registerFitnessDataListener(dataSource, DataType.TYPE_LOCATION_SAMPLE) - } - } - } - .addOnFailureListener { e -> Log.e(TAG, "failed", e) } - // [END find_data_sources] - } - - /** - * Registers a listener with the Sensors API for the provided [DataSource] and [DataType] combo. - */ - private fun registerFitnessDataListener(dataSource: DataSource, dataType: DataType) { - // [START register_data_listener] - dataPointListener = OnDataPointListener { dataPoint -> - for (field in dataPoint.dataType.fields) { - val value = dataPoint.getValue(field) - Log.i(TAG, "Detected DataPoint field: ${field.name}") - Log.i(TAG, "Detected DataPoint value: $value") - } - } - Fitness.getSensorsClient(activity, getGoogleAccount()) - .add( - SensorRequest.Builder() - .setDataSource(dataSource) // Optional but recommended for custom data sets. - .setDataType(dataType) // Can't be omitted. - .setSamplingRate(10, TimeUnit.SECONDS) - .build(), - dataPointListener!! - ) - .addOnCompleteListener { task -> - if (task.isSuccessful) { - Log.i(TAG, "Listener registered!") - } else { - Log.e(TAG, "Listener not registered.", task.exception) - } - } - // [END register_data_listener] - } - - @RequiresApi(Build.VERSION_CODES.O) - fun getHistory() { - - val calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC")) - val now = Date() - calendar.time = now - val endTime = calendar.timeInMillis - calendar.set(Calendar.YEAR, 2013) // Set year to 2013 to be sure to get data from when Google Fit Started to today - val startTime = calendar.timeInMillis - val readRequest = DataReadRequest.Builder() - .aggregate(DataType.AGGREGATE_CALORIES_EXPENDED) - .bucketByActivityType(1, TimeUnit.SECONDS) - .setTimeRange(startTime, endTime, TimeUnit.SECONDS) - .build() - - Fitness.getHistoryClient(activity, GoogleSignIn.getAccountForExtension(activity, fitnessOptions)) - .readData(readRequest) - .addOnSuccessListener { response -> - // The aggregate query puts datasets into buckets, so flatten into a - // single list of datasets - for (dataSet in response.buckets.flatMap { it.dataSets }) { - dumpDataSet(dataSet) - } - } - .addOnFailureListener { e -> - Log.w(TAG,"There was an error reading data from Google Fit", e) - } - - } - - fun importWeight(callback : () -> Unit) { - - val calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC")) - val now = Date() - calendar.time = now - val endTime = calendar.timeInMillis - calendar.set(Calendar.YEAR, 2013) // Set year to 2013 to be sure to get data from when Google Fit Started to today - val startTime = calendar.timeInMillis - - val dateFormat = DateFormat.getDateInstance() - Log.i(TAG, "Range Start: ${dateFormat.format(startTime)}") - Log.i(TAG, "Range End: ${dateFormat.format(endTime)}") - - - runRequest(DataReadRequest.Builder() - // The data request can specify multiple data types to return, effectively - // combining multiple data queries into one call. - // In this example, it's very unlikely that the request is for several hundred - // datapoints each consisting of a few steps and a timestamp. The more likely - // scenario is wanting to see how many steps were walked per day, for 7 days. -// .aggregate() - .read(DataType.TYPE_WEIGHT) - - // Analogous to a "Group By" in SQL, defines how data should be aggregated. - // bucketByTime allows for a time span, whereas bucketBySession would allow - // bucketing by "sessions", which would need to be defined in code. -// .bucketByTime(1, TimeUnit.MINUTES) -// .bucketByActivityType(1, TimeUnit.SECONDS) -// .bucketBySession() - .setTimeRange(startTime, endTime, TimeUnit.MILLISECONDS) - .build(), callback) - - } - - private fun runRequest(request: DataReadRequest, callback: () -> Unit) { - Fitness.getHistoryClient(activity, GoogleSignIn.getAccountForExtension(activity, fitnessOptions)) - .readData(request) - .addOnSuccessListener { response -> - // The aggregate query puts datasets into buckets, so flatten into a - // single list of datasets - Log.d(TAG, "Received response! ${response.dataSets.size} ${response.buckets.size}") - for (dataSet in response.dataSets) { - dumpDataSet(dataSet) - } - for (dataSet in response.buckets.flatMap { it.dataSets }) { - dumpDataSet(dataSet) - } - callback.invoke() - } - .addOnFailureListener { e -> - Log.w(TAG,"There was an error reading data from Google Fit", e) - callback.invoke() - } - } - - private fun dumpDataSet(dataSet: DataSet) { - Log.i(TAG, "Data returned for Data type: ${dataSet.dataType.name} ${dataSet.dataPoints.size}") - for (dp in dataSet.dataPoints) { - val weight = Weight() - Log.i(TAG,"Data point:") - Log.i(TAG,"\tType: ${dp.dataType.name}") - Log.i(TAG,"\tStart: ${dp.getStartTimeString()}") - Log.i(TAG,"\tEnd: ${dp.getEndTimeString()}") - weight.timestamp = dp.getStartTime(TimeUnit.SECONDS) - weight.source = "GoogleFit" - for (field in dp.dataType.fields) { - weight.weight = dp.getValue(field).asFloat() - Log.i(TAG,"\tField: ${field.name.toString()} Value: ${dp.getValue(field)}") - } - // AppDatabase.getInstance(activity).weightDao().insert(weight) - } - } - - fun DataPoint.getStartTimeString(): String = - SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSSSSS", Locale.FRANCE) - .format(Date(this.getStartTime(TimeUnit.SECONDS) * 1000L)) - - fun DataPoint.getEndTimeString(): String = - SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSSSSS", Locale.FRANCE) - .format(Date(this.getEndTime(TimeUnit.SECONDS) * 1000L)) - - - /** Unregisters the listener with the Sensors API. */ - private fun unregisterFitnessDataListener() { - if (dataPointListener == null) { - // This code only activates one listener at a time. If there's no listener, there's - // nothing to unregister. - return - } - // [START unregister_data_listener] - // Waiting isn't actually necessary as the unregister call will complete regardless, - // even if called from within onStop, but a callback can still be added in order to - // inspect the results. - Fitness.getSensorsClient(activity, getGoogleAccount()) - .remove(dataPointListener!!) - .addOnCompleteListener { task -> - if (task.isSuccessful && task.result!!) { - Log.i(TAG, "Listener was removed!") - } else { - Log.i(TAG, "Listener was not removed.") - } - } - // [END unregister_data_listener] - } - - /** Returns a [DataReadRequest] for all step count changes in the past week. */ - private fun queryFitnessData(): DataReadRequest { - // [START build_read_data_request] - // Setting a start and end date using a range of 1 week before this moment. - val calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC")) - val now = Date() - calendar.time = now - val endTime = calendar.timeInMillis - calendar.add(Calendar.YEAR, -1) - val startTime = calendar.timeInMillis - - val dateFormat = DateFormat.getDateInstance() - Log.i(TAG, "Range Start: ${dateFormat.format(startTime)}") - Log.i(TAG, "Range End: ${dateFormat.format(endTime)}") - - return DataReadRequest.Builder() - // The data request can specify multiple data types to return, effectively - // combining multiple data queries into one call. - // In this example, it's very unlikely that the request is for several hundred - // datapoints each consisting of a few steps and a timestamp. The more likely - // scenario is wanting to see how many steps were walked per day, for 7 days. - .aggregate(DataType.TYPE_STEP_COUNT_DELTA) - // Analogous to a "Group By" in SQL, defines how data should be aggregated. - // bucketByTime allows for a time span, whereas bucketBySession would allow - // bucketing by "sessions", which would need to be defined in code. - .bucketByTime(1, TimeUnit.SECONDS) -// .bucketBySession() - .setTimeRange(startTime, endTime, TimeUnit.MILLISECONDS) - .build() - } - -} \ No newline at end of file diff --git a/app/src/main/java/com/dzeio/openhealth/extensions/samsunghealth/SamsungHealth.kt b/app/src/main/java/com/dzeio/openhealth/extensions/samsunghealth/SamsungHealth.kt index 14edd14..e301f7c 100644 --- a/app/src/main/java/com/dzeio/openhealth/extensions/samsunghealth/SamsungHealth.kt +++ b/app/src/main/java/com/dzeio/openhealth/extensions/samsunghealth/SamsungHealth.kt @@ -5,11 +5,12 @@ import android.content.Intent import android.os.Handler import android.os.Looper import android.util.Log -import com.dzeio.openhealth.extensions.Extension import com.dzeio.openhealth.data.weight.Weight -import com.samsung.android.sdk.healthdata.* +import com.samsung.android.sdk.healthdata.HealthConnectionErrorResult import com.samsung.android.sdk.healthdata.HealthConstants.StepCount +import com.samsung.android.sdk.healthdata.HealthDataStore import com.samsung.android.sdk.healthdata.HealthDataStore.ConnectionListener +import com.samsung.android.sdk.healthdata.HealthPermissionManager import com.samsung.android.sdk.healthdata.HealthPermissionManager.* @@ -18,7 +19,7 @@ import com.samsung.android.sdk.healthdata.HealthPermissionManager.* */ class SamsungHealth( private val context: Activity -) : Extension() { +) { companion object { const val TAG = "SamsungHealthConnector" @@ -33,6 +34,7 @@ class SamsungHealth( requestPermission() } } + override fun onConnectionFailed(p0: HealthConnectionErrorResult?) { Log.d(TAG, "Health data service is not available.") } @@ -43,7 +45,7 @@ class SamsungHealth( } - private val store : HealthDataStore = HealthDataStore(context, listener) + private val store: HealthDataStore = HealthDataStore(context, listener) private fun isPermissionAcquired(): Boolean { val permKey = PermissionKey(StepCount.HEALTH_DATA_TYPE, PermissionType.READ) @@ -86,23 +88,25 @@ class SamsungHealth( } } - private val reporter = StepCountReporter(store, stepCountObserver, Handler(Looper.getMainLooper())) + private val reporter = + StepCountReporter(store, stepCountObserver, Handler(Looper.getMainLooper())) /** * Connector */ - override val sourceID: String = "SamsungHealth" + val sourceID: String = "SamsungHealth" - override fun onRequestPermissionResult( + fun onRequestPermissionResult( requestCode: Int, permission: Array, grantResult: IntArray - ) {} + ) { + } - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {} + fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {} - override fun importWeight(callback: (weight: Weight, end: Boolean) -> Unit) { + fun importWeight(callback: (weight: Weight, end: Boolean) -> Unit) { store.connectService() } } \ No newline at end of file diff --git a/app/src/main/java/com/dzeio/openhealth/interfaces/NotificationChannels.kt b/app/src/main/java/com/dzeio/openhealth/interfaces/NotificationChannels.kt index 77ee1c0..b64a600 100644 --- a/app/src/main/java/com/dzeio/openhealth/interfaces/NotificationChannels.kt +++ b/app/src/main/java/com/dzeio/openhealth/interfaces/NotificationChannels.kt @@ -1,12 +1,10 @@ package com.dzeio.openhealth.interfaces -import android.app.NotificationManager - enum class NotificationChannels( val id: String, val channelName: String, val importance: Int ) { // 3 is IMPORTANCE_DEFAULT - DEFAULT("default", "Default Channel", 3) + DEFAULT("openhealth_default", "Default Channel", 3) } \ No newline at end of file diff --git a/app/src/main/java/com/dzeio/openhealth/ui/extension/ExtensionFragment.kt b/app/src/main/java/com/dzeio/openhealth/ui/extension/ExtensionFragment.kt new file mode 100644 index 0000000..2171953 --- /dev/null +++ b/app/src/main/java/com/dzeio/openhealth/ui/extension/ExtensionFragment.kt @@ -0,0 +1,59 @@ +package com.dzeio.openhealth.ui.extension + +import android.app.ProgressDialog +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.lifecycle.lifecycleScope +import androidx.navigation.fragment.navArgs +import com.dzeio.openhealth.core.BaseFragment +import com.dzeio.openhealth.databinding.FragmentExtensionBinding +import com.dzeio.openhealth.extensions.Extension +import com.dzeio.openhealth.extensions.ExtensionFactory +import dagger.hilt.android.AndroidEntryPoint +import java.lang.Exception + +@AndroidEntryPoint +class ExtensionFragment : + BaseFragment(ExtensionViewModel::class.java) { + + override val bindingInflater: (LayoutInflater, ViewGroup?, Boolean) -> FragmentExtensionBinding = + FragmentExtensionBinding::inflate + + + private val args: ExtensionFragmentArgs by navArgs() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + val extension = ExtensionFactory.getExtension(args.extension) + ?: throw Exception("No Extension found!") + + extension.init(requireActivity()) + + binding.importButton.setOnClickListener { + val dialog = ProgressDialog(requireContext()) + dialog.setTitle("Importing...") + dialog.setMessage("Imported 0 values") + dialog.show() + val data = extension.importWeight() + data.observe(viewLifecycleOwner) { state -> + Log.d("ExtensionFragment", state.state.name) + Log.d("ExtensionFragment", state.list.size.toString()) + dialog.setMessage("Imported ${state.list.size} values") + if (state.state == Extension.States.DONE) { + dialog.setMessage("Finishing Import...") + lifecycleScope.launchWhenStarted { + state.list.forEach { + it.source = extension.id + viewModel.importWeight(it) + } + dialog.dismiss() + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/dzeio/openhealth/ui/extension/ExtensionViewModel.kt b/app/src/main/java/com/dzeio/openhealth/ui/extension/ExtensionViewModel.kt new file mode 100644 index 0000000..74597fc --- /dev/null +++ b/app/src/main/java/com/dzeio/openhealth/ui/extension/ExtensionViewModel.kt @@ -0,0 +1,30 @@ +package com.dzeio.openhealth.ui.extension + +import androidx.lifecycle.MutableLiveData +import com.dzeio.openhealth.core.BaseViewModel +import com.dzeio.openhealth.data.weight.Weight +import com.dzeio.openhealth.data.weight.WeightRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject + +@HiltViewModel +class ExtensionViewModel @Inject internal constructor( + private val weightRepository: WeightRepository +) : BaseViewModel() { + + val text = MutableLiveData().apply { + value = "This is slideshow Fragment" + } + val importProgress = MutableLiveData().apply { + value = 0 + } + // If -1 progress is undetermined + // If 0 no progress bar + // Else progress bar + val importProgressTotal = MutableLiveData().apply { + value = 0 + } + + suspend fun importWeight(weight: Weight) = weightRepository.addWeight(weight) + suspend fun deleteFromSource(source: String) = weightRepository.deleteFromSource(source) +} \ No newline at end of file diff --git a/app/src/main/java/com/dzeio/openhealth/ui/extensions/ExtensionsFragment.kt b/app/src/main/java/com/dzeio/openhealth/ui/extensions/ExtensionsFragment.kt index 9cdd70b..cdb3831 100644 --- a/app/src/main/java/com/dzeio/openhealth/ui/extensions/ExtensionsFragment.kt +++ b/app/src/main/java/com/dzeio/openhealth/ui/extensions/ExtensionsFragment.kt @@ -10,13 +10,13 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.annotation.RequiresApi -import androidx.lifecycle.lifecycleScope -import com.dzeio.openhealth.extensions.Extension -import com.dzeio.openhealth.extensions.GoogleFit -//import com.dzeio.openhealth.connectors.GoogleFit -import com.dzeio.openhealth.extensions.samsunghealth.SamsungHealth +import androidx.navigation.fragment.findNavController +import androidx.recyclerview.widget.LinearLayoutManager +import com.dzeio.openhealth.adapters.ExtensionAdapter import com.dzeio.openhealth.core.BaseFragment import com.dzeio.openhealth.databinding.FragmentExtensionsBinding +import com.dzeio.openhealth.extensions.Extension +import com.dzeio.openhealth.extensions.GoogleFit import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint @@ -24,92 +24,55 @@ class ExtensionsFragment : BaseFragment(ExtensionsViewModel::class.java) { companion object { - const val TAG = "ImportFragment" + const val TAG = "ExtensionsFragment" } override val bindingInflater: (LayoutInflater, ViewGroup?, Boolean) -> FragmentExtensionsBinding = FragmentExtensionsBinding::inflate - private lateinit var progressDialog: ProgressDialog - - private lateinit var fit: Extension + private lateinit var activeExtension: Extension override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - progressDialog = ProgressDialog(requireContext()) + val recycler = binding.list - progressDialog.apply { - setCancelable(false) - setTitle("Importing from source...") - } + val manager = LinearLayoutManager(requireContext()) + recycler.layoutManager = manager - binding.importGoogleFit.setOnClickListener { - importFromGoogleFit() - } - binding.importSamsungHealth.setOnClickListener { - importFromSamsungHealth() - } - } - - private fun importFromGoogleFit() { - progressDialog.show() - fit = GoogleFit(requireActivity()) - - var imported = 0 - - lifecycleScope.launchWhenStarted { - viewModel.deleteFromSource(fit.sourceID) - }.invokeOnCompletion { - //progressDialog.show() - fit.importWeight { weight, end -> - Log.d("Importer", "Importing $weight") - weight.source = fit.sourceID - progressDialog.setTitle("Importing from source... ${++imported}") - lifecycleScope.launchWhenStarted { - viewModel.importWeight(weight) + val adapter = ExtensionAdapter() + adapter.onItemClick = { + activeExtension = it + Log.d(it.id, it.name) + if (it.isConnected()) { + Log.d(it.id, "Continue!") + findNavController().navigate( + ExtensionsFragmentDirections.actionNavExtensionsToNavExtension( + it.id + ) + ) + } else { + val ls = it.connect() + ls.observe(viewLifecycleOwner) { st -> + Log.d("States", st.name) } - if (end) { - Log.d("Importer", "Finished Importing") - progressDialog.dismiss() - return@importWeight - } - } } + recycler.adapter = adapter + val list = arrayOf( + GoogleFit() + ).toList() - } - - private fun importFromSamsungHealth() { - progressDialog.show() - fit = SamsungHealth(requireActivity()) - - var imported = 0 - - lifecycleScope.launchWhenStarted { - viewModel.deleteFromSource(fit.sourceID) - }.invokeOnCompletion { - //progressDialog.show() - fit.importWeight { weight, end -> - Log.d("Importer", "Importing $weight") - weight.source = fit.sourceID - progressDialog.setTitle("Importing from source... ${++imported}") - lifecycleScope.launchWhenStarted { - viewModel.importWeight(weight) - } - if (end) { - Log.d("Importer", "Finished Importing") - progressDialog.dismiss() - return@importWeight - } - - } + list.forEach { + it.init(requireActivity()) } + + adapter.set(list) } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - fit.onActivityResult(requestCode, resultCode, data) + activeExtension.onActivityResult(requestCode, resultCode, data) } @RequiresApi(Build.VERSION_CODES.O) @@ -126,7 +89,7 @@ class ExtensionsFragment : grantResults[0] == PackageManager.PERMISSION_GRANTED -> { Log.d(TAG, "Granted") - fit.onRequestPermissionResult(requestCode, permissions, grantResults) + activeExtension.onRequestPermissionResult(requestCode, permissions, grantResults) } else -> { // Permission denied. diff --git a/app/src/main/java/com/dzeio/openhealth/ui/home/HomeFragment.kt b/app/src/main/java/com/dzeio/openhealth/ui/home/HomeFragment.kt index 1b8ff51..c6326e7 100644 --- a/app/src/main/java/com/dzeio/openhealth/ui/home/HomeFragment.kt +++ b/app/src/main/java/com/dzeio/openhealth/ui/home/HomeFragment.kt @@ -1,9 +1,9 @@ package com.dzeio.openhealth.ui.home import android.animation.ValueAnimator -import android.graphics.BitmapFactory +import android.graphics.Bitmap import android.graphics.Canvas -import android.graphics.Rect +import android.graphics.RectF import android.os.Bundle import android.util.Log import android.view.LayoutInflater @@ -13,28 +13,21 @@ import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController import androidx.preference.PreferenceManager import com.dzeio.openhealth.Application -import com.dzeio.openhealth.R import com.dzeio.openhealth.core.BaseFragment import com.dzeio.openhealth.data.water.Water -import com.dzeio.openhealth.databinding.FragmentHomeBinding import com.dzeio.openhealth.data.weight.Weight +import com.dzeio.openhealth.databinding.FragmentHomeBinding import com.dzeio.openhealth.ui.weight.AddWeightDialog -import com.dzeio.openhealth.utils.BitmapUtils import com.dzeio.openhealth.utils.DrawUtils -import com.github.mikephil.charting.components.AxisBase -import com.github.mikephil.charting.components.Description -import com.github.mikephil.charting.components.XAxis +import com.dzeio.openhealth.utils.GraphUtils import com.github.mikephil.charting.data.Entry import com.github.mikephil.charting.data.LineData import com.github.mikephil.charting.data.LineDataSet -import com.github.mikephil.charting.formatter.ValueFormatter import com.google.android.material.color.MaterialColors import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch -import java.text.DateFormat -import java.text.SimpleDateFormat -import java.util.* import kotlin.math.min import kotlin.properties.Delegates @@ -53,8 +46,6 @@ class HomeFragment : BaseFragment(HomeViewMo override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - viewModel.init() - binding.addWeight.setOnClickListener { AddWeightDialog().show(requireActivity().supportFragmentManager, null) } @@ -112,14 +103,23 @@ class HomeFragment : BaseFragment(HomeViewMo } binding.listWeight.setOnClickListener { - Log.d("T", "Trying to move") - findNavController().navigate(HomeFragmentDirections.actionNavHomeToNavListWeight()) } binding.gotoWaterHome.setOnClickListener { findNavController().navigate(HomeFragmentDirections.actionNavHomeToNavWaterHome()) } + + GraphUtils.lineChartSetup( + binding.weightGraph, + MaterialColors.getColor( + requireView(), + com.google.android.material.R.attr.colorPrimary + ), MaterialColors.getColor( + requireView(), + com.google.android.material.R.attr.colorOnBackground + ) + ) } private fun updateGraph(list: List) { @@ -130,35 +130,9 @@ class HomeFragment : BaseFragment(HomeViewMo } val dataSet = LineDataSet(entries, "Label") + binding.weightGraph.apply { - // Setup - isAutoScaleMinMaxEnabled = true - legend.isEnabled = false - isDragEnabled = true - isScaleYEnabled = false - description = Description().apply { isEnabled = false } - isScaleXEnabled = true - setPinchZoom(false) - setDrawGridBackground(false) - setDrawBorders(false) - axisLeft.setLabelCount(0, true) - - xAxis.apply { - valueFormatter = object : ValueFormatter() { - override fun getAxisLabel(value: Float, axis: AxisBase?): String { - return SimpleDateFormat( - "yyyy-MM-dd", - Locale.getDefault() - ).format(Date(value.toLong())) - //return super.getAxisLabel(value, axis) - } - } - position = XAxis.XAxisPosition.BOTTOM - setDrawGridLines(false) - setLabelCount(3, true) - } - // Apply new dataset data = LineData(dataSet) @@ -181,6 +155,8 @@ class HomeFragment : BaseFragment(HomeViewMo viewModel.fetchWeights().collectLatest { updateGraph(it) } + updateWater(0) + updateWater(1234) } viewModel.water.observe(viewLifecycleOwner) { @@ -192,38 +168,75 @@ class HomeFragment : BaseFragment(HomeViewMo } } - updateWater(0) - } private fun updateWater(water: Int) { val oldValue = binding.fragmentHomeWaterCurrent.text.toString().replace("ml", "").toInt() binding.fragmentHomeWaterCurrent.text = "${water}ml" - val graph = BitmapUtils.convertToMutable( - requireContext(), - BitmapFactory.decodeResource(resources, R.drawable.ellipse) + var width = 1500 + var height = 750 + + if (binding.background.width != 0) { + width = binding.background.width + height = binding.background.height + } + + val graph = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) + Log.d("Test2", "$width $height") + + val canvas = Canvas(graph) + val rect = RectF( + 10f, + 15f, + 90f, + 85f ) - graph?.let { btmp -> - ValueAnimator.ofFloat(min(oldValue.toFloat(), intake), min(water.toFloat(), intake)) - .apply { - duration = 300 - addUpdateListener { - val canvas = Canvas(btmp) - DrawUtils.drawArc( - canvas, - 100 * it.animatedValue as Float / intake, - MaterialColors.getColor( - requireView(), - com.google.android.material.R.attr.colorPrimary - ) - ) - canvas.save() - binding.background.setImageBitmap(graph) - } - start() +// DrawUtils.drawRect( +// canvas, +// RectF( +// 0f, +// 0f, +// 100f, +// 100f +// ), +// MaterialColors.getColor( +// requireView(), +// com.google.android.material.R.attr.colorOnPrimary +// ), +// 3f +// ) + + DrawUtils.drawArc( + canvas, + 100f, + rect, + MaterialColors.getColor( + requireView(), + com.google.android.material.R.attr.colorOnPrimary + ), + 3f + ) + + Log.d("Test", "${min(oldValue.toFloat(), intake)} ${min(water.toFloat(), intake)}") + ValueAnimator.ofFloat(min(oldValue.toFloat(), intake), min(water.toFloat(), intake)) + .apply { + duration = 300 + addUpdateListener { + DrawUtils.drawArc( + canvas, + 100 * it.animatedValue as Float / intake, + rect, + MaterialColors.getColor( + requireView(), + com.google.android.material.R.attr.colorPrimary + ), 6f + ) + canvas.save() + binding.background.setImageBitmap(graph) } - } + start() + } } } \ No newline at end of file diff --git a/app/src/main/java/com/dzeio/openhealth/ui/home/HomeViewModel.kt b/app/src/main/java/com/dzeio/openhealth/ui/home/HomeViewModel.kt index 3c07b95..b1f824a 100644 --- a/app/src/main/java/com/dzeio/openhealth/ui/home/HomeViewModel.kt +++ b/app/src/main/java/com/dzeio/openhealth/ui/home/HomeViewModel.kt @@ -1,5 +1,6 @@ package com.dzeio.openhealth.ui.home +import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope import com.dzeio.openhealth.core.BaseViewModel @@ -18,8 +19,23 @@ class HomeViewModel @Inject internal constructor( private val waterRepository: WaterRepository ) : BaseViewModel() { + init { + viewModelScope.launch { + waterRepository.todayWater().collectLatest { + _water.value = it + } + } + } + + /** + * @deprecated + */ fun fetchWeights() = weightRepository.getWeights() + + /** + * @deprecated + */ fun lastWeight() = weightRepository.lastWeight() fun fetchWeight(id: Long) = weightRepository.getWeight(id) @@ -28,17 +44,11 @@ class HomeViewModel @Inject internal constructor( suspend fun addWeight(weight: Weight) = weightRepository.addWeight(weight) - fun fetchTodayWater() = waterRepository.todayWater() + suspend fun fetchTodayWater() = waterRepository.todayWater() - val water: MutableLiveData = MutableLiveData(null) + private val _water = MutableLiveData(null) + val water: LiveData = _water - fun init() { - viewModelScope.launch { - waterRepository.todayWater().collectLatest { - water.postValue(it) - } - } - } fun updateWater(water: Water) { viewModelScope.launch { @@ -49,7 +59,7 @@ class HomeViewModel @Inject internal constructor( fun deleteWater(item: Water) { viewModelScope.launch { waterRepository.deleteWater(item) - water.postValue(null) + _water.postValue(null) } } } \ No newline at end of file diff --git a/app/src/main/java/com/dzeio/openhealth/ui/water/EditWaterDialog.kt b/app/src/main/java/com/dzeio/openhealth/ui/water/EditWaterDialog.kt index 6b02880..2b683d7 100644 --- a/app/src/main/java/com/dzeio/openhealth/ui/water/EditWaterDialog.kt +++ b/app/src/main/java/com/dzeio/openhealth/ui/water/EditWaterDialog.kt @@ -1,7 +1,9 @@ package com.dzeio.openhealth.ui.water import android.app.Dialog +import android.os.Build import android.os.Bundle +import android.util.Log import android.view.LayoutInflater import android.view.MenuItem import android.view.Window @@ -13,8 +15,12 @@ import androidx.navigation.fragment.navArgs import com.dzeio.openhealth.R import com.dzeio.openhealth.core.BaseFullscreenDialog import com.dzeio.openhealth.databinding.DialogWaterEditWaterBinding +import com.google.android.material.datepicker.CalendarConstraints +import com.google.android.material.datepicker.DateValidatorPointBackward +import com.google.android.material.datepicker.MaterialDatePicker import com.google.android.material.dialog.MaterialAlertDialogBuilder import dagger.hilt.android.AndroidEntryPoint +import java.util.* @AndroidEntryPoint class EditWaterDialog : @@ -40,13 +46,53 @@ class EditWaterDialog : viewModel.water.observe(viewLifecycleOwner) { binding.editTextNumber.setText(it.value.toString()) + binding.date.text = it.formatTimestamp() } binding.editTextNumber.doOnTextChanged { text, start, before, count -> - newValue = text.toString().toInt() + val value = text.toString() + newValue = if (value == "") 0 + else text.toString().toInt() + } + + binding.date.setOnClickListener { + val water = viewModel.water.value!! + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + val date = Date(water.timestamp) + val datePicker = MaterialDatePicker.Builder.datePicker() + .setTitleText("Select Date") + .setSelection(water.timestamp) + .setCalendarConstraints( + CalendarConstraints.Builder() + .setValidator(DateValidatorPointBackward.now()) + .setEnd(Date().time) + .build() + ) + .build() + + val fragManager = requireActivity().supportFragmentManager + + datePicker.addOnPositiveButtonClickListener { tsp -> + + water.timestamp = tsp + binding.date.setText(water.formatTimestamp()) + + } + datePicker.show(fragManager, "dialog") + Log.d("Tag", "${date.year + 1900}, ${date.month}, ${date.day}") +// val dg = DatePickerDialog(requireActivity()) +// dg.setOnDateSetListener { _, year, month, day -> +// +// } +// dg.updateDate(date.year + 1900, date.month, date.day) +// dg.show() + } else { + TODO("VERSION.SDK_INT < N") + } + + } viewModel.init(args.id) - } private fun save() { diff --git a/app/src/main/java/com/dzeio/openhealth/ui/water/WaterHomeFragment.kt b/app/src/main/java/com/dzeio/openhealth/ui/water/WaterHomeFragment.kt index bd9d61b..7d78361 100644 --- a/app/src/main/java/com/dzeio/openhealth/ui/water/WaterHomeFragment.kt +++ b/app/src/main/java/com/dzeio/openhealth/ui/water/WaterHomeFragment.kt @@ -1,30 +1,20 @@ package com.dzeio.openhealth.ui.water -import android.graphics.Color import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController import androidx.recyclerview.widget.LinearLayoutManager import com.dzeio.openhealth.adapters.WaterAdapter -import com.dzeio.openhealth.adapters.WeightAdapter import com.dzeio.openhealth.core.BaseFragment -import com.dzeio.openhealth.databinding.FragmentListWeightBinding import com.dzeio.openhealth.databinding.FragmentMainWaterHomeBinding -import com.dzeio.openhealth.ui.home.HomeViewModel -import com.dzeio.openhealth.ui.weight.ListWeightFragmentDirections import com.dzeio.openhealth.utils.GraphUtils import com.github.mikephil.charting.data.BarData import com.github.mikephil.charting.data.BarDataSet import com.github.mikephil.charting.data.BarEntry -import com.github.mikephil.charting.data.Entry import com.google.android.material.color.MaterialColors import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.flow.collectLatest -import java.time.Instant -import java.time.temporal.ChronoUnit import java.util.* @AndroidEntryPoint @@ -60,6 +50,9 @@ class WaterHomeFragment : chart, MaterialColors.getColor( requireView(), com.google.android.material.R.attr.colorPrimary + ), MaterialColors.getColor( + requireView(), + com.google.android.material.R.attr.colorOnBackground ) ) @@ -73,7 +66,7 @@ class WaterHomeFragment : epoch.time = Date(0) epoch.add(Calendar.MILLISECOND, it.timestamp.toInt()) return@map BarEntry( - epoch.get(Calendar.DATE).toFloat(), + (epoch.timeInMillis / 1000 / 60 / 60).toFloat(), it.value.toFloat() ) }, diff --git a/app/src/main/java/com/dzeio/openhealth/utils/DrawUtils.kt b/app/src/main/java/com/dzeio/openhealth/utils/DrawUtils.kt index 1000ece..35fcde5 100644 --- a/app/src/main/java/com/dzeio/openhealth/utils/DrawUtils.kt +++ b/app/src/main/java/com/dzeio/openhealth/utils/DrawUtils.kt @@ -1,24 +1,23 @@ package com.dzeio.openhealth.utils -import android.graphics.* +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.RectF object DrawUtils { /** * Fuck Graphics */ - fun drawArc(canvas: Canvas, percent: Float, pColor: Int) { - canvas.width - val spacing = 120f + fun drawArc(canvas: Canvas, percent: Float, rect: RectF, pColor: Int, strokeWidth: Float = 1f) { val r1 = RectF( - spacing, - spacing, - canvas.width - spacing, - canvas.height * 2 - spacing * 3 + canvas.realSize(true, rect.left), + canvas.realSize(false, rect.top), + canvas.realSize(true, rect.right), + canvas.realSize(false, rect.bottom, 2) ) - val paint = Paint() - paint.apply { - strokeWidth = 200f + val paint = Paint().apply { + this.strokeWidth = canvas.realSize(true, strokeWidth) style = Paint.Style.STROKE color = pColor isAntiAlias = true @@ -27,4 +26,33 @@ object DrawUtils { canvas.drawArc(r1, 180f, 180 * percent / 100f, false, paint) } + /** + * Fuck Graphics + */ + fun drawRect(canvas: Canvas, rect: RectF, pColor: Int, strokeWidth: Float = 1f) { + val r1 = RectF( + canvas.realSize(true, rect.left), + canvas.realSize(false, rect.top), + canvas.realSize(true, rect.right), + canvas.realSize(false, rect.bottom) + ) + val paint = Paint().apply { + this.strokeWidth = canvas.realSize(true, strokeWidth) + style = Paint.Style.STROKE + color = pColor + isAntiAlias = true + } + canvas.drawRect(r1, paint) + } + + private fun Canvas.realSize(isWidth: Boolean, value: Float): Float { + val it = if (isWidth) this.width else this.height + return it * value / 100 + } + + private fun Canvas.realSize(isWidth: Boolean, value: Float, multiplier: Int): Float { + val it = (if (isWidth) this.width else this.height) * multiplier + return it * value / 100 + } + } \ No newline at end of file diff --git a/app/src/main/java/com/dzeio/openhealth/utils/GraphUtils.kt b/app/src/main/java/com/dzeio/openhealth/utils/GraphUtils.kt index c80c759..2027296 100644 --- a/app/src/main/java/com/dzeio/openhealth/utils/GraphUtils.kt +++ b/app/src/main/java/com/dzeio/openhealth/utils/GraphUtils.kt @@ -1,35 +1,34 @@ package com.dzeio.openhealth.utils -import android.graphics.Color import com.github.mikephil.charting.charts.BarChart import com.github.mikephil.charting.charts.BarLineChartBase -import com.github.mikephil.charting.charts.Chart import com.github.mikephil.charting.charts.LineChart import com.github.mikephil.charting.components.AxisBase import com.github.mikephil.charting.components.Description import com.github.mikephil.charting.components.XAxis import com.github.mikephil.charting.data.BarLineScatterCandleBubbleData -import com.github.mikephil.charting.data.ChartData import com.github.mikephil.charting.data.Entry -import com.github.mikephil.charting.data.LineData import com.github.mikephil.charting.formatter.ValueFormatter import com.github.mikephil.charting.interfaces.datasets.IBarLineScatterCandleBubbleDataSet -import com.github.mikephil.charting.interfaces.datasets.IDataSet -import com.google.android.material.color.MaterialColors import java.text.SimpleDateFormat import java.util.* object GraphUtils { - fun lineChartSetup(chart: LineChart, mainColor: Int) { - barLineChartSetup(chart, mainColor) + fun lineChartSetup(chart: LineChart, mainColor: Int, textColor: Int) { + barLineChartSetup(chart, mainColor, textColor) } - fun barChartSetup(chart: BarChart, mainColor: Int) { - barLineChartSetup(chart, mainColor) + fun barChartSetup(chart: BarChart, mainColor: Int, textColor: Int) { + barLineChartSetup(chart, mainColor, textColor) } - fun >?> barLineChartSetup(chart: BarLineChartBase, mainColor: Int) { + private fun >?> barLineChartSetup( + chart: BarLineChartBase, + mainColor: Int, + textColor: Int + ) { + chart.apply { // Setup @@ -49,18 +48,25 @@ object GraphUtils { position = XAxis.XAxisPosition.BOTTOM setDrawGridLines(false) setLabelCount(3, true) - textColor = Color.WHITE + this.textColor = textColor + //setDrawGridLines(false) + //setDrawZeroLine(false) + setDrawAxisLine(false) + disableGridDashedLine() + invalidateOutline() } axisLeft.apply { axisLineColor = mainColor - textColor = Color.WHITE + this.textColor = textColor +// setDrawZeroLine(false) setLabelCount(0, true) + setDrawGridLines(false) } axisRight.apply { - textColor = Color.WHITE + this.textColor = textColor } - setNoDataTextColor(Color.WHITE) + setNoDataTextColor(textColor) isAutoScaleMinMaxEnabled = true @@ -70,8 +76,8 @@ object GraphUtils { description = Description().apply { isEnabled = false } isScaleXEnabled = true setPinchZoom(false) - //setDrawGridBackground(false) - //setDrawBorders(false) + setDrawGridBackground(false) + setDrawBorders(false) } } } \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_outline_hexagon_24.xml b/app/src/main/res/drawable/ic_outline_hexagon_24.xml index c36a3e4..e27f182 100644 --- a/app/src/main/res/drawable/ic_outline_hexagon_24.xml +++ b/app/src/main/res/drawable/ic_outline_hexagon_24.xml @@ -2,7 +2,8 @@ android:width="24dp" android:height="24dp" android:viewportWidth="24" - android:viewportHeight="24"> + android:viewportHeight="24" + android:tint="?attr/colorControlNormal"> - - + + + + + + - + + - - - - - - + app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintRight_toRightOf="parent" + app:layout_constraintTop_toTopOf="parent" - - app:layout_constraintLeft_toLeftOf="parent" - app:layout_constraintRight_toRightOf="parent" - app:layout_constraintTop_toTopOf="parent" + - app:defaultNavHost="true" - app:navGraph="@navigation/mobile_navigation" /> - - - - - - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_water_edit_water.xml b/app/src/main/res/layout/dialog_water_edit_water.xml index 6862567..a78d44d 100644 --- a/app/src/main/res/layout/dialog_water_edit_water.xml +++ b/app/src/main/res/layout/dialog_water_edit_water.xml @@ -1,5 +1,6 @@ @@ -8,8 +9,20 @@ android:id="@+id/editTextNumber" android:layout_width="wrap_content" android:layout_height="wrap_content" + android:layout_marginTop="32dp" android:ems="10" android:inputType="number" - tools:layout_editor_absoluteX="101dp" - tools:layout_editor_absoluteY="107dp" /> + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_extension.xml b/app/src/main/res/layout/fragment_extension.xml new file mode 100644 index 0000000..d5f2a80 --- /dev/null +++ b/app/src/main/res/layout/fragment_extension.xml @@ -0,0 +1,39 @@ + + + + + +