Compare commits
171 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 53c9dca3e2 | |||
| 8b37daa9fb | |||
| f9c2e83c79 | |||
| 71bdc70c1a | |||
| 5e4a40579a | |||
| d326be1224 | |||
| fb8a09c95c | |||
| 1635b9905d | |||
| 2d88fc0b20 | |||
| 1d74359c06 | |||
| f75f77cec7 | |||
| 8b10138cd6 | |||
| 2b40633110 | |||
| 1102d05a61 | |||
| 51e8c2f111 | |||
| 547e4e5f63 | |||
| 84d5c2aac6 | |||
| 2b3b423fa3 | |||
| 3b28c37c5e | |||
| e749e787ad | |||
| 9c5d582f24 | |||
| 75a4edb61c | |||
| 34c0758308 | |||
| 85963ae061 | |||
| e3390d5397 | |||
| 59a2f31a73 | |||
| c01f8450d3 | |||
| cea5241135 | |||
| 7784fc5c75 | |||
| 6dd017f33e | |||
| c8cd9f85f6 | |||
| d038bdb741 | |||
| f55e8d2c85 | |||
| f8dc1d9eae | |||
| 85393b0d40 | |||
| 75599ad20c | |||
| c6b948cbf5 | |||
| 4d42133a4b | |||
| 5b151805ff | |||
| aa86826bdb | |||
| 821373a340 | |||
| 8f37e293b1 | |||
| 0fb8ed0b53 | |||
| 0c696b2eb2 | |||
| cd127bc3f8 | |||
| 2cfc809490 | |||
| ba31dee16a | |||
| 146c743fb8 | |||
| 0c00e9ec2d | |||
| 49af55a2de | |||
| 0114b48197 | |||
| 09f615a5e6 | |||
| 9014acc548 | |||
| 4fb386be86 | |||
| ea8606be97 | |||
| e0527dc8ff | |||
| aaf2789a21 | |||
| f8dc64cc6b | |||
| ced5b751be | |||
| 8a60dda74e | |||
| c8c4df6ef7 | |||
| 0c0ac9dee5 | |||
| fdf6c91929 | |||
| 08d6e39a17 | |||
| b9bc7bd1b5 | |||
| 7b73238448 | |||
| 83ccfb82ac | |||
| 199d35d9c7 | |||
| 382a2fe600 | |||
| e02f9c1d60 | |||
| 5e7636d7ff | |||
| 616322cd35 | |||
| 299c6a0c1d | |||
| b4911b6cb4 | |||
| f41f877107 | |||
| 58aa7f6687 | |||
| d196e01da0 | |||
| 1fbd12a947 | |||
| 7493291ade | |||
| 2a750704d9 | |||
| eb79113e4c | |||
| cb2f3823cd | |||
| 39e29dabb8 | |||
| 51d1b93d03 | |||
| 8acbc63914 | |||
| ac8e78ff24 | |||
| 162ded66d8 | |||
| 5428209543 | |||
| 141fd30d70 | |||
| 48d446a243 | |||
| ae7869d3a2 | |||
| b8cacaffa9 | |||
| 4def8f0409 | |||
| f0d12e9925 | |||
| a2331260e4 | |||
| c1a846d42b | |||
| 031d684b3e | |||
| 3a770e71e3 | |||
| b29dd8ea79 | |||
| 7f1553a4a1 | |||
| d748f5d6de | |||
| 7234e072e6 | |||
| 7f71f46367 | |||
| c1dae021bf | |||
| 88455acc76 | |||
| 6a1cb09ca2 | |||
| 33d7ab52ca | |||
| d2682358c2 | |||
| f511ca2028 | |||
| d5774e8511 | |||
| b6e7e72f5a | |||
| 27220c9ab2 | |||
| f1424e5820 | |||
| a18e0fbda0 | |||
| 4c3a72df81 | |||
| e9ce50f686 | |||
| acb26964f3 | |||
| 917d1218ae | |||
| f5ccd7d8c3 | |||
| 456a9e49a9 | |||
| 6b05004647 | |||
| f9a9339042 | |||
| 5e21d877c5 | |||
| af0ef90e4d | |||
| 02b3ea58cf | |||
| 0dcedb3c59 | |||
| 637d75fd2e | |||
| 0cd8dd973b | |||
| 2898a21157 | |||
| 0fa25c6701 | |||
| 843f3b06a1 | |||
| a9b8b7e1e2 | |||
| 966e7ccd8a | |||
| d58be08fee | |||
| eb057b51d3 | |||
| 8c88e7fd5b | |||
| 756d3aa48f | |||
| 1a4dbd9cba | |||
| c68176ad09 | |||
| b0ccf3464f | |||
| dcaff3d1b8 | |||
| 693e0143b5 | |||
| cdc80bdbbd | |||
| 948bca1150 | |||
| 68c4b26031 | |||
| 56bed8206e | |||
| 8e2f06c211 | |||
| 08f2fe84d7 | |||
| 0e5764cf5d | |||
| e6a5751959 | |||
| eefc738ee2 | |||
| 322650345c | |||
| 5c77a44611 | |||
| 28900d0981 | |||
| 77281e11f5 | |||
| e06b0e79cc | |||
| d6f31b8775 | |||
| d862c85874 | |||
| 5bd1b70cd9 | |||
| 191b9b9c1f | |||
| c36cdb1e42 | |||
| 4c53bd3763 | |||
| 7984670a3c | |||
| a58a95f125 | |||
| 378b4cb84b | |||
| ec32f3b681 | |||
| ef5f1a8f8c | |||
| 46f152d5d2 | |||
| f3a096b660 | |||
| c8de4e13f9 | |||
| 1ee3fc79f0 |
2
.gitignore
vendored
@@ -31,3 +31,5 @@ Thumbs.db
|
|||||||
.gradle
|
.gradle
|
||||||
build/
|
build/
|
||||||
*.iml
|
*.iml
|
||||||
|
|
||||||
|
art/
|
||||||
|
|||||||
3
.gitmodules
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[submodule "libs/drag-sort-listview"]
|
||||||
|
path = libs/drag-sort-listview
|
||||||
|
url = https://github.com/iSoron/drag-sort-listview.git
|
||||||
34
CHANGELOG.md
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
### 1.3.0 (March 12, 2016)
|
||||||
|
|
||||||
|
* New frequency plot: view total repetitions per day of week
|
||||||
|
* New history editor: put checkmarks in the past
|
||||||
|
* German, French and Japanese translations
|
||||||
|
* Add about screen, with credits to all contributors
|
||||||
|
* Fix small bug that prevented habit from being reordered
|
||||||
|
* Fix small bug caused by rotating the device
|
||||||
|
|
||||||
|
### 1.2.0 (March 4, 2016)
|
||||||
|
|
||||||
|
* Ability to export habit data as CSV
|
||||||
|
* Widgets (checkmark, history, score and streaks)
|
||||||
|
* More natural scrolling on data views (fling)
|
||||||
|
* Minor UI improvements on pre-Lollipop devices
|
||||||
|
* Fix crash on Samsung Galaxy TabS 8.4
|
||||||
|
* Other minor bug fixes
|
||||||
|
|
||||||
|
### 1.1.1 (February 24, 2016)
|
||||||
|
|
||||||
|
* Show reminder only on chosen days of the week
|
||||||
|
* Rearrange habits by long-pressing then dragging
|
||||||
|
* Select and modify multiple habits simultaneously
|
||||||
|
* 12/24 hour format according to phone preferences
|
||||||
|
* Permanently delete habits
|
||||||
|
* Usage hints during startup
|
||||||
|
* Translation to Brazilian Portuguese and Chinese
|
||||||
|
* Other minor fixes
|
||||||
|
|
||||||
|
### 1.0.0 (February 19, 2016)
|
||||||
|
|
||||||
|
* Initial release
|
||||||
91
NOTICE.md
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
# Copyright Notices
|
||||||
|
|
||||||
|
### ActiveAndroid
|
||||||
|
|
||||||
|
<https://github.com/pardom/ActiveAndroid>
|
||||||
|
|
||||||
|
Copyright (C) 2010 Michael Pardo
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
http://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.
|
||||||
|
|
||||||
|
### Android Open Source Project
|
||||||
|
|
||||||
|
<https://source.android.com/>
|
||||||
|
|
||||||
|
Copyright (C) 2013 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
|
||||||
|
|
||||||
|
http://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.
|
||||||
|
|
||||||
|
### FontAwesome
|
||||||
|
|
||||||
|
<http://fontawesome.io>
|
||||||
|
|
||||||
|
Font Awesome is a full suite of 605 pictographic icons for easy scalable
|
||||||
|
vector graphics on websites, created and maintained by Dave Gandy. Licensed
|
||||||
|
under the SIL OFL 1.1.
|
||||||
|
|
||||||
|
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||||
|
development of collaborative font projects, to support the font creation
|
||||||
|
efforts of academic and linguistic communities, and to provide a free and
|
||||||
|
open framework in which fonts may be shared and improved in partnership
|
||||||
|
with others.
|
||||||
|
|
||||||
|
The OFL allows the licensed fonts to be used, studied, modified and
|
||||||
|
redistributed freely as long as they are not sold by themselves. The
|
||||||
|
fonts, including any derivative works, can be bundled, embedded,
|
||||||
|
redistributed and/or sold with any software provided that any reserved
|
||||||
|
names are not used by derivative works. The fonts and derivatives,
|
||||||
|
however, cannot be released under any other type of license. The
|
||||||
|
requirement for fonts to remain under this license does not apply
|
||||||
|
to any document created using the fonts or their derivatives.
|
||||||
|
|
||||||
|
|
||||||
|
### DragSortListView
|
||||||
|
|
||||||
|
<https://github.com/bauerca/drag-sort-listview>
|
||||||
|
|
||||||
|
A subclass of the Android ListView component that enables drag
|
||||||
|
and drop re-ordering of list items.
|
||||||
|
|
||||||
|
Copyright 2012 Carl Bauer
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
http://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.
|
||||||
|
|
||||||
|
### Material Design Icons
|
||||||
|
|
||||||
|
<https://github.com/google/material-design-icons>
|
||||||
|
|
||||||
|
Material design icons are the official icon set from Google that are designed
|
||||||
|
under the material design guidelines. Available under the Creative Common
|
||||||
|
Attribution 4.0 International License (CC-BY 4.0).
|
||||||
|
|
||||||
93
README.md
@@ -1,17 +1,43 @@
|
|||||||
# Habits Tracker
|
# Loop Habit Tracker
|
||||||
|
|
||||||
Habits Tracker is a simple Android app that helps you create and maintain good habits. Detailed graphs and statistics show you how your habits improved over time. It is completely ad-free and open source, with no intrusive permissions. Join the open beta at [Google Play Store](https://play.google.com/apps/testing/org.isoron.uhabits).
|
Loop is a simple Android app that helps you create and maintain good habits,
|
||||||
|
allowing you to achieve your long-term goals. Detailed graphs and statistics
|
||||||
|
show you how your habits improved over time. It is completely ad-free and open
|
||||||
|
source.
|
||||||
|
|
||||||
|
<a href="https://play.google.com/store/apps/details?id=org.isoron.uhabits&utm_source=global_co&utm_medium=prtnr&utm_content=Mar2515&utm_campaign=PartBadge&pcampaignid=MKT-AC-global-none-all-co-pr-py-PartBadges-Oct1515-1"><img alt="Get it on Google Play" src="https://play.google.com/intl/en_us/badges/images/apps/en-play-badge-border.png" height="75px"/></a>
|
||||||
|
<a href="http://f-droid.org/app/org.isoron.uhabits"><img alt="Git if on F-Droid" src="http://i.imgur.com/baSPE7X.png" height="75px"/></a>
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
* Simple and beautiful interface, following the Material Design guidelines.
|
* **Simple, beautiful and modern interface.** Loop has a minimalistic interface
|
||||||
* Advanced algorithms for calculating the strength of your habits. Every repetition makes your habit stronger, and every missed day makes it weaker. A few missed days after a long streak, however, will not completely destroy your entire progress.
|
that is easy to use and follows the material design guidelines.
|
||||||
* Detailed graphs and statistics, showing how did you habits improve over time. Scroll back to see the complete history of your habit.
|
|
||||||
* Support for both daily habits and habits with more complex schedules, such as 3 times every week; one time every other week; or every other day.
|
* **Habit score.** In addition to showing your current streak, Loop has an
|
||||||
* Habit reminders at a chosen hour of the day.
|
advanced algorithm for calculating the strength of your habits. Every
|
||||||
* Support for Android Wear. Reminders can be checked or dismissed from the watch.
|
repetition makes your habit stronger, and every missed day makes it weaker. A
|
||||||
* Completely ad-free and open source. There are absolutely no advertisements, annoying notifications or intrusive permissions in this app, and there will never be. The complete source code is available under the GPLv3.
|
few missed days after a long streak, however, will not completely destroy
|
||||||
|
your entire progress.
|
||||||
|
|
||||||
|
* **Detailed graphs and statistics.** Clearly see how your habits improved over
|
||||||
|
time with beautiful and detailed graphs. Scroll back to see the complete
|
||||||
|
history of your habits.
|
||||||
|
|
||||||
|
* **Flexible schedules.** Supports both daily habits and habits with more
|
||||||
|
complex schedules, such as 3 times every week; one time every other week; or
|
||||||
|
every other day.
|
||||||
|
|
||||||
|
* **Reminders.** Create an individual reminder for each habit, at a chosen hour
|
||||||
|
of the day. Easily check, dismiss or snooze your habit directly from the
|
||||||
|
notification, without opening the app.
|
||||||
|
|
||||||
|
* **Optimized for smartwatches.** Reminders can be checked, snoozed or
|
||||||
|
dismissed directly from your Android Wear watch.
|
||||||
|
|
||||||
|
* **Completely ad-free and open source.** There are absolutely no
|
||||||
|
advertisements, annoying notifications or intrusive permissions in this app,
|
||||||
|
and there will never be. The complete source code is available under the
|
||||||
|
GPLv3.
|
||||||
|
|
||||||
## Screenshots
|
## Screenshots
|
||||||
|
|
||||||
@@ -19,12 +45,61 @@ Habits Tracker is a simple Android app that helps you create and maintain good h
|
|||||||
[![Edit habit][screen2th]][screen2]
|
[![Edit habit][screen2th]][screen2]
|
||||||
[![Habit strength][screen3th]][screen3]
|
[![Habit strength][screen3th]][screen3]
|
||||||
[![Habit history and streaks][screen4th]][screen4]
|
[![Habit history and streaks][screen4th]][screen4]
|
||||||
|
[![Widgets][screen5th]][screen5]
|
||||||
|
|
||||||
|
## Installing
|
||||||
|
|
||||||
|
The easiest way to install Loop is through the [Google Play Store][playstore] or [F-Droid][fdroid].
|
||||||
|
You may also download and install the APK from the [releases page][releases];
|
||||||
|
note, however, that the app will not be updated automatically. To build this
|
||||||
|
app from the source code, see [building instructions][build].
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
Loop is an open source project developed entirely by volunteers. If you would
|
||||||
|
like to contribute to the project, you are very welcome. There are many ways to
|
||||||
|
contribute, even if you are not a software developer.
|
||||||
|
|
||||||
|
* **Report bugs, suggest features.** The easiest way to contribute is to simply
|
||||||
|
use the app and let us know if you find any problems or have any suggestions
|
||||||
|
to improve it. You can either use the link inside the app, or open an issue
|
||||||
|
at GitHub.
|
||||||
|
|
||||||
|
* **Translate the app into your own language.** If you are not a native English
|
||||||
|
speaker, and would like to see the app translated into your own language,
|
||||||
|
please join our [open translation project at POEditor][poedit].
|
||||||
|
|
||||||
|
* **Write some code.** If you are an Android developer, you are very welcome to
|
||||||
|
contribute with code. Please, see the [developer guidelines][dev-guide] for more details.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify it
|
||||||
|
under the terms of the GNU General Public License as published by the Free
|
||||||
|
Software Foundation, either version 3 of the License, or (at your option)
|
||||||
|
any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful, but WITHOUT
|
||||||
|
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
|
more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License along
|
||||||
|
with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
[screen1]: screenshots/original/uhabits1.png
|
[screen1]: screenshots/original/uhabits1.png
|
||||||
[screen2]: screenshots/original/uhabits2.png
|
[screen2]: screenshots/original/uhabits2.png
|
||||||
[screen3]: screenshots/original/uhabits3.png
|
[screen3]: screenshots/original/uhabits3.png
|
||||||
[screen4]: screenshots/original/uhabits4.png
|
[screen4]: screenshots/original/uhabits4.png
|
||||||
|
[screen5]: screenshots/original/uhabits5.png
|
||||||
[screen1th]: screenshots/thumbs/uhabits1.png
|
[screen1th]: screenshots/thumbs/uhabits1.png
|
||||||
[screen2th]: screenshots/thumbs/uhabits2.png
|
[screen2th]: screenshots/thumbs/uhabits2.png
|
||||||
[screen3th]: screenshots/thumbs/uhabits3.png
|
[screen3th]: screenshots/thumbs/uhabits3.png
|
||||||
[screen4th]: screenshots/thumbs/uhabits4.png
|
[screen4th]: screenshots/thumbs/uhabits4.png
|
||||||
|
[screen5th]: screenshots/thumbs/uhabits5.png
|
||||||
|
[poedit]: https://poeditor.com/join/project/8DWX5pfjS0
|
||||||
|
[playstore]: https://play.google.com/store/apps/details?id=org.isoron.uhabits
|
||||||
|
[releases]: https://github.com/iSoron/uhabits/releases
|
||||||
|
[fdroid]: http://f-droid.org/app/org.isoron.uhabits
|
||||||
|
[dev-guide]: https://github.com/iSoron/uhabits/wiki/Developer-guidelines
|
||||||
|
[build]: https://github.com/iSoron/uhabits/wiki/Developer-guidelines#building
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ android {
|
|||||||
applicationId "org.isoron.uhabits"
|
applicationId "org.isoron.uhabits"
|
||||||
minSdkVersion 15
|
minSdkVersion 15
|
||||||
targetSdkVersion 23
|
targetSdkVersion 23
|
||||||
|
|
||||||
|
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
@@ -15,6 +17,9 @@ android {
|
|||||||
minifyEnabled false
|
minifyEnabled false
|
||||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
|
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
|
||||||
}
|
}
|
||||||
|
debug {
|
||||||
|
testCoverageEnabled = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
lintOptions {
|
lintOptions {
|
||||||
@@ -25,7 +30,13 @@ android {
|
|||||||
dependencies {
|
dependencies {
|
||||||
compile 'com.android.support:support-v4:23.1.1'
|
compile 'com.android.support:support-v4:23.1.1'
|
||||||
compile 'com.github.paolorotolo:appintro:3.4.0'
|
compile 'com.github.paolorotolo:appintro:3.4.0'
|
||||||
|
compile project(':libs:drag-sort-listview:library')
|
||||||
compile files('libs/ActiveAndroid.jar')
|
compile files('libs/ActiveAndroid.jar')
|
||||||
|
|
||||||
|
androidTestCompile 'com.android.support:support-annotations:23.1.1'
|
||||||
|
androidTestCompile 'com.android.support.test:runner:0.4.1'
|
||||||
|
androidTestCompile 'com.android.support.test:rules:0.4.1'
|
||||||
|
androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2.1'
|
||||||
|
androidTestCompile 'com.android.support.test.espresso:espresso-intents:2.2.1'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||||
|
*
|
||||||
|
* This file is part of Loop Habit Tracker.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by the
|
||||||
|
* Free Software Foundation, either version 3 of the License, or (at your
|
||||||
|
* option) any later version.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
|
* more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along
|
||||||
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.isoron.uhabits;
|
||||||
|
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.Adapter;
|
||||||
|
import android.widget.AdapterView;
|
||||||
|
|
||||||
|
import org.hamcrest.Description;
|
||||||
|
import org.hamcrest.Matcher;
|
||||||
|
import org.hamcrest.TypeSafeMatcher;
|
||||||
|
import org.isoron.uhabits.models.Habit;
|
||||||
|
|
||||||
|
public class HabitMatchers
|
||||||
|
{
|
||||||
|
public static Matcher<Habit> withName(final String name)
|
||||||
|
{
|
||||||
|
return new TypeSafeMatcher<Habit>()
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
public boolean matchesSafely(Habit habit)
|
||||||
|
{
|
||||||
|
return habit.name.equals(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void describeTo(Description description)
|
||||||
|
{
|
||||||
|
description.appendText("name should be ").appendText(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void describeMismatchSafely(Habit habit, Description description)
|
||||||
|
{
|
||||||
|
description.appendText("was ").appendText(habit.name);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Matcher<View> containsHabit(final Matcher<Habit> matcher)
|
||||||
|
{
|
||||||
|
return new TypeSafeMatcher<View>()
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
protected boolean matchesSafely(View view)
|
||||||
|
{
|
||||||
|
Adapter adapter = ((AdapterView) view).getAdapter();
|
||||||
|
for (int i = 0; i < adapter.getCount(); i++)
|
||||||
|
if (matcher.matches(adapter.getItem(i))) return true;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void describeTo(Description description)
|
||||||
|
{
|
||||||
|
description.appendText("with class name: ");
|
||||||
|
matcher.describeTo(description);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||||
|
*
|
||||||
|
* This file is part of Loop Habit Tracker.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by the
|
||||||
|
* Free Software Foundation, either version 3 of the License, or (at your
|
||||||
|
* option) any later version.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
|
* more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along
|
||||||
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.isoron.uhabits;
|
||||||
|
|
||||||
|
import android.support.test.espresso.UiController;
|
||||||
|
import android.support.test.espresso.ViewAction;
|
||||||
|
import android.support.test.espresso.action.CoordinatesProvider;
|
||||||
|
import android.support.test.espresso.action.GeneralClickAction;
|
||||||
|
import android.support.test.espresso.action.GeneralLocation;
|
||||||
|
import android.support.test.espresso.action.Press;
|
||||||
|
import android.support.test.espresso.action.Tap;
|
||||||
|
import android.support.test.espresso.matcher.ViewMatchers;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.LinearLayout;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import org.hamcrest.Matcher;
|
||||||
|
|
||||||
|
import java.security.InvalidParameterException;
|
||||||
|
import java.util.Random;
|
||||||
|
|
||||||
|
public class HabitViewActions
|
||||||
|
{
|
||||||
|
public static ViewAction toggleAllCheckmarks()
|
||||||
|
{
|
||||||
|
final GeneralClickAction clickAction =
|
||||||
|
new GeneralClickAction(Tap.LONG, GeneralLocation.CENTER, Press.FINGER);
|
||||||
|
|
||||||
|
return new ViewAction()
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
public Matcher<View> getConstraints()
|
||||||
|
{
|
||||||
|
return ViewMatchers.isDisplayed();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getDescription()
|
||||||
|
{
|
||||||
|
return "toggleAllCheckmarks";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void perform(UiController uiController, View view)
|
||||||
|
{
|
||||||
|
if (view.getId() != R.id.llButtons)
|
||||||
|
throw new InvalidParameterException("View must have id llButtons");
|
||||||
|
|
||||||
|
LinearLayout llButtons = (LinearLayout) view;
|
||||||
|
int count = llButtons.getChildCount();
|
||||||
|
|
||||||
|
for (int i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
TextView tvButton = (TextView) llButtons.getChildAt(i);
|
||||||
|
clickAction.perform(uiController, tvButton);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ViewAction clickAt(final int x, final int y)
|
||||||
|
{
|
||||||
|
return new GeneralClickAction(Tap.SINGLE, new CoordinatesProvider()
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
public float[] calculateCoordinates(View view)
|
||||||
|
{
|
||||||
|
int[] locations = new int[2];
|
||||||
|
view.getLocationOnScreen(locations);
|
||||||
|
|
||||||
|
final float locationX = locations[0] + x;
|
||||||
|
final float locationY = locations[1] + y;
|
||||||
|
|
||||||
|
return new float[]{locationX, locationY};
|
||||||
|
}
|
||||||
|
}, Press.FINGER);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ViewAction clickAtRandomLocations(final int count)
|
||||||
|
{
|
||||||
|
return new ViewAction()
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
public Matcher<View> getConstraints()
|
||||||
|
{
|
||||||
|
return ViewMatchers.isDisplayed();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getDescription()
|
||||||
|
{
|
||||||
|
return "clickAtRandomLocations";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void perform(UiController uiController, View view)
|
||||||
|
{
|
||||||
|
int width = view.getWidth();
|
||||||
|
int height = view.getHeight();
|
||||||
|
Random random = new Random();
|
||||||
|
|
||||||
|
for(int i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
int x = random.nextInt(width);
|
||||||
|
int y = random.nextInt(height);
|
||||||
|
|
||||||
|
ViewAction action = clickAt(x, y);
|
||||||
|
action.perform(uiController, view);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,178 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||||
|
*
|
||||||
|
* This file is part of Loop Habit Tracker.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by the
|
||||||
|
* Free Software Foundation, either version 3 of the License, or (at your
|
||||||
|
* option) any later version.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
|
* more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along
|
||||||
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.isoron.uhabits;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.support.test.InstrumentationRegistry;
|
||||||
|
|
||||||
|
import org.isoron.uhabits.models.Habit;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Random;
|
||||||
|
|
||||||
|
import static android.support.test.espresso.Espresso.onData;
|
||||||
|
import static android.support.test.espresso.Espresso.onView;
|
||||||
|
import static android.support.test.espresso.Espresso.openContextualActionModeOverflowMenu;
|
||||||
|
import static android.support.test.espresso.Espresso.pressBack;
|
||||||
|
import static android.support.test.espresso.action.ViewActions.click;
|
||||||
|
import static android.support.test.espresso.action.ViewActions.longClick;
|
||||||
|
import static android.support.test.espresso.action.ViewActions.replaceText;
|
||||||
|
import static android.support.test.espresso.assertion.ViewAssertions.matches;
|
||||||
|
import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed;
|
||||||
|
import static android.support.test.espresso.matcher.ViewMatchers.withContentDescription;
|
||||||
|
import static android.support.test.espresso.matcher.ViewMatchers.withId;
|
||||||
|
import static android.support.test.espresso.matcher.ViewMatchers.withText;
|
||||||
|
import static org.hamcrest.Matchers.allOf;
|
||||||
|
import static org.hamcrest.Matchers.instanceOf;
|
||||||
|
import static org.hamcrest.Matchers.is;
|
||||||
|
import static org.hamcrest.Matchers.not;
|
||||||
|
import static org.isoron.uhabits.HabitMatchers.containsHabit;
|
||||||
|
import static org.isoron.uhabits.HabitMatchers.withName;
|
||||||
|
|
||||||
|
public class MainActivityActions
|
||||||
|
{
|
||||||
|
public static String addHabit()
|
||||||
|
{
|
||||||
|
return addHabit(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String addHabit(boolean openDialogs)
|
||||||
|
{
|
||||||
|
String name = "New Habit " + new Random().nextInt(1000000);
|
||||||
|
String description = "Did you perform your new habit today?";
|
||||||
|
String num = "4";
|
||||||
|
String den = "8";
|
||||||
|
|
||||||
|
onView(withId(R.id.action_add))
|
||||||
|
.perform(click());
|
||||||
|
|
||||||
|
typeHabitData(name, description, num, den);
|
||||||
|
|
||||||
|
if(openDialogs)
|
||||||
|
{
|
||||||
|
onView(withId(R.id.buttonPickColor))
|
||||||
|
.perform(click());
|
||||||
|
pressBack();
|
||||||
|
onView(withId(R.id.inputReminderTime))
|
||||||
|
.perform(click());
|
||||||
|
onView(withText("Done"))
|
||||||
|
.perform(click());
|
||||||
|
onView(withId(R.id.inputReminderDays))
|
||||||
|
.perform(click());
|
||||||
|
onView(withText("OK"))
|
||||||
|
.perform(click());
|
||||||
|
}
|
||||||
|
|
||||||
|
onView(withId(R.id.buttonSave))
|
||||||
|
.perform(click());
|
||||||
|
|
||||||
|
onData(allOf(is(instanceOf(Habit.class)), withName(name)))
|
||||||
|
.onChildView(withId(R.id.label));
|
||||||
|
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void typeHabitData(String name, String description, String num, String den)
|
||||||
|
{
|
||||||
|
onView(withId(R.id.input_name))
|
||||||
|
.perform(replaceText(name));
|
||||||
|
onView(withId(R.id.input_description))
|
||||||
|
.perform(replaceText(description));
|
||||||
|
onView(withId(R.id.input_freq_num))
|
||||||
|
.perform(replaceText(num));
|
||||||
|
onView(withId(R.id.input_freq_den))
|
||||||
|
.perform(replaceText(den));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void selectHabit(String name)
|
||||||
|
{
|
||||||
|
selectHabits(Collections.singletonList(name));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void selectHabits(List<String> names)
|
||||||
|
{
|
||||||
|
boolean first = true;
|
||||||
|
for(String name : names)
|
||||||
|
{
|
||||||
|
onData(allOf(is(instanceOf(Habit.class)), withName(name)))
|
||||||
|
.onChildView(withId(R.id.label))
|
||||||
|
.perform(first ? longClick() : click());
|
||||||
|
|
||||||
|
first = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void assertHabitsDontExist(List<String> names)
|
||||||
|
{
|
||||||
|
for(String name : names)
|
||||||
|
onView(withId(R.id.listView))
|
||||||
|
.check(matches(not(containsHabit(withName(name)))));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void assertHabitExists(String name)
|
||||||
|
{
|
||||||
|
List<String> names = new LinkedList<>();
|
||||||
|
names.add(name);
|
||||||
|
assertHabitsExist(names);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void assertHabitsExist(List<String> names)
|
||||||
|
{
|
||||||
|
for(String name : names)
|
||||||
|
onData(allOf(is(instanceOf(Habit.class)), withName(name)))
|
||||||
|
.check(matches(isDisplayed()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void deleteHabit(String name)
|
||||||
|
{
|
||||||
|
deleteHabits(Collections.singletonList(name));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void deleteHabits(List<String> names)
|
||||||
|
{
|
||||||
|
selectHabits(names);
|
||||||
|
clickActionModeMenuItem(R.string.delete);
|
||||||
|
onView(withText("OK"))
|
||||||
|
.perform(click());
|
||||||
|
assertHabitsDontExist(names);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void clickActionModeMenuItem(int stringId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
onView(withText(stringId)).perform(click());
|
||||||
|
}
|
||||||
|
catch (Exception e1)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
onView(withContentDescription(stringId)).perform(click());
|
||||||
|
}
|
||||||
|
catch(Exception e2)
|
||||||
|
{
|
||||||
|
openContextualActionModeOverflowMenu();
|
||||||
|
onView(withText(stringId)).perform(click());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
200
app/src/androidTest/java/org/isoron/uhabits/MainTest.java
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
package org.isoron.uhabits;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.support.test.InstrumentationRegistry;
|
||||||
|
import android.support.test.espresso.NoMatchingViewException;
|
||||||
|
import android.support.test.espresso.intent.rule.IntentsTestRule;
|
||||||
|
import android.support.test.runner.AndroidJUnit4;
|
||||||
|
import android.test.suitebuilder.annotation.LargeTest;
|
||||||
|
|
||||||
|
import org.isoron.uhabits.models.Habit;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Rule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Random;
|
||||||
|
|
||||||
|
import static android.support.test.espresso.Espresso.onData;
|
||||||
|
import static android.support.test.espresso.Espresso.onView;
|
||||||
|
import static android.support.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu;
|
||||||
|
import static android.support.test.espresso.Espresso.pressBack;
|
||||||
|
import static android.support.test.espresso.action.ViewActions.click;
|
||||||
|
import static android.support.test.espresso.action.ViewActions.longClick;
|
||||||
|
import static android.support.test.espresso.action.ViewActions.scrollTo;
|
||||||
|
import static android.support.test.espresso.action.ViewActions.swipeLeft;
|
||||||
|
import static android.support.test.espresso.action.ViewActions.swipeRight;
|
||||||
|
import static android.support.test.espresso.action.ViewActions.swipeUp;
|
||||||
|
import static android.support.test.espresso.assertion.ViewAssertions.matches;
|
||||||
|
import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed;
|
||||||
|
import static android.support.test.espresso.matcher.ViewMatchers.isRoot;
|
||||||
|
import static android.support.test.espresso.matcher.ViewMatchers.withClassName;
|
||||||
|
import static android.support.test.espresso.matcher.ViewMatchers.withId;
|
||||||
|
import static android.support.test.espresso.matcher.ViewMatchers.withText;
|
||||||
|
import static org.hamcrest.Matchers.allOf;
|
||||||
|
import static org.hamcrest.Matchers.endsWith;
|
||||||
|
import static org.hamcrest.Matchers.instanceOf;
|
||||||
|
import static org.hamcrest.Matchers.is;
|
||||||
|
import static org.isoron.uhabits.HabitMatchers.withName;
|
||||||
|
import static org.isoron.uhabits.HabitViewActions.clickAtRandomLocations;
|
||||||
|
import static org.isoron.uhabits.HabitViewActions.toggleAllCheckmarks;
|
||||||
|
import static org.isoron.uhabits.MainActivityActions.addHabit;
|
||||||
|
import static org.isoron.uhabits.MainActivityActions.assertHabitExists;
|
||||||
|
import static org.isoron.uhabits.MainActivityActions.assertHabitsDontExist;
|
||||||
|
import static org.isoron.uhabits.MainActivityActions.assertHabitsExist;
|
||||||
|
import static org.isoron.uhabits.MainActivityActions.clickActionModeMenuItem;
|
||||||
|
import static org.isoron.uhabits.MainActivityActions.deleteHabit;
|
||||||
|
import static org.isoron.uhabits.MainActivityActions.deleteHabits;
|
||||||
|
import static org.isoron.uhabits.MainActivityActions.selectHabit;
|
||||||
|
import static org.isoron.uhabits.MainActivityActions.selectHabits;
|
||||||
|
import static org.isoron.uhabits.MainActivityActions.typeHabitData;
|
||||||
|
import static org.isoron.uhabits.ShowHabitActivityActions.openHistoryEditor;
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4.class)
|
||||||
|
@LargeTest
|
||||||
|
public class MainTest
|
||||||
|
{
|
||||||
|
@Rule
|
||||||
|
public IntentsTestRule<MainActivity> activityRule = new IntentsTestRule<>(
|
||||||
|
MainActivity.class);
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void skipTutorial()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
for (int i = 0; i < 10; i++)
|
||||||
|
onView(allOf(withClassName(endsWith("AppCompatImageButton")),
|
||||||
|
isDisplayed())).perform(click());
|
||||||
|
}
|
||||||
|
catch (NoMatchingViewException e)
|
||||||
|
{
|
||||||
|
// ignored
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testArchiveHabits()
|
||||||
|
{
|
||||||
|
List<String> names = new LinkedList<>();
|
||||||
|
Context context = InstrumentationRegistry.getTargetContext();
|
||||||
|
|
||||||
|
for(int i = 0; i < 3; i++)
|
||||||
|
names.add(addHabit());
|
||||||
|
|
||||||
|
selectHabits(names);
|
||||||
|
|
||||||
|
clickActionModeMenuItem(R.string.archive);
|
||||||
|
assertHabitsDontExist(names);
|
||||||
|
|
||||||
|
openActionBarOverflowOrOptionsMenu(context);
|
||||||
|
onView(withText(R.string.show_archived))
|
||||||
|
.perform(click());
|
||||||
|
|
||||||
|
assertHabitsExist(names);
|
||||||
|
selectHabits(names);
|
||||||
|
clickActionModeMenuItem(R.string.unarchive);
|
||||||
|
|
||||||
|
openActionBarOverflowOrOptionsMenu(context);
|
||||||
|
onView(withText(R.string.show_archived))
|
||||||
|
.perform(click());
|
||||||
|
|
||||||
|
assertHabitsExist(names);
|
||||||
|
deleteHabits(names);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testAddInvalidHabit()
|
||||||
|
{
|
||||||
|
onView(withId(R.id.action_add))
|
||||||
|
.perform(click());
|
||||||
|
|
||||||
|
typeHabitData("", "", "15", "7");
|
||||||
|
|
||||||
|
onView(withId(R.id.buttonSave)).perform(click());
|
||||||
|
onView(withId(R.id.input_name)).check(matches(isDisplayed()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testAddHabitAndViewStats()
|
||||||
|
{
|
||||||
|
String name = addHabit(true);
|
||||||
|
|
||||||
|
onData(allOf(is(instanceOf(Habit.class)), withName(name)))
|
||||||
|
.onChildView(withId(R.id.llButtons))
|
||||||
|
.perform(toggleAllCheckmarks());
|
||||||
|
|
||||||
|
onData(allOf(is(instanceOf(Habit.class)), withName(name)))
|
||||||
|
.onChildView(withId(R.id.label))
|
||||||
|
.perform(click());
|
||||||
|
|
||||||
|
onView(withId(R.id.scoreView))
|
||||||
|
.perform(swipeRight());
|
||||||
|
|
||||||
|
onView(withId(R.id.punchcardView))
|
||||||
|
.perform(scrollTo());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testEditHabit()
|
||||||
|
{
|
||||||
|
String name = addHabit();
|
||||||
|
|
||||||
|
onData(allOf(is(instanceOf(Habit.class)), withName(name)))
|
||||||
|
.onChildView(withId(R.id.label))
|
||||||
|
.perform(longClick());
|
||||||
|
|
||||||
|
clickActionModeMenuItem(R.string.edit);
|
||||||
|
|
||||||
|
String modifiedName = "Modified " + new Random().nextInt(10000);
|
||||||
|
typeHabitData(modifiedName, "", "1", "1");
|
||||||
|
|
||||||
|
onView(withId(R.id.buttonSave))
|
||||||
|
.perform(click());
|
||||||
|
|
||||||
|
assertHabitExists(modifiedName);
|
||||||
|
|
||||||
|
selectHabit(modifiedName);
|
||||||
|
clickActionModeMenuItem(R.string.color_picker_default_title);
|
||||||
|
pressBack();
|
||||||
|
|
||||||
|
deleteHabit(modifiedName);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testEditHistory()
|
||||||
|
{
|
||||||
|
String name = addHabit();
|
||||||
|
|
||||||
|
onData(allOf(is(instanceOf(Habit.class)), withName(name)))
|
||||||
|
.onChildView(withId(R.id.label))
|
||||||
|
.perform(click());
|
||||||
|
|
||||||
|
openHistoryEditor();
|
||||||
|
onView(withClassName(endsWith("HabitHistoryView")))
|
||||||
|
.perform(clickAtRandomLocations(20));
|
||||||
|
|
||||||
|
pressBack();
|
||||||
|
onView(withId(R.id.historyView))
|
||||||
|
.perform(scrollTo(), swipeRight(), swipeLeft());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSettings()
|
||||||
|
{
|
||||||
|
Context context = InstrumentationRegistry.getContext();
|
||||||
|
openActionBarOverflowOrOptionsMenu(context);
|
||||||
|
onView(withText(R.string.settings)).perform(click());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testAbout()
|
||||||
|
{
|
||||||
|
Context context = InstrumentationRegistry.getContext();
|
||||||
|
openActionBarOverflowOrOptionsMenu(context);
|
||||||
|
onView(withText(R.string.about)).perform(click());
|
||||||
|
onView(isRoot()).perform(swipeUp());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||||
|
*
|
||||||
|
* This file is part of Loop Habit Tracker.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by the
|
||||||
|
* Free Software Foundation, either version 3 of the License, or (at your
|
||||||
|
* option) any later version.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
|
* more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along
|
||||||
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.isoron.uhabits;
|
||||||
|
|
||||||
|
import static android.support.test.espresso.Espresso.onView;
|
||||||
|
import static android.support.test.espresso.action.ViewActions.click;
|
||||||
|
import static android.support.test.espresso.action.ViewActions.scrollTo;
|
||||||
|
import static android.support.test.espresso.matcher.ViewMatchers.withId;
|
||||||
|
|
||||||
|
public class ShowHabitActivityActions
|
||||||
|
{
|
||||||
|
public static void openHistoryEditor()
|
||||||
|
{
|
||||||
|
onView(withId(R.id.btEditHistory))
|
||||||
|
.perform(scrollTo(), click());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,44 +1,67 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
~ Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||||
|
~
|
||||||
|
~ This file is part of Loop Habit Tracker.
|
||||||
|
~
|
||||||
|
~ Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||||
|
~ it under the terms of the GNU General Public License as published by the
|
||||||
|
~ Free Software Foundation, either version 3 of the License, or (at your
|
||||||
|
~ option) any later version.
|
||||||
|
~
|
||||||
|
~ Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||||
|
~ WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
~ or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
|
~ more details.
|
||||||
|
~
|
||||||
|
~ You should have received a copy of the GNU General Public License along
|
||||||
|
~ with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
-->
|
||||||
|
|
||||||
<manifest
|
<manifest
|
||||||
package="org.isoron.uhabits"
|
package="org.isoron.uhabits"
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:versionCode="4"
|
android:versionCode="10"
|
||||||
android:versionName="1.0.0">
|
android:versionName="1.3.0">
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.VIBRATE"/>
|
||||||
|
|
||||||
<uses-permission
|
<uses-permission
|
||||||
android:name="android.permission.VIBRATE"/>
|
android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||||
|
android:maxSdkVersion="18"/>
|
||||||
|
|
||||||
|
<uses-permission
|
||||||
|
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||||
|
android:maxSdkVersion="18"/>
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name="com.activeandroid.app.Application"
|
android:name="com.activeandroid.app.Application"
|
||||||
|
android:allowBackup="true"
|
||||||
|
android:backupAgent=".HabitsBackupAgent"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="@string/app_name"
|
android:label="@string/main_activity_title"
|
||||||
android:theme="@style/AppBaseTheme"
|
android:theme="@style/AppBaseTheme">
|
||||||
android:backupAgent=".HabitsBackupAgent">
|
|
||||||
|
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="AA_DB_NAME"
|
android:name="AA_DB_NAME"
|
||||||
android:value="uhabits.db"/>
|
android:value="uhabits.db"/>
|
||||||
|
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="AA_DB_VERSION"
|
android:name="AA_DB_VERSION"
|
||||||
android:value="9"/>
|
android:value="12"/>
|
||||||
|
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="com.google.android.backup.api_key"
|
android:name="com.google.android.backup.api_key"
|
||||||
android:value="AEdPqrEAAAAI6aeWncbnMNo8E5GWeZ44dlc5cQ7tCROwFhOtiw" />
|
android:value="AEdPqrEAAAAI6aeWncbnMNo8E5GWeZ44dlc5cQ7tCROwFhOtiw"/>
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:label="@string/app_name">
|
android:label="@string/main_activity_title">
|
||||||
<intent-filter>
|
<intent-filter android:label="@string/app_name">
|
||||||
<action android:name="android.intent.action.MAIN"/>
|
<action android:name="android.intent.action.MAIN"/>
|
||||||
|
|
||||||
<category android:name="android.intent.category.LAUNCHER"/>
|
<category android:name="android.intent.category.LAUNCHER"/>
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<receiver
|
|
||||||
android:name=".ReminderAlarmReceiver" />
|
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ShowHabitActivity"
|
android:name=".ShowHabitActivity"
|
||||||
android:label="@string/title_activity_show_habit"
|
android:label="@string/title_activity_show_habit"
|
||||||
@@ -50,16 +73,94 @@
|
|||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".SettingsActivity"
|
android:name=".SettingsActivity"
|
||||||
android:label="Settings"
|
android:label="@string/settings"
|
||||||
android:parentActivityName=".MainActivity">
|
android:parentActivityName=".MainActivity">
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.support.PARENT_ACTIVITY"
|
android:name="android.support.PARENT_ACTIVITY"
|
||||||
android:value="org.isoron.uhabits.MainActivity"/>
|
android:value="org.isoron.uhabits.MainActivity"/>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<activity android:name=".IntroActivity"
|
<activity
|
||||||
android:label=""
|
android:name=".IntroActivity"
|
||||||
android:theme="@style/Theme.AppCompat.Light.NoActionBar" />
|
android:label=""
|
||||||
|
android:theme="@style/Theme.AppCompat.Light.NoActionBar"/>
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".widgets.HabitPickerDialog"
|
||||||
|
android:theme="@style/Theme.AppCompat.Light.Dialog">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE"/>
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".AboutActivity"
|
||||||
|
android:label="@string/about"
|
||||||
|
android:parentActivityName=".MainActivity">
|
||||||
|
</activity>
|
||||||
|
|
||||||
|
<receiver
|
||||||
|
android:name=".widgets.CheckmarkWidgetProvider"
|
||||||
|
android:label="@string/checkmark">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
|
<meta-data
|
||||||
|
android:name="android.appwidget.provider"
|
||||||
|
android:resource="@xml/widget_checkmark_info"/>
|
||||||
|
</receiver>
|
||||||
|
|
||||||
|
<receiver
|
||||||
|
android:name=".widgets.HistoryWidgetProvider"
|
||||||
|
android:label="@string/history">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
|
<meta-data
|
||||||
|
android:name="android.appwidget.provider"
|
||||||
|
android:resource="@xml/widget_history_info"/>
|
||||||
|
</receiver>
|
||||||
|
|
||||||
|
<receiver
|
||||||
|
android:name=".widgets.ScoreWidgetProvider"
|
||||||
|
android:label="@string/habit_strength">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
|
<meta-data
|
||||||
|
android:name="android.appwidget.provider"
|
||||||
|
android:resource="@xml/widget_score_info"/>
|
||||||
|
</receiver>
|
||||||
|
|
||||||
|
<receiver
|
||||||
|
android:name=".widgets.StreakWidgetProvider"
|
||||||
|
android:label="@string/streaks">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
|
<meta-data
|
||||||
|
android:name="android.appwidget.provider"
|
||||||
|
android:resource="@xml/widget_streak_info"/>
|
||||||
|
</receiver>
|
||||||
|
|
||||||
|
<receiver
|
||||||
|
android:name=".widgets.FrequencyWidgetProvider"
|
||||||
|
android:label="@string/frequency">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
|
<meta-data
|
||||||
|
android:name="android.appwidget.provider"
|
||||||
|
android:resource="@xml/widget_frequency_info"/>
|
||||||
|
</receiver>
|
||||||
|
|
||||||
|
<receiver android:name=".HabitBroadcastReceiver"/>
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
3
app/src/main/assets/migrations/10.sql
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
delete from Score;
|
||||||
|
delete from Streak;
|
||||||
|
delete from Checkmarks;
|
||||||
1
app/src/main/assets/migrations/11.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
alter table habits add column reminder_days integer not null default 127;
|
||||||
3
app/src/main/assets/migrations/12.sql
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
delete from Score;
|
||||||
|
delete from Streak;
|
||||||
|
delete from Checkmarks;
|
||||||
BIN
app/src/main/ic_small_widget_preview-web.png
Normal file
|
After Width: | Height: | Size: 35 KiB |
@@ -23,6 +23,7 @@ import java.util.Locale;
|
|||||||
import org.isoron.uhabits.R;
|
import org.isoron.uhabits.R;
|
||||||
|
|
||||||
import android.animation.ObjectAnimator;
|
import android.animation.ObjectAnimator;
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
import android.app.ActionBar.LayoutParams;
|
import android.app.ActionBar.LayoutParams;
|
||||||
import android.app.DialogFragment;
|
import android.app.DialogFragment;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
@@ -132,6 +133,7 @@ public class TimePickerDialog extends DialogFragment implements OnValueSelectedL
|
|||||||
// Empty constructor required for dialog fragment.
|
// Empty constructor required for dialog fragment.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("Java")
|
||||||
public TimePickerDialog(Context context, int theme, OnTimeSetListener callback,
|
public TimePickerDialog(Context context, int theme, OnTimeSetListener callback,
|
||||||
int hourOfDay, int minute, boolean is24HourMode) {
|
int hourOfDay, int minute, boolean is24HourMode) {
|
||||||
// Empty constructor required for dialog fragment.
|
// Empty constructor required for dialog fragment.
|
||||||
|
|||||||
@@ -1,471 +0,0 @@
|
|||||||
package com.mobeta.android.dslv;
|
|
||||||
|
|
||||||
import android.graphics.Point;
|
|
||||||
import android.view.GestureDetector;
|
|
||||||
import android.view.HapticFeedbackConstants;
|
|
||||||
import android.view.MotionEvent;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewConfiguration;
|
|
||||||
import android.widget.AdapterView;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class that starts and stops item drags on a {@link DragSortListView}
|
|
||||||
* based on touch gestures. This class also inherits from
|
|
||||||
* {@link SimpleFloatViewManager}, which provides basic float View
|
|
||||||
* creation.
|
|
||||||
*
|
|
||||||
* An instance of this class is meant to be passed to the methods
|
|
||||||
* {@link DragSortListView#setTouchListener()} and
|
|
||||||
* {@link DragSortListView#setFloatViewManager()} of your
|
|
||||||
* {@link DragSortListView} instance.
|
|
||||||
*/
|
|
||||||
public class DragSortController extends SimpleFloatViewManager implements View.OnTouchListener, GestureDetector.OnGestureListener {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Drag init mode enum.
|
|
||||||
*/
|
|
||||||
public static final int ON_DOWN = 0;
|
|
||||||
public static final int ON_DRAG = 1;
|
|
||||||
public static final int ON_LONG_PRESS = 2;
|
|
||||||
|
|
||||||
private int mDragInitMode = ON_DOWN;
|
|
||||||
|
|
||||||
private boolean mSortEnabled = true;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove mode enum.
|
|
||||||
*/
|
|
||||||
public static final int CLICK_REMOVE = 0;
|
|
||||||
public static final int FLING_REMOVE = 1;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The current remove mode.
|
|
||||||
*/
|
|
||||||
private int mRemoveMode;
|
|
||||||
|
|
||||||
private boolean mRemoveEnabled = false;
|
|
||||||
private boolean mIsRemoving = false;
|
|
||||||
|
|
||||||
private GestureDetector mDetector;
|
|
||||||
|
|
||||||
private GestureDetector mFlingRemoveDetector;
|
|
||||||
|
|
||||||
private int mTouchSlop;
|
|
||||||
|
|
||||||
public static final int MISS = -1;
|
|
||||||
|
|
||||||
private int mHitPos = MISS;
|
|
||||||
private int mFlingHitPos = MISS;
|
|
||||||
|
|
||||||
private int mClickRemoveHitPos = MISS;
|
|
||||||
|
|
||||||
private int[] mTempLoc = new int[2];
|
|
||||||
|
|
||||||
private int mItemX;
|
|
||||||
private int mItemY;
|
|
||||||
|
|
||||||
private int mCurrX;
|
|
||||||
private int mCurrY;
|
|
||||||
|
|
||||||
private boolean mDragging = false;
|
|
||||||
|
|
||||||
private float mFlingSpeed = 500f;
|
|
||||||
|
|
||||||
private int mDragHandleId;
|
|
||||||
|
|
||||||
private int mClickRemoveId;
|
|
||||||
|
|
||||||
private int mFlingHandleId;
|
|
||||||
private boolean mCanDrag;
|
|
||||||
|
|
||||||
private DragSortListView mDslv;
|
|
||||||
private int mPositionX;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calls {@link #DragSortController(DragSortListView, int)} with a
|
|
||||||
* 0 drag handle id, FLING_RIGHT_REMOVE remove mode,
|
|
||||||
* and ON_DOWN drag init. By default, sorting is enabled, and
|
|
||||||
* removal is disabled.
|
|
||||||
*
|
|
||||||
* @param dslv The DSLV instance
|
|
||||||
*/
|
|
||||||
public DragSortController(DragSortListView dslv) {
|
|
||||||
this(dslv, 0, ON_DOWN, FLING_REMOVE);
|
|
||||||
}
|
|
||||||
|
|
||||||
public DragSortController(DragSortListView dslv, int dragHandleId, int dragInitMode, int removeMode) {
|
|
||||||
this(dslv, dragHandleId, dragInitMode, removeMode, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public DragSortController(DragSortListView dslv, int dragHandleId, int dragInitMode, int removeMode, int clickRemoveId) {
|
|
||||||
this(dslv, dragHandleId, dragInitMode, removeMode, clickRemoveId, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* By default, sorting is enabled, and removal is disabled.
|
|
||||||
*
|
|
||||||
* @param dslv The DSLV instance
|
|
||||||
* @param dragHandleId The resource id of the View that represents
|
|
||||||
* the drag handle in a list item.
|
|
||||||
*/
|
|
||||||
public DragSortController(DragSortListView dslv, int dragHandleId, int dragInitMode,
|
|
||||||
int removeMode, int clickRemoveId, int flingHandleId) {
|
|
||||||
super(dslv);
|
|
||||||
mDslv = dslv;
|
|
||||||
mDetector = new GestureDetector(dslv.getContext(), this);
|
|
||||||
mFlingRemoveDetector = new GestureDetector(dslv.getContext(), mFlingRemoveListener);
|
|
||||||
mFlingRemoveDetector.setIsLongpressEnabled(false);
|
|
||||||
mTouchSlop = ViewConfiguration.get(dslv.getContext()).getScaledTouchSlop();
|
|
||||||
mDragHandleId = dragHandleId;
|
|
||||||
mClickRemoveId = clickRemoveId;
|
|
||||||
mFlingHandleId = flingHandleId;
|
|
||||||
setRemoveMode(removeMode);
|
|
||||||
setDragInitMode(dragInitMode);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public int getDragInitMode() {
|
|
||||||
return mDragInitMode;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set how a drag is initiated. Needs to be one of
|
|
||||||
* {@link ON_DOWN}, {@link ON_DRAG}, or {@link ON_LONG_PRESS}.
|
|
||||||
*
|
|
||||||
* @param mode The drag init mode.
|
|
||||||
*/
|
|
||||||
public void setDragInitMode(int mode) {
|
|
||||||
mDragInitMode = mode;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Enable/Disable list item sorting. Disabling is useful if only item
|
|
||||||
* removal is desired. Prevents drags in the vertical direction.
|
|
||||||
*
|
|
||||||
* @param enabled Set <code>true</code> to enable list
|
|
||||||
* item sorting.
|
|
||||||
*/
|
|
||||||
public void setSortEnabled(boolean enabled) {
|
|
||||||
mSortEnabled = enabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isSortEnabled() {
|
|
||||||
return mSortEnabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* One of {@link CLICK_REMOVE}, {@link FLING_RIGHT_REMOVE},
|
|
||||||
* {@link FLING_LEFT_REMOVE},
|
|
||||||
* {@link SLIDE_RIGHT_REMOVE}, or {@link SLIDE_LEFT_REMOVE}.
|
|
||||||
*/
|
|
||||||
public void setRemoveMode(int mode) {
|
|
||||||
mRemoveMode = mode;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getRemoveMode() {
|
|
||||||
return mRemoveMode;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Enable/Disable item removal without affecting remove mode.
|
|
||||||
*/
|
|
||||||
public void setRemoveEnabled(boolean enabled) {
|
|
||||||
mRemoveEnabled = enabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isRemoveEnabled() {
|
|
||||||
return mRemoveEnabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the resource id for the View that represents the drag
|
|
||||||
* handle in a list item.
|
|
||||||
*
|
|
||||||
* @param id An android resource id.
|
|
||||||
*/
|
|
||||||
public void setDragHandleId(int id) {
|
|
||||||
mDragHandleId = id;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the resource id for the View that represents the fling
|
|
||||||
* handle in a list item.
|
|
||||||
*
|
|
||||||
* @param id An android resource id.
|
|
||||||
*/
|
|
||||||
public void setFlingHandleId(int id) {
|
|
||||||
mFlingHandleId = id;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the resource id for the View that represents click
|
|
||||||
* removal button.
|
|
||||||
*
|
|
||||||
* @param id An android resource id.
|
|
||||||
*/
|
|
||||||
public void setClickRemoveId(int id) {
|
|
||||||
mClickRemoveId = id;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets flags to restrict certain motions of the floating View
|
|
||||||
* based on DragSortController settings (such as remove mode).
|
|
||||||
* Starts the drag on the DragSortListView.
|
|
||||||
*
|
|
||||||
* @param position The list item position (includes headers).
|
|
||||||
* @param deltaX Touch x-coord minus left edge of floating View.
|
|
||||||
* @param deltaY Touch y-coord minus top edge of floating View.
|
|
||||||
*
|
|
||||||
* @return True if drag started, false otherwise.
|
|
||||||
*/
|
|
||||||
public boolean startDrag(int position, int deltaX, int deltaY) {
|
|
||||||
|
|
||||||
int dragFlags = 0;
|
|
||||||
if (mSortEnabled && !mIsRemoving) {
|
|
||||||
dragFlags |= DragSortListView.DRAG_POS_Y | DragSortListView.DRAG_NEG_Y;
|
|
||||||
}
|
|
||||||
if (mRemoveEnabled && mIsRemoving) {
|
|
||||||
dragFlags |= DragSortListView.DRAG_POS_X;
|
|
||||||
dragFlags |= DragSortListView.DRAG_NEG_X;
|
|
||||||
}
|
|
||||||
|
|
||||||
mDragging = mDslv.startDrag(position - mDslv.getHeaderViewsCount(), dragFlags, deltaX,
|
|
||||||
deltaY);
|
|
||||||
return mDragging;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onTouch(View v, MotionEvent ev) {
|
|
||||||
if (!mDslv.isDragEnabled() || mDslv.listViewIntercepted()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
mDetector.onTouchEvent(ev);
|
|
||||||
if (mRemoveEnabled && mDragging && mRemoveMode == FLING_REMOVE) {
|
|
||||||
mFlingRemoveDetector.onTouchEvent(ev);
|
|
||||||
}
|
|
||||||
|
|
||||||
int action = ev.getAction() & MotionEvent.ACTION_MASK;
|
|
||||||
switch (action) {
|
|
||||||
case MotionEvent.ACTION_DOWN:
|
|
||||||
mCurrX = (int) ev.getX();
|
|
||||||
mCurrY = (int) ev.getY();
|
|
||||||
break;
|
|
||||||
case MotionEvent.ACTION_UP:
|
|
||||||
if (mRemoveEnabled && mIsRemoving) {
|
|
||||||
int x = mPositionX >= 0 ? mPositionX : -mPositionX;
|
|
||||||
int removePoint = mDslv.getWidth() / 2;
|
|
||||||
if (x > removePoint) {
|
|
||||||
mDslv.stopDragWithVelocity(true, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case MotionEvent.ACTION_CANCEL:
|
|
||||||
mIsRemoving = false;
|
|
||||||
mDragging = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Overrides to provide fading when slide removal is enabled.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void onDragFloatView(View floatView, Point position, Point touch) {
|
|
||||||
|
|
||||||
if (mRemoveEnabled && mIsRemoving) {
|
|
||||||
mPositionX = position.x;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the position to start dragging based on the ACTION_DOWN
|
|
||||||
* MotionEvent. This function simply calls
|
|
||||||
* {@link #dragHandleHitPosition(MotionEvent)}. Override
|
|
||||||
* to change drag handle behavior;
|
|
||||||
* this function is called internally when an ACTION_DOWN
|
|
||||||
* event is detected.
|
|
||||||
*
|
|
||||||
* @param ev The ACTION_DOWN MotionEvent.
|
|
||||||
*
|
|
||||||
* @return The list position to drag if a drag-init gesture is
|
|
||||||
* detected; MISS if unsuccessful.
|
|
||||||
*/
|
|
||||||
public int startDragPosition(MotionEvent ev) {
|
|
||||||
return dragHandleHitPosition(ev);
|
|
||||||
}
|
|
||||||
|
|
||||||
public int startFlingPosition(MotionEvent ev) {
|
|
||||||
return mRemoveMode == FLING_REMOVE ? flingHandleHitPosition(ev) : MISS;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks for the touch of an item's drag handle (specified by
|
|
||||||
* {@link #setDragHandleId(int)}), and returns that item's position
|
|
||||||
* if a drag handle touch was detected.
|
|
||||||
*
|
|
||||||
* @param ev The ACTION_DOWN MotionEvent.
|
|
||||||
|
|
||||||
* @return The list position of the item whose drag handle was
|
|
||||||
* touched; MISS if unsuccessful.
|
|
||||||
*/
|
|
||||||
public int dragHandleHitPosition(MotionEvent ev) {
|
|
||||||
return viewIdHitPosition(ev, mDragHandleId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public int flingHandleHitPosition(MotionEvent ev) {
|
|
||||||
return viewIdHitPosition(ev, mFlingHandleId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public int viewIdHitPosition(MotionEvent ev, int id) {
|
|
||||||
final int x = (int) ev.getX();
|
|
||||||
final int y = (int) ev.getY();
|
|
||||||
|
|
||||||
int touchPos = mDslv.pointToPosition(x, y); // includes headers/footers
|
|
||||||
|
|
||||||
final int numHeaders = mDslv.getHeaderViewsCount();
|
|
||||||
final int numFooters = mDslv.getFooterViewsCount();
|
|
||||||
final int count = mDslv.getCount();
|
|
||||||
|
|
||||||
// Log.d("mobeta", "touch down on position " + itemnum);
|
|
||||||
// We're only interested if the touch was on an
|
|
||||||
// item that's not a header or footer.
|
|
||||||
if (touchPos != AdapterView.INVALID_POSITION && touchPos >= numHeaders
|
|
||||||
&& touchPos < (count - numFooters)) {
|
|
||||||
final View item = mDslv.getChildAt(touchPos - mDslv.getFirstVisiblePosition());
|
|
||||||
final int rawX = (int) ev.getRawX();
|
|
||||||
final int rawY = (int) ev.getRawY();
|
|
||||||
|
|
||||||
View dragBox = id == 0 ? item : (View) item.findViewById(id);
|
|
||||||
if (dragBox != null) {
|
|
||||||
dragBox.getLocationOnScreen(mTempLoc);
|
|
||||||
|
|
||||||
if (rawX > mTempLoc[0] && rawY > mTempLoc[1] &&
|
|
||||||
rawX < mTempLoc[0] + dragBox.getWidth() &&
|
|
||||||
rawY < mTempLoc[1] + dragBox.getHeight()) {
|
|
||||||
|
|
||||||
mItemX = item.getLeft();
|
|
||||||
mItemY = item.getTop();
|
|
||||||
|
|
||||||
return touchPos;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return MISS;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onDown(MotionEvent ev) {
|
|
||||||
if (mRemoveEnabled && mRemoveMode == CLICK_REMOVE) {
|
|
||||||
mClickRemoveHitPos = viewIdHitPosition(ev, mClickRemoveId);
|
|
||||||
}
|
|
||||||
|
|
||||||
mHitPos = startDragPosition(ev);
|
|
||||||
if (mHitPos != MISS && mDragInitMode == ON_DOWN) {
|
|
||||||
startDrag(mHitPos, (int) ev.getX() - mItemX, (int) ev.getY() - mItemY);
|
|
||||||
}
|
|
||||||
|
|
||||||
mIsRemoving = false;
|
|
||||||
mCanDrag = true;
|
|
||||||
mPositionX = 0;
|
|
||||||
mFlingHitPos = startFlingPosition(ev);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
|
|
||||||
|
|
||||||
if(e1 == null) return false;
|
|
||||||
if(e2 == null) return false;
|
|
||||||
|
|
||||||
final int x1 = (int) e1.getX();
|
|
||||||
final int y1 = (int) e1.getY();
|
|
||||||
final int x2 = (int) e2.getX();
|
|
||||||
final int y2 = (int) e2.getY();
|
|
||||||
final int deltaX = x2 - mItemX;
|
|
||||||
final int deltaY = y2 - mItemY;
|
|
||||||
|
|
||||||
if (mCanDrag && !mDragging && (mHitPos != MISS || mFlingHitPos != MISS)) {
|
|
||||||
if (mHitPos != MISS) {
|
|
||||||
if (mDragInitMode == ON_DRAG && Math.abs(y2 - y1) > mTouchSlop && mSortEnabled) {
|
|
||||||
startDrag(mHitPos, deltaX, deltaY);
|
|
||||||
}
|
|
||||||
else if (mDragInitMode != ON_DOWN && Math.abs(x2 - x1) > mTouchSlop && mRemoveEnabled)
|
|
||||||
{
|
|
||||||
mIsRemoving = true;
|
|
||||||
startDrag(mFlingHitPos, deltaX, deltaY);
|
|
||||||
}
|
|
||||||
} else if (mFlingHitPos != MISS) {
|
|
||||||
if (Math.abs(x2 - x1) > mTouchSlop && mRemoveEnabled) {
|
|
||||||
mIsRemoving = true;
|
|
||||||
startDrag(mFlingHitPos, deltaX, deltaY);
|
|
||||||
} else if (Math.abs(y2 - y1) > mTouchSlop) {
|
|
||||||
mCanDrag = false; // if started to scroll the list then
|
|
||||||
// don't allow sorting nor fling-removing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// return whatever
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onLongPress(MotionEvent e) {
|
|
||||||
// Log.d("mobeta", "lift listener long pressed");
|
|
||||||
if (mHitPos != MISS && mDragInitMode == ON_LONG_PRESS) {
|
|
||||||
mDslv.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
|
|
||||||
startDrag(mHitPos, mCurrX - mItemX, mCurrY - mItemY);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// complete the OnGestureListener interface
|
|
||||||
@Override
|
|
||||||
public final boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// complete the OnGestureListener interface
|
|
||||||
@Override
|
|
||||||
public boolean onSingleTapUp(MotionEvent ev) {
|
|
||||||
if (mRemoveEnabled && mRemoveMode == CLICK_REMOVE) {
|
|
||||||
if (mClickRemoveHitPos != MISS) {
|
|
||||||
mDslv.removeItem(mClickRemoveHitPos - mDslv.getHeaderViewsCount());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// complete the OnGestureListener interface
|
|
||||||
@Override
|
|
||||||
public void onShowPress(MotionEvent ev) {
|
|
||||||
// do nothing
|
|
||||||
}
|
|
||||||
|
|
||||||
private GestureDetector.OnGestureListener mFlingRemoveListener =
|
|
||||||
new GestureDetector.SimpleOnGestureListener() {
|
|
||||||
@Override
|
|
||||||
public final boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
|
|
||||||
float velocityY) {
|
|
||||||
// Log.d("mobeta", "on fling remove called");
|
|
||||||
if (mRemoveEnabled && mIsRemoving) {
|
|
||||||
int w = mDslv.getWidth();
|
|
||||||
int minPos = w / 5;
|
|
||||||
if (velocityX > mFlingSpeed) {
|
|
||||||
if (mPositionX > -minPos) {
|
|
||||||
mDslv.stopDragWithVelocity(true, velocityX);
|
|
||||||
}
|
|
||||||
} else if (velocityX < -mFlingSpeed) {
|
|
||||||
if (mPositionX < minPos) {
|
|
||||||
mDslv.stopDragWithVelocity(true, velocityX);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
mIsRemoving = false;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,241 +0,0 @@
|
|||||||
package com.mobeta.android.dslv;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.database.Cursor;
|
|
||||||
import android.util.SparseIntArray;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.support.v4.widget.CursorAdapter;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A subclass of {@link android.widget.CursorAdapter} that provides
|
|
||||||
* reordering of the elements in the Cursor based on completed
|
|
||||||
* drag-sort operations. The reordering is a simple mapping of
|
|
||||||
* list positions into Cursor positions (the Cursor is unchanged).
|
|
||||||
* To persist changes made by drag-sorts, one can retrieve the
|
|
||||||
* mapping with the {@link #getCursorPositions()} method, which
|
|
||||||
* returns the reordered list of Cursor positions.
|
|
||||||
*
|
|
||||||
* An instance of this class is passed
|
|
||||||
* to {@link DragSortListView#setAdapter(ListAdapter)} and, since
|
|
||||||
* this class implements the {@link DragSortListView.DragSortListener}
|
|
||||||
* interface, it is automatically set as the DragSortListener for
|
|
||||||
* the DragSortListView instance.
|
|
||||||
*/
|
|
||||||
public abstract class DragSortCursorAdapter extends CursorAdapter implements DragSortListView.DragSortListener {
|
|
||||||
|
|
||||||
public static final int REMOVED = -1;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Key is ListView position, value is Cursor position
|
|
||||||
*/
|
|
||||||
private SparseIntArray mListMapping = new SparseIntArray();
|
|
||||||
|
|
||||||
private ArrayList<Integer> mRemovedCursorPositions = new ArrayList<Integer>();
|
|
||||||
|
|
||||||
public DragSortCursorAdapter(Context context, Cursor c) {
|
|
||||||
super(context, c);
|
|
||||||
}
|
|
||||||
|
|
||||||
public DragSortCursorAdapter(Context context, Cursor c, boolean autoRequery) {
|
|
||||||
super(context, c, autoRequery);
|
|
||||||
}
|
|
||||||
|
|
||||||
public DragSortCursorAdapter(Context context, Cursor c, int flags) {
|
|
||||||
super(context, c, flags);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Swaps Cursor and clears list-Cursor mapping.
|
|
||||||
*
|
|
||||||
* @see android.widget.CursorAdapter#swapCursor(android.database.Cursor)
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public Cursor swapCursor(Cursor newCursor) {
|
|
||||||
Cursor old = super.swapCursor(newCursor);
|
|
||||||
resetMappings();
|
|
||||||
return old;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Changes Cursor and clears list-Cursor mapping.
|
|
||||||
*
|
|
||||||
* @see android.widget.CursorAdapter#changeCursor(android.database.Cursor)
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void changeCursor(Cursor cursor) {
|
|
||||||
super.changeCursor(cursor);
|
|
||||||
resetMappings();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resets list-cursor mapping.
|
|
||||||
*/
|
|
||||||
public void reset() {
|
|
||||||
resetMappings();
|
|
||||||
notifyDataSetChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void resetMappings() {
|
|
||||||
mListMapping.clear();
|
|
||||||
mRemovedCursorPositions.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Object getItem(int position) {
|
|
||||||
return super.getItem(mListMapping.get(position, position));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public long getItemId(int position) {
|
|
||||||
return super.getItemId(mListMapping.get(position, position));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public View getDropDownView(int position, View convertView, ViewGroup parent) {
|
|
||||||
return super.getDropDownView(mListMapping.get(position, position), convertView, parent);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public View getView(int position, View convertView, ViewGroup parent) {
|
|
||||||
return super.getView(mListMapping.get(position, position), convertView, parent);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* On drop, this updates the mapping between Cursor positions
|
|
||||||
* and ListView positions. The Cursor is unchanged. Retrieve
|
|
||||||
* the current mapping with {@link getCursorPositions()}.
|
|
||||||
*
|
|
||||||
* @see DragSortListView.DropListener#drop(int, int)
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void drop(int from, int to) {
|
|
||||||
if (from != to) {
|
|
||||||
int cursorFrom = mListMapping.get(from, from);
|
|
||||||
|
|
||||||
if (from > to) {
|
|
||||||
for (int i = from; i > to; --i) {
|
|
||||||
mListMapping.put(i, mListMapping.get(i - 1, i - 1));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for (int i = from; i < to; ++i) {
|
|
||||||
mListMapping.put(i, mListMapping.get(i + 1, i + 1));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
mListMapping.put(to, cursorFrom);
|
|
||||||
|
|
||||||
cleanMapping();
|
|
||||||
notifyDataSetChanged();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* On remove, this updates the mapping between Cursor positions
|
|
||||||
* and ListView positions. The Cursor is unchanged. Retrieve
|
|
||||||
* the current mapping with {@link getCursorPositions()}.
|
|
||||||
*
|
|
||||||
* @see DragSortListView.RemoveListener#remove(int)
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void remove(int which) {
|
|
||||||
int cursorPos = mListMapping.get(which, which);
|
|
||||||
if (!mRemovedCursorPositions.contains(cursorPos)) {
|
|
||||||
mRemovedCursorPositions.add(cursorPos);
|
|
||||||
}
|
|
||||||
|
|
||||||
int newCount = getCount();
|
|
||||||
for (int i = which; i < newCount; ++i) {
|
|
||||||
mListMapping.put(i, mListMapping.get(i + 1, i + 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
mListMapping.delete(newCount);
|
|
||||||
|
|
||||||
cleanMapping();
|
|
||||||
notifyDataSetChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Does nothing. Just completes DragSortListener interface.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void drag(int from, int to) {
|
|
||||||
// do nothing
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove unnecessary mappings from sparse array.
|
|
||||||
*/
|
|
||||||
private void cleanMapping() {
|
|
||||||
ArrayList<Integer> toRemove = new ArrayList<Integer>();
|
|
||||||
|
|
||||||
int size = mListMapping.size();
|
|
||||||
for (int i = 0; i < size; ++i) {
|
|
||||||
if (mListMapping.keyAt(i) == mListMapping.valueAt(i)) {
|
|
||||||
toRemove.add(mListMapping.keyAt(i));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
size = toRemove.size();
|
|
||||||
for (int i = 0; i < size; ++i) {
|
|
||||||
mListMapping.delete(toRemove.get(i));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getCount() {
|
|
||||||
return super.getCount() - mRemovedCursorPositions.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the Cursor position mapped to by the provided list position
|
|
||||||
* (given all previously handled drag-sort
|
|
||||||
* operations).
|
|
||||||
*
|
|
||||||
* @param position List position
|
|
||||||
*
|
|
||||||
* @return The mapped-to Cursor position
|
|
||||||
*/
|
|
||||||
public int getCursorPosition(int position) {
|
|
||||||
return mListMapping.get(position, position);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the current order of Cursor positions presented by the
|
|
||||||
* list.
|
|
||||||
*/
|
|
||||||
public ArrayList<Integer> getCursorPositions() {
|
|
||||||
ArrayList<Integer> result = new ArrayList<Integer>();
|
|
||||||
|
|
||||||
for (int i = 0; i < getCount(); ++i) {
|
|
||||||
result.add(mListMapping.get(i, i));
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the list position mapped to by the provided Cursor position.
|
|
||||||
* If the provided Cursor position has been removed by a drag-sort,
|
|
||||||
* this returns {@link #REMOVED}.
|
|
||||||
*
|
|
||||||
* @param cursorPosition A Cursor position
|
|
||||||
* @return The mapped-to list position or REMOVED
|
|
||||||
*/
|
|
||||||
public int getListPosition(int cursorPosition) {
|
|
||||||
if (mRemovedCursorPositions.contains(cursorPosition)) {
|
|
||||||
return REMOVED;
|
|
||||||
}
|
|
||||||
|
|
||||||
int index = mListMapping.indexOfValue(cursorPosition);
|
|
||||||
if (index < 0) {
|
|
||||||
return cursorPosition;
|
|
||||||
} else {
|
|
||||||
return mListMapping.keyAt(index);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
package com.mobeta.android.dslv;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.view.Gravity;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.View.MeasureSpec;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.AbsListView;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Lightweight ViewGroup that wraps list items obtained from user's
|
|
||||||
* ListAdapter. ItemView expects a single child that has a definite
|
|
||||||
* height (i.e. the child's layout height is not MATCH_PARENT).
|
|
||||||
* The width of
|
|
||||||
* ItemView will always match the width of its child (that is,
|
|
||||||
* the width MeasureSpec given to ItemView is passed directly
|
|
||||||
* to the child, and the ItemView measured width is set to the
|
|
||||||
* child's measured width). The height of ItemView can be anything;
|
|
||||||
* the
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* The purpose of this class is to optimize slide
|
|
||||||
* shuffle animations.
|
|
||||||
*/
|
|
||||||
public class DragSortItemView extends ViewGroup {
|
|
||||||
|
|
||||||
private int mGravity = Gravity.TOP;
|
|
||||||
|
|
||||||
public DragSortItemView(Context context) {
|
|
||||||
super(context);
|
|
||||||
|
|
||||||
// always init with standard ListView layout params
|
|
||||||
setLayoutParams(new AbsListView.LayoutParams(
|
|
||||||
ViewGroup.LayoutParams.FILL_PARENT,
|
|
||||||
ViewGroup.LayoutParams.WRAP_CONTENT));
|
|
||||||
|
|
||||||
//setClipChildren(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setGravity(int gravity) {
|
|
||||||
mGravity = gravity;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getGravity() {
|
|
||||||
return mGravity;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
|
|
||||||
final View child = getChildAt(0);
|
|
||||||
|
|
||||||
if (child == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mGravity == Gravity.TOP) {
|
|
||||||
child.layout(0, 0, getMeasuredWidth(), child.getMeasuredHeight());
|
|
||||||
} else {
|
|
||||||
child.layout(0, getMeasuredHeight() - child.getMeasuredHeight(), getMeasuredWidth(), getMeasuredHeight());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
|
||||||
|
|
||||||
int height = MeasureSpec.getSize(heightMeasureSpec);
|
|
||||||
int width = MeasureSpec.getSize(widthMeasureSpec);
|
|
||||||
|
|
||||||
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
|
|
||||||
|
|
||||||
final View child = getChildAt(0);
|
|
||||||
if (child == null) {
|
|
||||||
setMeasuredDimension(0, width);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (child.isLayoutRequested()) {
|
|
||||||
// Always let child be as tall as it wants.
|
|
||||||
measureChild(child, widthMeasureSpec,
|
|
||||||
MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (heightMode == MeasureSpec.UNSPECIFIED) {
|
|
||||||
ViewGroup.LayoutParams lp = getLayoutParams();
|
|
||||||
|
|
||||||
if (lp.height > 0) {
|
|
||||||
height = lp.height;
|
|
||||||
} else {
|
|
||||||
height = child.getMeasuredHeight();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setMeasuredDimension(width, height);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
package com.mobeta.android.dslv;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.view.Gravity;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.View.MeasureSpec;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.AbsListView;
|
|
||||||
import android.widget.Checkable;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Lightweight ViewGroup that wraps list items obtained from user's
|
|
||||||
* ListAdapter. ItemView expects a single child that has a definite
|
|
||||||
* height (i.e. the child's layout height is not MATCH_PARENT).
|
|
||||||
* The width of
|
|
||||||
* ItemView will always match the width of its child (that is,
|
|
||||||
* the width MeasureSpec given to ItemView is passed directly
|
|
||||||
* to the child, and the ItemView measured width is set to the
|
|
||||||
* child's measured width). The height of ItemView can be anything;
|
|
||||||
* the
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* The purpose of this class is to optimize slide
|
|
||||||
* shuffle animations.
|
|
||||||
*/
|
|
||||||
public class DragSortItemViewCheckable extends DragSortItemView implements Checkable {
|
|
||||||
|
|
||||||
public DragSortItemViewCheckable(Context context) {
|
|
||||||
super(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isChecked() {
|
|
||||||
View child = getChildAt(0);
|
|
||||||
if (child instanceof Checkable)
|
|
||||||
return ((Checkable) child).isChecked();
|
|
||||||
else
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setChecked(boolean checked) {
|
|
||||||
View child = getChildAt(0);
|
|
||||||
if (child instanceof Checkable)
|
|
||||||
((Checkable) child).setChecked(checked);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void toggle() {
|
|
||||||
View child = getChildAt(0);
|
|
||||||
if (child instanceof Checkable)
|
|
||||||
((Checkable) child).toggle();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,133 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2011 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
|
|
||||||
*
|
|
||||||
* http://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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.mobeta.android.dslv;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.database.Cursor;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
|
|
||||||
// taken from v4 rev. 10 ResourceCursorAdapter.java
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Static library support version of the framework's {@link android.widget.ResourceCursorAdapter}.
|
|
||||||
* Used to write apps that run on platforms prior to Android 3.0. When running
|
|
||||||
* on Android 3.0 or above, this implementation is still used; it does not try
|
|
||||||
* to switch to the framework's implementation. See the framework SDK
|
|
||||||
* documentation for a class overview.
|
|
||||||
*/
|
|
||||||
public abstract class ResourceDragSortCursorAdapter extends DragSortCursorAdapter {
|
|
||||||
private int mLayout;
|
|
||||||
|
|
||||||
private int mDropDownLayout;
|
|
||||||
|
|
||||||
private LayoutInflater mInflater;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructor the enables auto-requery.
|
|
||||||
*
|
|
||||||
* @deprecated This option is discouraged, as it results in Cursor queries
|
|
||||||
* being performed on the application's UI thread and thus can cause poor
|
|
||||||
* responsiveness or even Application Not Responding errors. As an alternative,
|
|
||||||
* use {@link android.app.LoaderManager} with a {@link android.content.CursorLoader}.
|
|
||||||
*
|
|
||||||
* @param context The context where the ListView associated with this adapter is running
|
|
||||||
* @param layout resource identifier of a layout file that defines the views
|
|
||||||
* for this list item. Unless you override them later, this will
|
|
||||||
* define both the item views and the drop down views.
|
|
||||||
*/
|
|
||||||
@Deprecated
|
|
||||||
public ResourceDragSortCursorAdapter(Context context, int layout, Cursor c) {
|
|
||||||
super(context, c);
|
|
||||||
mLayout = mDropDownLayout = layout;
|
|
||||||
mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructor with default behavior as per
|
|
||||||
* {@link CursorAdapter#CursorAdapter(Context, Cursor, boolean)}; it is recommended
|
|
||||||
* you not use this, but instead {@link #ResourceCursorAdapter(Context, int, Cursor, int)}.
|
|
||||||
* When using this constructor, {@link #FLAG_REGISTER_CONTENT_OBSERVER}
|
|
||||||
* will always be set.
|
|
||||||
*
|
|
||||||
* @param context The context where the ListView associated with this adapter is running
|
|
||||||
* @param layout resource identifier of a layout file that defines the views
|
|
||||||
* for this list item. Unless you override them later, this will
|
|
||||||
* define both the item views and the drop down views.
|
|
||||||
* @param c The cursor from which to get the data.
|
|
||||||
* @param autoRequery If true the adapter will call requery() on the
|
|
||||||
* cursor whenever it changes so the most recent
|
|
||||||
* data is always displayed. Using true here is discouraged.
|
|
||||||
*/
|
|
||||||
public ResourceDragSortCursorAdapter(Context context, int layout, Cursor c, boolean autoRequery) {
|
|
||||||
super(context, c, autoRequery);
|
|
||||||
mLayout = mDropDownLayout = layout;
|
|
||||||
mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Standard constructor.
|
|
||||||
*
|
|
||||||
* @param context The context where the ListView associated with this adapter is running
|
|
||||||
* @param layout Resource identifier of a layout file that defines the views
|
|
||||||
* for this list item. Unless you override them later, this will
|
|
||||||
* define both the item views and the drop down views.
|
|
||||||
* @param c The cursor from which to get the data.
|
|
||||||
* @param flags Flags used to determine the behavior of the adapter,
|
|
||||||
* as per {@link CursorAdapter#CursorAdapter(Context, Cursor, int)}.
|
|
||||||
*/
|
|
||||||
public ResourceDragSortCursorAdapter(Context context, int layout, Cursor c, int flags) {
|
|
||||||
super(context, c, flags);
|
|
||||||
mLayout = mDropDownLayout = layout;
|
|
||||||
mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Inflates view(s) from the specified XML file.
|
|
||||||
*
|
|
||||||
* @see android.widget.CursorAdapter#newView(android.content.Context,
|
|
||||||
* android.database.Cursor, ViewGroup)
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public View newView(Context context, Cursor cursor, ViewGroup parent) {
|
|
||||||
return mInflater.inflate(mLayout, parent, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public View newDropDownView(Context context, Cursor cursor, ViewGroup parent) {
|
|
||||||
return mInflater.inflate(mDropDownLayout, parent, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* <p>Sets the layout resource of the item views.</p>
|
|
||||||
*
|
|
||||||
* @param layout the layout resources used to create item views
|
|
||||||
*/
|
|
||||||
public void setViewResource(int layout) {
|
|
||||||
mLayout = layout;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* <p>Sets the layout resource of the drop down views.</p>
|
|
||||||
*
|
|
||||||
* @param dropDownLayout the layout resources used to create drop down views
|
|
||||||
*/
|
|
||||||
public void setDropDownViewResource(int dropDownLayout) {
|
|
||||||
mDropDownLayout = dropDownLayout;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,422 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2006 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
|
|
||||||
*
|
|
||||||
* http://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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.mobeta.android.dslv;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.database.Cursor;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.view.View;
|
|
||||||
import android.widget.TextView;
|
|
||||||
import android.widget.ImageView;
|
|
||||||
|
|
||||||
// taken from sdk/sources/android-16/android/widget/SimpleCursorAdapter.java
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An easy adapter to map columns from a cursor to TextViews or ImageViews
|
|
||||||
* defined in an XML file. You can specify which columns you want, which
|
|
||||||
* views you want to display the columns, and the XML file that defines
|
|
||||||
* the appearance of these views.
|
|
||||||
*
|
|
||||||
* Binding occurs in two phases. First, if a
|
|
||||||
* {@link android.widget.SimpleCursorAdapter.ViewBinder} is available,
|
|
||||||
* {@link ViewBinder#setViewValue(android.view.View, android.database.Cursor, int)}
|
|
||||||
* is invoked. If the returned value is true, binding has occured. If the
|
|
||||||
* returned value is false and the view to bind is a TextView,
|
|
||||||
* {@link #setViewText(TextView, String)} is invoked. If the returned value
|
|
||||||
* is false and the view to bind is an ImageView,
|
|
||||||
* {@link #setViewImage(ImageView, String)} is invoked. If no appropriate
|
|
||||||
* binding can be found, an {@link IllegalStateException} is thrown.
|
|
||||||
*
|
|
||||||
* If this adapter is used with filtering, for instance in an
|
|
||||||
* {@link android.widget.AutoCompleteTextView}, you can use the
|
|
||||||
* {@link android.widget.SimpleCursorAdapter.CursorToStringConverter} and the
|
|
||||||
* {@link android.widget.FilterQueryProvider} interfaces
|
|
||||||
* to get control over the filtering process. You can refer to
|
|
||||||
* {@link #convertToString(android.database.Cursor)} and
|
|
||||||
* {@link #runQueryOnBackgroundThread(CharSequence)} for more information.
|
|
||||||
*/
|
|
||||||
public class SimpleDragSortCursorAdapter extends ResourceDragSortCursorAdapter {
|
|
||||||
/**
|
|
||||||
* A list of columns containing the data to bind to the UI.
|
|
||||||
* This field should be made private, so it is hidden from the SDK.
|
|
||||||
* {@hide}
|
|
||||||
*/
|
|
||||||
protected int[] mFrom;
|
|
||||||
/**
|
|
||||||
* A list of View ids representing the views to which the data must be bound.
|
|
||||||
* This field should be made private, so it is hidden from the SDK.
|
|
||||||
* {@hide}
|
|
||||||
*/
|
|
||||||
protected int[] mTo;
|
|
||||||
|
|
||||||
private int mStringConversionColumn = -1;
|
|
||||||
private CursorToStringConverter mCursorToStringConverter;
|
|
||||||
private ViewBinder mViewBinder;
|
|
||||||
|
|
||||||
String[] mOriginalFrom;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructor the enables auto-requery.
|
|
||||||
*
|
|
||||||
* @deprecated This option is discouraged, as it results in Cursor queries
|
|
||||||
* being performed on the application's UI thread and thus can cause poor
|
|
||||||
* responsiveness or even Application Not Responding errors. As an alternative,
|
|
||||||
* use {@link android.app.LoaderManager} with a {@link android.content.CursorLoader}.
|
|
||||||
*/
|
|
||||||
@Deprecated
|
|
||||||
public SimpleDragSortCursorAdapter(Context context, int layout, Cursor c, String[] from, int[] to) {
|
|
||||||
super(context, layout, c);
|
|
||||||
mTo = to;
|
|
||||||
mOriginalFrom = from;
|
|
||||||
findColumns(c, from);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Standard constructor.
|
|
||||||
*
|
|
||||||
* @param context The context where the ListView associated with this
|
|
||||||
* SimpleListItemFactory is running
|
|
||||||
* @param layout resource identifier of a layout file that defines the views
|
|
||||||
* for this list item. The layout file should include at least
|
|
||||||
* those named views defined in "to"
|
|
||||||
* @param c The database cursor. Can be null if the cursor is not available yet.
|
|
||||||
* @param from A list of column names representing the data to bind to the UI. Can be null
|
|
||||||
* if the cursor is not available yet.
|
|
||||||
* @param to The views that should display column in the "from" parameter.
|
|
||||||
* These should all be TextViews. The first N views in this list
|
|
||||||
* are given the values of the first N columns in the from
|
|
||||||
* parameter. Can be null if the cursor is not available yet.
|
|
||||||
* @param flags Flags used to determine the behavior of the adapter,
|
|
||||||
* as per {@link CursorAdapter#CursorAdapter(Context, Cursor, int)}.
|
|
||||||
*/
|
|
||||||
public SimpleDragSortCursorAdapter(Context context, int layout,
|
|
||||||
Cursor c, String[] from, int[] to, int flags) {
|
|
||||||
super(context, layout, c, flags);
|
|
||||||
mTo = to;
|
|
||||||
mOriginalFrom = from;
|
|
||||||
findColumns(c, from);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Binds all of the field names passed into the "to" parameter of the
|
|
||||||
* constructor with their corresponding cursor columns as specified in the
|
|
||||||
* "from" parameter.
|
|
||||||
*
|
|
||||||
* Binding occurs in two phases. First, if a
|
|
||||||
* {@link android.widget.SimpleCursorAdapter.ViewBinder} is available,
|
|
||||||
* {@link ViewBinder#setViewValue(android.view.View, android.database.Cursor, int)}
|
|
||||||
* is invoked. If the returned value is true, binding has occured. If the
|
|
||||||
* returned value is false and the view to bind is a TextView,
|
|
||||||
* {@link #setViewText(TextView, String)} is invoked. If the returned value is
|
|
||||||
* false and the view to bind is an ImageView,
|
|
||||||
* {@link #setViewImage(ImageView, String)} is invoked. If no appropriate
|
|
||||||
* binding can be found, an {@link IllegalStateException} is thrown.
|
|
||||||
*
|
|
||||||
* @throws IllegalStateException if binding cannot occur
|
|
||||||
*
|
|
||||||
* @see android.widget.CursorAdapter#bindView(android.view.View,
|
|
||||||
* android.content.Context, android.database.Cursor)
|
|
||||||
* @see #getViewBinder()
|
|
||||||
* @see #setViewBinder(android.widget.SimpleCursorAdapter.ViewBinder)
|
|
||||||
* @see #setViewImage(ImageView, String)
|
|
||||||
* @see #setViewText(TextView, String)
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void bindView(View view, Context context, Cursor cursor) {
|
|
||||||
final ViewBinder binder = mViewBinder;
|
|
||||||
final int count = mTo.length;
|
|
||||||
final int[] from = mFrom;
|
|
||||||
final int[] to = mTo;
|
|
||||||
|
|
||||||
for (int i = 0; i < count; i++) {
|
|
||||||
final View v = view.findViewById(to[i]);
|
|
||||||
if (v != null) {
|
|
||||||
boolean bound = false;
|
|
||||||
if (binder != null) {
|
|
||||||
bound = binder.setViewValue(v, cursor, from[i]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!bound) {
|
|
||||||
String text = cursor.getString(from[i]);
|
|
||||||
if (text == null) {
|
|
||||||
text = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (v instanceof TextView) {
|
|
||||||
setViewText((TextView) v, text);
|
|
||||||
} else if (v instanceof ImageView) {
|
|
||||||
setViewImage((ImageView) v, text);
|
|
||||||
} else {
|
|
||||||
throw new IllegalStateException(v.getClass().getName() + " is not a " +
|
|
||||||
" view that can be bounds by this SimpleCursorAdapter");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the {@link ViewBinder} used to bind data to views.
|
|
||||||
*
|
|
||||||
* @return a ViewBinder or null if the binder does not exist
|
|
||||||
*
|
|
||||||
* @see #bindView(android.view.View, android.content.Context, android.database.Cursor)
|
|
||||||
* @see #setViewBinder(android.widget.SimpleCursorAdapter.ViewBinder)
|
|
||||||
*/
|
|
||||||
public ViewBinder getViewBinder() {
|
|
||||||
return mViewBinder;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the binder used to bind data to views.
|
|
||||||
*
|
|
||||||
* @param viewBinder the binder used to bind data to views, can be null to
|
|
||||||
* remove the existing binder
|
|
||||||
*
|
|
||||||
* @see #bindView(android.view.View, android.content.Context, android.database.Cursor)
|
|
||||||
* @see #getViewBinder()
|
|
||||||
*/
|
|
||||||
public void setViewBinder(ViewBinder viewBinder) {
|
|
||||||
mViewBinder = viewBinder;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called by bindView() to set the image for an ImageView but only if
|
|
||||||
* there is no existing ViewBinder or if the existing ViewBinder cannot
|
|
||||||
* handle binding to an ImageView.
|
|
||||||
*
|
|
||||||
* By default, the value will be treated as an image resource. If the
|
|
||||||
* value cannot be used as an image resource, the value is used as an
|
|
||||||
* image Uri.
|
|
||||||
*
|
|
||||||
* Intended to be overridden by Adapters that need to filter strings
|
|
||||||
* retrieved from the database.
|
|
||||||
*
|
|
||||||
* @param v ImageView to receive an image
|
|
||||||
* @param value the value retrieved from the cursor
|
|
||||||
*/
|
|
||||||
public void setViewImage(ImageView v, String value) {
|
|
||||||
try {
|
|
||||||
v.setImageResource(Integer.parseInt(value));
|
|
||||||
} catch (NumberFormatException nfe) {
|
|
||||||
v.setImageURI(Uri.parse(value));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called by bindView() to set the text for a TextView but only if
|
|
||||||
* there is no existing ViewBinder or if the existing ViewBinder cannot
|
|
||||||
* handle binding to a TextView.
|
|
||||||
*
|
|
||||||
* Intended to be overridden by Adapters that need to filter strings
|
|
||||||
* retrieved from the database.
|
|
||||||
*
|
|
||||||
* @param v TextView to receive text
|
|
||||||
* @param text the text to be set for the TextView
|
|
||||||
*/
|
|
||||||
public void setViewText(TextView v, String text) {
|
|
||||||
v.setText(text);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the index of the column used to get a String representation
|
|
||||||
* of the Cursor.
|
|
||||||
*
|
|
||||||
* @return a valid index in the current Cursor or -1
|
|
||||||
*
|
|
||||||
* @see android.widget.CursorAdapter#convertToString(android.database.Cursor)
|
|
||||||
* @see #setStringConversionColumn(int)
|
|
||||||
* @see #setCursorToStringConverter(android.widget.SimpleCursorAdapter.CursorToStringConverter)
|
|
||||||
* @see #getCursorToStringConverter()
|
|
||||||
*/
|
|
||||||
public int getStringConversionColumn() {
|
|
||||||
return mStringConversionColumn;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Defines the index of the column in the Cursor used to get a String
|
|
||||||
* representation of that Cursor. The column is used to convert the
|
|
||||||
* Cursor to a String only when the current CursorToStringConverter
|
|
||||||
* is null.
|
|
||||||
*
|
|
||||||
* @param stringConversionColumn a valid index in the current Cursor or -1 to use the default
|
|
||||||
* conversion mechanism
|
|
||||||
*
|
|
||||||
* @see android.widget.CursorAdapter#convertToString(android.database.Cursor)
|
|
||||||
* @see #getStringConversionColumn()
|
|
||||||
* @see #setCursorToStringConverter(android.widget.SimpleCursorAdapter.CursorToStringConverter)
|
|
||||||
* @see #getCursorToStringConverter()
|
|
||||||
*/
|
|
||||||
public void setStringConversionColumn(int stringConversionColumn) {
|
|
||||||
mStringConversionColumn = stringConversionColumn;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the converter used to convert the filtering Cursor
|
|
||||||
* into a String.
|
|
||||||
*
|
|
||||||
* @return null if the converter does not exist or an instance of
|
|
||||||
* {@link android.widget.SimpleCursorAdapter.CursorToStringConverter}
|
|
||||||
*
|
|
||||||
* @see #setCursorToStringConverter(android.widget.SimpleCursorAdapter.CursorToStringConverter)
|
|
||||||
* @see #getStringConversionColumn()
|
|
||||||
* @see #setStringConversionColumn(int)
|
|
||||||
* @see android.widget.CursorAdapter#convertToString(android.database.Cursor)
|
|
||||||
*/
|
|
||||||
public CursorToStringConverter getCursorToStringConverter() {
|
|
||||||
return mCursorToStringConverter;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the converter used to convert the filtering Cursor
|
|
||||||
* into a String.
|
|
||||||
*
|
|
||||||
* @param cursorToStringConverter the Cursor to String converter, or
|
|
||||||
* null to remove the converter
|
|
||||||
*
|
|
||||||
* @see #setCursorToStringConverter(android.widget.SimpleCursorAdapter.CursorToStringConverter)
|
|
||||||
* @see #getStringConversionColumn()
|
|
||||||
* @see #setStringConversionColumn(int)
|
|
||||||
* @see android.widget.CursorAdapter#convertToString(android.database.Cursor)
|
|
||||||
*/
|
|
||||||
public void setCursorToStringConverter(CursorToStringConverter cursorToStringConverter) {
|
|
||||||
mCursorToStringConverter = cursorToStringConverter;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a CharSequence representation of the specified Cursor as defined
|
|
||||||
* by the current CursorToStringConverter. If no CursorToStringConverter
|
|
||||||
* has been set, the String conversion column is used instead. If the
|
|
||||||
* conversion column is -1, the returned String is empty if the cursor
|
|
||||||
* is null or Cursor.toString().
|
|
||||||
*
|
|
||||||
* @param cursor the Cursor to convert to a CharSequence
|
|
||||||
*
|
|
||||||
* @return a non-null CharSequence representing the cursor
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public CharSequence convertToString(Cursor cursor) {
|
|
||||||
if (mCursorToStringConverter != null) {
|
|
||||||
return mCursorToStringConverter.convertToString(cursor);
|
|
||||||
} else if (mStringConversionColumn > -1) {
|
|
||||||
return cursor.getString(mStringConversionColumn);
|
|
||||||
}
|
|
||||||
|
|
||||||
return super.convertToString(cursor);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a map from an array of strings to an array of column-id integers in cursor c.
|
|
||||||
* If c is null, the array will be discarded.
|
|
||||||
*
|
|
||||||
* @param c the cursor to find the columns from
|
|
||||||
* @param from the Strings naming the columns of interest
|
|
||||||
*/
|
|
||||||
private void findColumns(Cursor c, String[] from) {
|
|
||||||
if (c != null) {
|
|
||||||
int i;
|
|
||||||
int count = from.length;
|
|
||||||
if (mFrom == null || mFrom.length != count) {
|
|
||||||
mFrom = new int[count];
|
|
||||||
}
|
|
||||||
for (i = 0; i < count; i++) {
|
|
||||||
mFrom[i] = c.getColumnIndexOrThrow(from[i]);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
mFrom = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Cursor swapCursor(Cursor c) {
|
|
||||||
// super.swapCursor() will notify observers before we have
|
|
||||||
// a valid mapping, make sure we have a mapping before this
|
|
||||||
// happens
|
|
||||||
findColumns(c, mOriginalFrom);
|
|
||||||
return super.swapCursor(c);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Change the cursor and change the column-to-view mappings at the same time.
|
|
||||||
*
|
|
||||||
* @param c The database cursor. Can be null if the cursor is not available yet.
|
|
||||||
* @param from A list of column names representing the data to bind to the UI. Can be null
|
|
||||||
* if the cursor is not available yet.
|
|
||||||
* @param to The views that should display column in the "from" parameter.
|
|
||||||
* These should all be TextViews. The first N views in this list
|
|
||||||
* are given the values of the first N columns in the from
|
|
||||||
* parameter. Can be null if the cursor is not available yet.
|
|
||||||
*/
|
|
||||||
public void changeCursorAndColumns(Cursor c, String[] from, int[] to) {
|
|
||||||
mOriginalFrom = from;
|
|
||||||
mTo = to;
|
|
||||||
// super.changeCursor() will notify observers before we have
|
|
||||||
// a valid mapping, make sure we have a mapping before this
|
|
||||||
// happens
|
|
||||||
findColumns(c, mOriginalFrom);
|
|
||||||
super.changeCursor(c);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This class can be used by external clients of SimpleCursorAdapter
|
|
||||||
* to bind values fom the Cursor to views.
|
|
||||||
*
|
|
||||||
* You should use this class to bind values from the Cursor to views
|
|
||||||
* that are not directly supported by SimpleCursorAdapter or to
|
|
||||||
* change the way binding occurs for views supported by
|
|
||||||
* SimpleCursorAdapter.
|
|
||||||
*
|
|
||||||
* @see SimpleCursorAdapter#bindView(android.view.View, android.content.Context, android.database.Cursor)
|
|
||||||
* @see SimpleCursorAdapter#setViewImage(ImageView, String)
|
|
||||||
* @see SimpleCursorAdapter#setViewText(TextView, String)
|
|
||||||
*/
|
|
||||||
public static interface ViewBinder {
|
|
||||||
/**
|
|
||||||
* Binds the Cursor column defined by the specified index to the specified view.
|
|
||||||
*
|
|
||||||
* When binding is handled by this ViewBinder, this method must return true.
|
|
||||||
* If this method returns false, SimpleCursorAdapter will attempts to handle
|
|
||||||
* the binding on its own.
|
|
||||||
*
|
|
||||||
* @param view the view to bind the data to
|
|
||||||
* @param cursor the cursor to get the data from
|
|
||||||
* @param columnIndex the column at which the data can be found in the cursor
|
|
||||||
*
|
|
||||||
* @return true if the data was bound to the view, false otherwise
|
|
||||||
*/
|
|
||||||
boolean setViewValue(View view, Cursor cursor, int columnIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This class can be used by external clients of SimpleCursorAdapter
|
|
||||||
* to define how the Cursor should be converted to a String.
|
|
||||||
*
|
|
||||||
* @see android.widget.CursorAdapter#convertToString(android.database.Cursor)
|
|
||||||
*/
|
|
||||||
public static interface CursorToStringConverter {
|
|
||||||
/**
|
|
||||||
* Returns a CharSequence representing the specified Cursor.
|
|
||||||
*
|
|
||||||
* @param cursor the cursor for which a CharSequence representation
|
|
||||||
* is requested
|
|
||||||
*
|
|
||||||
* @return a non-null CharSequence representing the cursor
|
|
||||||
*/
|
|
||||||
CharSequence convertToString(Cursor cursor);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
package com.mobeta.android.dslv;
|
|
||||||
|
|
||||||
import android.graphics.Bitmap;
|
|
||||||
import android.graphics.Point;
|
|
||||||
import android.graphics.Color;
|
|
||||||
import android.widget.ListView;
|
|
||||||
import android.widget.ImageView;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Simple implementation of the FloatViewManager class. Uses list
|
|
||||||
* items as they appear in the ListView to create the floating View.
|
|
||||||
*/
|
|
||||||
public class SimpleFloatViewManager implements DragSortListView.FloatViewManager {
|
|
||||||
|
|
||||||
private Bitmap mFloatBitmap;
|
|
||||||
|
|
||||||
private ImageView mImageView;
|
|
||||||
|
|
||||||
private int mFloatBGColor = Color.BLACK;
|
|
||||||
|
|
||||||
private ListView mListView;
|
|
||||||
|
|
||||||
public SimpleFloatViewManager(ListView lv) {
|
|
||||||
mListView = lv;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setBackgroundColor(int color) {
|
|
||||||
mFloatBGColor = color;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This simple implementation creates a Bitmap copy of the
|
|
||||||
* list item currently shown at ListView <code>position</code>.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public View onCreateFloatView(int position) {
|
|
||||||
// Guaranteed that this will not be null? I think so. Nope, got
|
|
||||||
// a NullPointerException once...
|
|
||||||
View v = mListView.getChildAt(position + mListView.getHeaderViewsCount() - mListView.getFirstVisiblePosition());
|
|
||||||
|
|
||||||
if (v == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
v.setPressed(false);
|
|
||||||
|
|
||||||
// Create a copy of the drawing cache so that it does not get
|
|
||||||
// recycled by the framework when the list tries to clean up memory
|
|
||||||
//v.setDrawingCacheQuality(View.DRAWING_CACHE_QUALITY_HIGH);
|
|
||||||
v.setDrawingCacheEnabled(true);
|
|
||||||
mFloatBitmap = Bitmap.createBitmap(v.getDrawingCache());
|
|
||||||
v.setDrawingCacheEnabled(false);
|
|
||||||
|
|
||||||
if (mImageView == null) {
|
|
||||||
mImageView = new ImageView(mListView.getContext());
|
|
||||||
}
|
|
||||||
mImageView.setBackgroundColor(mFloatBGColor);
|
|
||||||
mImageView.setPadding(0, 0, 0, 0);
|
|
||||||
mImageView.setImageBitmap(mFloatBitmap);
|
|
||||||
mImageView.setLayoutParams(new ViewGroup.LayoutParams(v.getWidth(), v.getHeight()));
|
|
||||||
|
|
||||||
return mImageView;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This does nothing
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void onDragFloatView(View floatView, Point position, Point touch) {
|
|
||||||
// do nothing
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes the Bitmap from the ImageView created in
|
|
||||||
* onCreateFloatView() and tells the system to recycle it.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void onDestroyFloatView(View floatView) {
|
|
||||||
((ImageView) floatView).setImageDrawable(null);
|
|
||||||
|
|
||||||
mFloatBitmap.recycle();
|
|
||||||
mFloatBitmap = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,17 +1,20 @@
|
|||||||
/* Copyright (C) 2016 Alinson Santos Xavier
|
/*
|
||||||
|
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify it
|
* This file is part of Loop Habit Tracker.
|
||||||
* under the terms of the GNU General Public License as published by the Free
|
|
||||||
* Software Foundation, either version 3 of the License, or (at your option)
|
|
||||||
* any later version.
|
|
||||||
*
|
*
|
||||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
* it under the terms of the GNU General Public License as published by the
|
||||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
* Free Software Foundation, either version 3 of the License, or (at your
|
||||||
|
* option) any later version.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
* more details.
|
* more details.
|
||||||
*
|
*
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License along
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.isoron.helpers;
|
package org.isoron.helpers;
|
||||||
@@ -21,40 +24,71 @@ import android.graphics.Color;
|
|||||||
public class ColorHelper
|
public class ColorHelper
|
||||||
{
|
{
|
||||||
public static final int[] palette =
|
public static final int[] palette =
|
||||||
{
|
{
|
||||||
Color.parseColor("#D32F2F"), // red
|
Color.parseColor("#D32F2F"), // red
|
||||||
Color.parseColor("#E64A19"), // orange
|
Color.parseColor("#E64A19"), // orange
|
||||||
Color.parseColor("#F9A825"), // yellow
|
Color.parseColor("#F9A825"), // yellow
|
||||||
Color.parseColor("#AFB42B"), // light green
|
Color.parseColor("#AFB42B"), // light green
|
||||||
Color.parseColor("#388E3C"), // dark green
|
Color.parseColor("#388E3C"), // dark green
|
||||||
Color.parseColor("#00897B"), // teal
|
Color.parseColor("#00897B"), // teal
|
||||||
Color.parseColor("#00ACC1"), // cyan
|
Color.parseColor("#00ACC1"), // cyan
|
||||||
Color.parseColor("#039BE5"), // blue
|
Color.parseColor("#039BE5"), // blue
|
||||||
Color.parseColor("#5E35B1"), // deep purple
|
Color.parseColor("#5E35B1"), // deep purple
|
||||||
Color.parseColor("#8E24AA"), // purple
|
Color.parseColor("#8E24AA"), // purple
|
||||||
Color.parseColor("#D81B60"), // pink
|
Color.parseColor("#D81B60"), // pink
|
||||||
Color.parseColor("#303030"), // dark grey
|
Color.parseColor("#303030"), // dark grey
|
||||||
Color.parseColor("#aaaaaa") // light grey
|
Color.parseColor("#aaaaaa") // light grey
|
||||||
};
|
};
|
||||||
|
|
||||||
public static int mixColors(int color1, int color2, float amount)
|
public static int mixColors(int color1, int color2, float amount)
|
||||||
{
|
{
|
||||||
final byte ALPHA_CHANNEL = 24;
|
final byte ALPHA_CHANNEL = 24;
|
||||||
final byte RED_CHANNEL = 16;
|
final byte RED_CHANNEL = 16;
|
||||||
final byte GREEN_CHANNEL = 8;
|
final byte GREEN_CHANNEL = 8;
|
||||||
final byte BLUE_CHANNEL = 0;
|
final byte BLUE_CHANNEL = 0;
|
||||||
|
|
||||||
final float inverseAmount = 1.0f - amount;
|
final float inverseAmount = 1.0f - amount;
|
||||||
|
|
||||||
int a = ((int) (((float) (color1 >> ALPHA_CHANNEL & 0xff) * amount) +
|
int a = ((int) (((float) (color1 >> ALPHA_CHANNEL & 0xff) * amount) +
|
||||||
((float) (color2 >> ALPHA_CHANNEL & 0xff) * inverseAmount))) & 0xff;
|
((float) (color2 >> ALPHA_CHANNEL & 0xff) * inverseAmount))) & 0xff;
|
||||||
int r = ((int) (((float) (color1 >> RED_CHANNEL & 0xff) * amount) +
|
int r = ((int) (((float) (color1 >> RED_CHANNEL & 0xff) * amount) +
|
||||||
((float) (color2 >> RED_CHANNEL & 0xff) * inverseAmount))) & 0xff;
|
((float) (color2 >> RED_CHANNEL & 0xff) * inverseAmount))) & 0xff;
|
||||||
int g = ((int) (((float) (color1 >> GREEN_CHANNEL & 0xff) * amount) +
|
int g = ((int) (((float) (color1 >> GREEN_CHANNEL & 0xff) * amount) +
|
||||||
((float) (color2 >> GREEN_CHANNEL & 0xff) * inverseAmount))) & 0xff;
|
((float) (color2 >> GREEN_CHANNEL & 0xff) * inverseAmount))) & 0xff;
|
||||||
int b = ((int) (((float) (color1 & 0xff) * amount) +
|
int b = ((int) (((float) (color1 & 0xff) * amount) +
|
||||||
((float) (color2 & 0xff) * inverseAmount))) & 0xff;
|
((float) (color2 & 0xff) * inverseAmount))) & 0xff;
|
||||||
|
|
||||||
return a << ALPHA_CHANNEL | r << RED_CHANNEL | g << GREEN_CHANNEL | b << BLUE_CHANNEL;
|
return a << ALPHA_CHANNEL | r << RED_CHANNEL | g << GREEN_CHANNEL | b << BLUE_CHANNEL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static int setHue(int color, float newHue)
|
||||||
|
{
|
||||||
|
return setHSVParameter(color, newHue, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int setSaturation(int color, float newSaturation)
|
||||||
|
{
|
||||||
|
return setHSVParameter(color, newSaturation, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int setValue(int color, float newValue)
|
||||||
|
{
|
||||||
|
return setHSVParameter(color, newValue, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int setMinValue(int color, float newValue)
|
||||||
|
{
|
||||||
|
float hsv[] = new float[3];
|
||||||
|
Color.colorToHSV(color, hsv);
|
||||||
|
hsv[2] = Math.max(hsv[2], newValue);
|
||||||
|
return Color.HSVToColor(hsv);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int setHSVParameter(int color, float newValue, int index)
|
||||||
|
{
|
||||||
|
float hsv[] = new float[3];
|
||||||
|
Color.colorToHSV(color, hsv);
|
||||||
|
hsv[index] = newValue;
|
||||||
|
return Color.HSVToColor(hsv);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,17 +1,20 @@
|
|||||||
/* Copyright (C) 2016 Alinson Santos Xavier
|
/*
|
||||||
|
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify it
|
* This file is part of Loop Habit Tracker.
|
||||||
* under the terms of the GNU General Public License as published by the Free
|
|
||||||
* Software Foundation, either version 3 of the License, or (at your option)
|
|
||||||
* any later version.
|
|
||||||
*
|
*
|
||||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
* it under the terms of the GNU General Public License as published by the
|
||||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
* Free Software Foundation, either version 3 of the License, or (at your
|
||||||
|
* option) any later version.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
* more details.
|
* more details.
|
||||||
*
|
*
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License along
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.isoron.helpers;
|
package org.isoron.helpers;
|
||||||
|
|||||||
@@ -1,116 +1,191 @@
|
|||||||
/* Copyright (C) 2016 Alinson Santos Xavier
|
/*
|
||||||
|
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify it
|
* This file is part of Loop Habit Tracker.
|
||||||
* under the terms of the GNU General Public License as published by the Free
|
|
||||||
* Software Foundation, either version 3 of the License, or (at your option)
|
|
||||||
* any later version.
|
|
||||||
*
|
*
|
||||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
* it under the terms of the GNU General Public License as published by the
|
||||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
* Free Software Foundation, either version 3 of the License, or (at your
|
||||||
|
* option) any later version.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
* more details.
|
* more details.
|
||||||
*
|
*
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License along
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.isoron.helpers;
|
package org.isoron.helpers;
|
||||||
|
|
||||||
import java.text.DecimalFormat;
|
import android.content.Context;
|
||||||
import java.text.SimpleDateFormat;
|
import android.text.format.DateFormat;
|
||||||
|
|
||||||
|
import org.isoron.uhabits.R;
|
||||||
|
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
import java.util.GregorianCalendar;
|
||||||
|
import java.util.Locale;
|
||||||
import java.util.TimeZone;
|
import java.util.TimeZone;
|
||||||
|
|
||||||
public class DateHelper
|
public class DateHelper
|
||||||
{
|
{
|
||||||
public static int millisecondsInOneDay = 24 * 60 * 60 * 1000;
|
public static int millisecondsInOneDay = 24 * 60 * 60 * 1000;
|
||||||
|
|
||||||
public static long getLocalTime()
|
|
||||||
{
|
|
||||||
TimeZone tz = TimeZone.getDefault();
|
|
||||||
long now = new Date().getTime();
|
|
||||||
return now + tz.getOffset(now);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static long getStartOfDay(long timestamp)
|
|
||||||
{
|
|
||||||
return (timestamp / millisecondsInOneDay) * millisecondsInOneDay;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static long getStartOfToday()
|
|
||||||
{
|
|
||||||
return getStartOfDay(DateHelper.getLocalTime());
|
|
||||||
}
|
|
||||||
|
|
||||||
// public static Date getStartOfDay(Date date)
|
public static long getLocalTime()
|
||||||
// {
|
{
|
||||||
// Calendar calendar = Calendar.getInstance();
|
TimeZone tz = TimeZone.getDefault();
|
||||||
// calendar.setTime(date);
|
long now = new Date().getTime();
|
||||||
// calendar.set(Calendar.HOUR_OF_DAY, 0);
|
return now + tz.getOffset(now);
|
||||||
// calendar.set(Calendar.MINUTE, 0);
|
}
|
||||||
// calendar.set(Calendar.SECOND, 0);
|
|
||||||
// calendar.set(Calendar.MILLISECOND, 0);
|
|
||||||
// return calendar.getTime();
|
|
||||||
// }
|
|
||||||
|
|
||||||
public static int differenceInDays(Date from, Date to)
|
public static long toLocalTime(long timestamp)
|
||||||
{
|
{
|
||||||
long milliseconds = getStartOfDay(to.getTime()) - getStartOfDay(from.getTime());
|
TimeZone tz = TimeZone.getDefault();
|
||||||
|
long now = new Date(timestamp).getTime();
|
||||||
|
return now + tz.getOffset(now);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static long getStartOfDay(long timestamp)
|
||||||
|
{
|
||||||
|
return (timestamp / millisecondsInOneDay) * millisecondsInOneDay;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static GregorianCalendar getStartOfTodayCalendar()
|
||||||
|
{
|
||||||
|
GregorianCalendar day = new GregorianCalendar(TimeZone.getTimeZone("GMT"));
|
||||||
|
day.setTimeInMillis(DateHelper.getStartOfDay(DateHelper.getLocalTime()));
|
||||||
|
return day;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static GregorianCalendar getCalendar(long timestamp)
|
||||||
|
{
|
||||||
|
GregorianCalendar day = new GregorianCalendar(TimeZone.getTimeZone("GMT"));
|
||||||
|
day.setTimeInMillis(timestamp);
|
||||||
|
return day;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int getWeekday(long timestamp)
|
||||||
|
{
|
||||||
|
GregorianCalendar day = getCalendar(timestamp);
|
||||||
|
return day.get(GregorianCalendar.DAY_OF_WEEK) % 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static long getStartOfToday()
|
||||||
|
{
|
||||||
|
return getStartOfDay(DateHelper.getLocalTime());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String formatTime(Context context, int hours, int minutes)
|
||||||
|
{
|
||||||
|
int reminderMilliseconds = (hours * 60 + minutes) * 60 * 1000;
|
||||||
|
|
||||||
|
Date date = new Date(reminderMilliseconds);
|
||||||
|
java.text.DateFormat df = DateFormat.getTimeFormat(context);
|
||||||
|
df.setTimeZone(TimeZone.getTimeZone("UTC"));
|
||||||
|
|
||||||
|
return df.format(date);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String formatHeaderDate(GregorianCalendar day)
|
||||||
|
{
|
||||||
|
String dayOfMonth = Integer.toString(day.get(GregorianCalendar.DAY_OF_MONTH));
|
||||||
|
String dayOfWeek = day.getDisplayName(GregorianCalendar.DAY_OF_WEEK,
|
||||||
|
GregorianCalendar.SHORT, Locale.getDefault());
|
||||||
|
|
||||||
|
return dayOfWeek + "\n" + dayOfMonth;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int differenceInDays(Date from, Date to)
|
||||||
|
{
|
||||||
|
long milliseconds = getStartOfDay(to.getTime()) - getStartOfDay(from.getTime());
|
||||||
return (int) (milliseconds / millisecondsInOneDay);
|
return (int) (milliseconds / millisecondsInOneDay);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String differenceInWords(Date from, Date to)
|
public static String[] getShortDayNames()
|
||||||
{
|
{
|
||||||
Integer days = differenceInDays(from, to);
|
return getDayNames(GregorianCalendar.SHORT);
|
||||||
boolean negative = (days < 0);
|
}
|
||||||
days = Math.abs(days);
|
|
||||||
|
|
||||||
Integer weeks = (int) Math.round(days / 7.0);
|
public static String[] getLongDayNames()
|
||||||
Double months = days / 30.4;
|
{
|
||||||
Double years = days / 365.0;
|
return getDayNames(GregorianCalendar.LONG);
|
||||||
|
}
|
||||||
|
|
||||||
StringBuffer s = new StringBuffer();
|
|
||||||
DecimalFormat df = new DecimalFormat("#.#");
|
|
||||||
|
|
||||||
if(months > 18)
|
public static String[] getDayNames(int format)
|
||||||
{
|
{
|
||||||
s.append(df.format(years));
|
String[] wdays = new String[7];
|
||||||
s.append(" years");
|
|
||||||
}
|
|
||||||
else if(weeks > 6)
|
|
||||||
{
|
|
||||||
s.append(df.format(months));
|
|
||||||
s.append(" months");
|
|
||||||
}
|
|
||||||
else if(days > 13)
|
|
||||||
{
|
|
||||||
s.append(weeks);
|
|
||||||
s.append(" weeks");
|
|
||||||
}
|
|
||||||
else if(days > 6)
|
|
||||||
{
|
|
||||||
s.append(days);
|
|
||||||
s.append(" days");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if(days == 0)
|
|
||||||
s.append("Today");
|
|
||||||
else if(days == 1 && negative)
|
|
||||||
s.append("Yesterday");
|
|
||||||
else if(days == 1 && !negative)
|
|
||||||
s.append("Tomorrow");
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if(negative)
|
|
||||||
s.append("past ");
|
|
||||||
s.append(new SimpleDateFormat("EEEE").format(to));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if(negative && days > 6)
|
GregorianCalendar day = new GregorianCalendar();
|
||||||
s.append(" ago");
|
day.set(GregorianCalendar.DAY_OF_WEEK, 0);
|
||||||
|
|
||||||
|
for (int i = 0; i < 7; i++)
|
||||||
|
{
|
||||||
|
wdays[i] = day.getDisplayName(GregorianCalendar.DAY_OF_WEEK, format,
|
||||||
|
Locale.getDefault());
|
||||||
|
day.add(GregorianCalendar.DAY_OF_MONTH, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return wdays;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String formatWeekdayList(Context context, boolean weekday[])
|
||||||
|
{
|
||||||
|
String shortDayNames[] = getShortDayNames();
|
||||||
|
String longDayNames[] = getLongDayNames();
|
||||||
|
StringBuilder buffer = new StringBuilder();
|
||||||
|
|
||||||
|
int count = 0;
|
||||||
|
int first = 0;
|
||||||
|
boolean isFirst = true;
|
||||||
|
for(int i = 0; i < 7; i++)
|
||||||
|
{
|
||||||
|
if(weekday[i])
|
||||||
|
{
|
||||||
|
if(isFirst) first = i;
|
||||||
|
else buffer.append(", ");
|
||||||
|
|
||||||
|
buffer.append(shortDayNames[i]);
|
||||||
|
isFirst = false;
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(count == 1) return longDayNames[first];
|
||||||
|
if(count == 2 && weekday[0] && weekday[1]) return context.getString(R.string.weekends);
|
||||||
|
if(count == 5 && !weekday[0] && !weekday[1]) return context.getString(R.string.any_weekday);
|
||||||
|
if(count == 7) return context.getString(R.string.any_day);
|
||||||
|
return buffer.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Integer packWeekdayList(boolean weekday[])
|
||||||
|
{
|
||||||
|
int list = 0;
|
||||||
|
int current = 1;
|
||||||
|
|
||||||
|
for(int i = 0; i < 7; i++)
|
||||||
|
{
|
||||||
|
if(weekday[i]) list |= current;
|
||||||
|
current = current << 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean[] unpackWeekdayList(int list)
|
||||||
|
{
|
||||||
|
boolean[] weekday = new boolean[7];
|
||||||
|
int current = 1;
|
||||||
|
|
||||||
|
for(int i = 0; i < 7; i++)
|
||||||
|
{
|
||||||
|
if((list & current) != 0) weekday[i] = true;
|
||||||
|
current = current << 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return weekday;
|
||||||
|
}
|
||||||
|
|
||||||
return s.toString();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,57 +1,95 @@
|
|||||||
/* Copyright (C) 2016 Alinson Santos Xavier
|
/*
|
||||||
|
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify it
|
* This file is part of Loop Habit Tracker.
|
||||||
* under the terms of the GNU General Public License as published by the Free
|
|
||||||
* Software Foundation, either version 3 of the License, or (at your option)
|
|
||||||
* any later version.
|
|
||||||
*
|
*
|
||||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
* it under the terms of the GNU General Public License as published by the
|
||||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
* Free Software Foundation, either version 3 of the License, or (at your
|
||||||
|
* option) any later version.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
* more details.
|
* more details.
|
||||||
*
|
*
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License along
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.isoron.helpers;
|
package org.isoron.helpers;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.DialogInterface;
|
import android.content.SharedPreferences;
|
||||||
import android.content.DialogInterface.OnClickListener;
|
import android.content.res.Resources;
|
||||||
|
import android.graphics.Typeface;
|
||||||
|
import android.os.Vibrator;
|
||||||
|
import android.preference.PreferenceManager;
|
||||||
|
import android.util.AttributeSet;
|
||||||
|
import android.util.DisplayMetrics;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.inputmethod.InputMethodManager;
|
import android.view.inputmethod.InputMethodManager;
|
||||||
|
|
||||||
|
import org.isoron.uhabits.BuildConfig;
|
||||||
|
|
||||||
public abstract class DialogHelper
|
public abstract class DialogHelper
|
||||||
{
|
{
|
||||||
|
|
||||||
// public static AlertDialog alert(Activity context, String title, String message, OnClickListener positiveClickListener) {
|
public static final String ISORON_NAMESPACE = "http://isoron.org/android";
|
||||||
// return new AlertDialog.Builder(context)
|
private static Typeface fontawesome;
|
||||||
// .setTitle(title)
|
|
||||||
// .setMessage(message)
|
|
||||||
// .setPositiveButton(android.R.string.yes, positiveClickListener)
|
|
||||||
// .setNegativeButton(android.R.string.no, null).show();
|
|
||||||
// }
|
|
||||||
|
|
||||||
public static abstract class SimpleClickListener implements OnClickListener
|
public interface OnSavedListener
|
||||||
{
|
{
|
||||||
public abstract void onClick();
|
void onSaved(Command command, Object savedObject);
|
||||||
|
}
|
||||||
|
|
||||||
public void onClick(DialogInterface dialog, int whichButton)
|
public static void showSoftKeyboard(View view)
|
||||||
{
|
{
|
||||||
onClick();
|
InputMethodManager imm = (InputMethodManager) view.getContext()
|
||||||
}
|
.getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||||
}
|
imm.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT);
|
||||||
|
}
|
||||||
|
|
||||||
public static interface OnSavedListener
|
public static void vibrate(Context context, int duration)
|
||||||
{
|
{
|
||||||
public void onSaved(Command command, Object savedObject);
|
Vibrator vb = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE);
|
||||||
}
|
vb.vibrate(duration);
|
||||||
|
}
|
||||||
|
|
||||||
public static void showSoftKeyboard(View view)
|
|
||||||
{
|
public static void incrementLaunchCount(Context context)
|
||||||
InputMethodManager imm = (InputMethodManager)
|
{
|
||||||
view.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
|
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||||
imm.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT);
|
int count = prefs.getInt("launch_count", 0);
|
||||||
}
|
prefs.edit().putInt("launch_count", count + 1).apply();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void updateLastAppVersion(Context context)
|
||||||
|
{
|
||||||
|
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||||
|
prefs.edit().putInt("last_version", BuildConfig.VERSION_CODE).apply();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int getLaunchCount(Context context)
|
||||||
|
{
|
||||||
|
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||||
|
return prefs.getInt("launch_count", 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getAttribute(Context context, AttributeSet attrs, String name)
|
||||||
|
{
|
||||||
|
int resId = attrs.getAttributeResourceValue(ISORON_NAMESPACE, name, 0);
|
||||||
|
|
||||||
|
if(resId != 0)
|
||||||
|
return context.getResources().getString(resId);
|
||||||
|
else
|
||||||
|
return attrs.getAttributeValue(ISORON_NAMESPACE, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static float dpToPixels(Context context, float dp)
|
||||||
|
{
|
||||||
|
Resources resources = context.getResources();
|
||||||
|
DisplayMetrics metrics = resources.getDisplayMetrics();
|
||||||
|
return dp * (metrics.densityDpi / DisplayMetrics.DENSITY_DEFAULT);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,20 @@
|
|||||||
/* Copyright (C) 2016 Alinson Santos Xavier
|
/*
|
||||||
|
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify it
|
* This file is part of Loop Habit Tracker.
|
||||||
* under the terms of the GNU General Public License as published by the Free
|
|
||||||
* Software Foundation, either version 3 of the License, or (at your option)
|
|
||||||
* any later version.
|
|
||||||
*
|
*
|
||||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
* it under the terms of the GNU General Public License as published by the
|
||||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
* Free Software Foundation, either version 3 of the License, or (at your
|
||||||
|
* option) any later version.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
* more details.
|
* more details.
|
||||||
*
|
*
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License along
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.isoron.helpers;
|
package org.isoron.helpers;
|
||||||
@@ -81,8 +84,7 @@ abstract public class ReplayableActivity extends Activity
|
|||||||
toast.show();
|
toast.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void executeCommand(final Command command, Boolean clearRedoStack,
|
public void executeCommand(final Command command, Boolean clearRedoStack, final Long refreshKey)
|
||||||
final Long refreshKey)
|
|
||||||
{
|
{
|
||||||
undoList.push(command);
|
undoList.push(command);
|
||||||
|
|
||||||
|
|||||||
96
app/src/main/java/org/isoron/uhabits/AboutActivity.java
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||||
|
*
|
||||||
|
* This file is part of Loop Habit Tracker.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by the
|
||||||
|
* Free Software Foundation, either version 3 of the License, or (at your
|
||||||
|
* option) any later version.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
|
* more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along
|
||||||
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.isoron.uhabits;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.graphics.Color;
|
||||||
|
import android.graphics.drawable.ColorDrawable;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import org.isoron.helpers.ColorHelper;
|
||||||
|
|
||||||
|
public class AboutActivity extends Activity implements View.OnClickListener
|
||||||
|
{
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState)
|
||||||
|
{
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setContentView(R.layout.about);
|
||||||
|
|
||||||
|
if (android.os.Build.VERSION.SDK_INT >= 21)
|
||||||
|
{
|
||||||
|
int color = getResources().getColor(R.color.blue_700);
|
||||||
|
int darkerColor = ColorHelper.mixColors(color, Color.BLACK, 0.75f);
|
||||||
|
getActionBar().setBackgroundDrawable(new ColorDrawable(color));
|
||||||
|
getWindow().setStatusBarColor(darkerColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
TextView tvVersion = (TextView) findViewById(R.id.tvVersion);
|
||||||
|
TextView tvRate = (TextView) findViewById(R.id.tvRate);
|
||||||
|
TextView tvFeedback = (TextView) findViewById(R.id.tvFeedback);
|
||||||
|
TextView tvSource = (TextView) findViewById(R.id.tvSource);
|
||||||
|
|
||||||
|
tvVersion.setText(String.format(getResources().getString(R.string.version_n),
|
||||||
|
BuildConfig.VERSION_NAME));
|
||||||
|
tvRate.setOnClickListener(this);
|
||||||
|
tvFeedback.setOnClickListener(this);
|
||||||
|
tvSource.setOnClickListener(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onClick(View v)
|
||||||
|
{
|
||||||
|
switch (v.getId())
|
||||||
|
{
|
||||||
|
case R.id.tvRate:
|
||||||
|
{
|
||||||
|
Intent intent = new Intent();
|
||||||
|
intent.setAction(Intent.ACTION_VIEW);
|
||||||
|
intent.setData(Uri.parse("market://details?id=org.isoron.uhabits"));
|
||||||
|
startActivity(intent);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case R.id.tvFeedback:
|
||||||
|
{
|
||||||
|
Intent intent = new Intent();
|
||||||
|
intent.setAction(Intent.ACTION_SENDTO);
|
||||||
|
intent.setData(Uri.parse("mailto:isoron+habits@gmail.com?" +
|
||||||
|
"subject=Feedback%20about%20Loop%20Habit%20Tracker"));
|
||||||
|
startActivity(intent);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case R.id.tvSource:
|
||||||
|
{
|
||||||
|
Intent intent = new Intent();
|
||||||
|
intent.setAction(Intent.ACTION_VIEW);
|
||||||
|
intent.setData(Uri.parse("https://github.com/iSoron/uhabits"));
|
||||||
|
startActivity(intent);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
234
app/src/main/java/org/isoron/uhabits/HabitBroadcastReceiver.java
Normal file
@@ -0,0 +1,234 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||||
|
*
|
||||||
|
* This file is part of Loop Habit Tracker.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by the
|
||||||
|
* Free Software Foundation, either version 3 of the License, or (at your
|
||||||
|
* option) any later version.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
|
* more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along
|
||||||
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.isoron.uhabits;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.app.Notification;
|
||||||
|
import android.app.NotificationManager;
|
||||||
|
import android.app.PendingIntent;
|
||||||
|
import android.content.BroadcastReceiver;
|
||||||
|
import android.content.ContentUris;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
import android.graphics.BitmapFactory;
|
||||||
|
import android.media.RingtoneManager;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.preference.PreferenceManager;
|
||||||
|
import android.support.v4.app.NotificationCompat;
|
||||||
|
import android.support.v4.content.LocalBroadcastManager;
|
||||||
|
|
||||||
|
import org.isoron.helpers.DateHelper;
|
||||||
|
import org.isoron.uhabits.helpers.ReminderHelper;
|
||||||
|
import org.isoron.uhabits.models.Habit;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
public class HabitBroadcastReceiver extends BroadcastReceiver
|
||||||
|
{
|
||||||
|
public static final String ACTION_CHECK = "org.isoron.uhabits.ACTION_CHECK";
|
||||||
|
public static final String ACTION_DISMISS = "org.isoron.uhabits.ACTION_DISMISS";
|
||||||
|
public static final String ACTION_SHOW_REMINDER = "org.isoron.uhabits.ACTION_SHOW_REMINDER";
|
||||||
|
public static final String ACTION_SNOOZE = "org.isoron.uhabits.ACTION_SNOOZE";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onReceive(final Context context, Intent intent)
|
||||||
|
{
|
||||||
|
switch (intent.getAction())
|
||||||
|
{
|
||||||
|
case ACTION_SHOW_REMINDER:
|
||||||
|
createNotification(context, intent);
|
||||||
|
createReminderAlarms(context);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ACTION_DISMISS:
|
||||||
|
dismissAllHabits();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ACTION_CHECK:
|
||||||
|
checkHabit(context, intent);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ACTION_SNOOZE:
|
||||||
|
snoozeHabit(context, intent);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createReminderAlarms(final Context context)
|
||||||
|
{
|
||||||
|
new Handler().postDelayed(new Runnable()
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
public void run()
|
||||||
|
{
|
||||||
|
ReminderHelper.createReminderAlarms(context);
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void snoozeHabit(Context context, Intent intent)
|
||||||
|
{
|
||||||
|
Uri data = intent.getData();
|
||||||
|
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||||
|
long delayMinutes = Long.parseLong(prefs.getString("pref_snooze_interval", "15"));
|
||||||
|
|
||||||
|
Habit habit = Habit.get(ContentUris.parseId(data));
|
||||||
|
ReminderHelper.createReminderAlarm(context, habit,
|
||||||
|
new Date().getTime() + delayMinutes * 60 * 1000);
|
||||||
|
dismissNotification(context, habit);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkHabit(Context context, Intent intent)
|
||||||
|
{
|
||||||
|
Uri data = intent.getData();
|
||||||
|
Long timestamp = intent.getLongExtra("timestamp", DateHelper.getStartOfToday());
|
||||||
|
|
||||||
|
Habit habit = Habit.get(ContentUris.parseId(data));
|
||||||
|
habit.repetitions.toggle(timestamp);
|
||||||
|
habit.save();
|
||||||
|
dismissNotification(context, habit);
|
||||||
|
|
||||||
|
sendRefreshBroadcast(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void sendRefreshBroadcast(Context context)
|
||||||
|
{
|
||||||
|
LocalBroadcastManager manager = LocalBroadcastManager.getInstance(context);
|
||||||
|
Intent refreshIntent = new Intent(MainActivity.ACTION_REFRESH);
|
||||||
|
manager.sendBroadcast(refreshIntent);
|
||||||
|
|
||||||
|
MainActivity.updateWidgets(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void dismissAllHabits()
|
||||||
|
{
|
||||||
|
for (Habit h : Habit.getHighlightedHabits())
|
||||||
|
{
|
||||||
|
h.highlight = 0;
|
||||||
|
h.save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void dismissNotification(Context context, Habit habit)
|
||||||
|
{
|
||||||
|
NotificationManager notificationManager =
|
||||||
|
(NotificationManager) context.getSystemService(Activity.NOTIFICATION_SERVICE);
|
||||||
|
|
||||||
|
int notificationId = (int) (habit.getId() % Integer.MAX_VALUE);
|
||||||
|
notificationManager.cancel(notificationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void createNotification(Context context, Intent intent)
|
||||||
|
{
|
||||||
|
Uri data = intent.getData();
|
||||||
|
Habit habit = Habit.get(ContentUris.parseId(data));
|
||||||
|
Long timestamp = intent.getLongExtra("timestamp", DateHelper.getStartOfToday());
|
||||||
|
Long reminderTime = intent.getLongExtra("reminderTime", DateHelper.getStartOfToday());
|
||||||
|
|
||||||
|
if (habit.repetitions.hasImplicitRepToday()) return;
|
||||||
|
|
||||||
|
habit.highlight = 1;
|
||||||
|
habit.save();
|
||||||
|
|
||||||
|
if (!checkWeekday(intent, habit)) return;
|
||||||
|
|
||||||
|
// Check if reminder has been turned off after alarm was scheduled
|
||||||
|
if (habit.reminderHour == null) return;
|
||||||
|
|
||||||
|
Intent contentIntent = new Intent(context, MainActivity.class);
|
||||||
|
contentIntent.setData(data);
|
||||||
|
PendingIntent contentPendingIntent =
|
||||||
|
PendingIntent.getActivity(context, 0, contentIntent, 0);
|
||||||
|
|
||||||
|
PendingIntent dismissPendingIntent = buildDismissIntent(context);
|
||||||
|
PendingIntent checkIntentPending = buildCheckIntent(context, habit, timestamp);
|
||||||
|
PendingIntent snoozeIntentPending = buildSnoozeIntent(context, habit);
|
||||||
|
|
||||||
|
Uri soundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);
|
||||||
|
|
||||||
|
NotificationCompat.WearableExtender wearableExtender =
|
||||||
|
new NotificationCompat.WearableExtender().setBackground(
|
||||||
|
BitmapFactory.decodeResource(context.getResources(), R.drawable.stripe));
|
||||||
|
|
||||||
|
Notification notification =
|
||||||
|
new NotificationCompat.Builder(context).setSmallIcon(R.drawable.ic_notification)
|
||||||
|
.setContentTitle(habit.name)
|
||||||
|
.setContentText(habit.description)
|
||||||
|
.setContentIntent(contentPendingIntent)
|
||||||
|
.setDeleteIntent(dismissPendingIntent)
|
||||||
|
.addAction(R.drawable.ic_action_check,
|
||||||
|
context.getString(R.string.check), checkIntentPending)
|
||||||
|
.addAction(R.drawable.ic_action_snooze,
|
||||||
|
context.getString(R.string.snooze), snoozeIntentPending)
|
||||||
|
.setSound(soundUri)
|
||||||
|
.extend(wearableExtender)
|
||||||
|
.setWhen(reminderTime)
|
||||||
|
.setShowWhen(true)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
notification.flags |= Notification.FLAG_AUTO_CANCEL;
|
||||||
|
|
||||||
|
NotificationManager notificationManager =
|
||||||
|
(NotificationManager) context.getSystemService(Activity.NOTIFICATION_SERVICE);
|
||||||
|
|
||||||
|
int notificationId = (int) (habit.getId() % Integer.MAX_VALUE);
|
||||||
|
notificationManager.notify(notificationId, notification);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PendingIntent buildSnoozeIntent(Context context, Habit habit)
|
||||||
|
{
|
||||||
|
Uri data = habit.getUri();
|
||||||
|
Intent snoozeIntent = new Intent(context, HabitBroadcastReceiver.class);
|
||||||
|
snoozeIntent.setData(data);
|
||||||
|
snoozeIntent.setAction(ACTION_SNOOZE);
|
||||||
|
return PendingIntent.getBroadcast(context, 0, snoozeIntent, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PendingIntent buildCheckIntent(Context context, Habit habit, Long timestamp)
|
||||||
|
{
|
||||||
|
Uri data = habit.getUri();
|
||||||
|
Intent checkIntent = new Intent(context, HabitBroadcastReceiver.class);
|
||||||
|
checkIntent.setData(data);
|
||||||
|
checkIntent.setAction(ACTION_CHECK);
|
||||||
|
if(timestamp != null) checkIntent.putExtra("timestamp", timestamp);
|
||||||
|
return PendingIntent.getBroadcast(context, 0, checkIntent, PendingIntent.FLAG_ONE_SHOT);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PendingIntent buildDismissIntent(Context context)
|
||||||
|
{
|
||||||
|
Intent deleteIntent = new Intent(context, HabitBroadcastReceiver.class);
|
||||||
|
deleteIntent.setAction(ACTION_DISMISS);
|
||||||
|
return PendingIntent.getBroadcast(context, 0, deleteIntent, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean checkWeekday(Intent intent, Habit habit)
|
||||||
|
{
|
||||||
|
Long timestamp = intent.getLongExtra("timestamp", DateHelper.getStartOfToday());
|
||||||
|
|
||||||
|
boolean reminderDays[] = DateHelper.unpackWeekdayList(habit.reminderDays);
|
||||||
|
int weekday = DateHelper.getWeekday(timestamp);
|
||||||
|
|
||||||
|
return reminderDays[weekday];
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,17 +1,20 @@
|
|||||||
/* Copyright (C) 2016 Alinson Santos Xavier
|
/*
|
||||||
|
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify it
|
* This file is part of Loop Habit Tracker.
|
||||||
* under the terms of the GNU General Public License as published by the Free
|
|
||||||
* Software Foundation, either version 3 of the License, or (at your option)
|
|
||||||
* any later version.
|
|
||||||
*
|
*
|
||||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
* it under the terms of the GNU General Public License as published by the
|
||||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
* Free Software Foundation, either version 3 of the License, or (at your
|
||||||
|
* option) any later version.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
* more details.
|
* more details.
|
||||||
*
|
*
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License along
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.isoron.uhabits;
|
package org.isoron.uhabits;
|
||||||
|
|||||||
@@ -1,22 +1,24 @@
|
|||||||
/* Copyright (C) 2016 Alinson Santos Xavier
|
/*
|
||||||
|
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify it
|
* This file is part of Loop Habit Tracker.
|
||||||
* under the terms of the GNU General Public License as published by the Free
|
|
||||||
* Software Foundation, either version 3 of the License, or (at your option)
|
|
||||||
* any later version.
|
|
||||||
*
|
*
|
||||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
* it under the terms of the GNU General Public License as published by the
|
||||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
* Free Software Foundation, either version 3 of the License, or (at your
|
||||||
|
* option) any later version.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
* more details.
|
* more details.
|
||||||
*
|
*
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License along
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.isoron.uhabits;
|
package org.isoron.uhabits;
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.graphics.Color;
|
import android.graphics.Color;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
|
||||||
@@ -30,27 +32,26 @@ public class IntroActivity extends AppIntro2
|
|||||||
{
|
{
|
||||||
showStatusBar(false);
|
showStatusBar(false);
|
||||||
|
|
||||||
addSlide(AppIntroFragment.newInstance("Welcome",
|
addSlide(AppIntroFragment.newInstance(getString(R.string.intro_title_1),
|
||||||
"Habits Tracker helps you create and maintain good habits.", R.drawable.tutorial_1,
|
getString(R.string.intro_description_1), R.drawable.intro_icon_1,
|
||||||
Color.parseColor("#194673")));
|
Color.parseColor("#194673")));
|
||||||
|
|
||||||
addSlide(AppIntroFragment.newInstance("Create some new habits",
|
addSlide(AppIntroFragment.newInstance(getString(R.string.intro_title_2),
|
||||||
"Every day, after performing your habit, put a checkmark on the app.",
|
getString(R.string.intro_description_2), R.drawable.intro_icon_2,
|
||||||
R.drawable.tutorial_2, Color.parseColor("#ffa726")));
|
Color.parseColor("#ffa726")));
|
||||||
|
|
||||||
addSlide(AppIntroFragment.newInstance("Keep doing it",
|
addSlide(AppIntroFragment.newInstance(getString(R.string.intro_title_3),
|
||||||
"Habits performed consistently for a long time will earn a full star.",
|
getString(R.string.intro_description_3), R.drawable.intro_icon_3,
|
||||||
R.drawable.tutorial_3, Color.parseColor("#7cb342")));
|
Color.parseColor("#7cb342")));
|
||||||
|
|
||||||
addSlide(AppIntroFragment.newInstance("Track your progress",
|
addSlide(AppIntroFragment.newInstance(getString(R.string.intro_title_4),
|
||||||
"Detailed graphs show you how your habits improved over time.",
|
getString(R.string.intro_description_4), R.drawable.intro_icon_4,
|
||||||
R.drawable.tutorial_4, Color.parseColor("#9575cd")));
|
Color.parseColor("#9575cd")));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onNextPressed()
|
public void onNextPressed()
|
||||||
{
|
{
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -62,6 +63,5 @@ public class IntroActivity extends AppIntro2
|
|||||||
@Override
|
@Override
|
||||||
public void onSlideChanged()
|
public void onSlideChanged()
|
||||||
{
|
{
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,37 +1,60 @@
|
|||||||
/* Copyright (C) 2016 Alinson Santos Xavier
|
/*
|
||||||
|
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify it
|
* This file is part of Loop Habit Tracker.
|
||||||
* under the terms of the GNU General Public License as published by the Free
|
|
||||||
* Software Foundation, either version 3 of the License, or (at your option)
|
|
||||||
* any later version.
|
|
||||||
*
|
*
|
||||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
* it under the terms of the GNU General Public License as published by the
|
||||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
* Free Software Foundation, either version 3 of the License, or (at your
|
||||||
|
* option) any later version.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
* more details.
|
* more details.
|
||||||
*
|
*
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License along
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.isoron.uhabits;
|
package org.isoron.uhabits;
|
||||||
|
|
||||||
|
import android.appwidget.AppWidgetManager;
|
||||||
|
import android.content.BroadcastReceiver;
|
||||||
|
import android.content.ComponentName;
|
||||||
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
|
import android.content.IntentFilter;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
|
import android.os.AsyncTask;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.preference.PreferenceManager;
|
import android.preference.PreferenceManager;
|
||||||
|
import android.support.v4.content.LocalBroadcastManager;
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
|
|
||||||
|
import org.isoron.helpers.DateHelper;
|
||||||
|
import org.isoron.helpers.DialogHelper;
|
||||||
import org.isoron.helpers.ReplayableActivity;
|
import org.isoron.helpers.ReplayableActivity;
|
||||||
import org.isoron.uhabits.dialogs.ListHabitsFragment;
|
import org.isoron.uhabits.fragments.ListHabitsFragment;
|
||||||
|
import org.isoron.uhabits.helpers.ReminderHelper;
|
||||||
import org.isoron.uhabits.models.Habit;
|
import org.isoron.uhabits.models.Habit;
|
||||||
|
import org.isoron.uhabits.widgets.CheckmarkWidgetProvider;
|
||||||
|
import org.isoron.uhabits.widgets.FrequencyWidgetProvider;
|
||||||
|
import org.isoron.uhabits.widgets.HistoryWidgetProvider;
|
||||||
|
import org.isoron.uhabits.widgets.ScoreWidgetProvider;
|
||||||
|
import org.isoron.uhabits.widgets.StreakWidgetProvider;
|
||||||
|
|
||||||
public class MainActivity extends ReplayableActivity
|
public class MainActivity extends ReplayableActivity
|
||||||
implements ListHabitsFragment.OnHabitClickListener
|
implements ListHabitsFragment.OnHabitClickListener
|
||||||
{
|
{
|
||||||
private ListHabitsFragment listHabitsFragment;
|
private ListHabitsFragment listHabitsFragment;
|
||||||
|
private SharedPreferences prefs;
|
||||||
|
private BroadcastReceiver receiver;
|
||||||
|
private LocalBroadcastManager localBroadcastManager;
|
||||||
|
|
||||||
|
public static final String ACTION_REFRESH = "org.isoron.uhabits.ACTION_REFRESH";
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState)
|
protected void onCreate(Bundle savedInstanceState)
|
||||||
@@ -39,26 +62,45 @@ public class MainActivity extends ReplayableActivity
|
|||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
setContentView(R.layout.list_habits_activity);
|
setContentView(R.layout.list_habits_activity);
|
||||||
|
|
||||||
|
prefs = PreferenceManager.getDefaultSharedPreferences(this);
|
||||||
|
listHabitsFragment =
|
||||||
|
(ListHabitsFragment) getFragmentManager().findFragmentById(R.id.fragment1);
|
||||||
|
|
||||||
|
receiver = new Receiver();
|
||||||
|
localBroadcastManager = LocalBroadcastManager.getInstance(this);
|
||||||
|
localBroadcastManager.registerReceiver(receiver, new IntentFilter(ACTION_REFRESH));
|
||||||
|
|
||||||
|
onStartup();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onStartup()
|
||||||
|
{
|
||||||
PreferenceManager.setDefaultValues(this, R.xml.preferences, false);
|
PreferenceManager.setDefaultValues(this, R.xml.preferences, false);
|
||||||
|
DialogHelper.incrementLaunchCount(this);
|
||||||
listHabitsFragment = (ListHabitsFragment) getFragmentManager().findFragmentById(
|
DialogHelper.updateLastAppVersion(this);
|
||||||
R.id.fragment1);
|
|
||||||
|
|
||||||
ReminderHelper.createReminderAlarms(MainActivity.this);
|
|
||||||
|
|
||||||
showTutorial();
|
showTutorial();
|
||||||
|
|
||||||
|
new AsyncTask<Void, Void, Void>() {
|
||||||
|
@Override
|
||||||
|
protected Void doInBackground(Void... params)
|
||||||
|
{
|
||||||
|
ReminderHelper.createReminderAlarms(MainActivity.this);
|
||||||
|
updateWidgets(MainActivity.this);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}.execute();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showTutorial()
|
private void showTutorial()
|
||||||
{
|
{
|
||||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
|
|
||||||
Boolean firstRun = prefs.getBoolean("pref_first_run", true);
|
Boolean firstRun = prefs.getBoolean("pref_first_run", true);
|
||||||
|
|
||||||
if(firstRun)
|
if (firstRun)
|
||||||
{
|
{
|
||||||
SharedPreferences.Editor editor = prefs.edit();
|
SharedPreferences.Editor editor = prefs.edit();
|
||||||
editor.putBoolean("pref_first_run", false);
|
editor.putBoolean("pref_first_run", false);
|
||||||
|
editor.putLong("last_hint_timestamp", DateHelper.getStartOfToday()).apply();
|
||||||
editor.apply();
|
editor.apply();
|
||||||
|
|
||||||
Intent intent = new Intent(this, IntroActivity.class);
|
Intent intent = new Intent(this, IntroActivity.class);
|
||||||
@@ -79,9 +121,18 @@ public class MainActivity extends ReplayableActivity
|
|||||||
switch (item.getItemId())
|
switch (item.getItemId())
|
||||||
{
|
{
|
||||||
case R.id.action_settings:
|
case R.id.action_settings:
|
||||||
|
{
|
||||||
Intent intent = new Intent(this, SettingsActivity.class);
|
Intent intent = new Intent(this, SettingsActivity.class);
|
||||||
startActivity(intent);
|
startActivity(intent);
|
||||||
return true;
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
case R.id.action_about:
|
||||||
|
{
|
||||||
|
Intent intent = new Intent(this, AboutActivity.class);
|
||||||
|
startActivity(intent);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return super.onOptionsItemSelected(item);
|
return super.onOptionsItemSelected(item);
|
||||||
@@ -100,5 +151,50 @@ public class MainActivity extends ReplayableActivity
|
|||||||
public void onPostExecuteCommand(Long refreshKey)
|
public void onPostExecuteCommand(Long refreshKey)
|
||||||
{
|
{
|
||||||
listHabitsFragment.onPostExecuteCommand(refreshKey);
|
listHabitsFragment.onPostExecuteCommand(refreshKey);
|
||||||
|
|
||||||
|
new AsyncTask<Void, Void, Void>()
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
protected Void doInBackground(Void... params)
|
||||||
|
{
|
||||||
|
updateWidgets(MainActivity.this);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void updateWidgets(Context context)
|
||||||
|
{
|
||||||
|
updateWidgets(context, CheckmarkWidgetProvider.class);
|
||||||
|
updateWidgets(context, HistoryWidgetProvider.class);
|
||||||
|
updateWidgets(context, ScoreWidgetProvider.class);
|
||||||
|
updateWidgets(context, StreakWidgetProvider.class);
|
||||||
|
updateWidgets(context, FrequencyWidgetProvider.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void updateWidgets(Context context, Class providerClass)
|
||||||
|
{
|
||||||
|
ComponentName provider = new ComponentName(context, providerClass);
|
||||||
|
Intent intent = new Intent(context, providerClass);
|
||||||
|
intent.setAction(AppWidgetManager.ACTION_APPWIDGET_UPDATE);
|
||||||
|
int ids[] = AppWidgetManager.getInstance(context).getAppWidgetIds(provider);
|
||||||
|
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, ids);
|
||||||
|
context.sendBroadcast(intent);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDestroy()
|
||||||
|
{
|
||||||
|
localBroadcastManager.unregisterReceiver(receiver);
|
||||||
|
super.onDestroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
class Receiver extends BroadcastReceiver
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
public void onReceive(Context context, Intent intent)
|
||||||
|
{
|
||||||
|
listHabitsFragment.onPostExecuteCommand(null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,161 +0,0 @@
|
|||||||
/* Copyright (C) 2016 Alinson Santos Xavier
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify it
|
|
||||||
* under the terms of the GNU General Public License as published by the Free
|
|
||||||
* Software Foundation, either version 3 of the License, or (at your option)
|
|
||||||
* any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
|
||||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
||||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
|
||||||
* more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.isoron.uhabits;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.app.Notification;
|
|
||||||
import android.app.NotificationManager;
|
|
||||||
import android.app.PendingIntent;
|
|
||||||
import android.content.BroadcastReceiver;
|
|
||||||
import android.content.ContentUris;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.graphics.BitmapFactory;
|
|
||||||
import android.media.RingtoneManager;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.preference.PreferenceManager;
|
|
||||||
import android.support.v4.app.NotificationCompat;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import org.isoron.uhabits.models.Habit;
|
|
||||||
|
|
||||||
import java.util.Date;
|
|
||||||
|
|
||||||
public class ReminderAlarmReceiver extends BroadcastReceiver
|
|
||||||
{
|
|
||||||
|
|
||||||
public static String ACTION_CHECK = "org.isoron.uhabits.ACTION_CHECK";
|
|
||||||
public static String ACTION_DISMISS = "org.isoron.uhabits.ACTION_DISMISS";
|
|
||||||
public static String ACTION_REMIND = "org.isoron.uhabits.ACTION_REMIND";
|
|
||||||
public static String ACTION_REMOVE_REMINDER = "org.isoron.uhabits.ACTION_REMOVE_REMINDER";
|
|
||||||
public static String ACTION_SNOOZE = "org.isoron.uhabits.ACTION_SNOOZE";
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onReceive(Context context, Intent intent)
|
|
||||||
{
|
|
||||||
String action = intent.getAction();
|
|
||||||
|
|
||||||
if (action.equals(ACTION_REMIND)) createNotification(context, intent.getData());
|
|
||||||
|
|
||||||
else if (action.equals(ACTION_DISMISS)) dismissAllHabits();
|
|
||||||
|
|
||||||
else if (action.equals(ACTION_CHECK)) checkHabit(context, intent.getData());
|
|
||||||
|
|
||||||
else if (action.equals(ACTION_SNOOZE)) snoozeHabit(context, intent.getData());
|
|
||||||
}
|
|
||||||
|
|
||||||
private void snoozeHabit(Context context, Uri data)
|
|
||||||
{
|
|
||||||
|
|
||||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
|
||||||
long delayMinutes = Long.parseLong(prefs.getString("pref_snooze_interval", "15"));
|
|
||||||
|
|
||||||
Habit habit = Habit.get(ContentUris.parseId(data));
|
|
||||||
ReminderHelper.createReminderAlarm(context, habit,
|
|
||||||
new Date().getTime() + delayMinutes * 60 * 1000);
|
|
||||||
dismissNotification(context, habit);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void checkHabit(Context context, Uri data)
|
|
||||||
{
|
|
||||||
Habit habit = Habit.get(ContentUris.parseId(data));
|
|
||||||
habit.toggleRepetitionToday();
|
|
||||||
habit.save();
|
|
||||||
dismissNotification(context, habit);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void dismissAllHabits()
|
|
||||||
{
|
|
||||||
for (Habit h : Habit.getHighlightedHabits())
|
|
||||||
{
|
|
||||||
h.highlight = 0;
|
|
||||||
h.save();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void dismissNotification(Context context, Habit habit)
|
|
||||||
{
|
|
||||||
NotificationManager notificationManager =
|
|
||||||
(NotificationManager) context.getSystemService(Activity.NOTIFICATION_SERVICE);
|
|
||||||
|
|
||||||
int notificationId = (int) (habit.getId() % Integer.MAX_VALUE);
|
|
||||||
notificationManager.cancel(notificationId);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private void createNotification(Context context, Uri data)
|
|
||||||
{
|
|
||||||
|
|
||||||
Habit habit = Habit.get(ContentUris.parseId(data));
|
|
||||||
|
|
||||||
if (habit.hasImplicitRepToday()) return;
|
|
||||||
|
|
||||||
Log.d("Alarm", String.format("Applying highlight: %s", habit.name));
|
|
||||||
habit.highlight = 1;
|
|
||||||
habit.save();
|
|
||||||
|
|
||||||
// Check if reminder has been turned off after alarm was scheduled
|
|
||||||
if (habit.reminder_hour == null) return;
|
|
||||||
|
|
||||||
Intent contentIntent = new Intent(context, MainActivity.class);
|
|
||||||
contentIntent.setData(data);
|
|
||||||
PendingIntent contentPendingIntent =
|
|
||||||
PendingIntent.getActivity(context, 0, contentIntent, 0);
|
|
||||||
|
|
||||||
Intent deleteIntent = new Intent(context, ReminderAlarmReceiver.class);
|
|
||||||
deleteIntent.setAction(ACTION_DISMISS);
|
|
||||||
PendingIntent deletePendingIntent = PendingIntent.getBroadcast(context, 0, deleteIntent, 0);
|
|
||||||
|
|
||||||
Intent checkIntent = new Intent(context, ReminderAlarmReceiver.class);
|
|
||||||
checkIntent.setData(data);
|
|
||||||
checkIntent.setAction(ACTION_CHECK);
|
|
||||||
PendingIntent checkIntentPending = PendingIntent.getBroadcast(context, 0, checkIntent, 0);
|
|
||||||
|
|
||||||
Intent snoozeIntent = new Intent(context, ReminderAlarmReceiver.class);
|
|
||||||
snoozeIntent.setData(data);
|
|
||||||
snoozeIntent.setAction(ACTION_SNOOZE);
|
|
||||||
PendingIntent snoozeIntentPending = PendingIntent.getBroadcast(context, 0, snoozeIntent, 0);
|
|
||||||
|
|
||||||
Uri soundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);
|
|
||||||
|
|
||||||
NotificationCompat.WearableExtender wearableExtender =
|
|
||||||
new NotificationCompat.WearableExtender().setBackground(
|
|
||||||
BitmapFactory.decodeResource(context.getResources(), R.drawable.stripe));
|
|
||||||
|
|
||||||
Notification notification =
|
|
||||||
new NotificationCompat.Builder(context).setSmallIcon(R.drawable.ic_notification)
|
|
||||||
.setContentTitle(habit.name)
|
|
||||||
.setContentText(habit.description)
|
|
||||||
.setContentIntent(contentPendingIntent)
|
|
||||||
.setDeleteIntent(deletePendingIntent)
|
|
||||||
.addAction(R.drawable.ic_action_check, "Check", checkIntentPending)
|
|
||||||
.addAction(R.drawable.ic_action_snooze, "Later", snoozeIntentPending)
|
|
||||||
.setSound(soundUri)
|
|
||||||
.extend(wearableExtender)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
notification.flags |= Notification.FLAG_AUTO_CANCEL;
|
|
||||||
|
|
||||||
NotificationManager notificationManager =
|
|
||||||
(NotificationManager) context.getSystemService(Activity.NOTIFICATION_SERVICE);
|
|
||||||
|
|
||||||
int notificationId = (int) (habit.getId() % Integer.MAX_VALUE);
|
|
||||||
notificationManager.notify(notificationId, notification);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
/* Copyright (C) 2016 Alinson Santos Xavier
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify it
|
|
||||||
* under the terms of the GNU General Public License as published by the Free
|
|
||||||
* Software Foundation, either version 3 of the License, or (at your option)
|
|
||||||
* any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
|
||||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
||||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
|
||||||
* more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.isoron.uhabits;
|
|
||||||
|
|
||||||
import android.app.AlarmManager;
|
|
||||||
import android.app.PendingIntent;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.Build;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import org.isoron.uhabits.models.Habit;
|
|
||||||
|
|
||||||
import java.text.DateFormat;
|
|
||||||
import java.util.Calendar;
|
|
||||||
import java.util.Date;
|
|
||||||
|
|
||||||
public class ReminderHelper
|
|
||||||
{
|
|
||||||
public static void createReminderAlarms(Context context)
|
|
||||||
{
|
|
||||||
for (Habit habit : Habit.getHabitsWithReminder())
|
|
||||||
createReminderAlarm(context, habit, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void createReminderAlarm(Context context, Habit habit, Long reminderTime)
|
|
||||||
{
|
|
||||||
Uri uri = Uri.parse("content://org.isoron.uhabits/habit/" + habit.getId());
|
|
||||||
|
|
||||||
if (reminderTime == null)
|
|
||||||
{
|
|
||||||
Calendar calendar = Calendar.getInstance();
|
|
||||||
calendar.setTimeInMillis(System.currentTimeMillis());
|
|
||||||
calendar.set(Calendar.HOUR_OF_DAY, habit.reminder_hour);
|
|
||||||
calendar.set(Calendar.MINUTE, habit.reminder_min);
|
|
||||||
calendar.set(Calendar.SECOND, 0);
|
|
||||||
|
|
||||||
reminderTime = calendar.getTimeInMillis();
|
|
||||||
|
|
||||||
if (System.currentTimeMillis() > reminderTime)
|
|
||||||
{
|
|
||||||
reminderTime += AlarmManager.INTERVAL_DAY;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Intent alarmIntent = new Intent(context, ReminderAlarmReceiver.class);
|
|
||||||
alarmIntent.setAction(ReminderAlarmReceiver.ACTION_REMIND);
|
|
||||||
alarmIntent.setData(uri);
|
|
||||||
|
|
||||||
PendingIntent pendingIntent = PendingIntent.getBroadcast(context,
|
|
||||||
((int) (habit.getId() % Integer.MAX_VALUE)) + 1, alarmIntent,
|
|
||||||
PendingIntent.FLAG_UPDATE_CURRENT);
|
|
||||||
|
|
||||||
AlarmManager manager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
|
|
||||||
if (Build.VERSION.SDK_INT >= 19)
|
|
||||||
{
|
|
||||||
manager.setExact(AlarmManager.RTC_WAKEUP, reminderTime, pendingIntent);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
manager.set(AlarmManager.RTC_WAKEUP, reminderTime, pendingIntent);
|
|
||||||
}
|
|
||||||
|
|
||||||
Log.d("Alarm", String.format("Setting alarm (%s): %s",
|
|
||||||
DateFormat.getDateTimeInstance().format(new Date(reminderTime)), habit.name));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,17 +1,20 @@
|
|||||||
/* Copyright (C) 2016 Alinson Santos Xavier
|
/*
|
||||||
|
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify it
|
* This file is part of Loop Habit Tracker.
|
||||||
* under the terms of the GNU General Public License as published by the Free
|
|
||||||
* Software Foundation, either version 3 of the License, or (at your option)
|
|
||||||
* any later version.
|
|
||||||
*
|
*
|
||||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
* it under the terms of the GNU General Public License as published by the
|
||||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
* Free Software Foundation, either version 3 of the License, or (at your
|
||||||
|
* option) any later version.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
* more details.
|
* more details.
|
||||||
*
|
*
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License along
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.isoron.uhabits;
|
package org.isoron.uhabits;
|
||||||
@@ -19,7 +22,7 @@ package org.isoron.uhabits;
|
|||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
|
||||||
import org.isoron.uhabits.dialogs.SettingsFragment;
|
import org.isoron.uhabits.fragments.SettingsFragment;
|
||||||
|
|
||||||
public class SettingsActivity extends Activity
|
public class SettingsActivity extends Activity
|
||||||
{
|
{
|
||||||
@@ -27,7 +30,8 @@ public class SettingsActivity extends Activity
|
|||||||
protected void onCreate(Bundle savedInstanceState)
|
protected void onCreate(Bundle savedInstanceState)
|
||||||
{
|
{
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
getFragmentManager().beginTransaction().replace(android.R.id.content,
|
getFragmentManager().beginTransaction()
|
||||||
new SettingsFragment()).commit();
|
.replace(android.R.id.content, new SettingsFragment())
|
||||||
|
.commit();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,37 +1,47 @@
|
|||||||
/* Copyright (C) 2016 Alinson Santos Xavier
|
/*
|
||||||
|
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify it
|
* This file is part of Loop Habit Tracker.
|
||||||
* under the terms of the GNU General Public License as published by the Free
|
|
||||||
* Software Foundation, either version 3 of the License, or (at your option)
|
|
||||||
* any later version.
|
|
||||||
*
|
*
|
||||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
* it under the terms of the GNU General Public License as published by the
|
||||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
* Free Software Foundation, either version 3 of the License, or (at your
|
||||||
|
* option) any later version.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
* more details.
|
* more details.
|
||||||
*
|
*
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License along
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.isoron.uhabits;
|
package org.isoron.uhabits;
|
||||||
|
|
||||||
import org.isoron.helpers.ReplayableActivity;
|
import android.app.ActionBar;
|
||||||
import org.isoron.uhabits.models.Habit;
|
import android.content.BroadcastReceiver;
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.content.ContentUris;
|
import android.content.ContentUris;
|
||||||
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
|
import android.content.IntentFilter;
|
||||||
import android.graphics.drawable.ColorDrawable;
|
import android.graphics.drawable.ColorDrawable;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.view.Menu;
|
import android.support.v4.content.LocalBroadcastManager;
|
||||||
import android.view.MenuItem;
|
|
||||||
|
import org.isoron.helpers.ReplayableActivity;
|
||||||
|
import org.isoron.uhabits.fragments.ShowHabitFragment;
|
||||||
|
import org.isoron.uhabits.models.Habit;
|
||||||
|
|
||||||
public class ShowHabitActivity extends ReplayableActivity
|
public class ShowHabitActivity extends ReplayableActivity
|
||||||
{
|
{
|
||||||
|
|
||||||
public Habit habit;
|
public Habit habit;
|
||||||
|
private Receiver receiver;
|
||||||
|
private LocalBroadcastManager localBroadcastManager;
|
||||||
|
|
||||||
|
private ShowHabitFragment fragment;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState)
|
protected void onCreate(Bundle savedInstanceState)
|
||||||
@@ -40,34 +50,39 @@ public class ShowHabitActivity extends ReplayableActivity
|
|||||||
|
|
||||||
Uri data = getIntent().getData();
|
Uri data = getIntent().getData();
|
||||||
habit = Habit.get(ContentUris.parseId(data));
|
habit = Habit.get(ContentUris.parseId(data));
|
||||||
getActionBar().setTitle(habit.name);
|
ActionBar actionBar = getActionBar();
|
||||||
|
|
||||||
if (android.os.Build.VERSION.SDK_INT >= 21)
|
if(actionBar != null)
|
||||||
{
|
{
|
||||||
getActionBar().setBackgroundDrawable(new ColorDrawable(habit.color));
|
actionBar.setTitle(habit.name);
|
||||||
|
|
||||||
|
if (android.os.Build.VERSION.SDK_INT >= 21)
|
||||||
|
actionBar.setBackgroundDrawable(new ColorDrawable(habit.color));
|
||||||
}
|
}
|
||||||
|
|
||||||
setContentView(R.layout.show_habit_activity);
|
setContentView(R.layout.show_habit_activity);
|
||||||
|
|
||||||
|
fragment = (ShowHabitFragment) getFragmentManager().findFragmentById(R.id.fragment2);
|
||||||
|
|
||||||
|
receiver = new Receiver();
|
||||||
|
localBroadcastManager = LocalBroadcastManager.getInstance(this);
|
||||||
|
localBroadcastManager.registerReceiver(receiver,
|
||||||
|
new IntentFilter(MainActivity.ACTION_REFRESH));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
class Receiver extends BroadcastReceiver
|
||||||
public boolean onCreateOptionsMenu(Menu menu)
|
|
||||||
{
|
{
|
||||||
getMenuInflater().inflate(R.menu.show_habit_activity_menu, menu);
|
@Override
|
||||||
return true;
|
public void onReceive(Context context, Intent intent)
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onOptionsItemSelected(MenuItem item)
|
|
||||||
{
|
|
||||||
switch (item.getItemId())
|
|
||||||
{
|
{
|
||||||
case R.id.action_settings:
|
fragment.refreshData();
|
||||||
Intent intent = new Intent(this, SettingsActivity.class);
|
|
||||||
startActivity(intent);
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return super.onOptionsItemSelected(item);
|
@Override
|
||||||
|
protected void onDestroy()
|
||||||
|
{
|
||||||
|
localBroadcastManager.unregisterReceiver(receiver);
|
||||||
|
super.onDestroy();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||||
|
*
|
||||||
|
* This file is part of Loop Habit Tracker.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by the
|
||||||
|
* Free Software Foundation, either version 3 of the License, or (at your
|
||||||
|
* option) any later version.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
|
* more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along
|
||||||
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.isoron.uhabits.commands;
|
||||||
|
|
||||||
|
import org.isoron.helpers.Command;
|
||||||
|
import org.isoron.uhabits.R;
|
||||||
|
import org.isoron.uhabits.models.Habit;
|
||||||
|
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class ArchiveHabitsCommand extends Command
|
||||||
|
{
|
||||||
|
|
||||||
|
private List<Habit> habits;
|
||||||
|
|
||||||
|
public ArchiveHabitsCommand(Habit habit)
|
||||||
|
{
|
||||||
|
habits = new LinkedList<>();
|
||||||
|
habits.add(habit);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ArchiveHabitsCommand(List<Habit> habits)
|
||||||
|
{
|
||||||
|
this.habits = habits;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void execute()
|
||||||
|
{
|
||||||
|
for(Habit h : habits)
|
||||||
|
h.archive();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void undo()
|
||||||
|
{
|
||||||
|
for(Habit h : habits)
|
||||||
|
h.unarchive();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getExecuteStringId()
|
||||||
|
{
|
||||||
|
return R.string.toast_habit_archived;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getUndoStringId()
|
||||||
|
{
|
||||||
|
return R.string.toast_habit_unarchived;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||||
|
*
|
||||||
|
* This file is part of Loop Habit Tracker.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by the
|
||||||
|
* Free Software Foundation, either version 3 of the License, or (at your
|
||||||
|
* option) any later version.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
|
* more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along
|
||||||
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.isoron.uhabits.commands;
|
||||||
|
|
||||||
|
import com.activeandroid.ActiveAndroid;
|
||||||
|
|
||||||
|
import org.isoron.helpers.Command;
|
||||||
|
import org.isoron.uhabits.R;
|
||||||
|
import org.isoron.uhabits.models.Habit;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class ChangeHabitColorCommand extends Command
|
||||||
|
{
|
||||||
|
List<Habit> habits;
|
||||||
|
List<Integer> originalColors;
|
||||||
|
Integer newColor;
|
||||||
|
|
||||||
|
public ChangeHabitColorCommand(List<Habit> habits, Integer newColor)
|
||||||
|
{
|
||||||
|
this.habits = habits;
|
||||||
|
this.newColor = newColor;
|
||||||
|
this.originalColors = new ArrayList<>(habits.size());
|
||||||
|
|
||||||
|
for(Habit h : habits)
|
||||||
|
originalColors.add(h.color);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void execute()
|
||||||
|
{
|
||||||
|
ActiveAndroid.beginTransaction();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
for(Habit h : habits)
|
||||||
|
{
|
||||||
|
h.color = newColor;
|
||||||
|
h.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
ActiveAndroid.setTransactionSuccessful();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
ActiveAndroid.endTransaction();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void undo()
|
||||||
|
{
|
||||||
|
ActiveAndroid.beginTransaction();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
int k = 0;
|
||||||
|
for(Habit h : habits)
|
||||||
|
{
|
||||||
|
h.color = originalColors.get(k++);
|
||||||
|
h.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
ActiveAndroid.setTransactionSuccessful();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
ActiveAndroid.endTransaction();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getExecuteStringId()
|
||||||
|
{
|
||||||
|
return R.string.toast_habit_changed;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getUndoStringId()
|
||||||
|
{
|
||||||
|
return R.string.toast_habit_changed;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||||
|
*
|
||||||
|
* This file is part of Loop Habit Tracker.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by the
|
||||||
|
* Free Software Foundation, either version 3 of the License, or (at your
|
||||||
|
* option) any later version.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
|
* more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along
|
||||||
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.isoron.uhabits.commands;
|
||||||
|
|
||||||
|
import org.isoron.helpers.Command;
|
||||||
|
import org.isoron.uhabits.R;
|
||||||
|
import org.isoron.uhabits.models.Habit;
|
||||||
|
|
||||||
|
public class CreateHabitCommand extends Command
|
||||||
|
{
|
||||||
|
private Habit model;
|
||||||
|
private Long savedId;
|
||||||
|
|
||||||
|
public CreateHabitCommand(Habit model)
|
||||||
|
{
|
||||||
|
this.model = model;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void execute()
|
||||||
|
{
|
||||||
|
Habit savedHabit = new Habit(model);
|
||||||
|
if (savedId == null)
|
||||||
|
{
|
||||||
|
savedHabit.save();
|
||||||
|
savedId = savedHabit.getId();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
savedHabit.save(savedId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void undo()
|
||||||
|
{
|
||||||
|
Habit.get(savedId).delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Integer getExecuteStringId()
|
||||||
|
{
|
||||||
|
return R.string.toast_habit_created;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Integer getUndoStringId()
|
||||||
|
{
|
||||||
|
return R.string.toast_habit_deleted;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||||
|
*
|
||||||
|
* This file is part of Loop Habit Tracker.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by the
|
||||||
|
* Free Software Foundation, either version 3 of the License, or (at your
|
||||||
|
* option) any later version.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
|
* more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along
|
||||||
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.isoron.uhabits.commands;
|
||||||
|
|
||||||
|
import org.isoron.helpers.Command;
|
||||||
|
import org.isoron.uhabits.R;
|
||||||
|
import org.isoron.uhabits.models.Habit;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class DeleteHabitsCommand extends Command
|
||||||
|
{
|
||||||
|
private List<Habit> habits;
|
||||||
|
|
||||||
|
public DeleteHabitsCommand(List<Habit> habits)
|
||||||
|
{
|
||||||
|
this.habits = habits;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void execute()
|
||||||
|
{
|
||||||
|
for(Habit h : habits)
|
||||||
|
h.cascadeDelete();
|
||||||
|
|
||||||
|
Habit.rebuildOrder();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void undo()
|
||||||
|
{
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getExecuteStringId()
|
||||||
|
{
|
||||||
|
return R.string.toast_habit_deleted;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getUndoStringId()
|
||||||
|
{
|
||||||
|
return R.string.toast_habit_restored;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||||
|
*
|
||||||
|
* This file is part of Loop Habit Tracker.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by the
|
||||||
|
* Free Software Foundation, either version 3 of the License, or (at your
|
||||||
|
* option) any later version.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
|
* more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along
|
||||||
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.isoron.uhabits.commands;
|
||||||
|
|
||||||
|
import org.isoron.helpers.Command;
|
||||||
|
import org.isoron.uhabits.R;
|
||||||
|
import org.isoron.uhabits.models.Habit;
|
||||||
|
|
||||||
|
public class EditHabitCommand extends Command
|
||||||
|
{
|
||||||
|
private Habit original;
|
||||||
|
private Habit modified;
|
||||||
|
private long savedId;
|
||||||
|
private boolean hasIntervalChanged;
|
||||||
|
|
||||||
|
public EditHabitCommand(Habit original, Habit modified)
|
||||||
|
{
|
||||||
|
this.savedId = original.getId();
|
||||||
|
this.modified = new Habit(modified);
|
||||||
|
this.original = new Habit(original);
|
||||||
|
|
||||||
|
hasIntervalChanged = (!this.original.freqDen.equals(this.modified.freqDen) ||
|
||||||
|
!this.original.freqNum.equals(this.modified.freqNum));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void execute()
|
||||||
|
{
|
||||||
|
Habit habit = Habit.get(savedId);
|
||||||
|
habit.copyAttributes(modified);
|
||||||
|
habit.save();
|
||||||
|
if (hasIntervalChanged)
|
||||||
|
{
|
||||||
|
habit.checkmarks.deleteNewerThan(0);
|
||||||
|
habit.streaks.deleteNewerThan(0);
|
||||||
|
habit.scores.deleteNewerThan(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void undo()
|
||||||
|
{
|
||||||
|
Habit habit = Habit.get(savedId);
|
||||||
|
habit.copyAttributes(original);
|
||||||
|
habit.save();
|
||||||
|
if (hasIntervalChanged)
|
||||||
|
{
|
||||||
|
habit.checkmarks.deleteNewerThan(0);
|
||||||
|
habit.streaks.deleteNewerThan(0);
|
||||||
|
habit.scores.deleteNewerThan(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getExecuteStringId()
|
||||||
|
{
|
||||||
|
return R.string.toast_habit_changed;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getUndoStringId()
|
||||||
|
{
|
||||||
|
return R.string.toast_habit_changed_back;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||||
|
*
|
||||||
|
* This file is part of Loop Habit Tracker.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by the
|
||||||
|
* Free Software Foundation, either version 3 of the License, or (at your
|
||||||
|
* option) any later version.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
|
* more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along
|
||||||
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.isoron.uhabits.commands;
|
||||||
|
|
||||||
|
import org.isoron.helpers.Command;
|
||||||
|
import org.isoron.uhabits.models.Habit;
|
||||||
|
|
||||||
|
public class ToggleRepetitionCommand extends Command
|
||||||
|
{
|
||||||
|
private Long offset;
|
||||||
|
private Habit habit;
|
||||||
|
|
||||||
|
public ToggleRepetitionCommand(Habit habit, long offset)
|
||||||
|
{
|
||||||
|
this.offset = offset;
|
||||||
|
this.habit = habit;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void execute()
|
||||||
|
{
|
||||||
|
habit.repetitions.toggle(offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void undo()
|
||||||
|
{
|
||||||
|
execute();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||||
|
*
|
||||||
|
* This file is part of Loop Habit Tracker.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by the
|
||||||
|
* Free Software Foundation, either version 3 of the License, or (at your
|
||||||
|
* option) any later version.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
|
* more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along
|
||||||
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.isoron.uhabits.commands;
|
||||||
|
|
||||||
|
import org.isoron.helpers.Command;
|
||||||
|
import org.isoron.uhabits.R;
|
||||||
|
import org.isoron.uhabits.models.Habit;
|
||||||
|
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class UnarchiveHabitsCommand extends Command
|
||||||
|
{
|
||||||
|
|
||||||
|
private List<Habit> habits;
|
||||||
|
|
||||||
|
public UnarchiveHabitsCommand(Habit habit)
|
||||||
|
{
|
||||||
|
habits = new LinkedList<>();
|
||||||
|
habits.add(habit);
|
||||||
|
}
|
||||||
|
|
||||||
|
public UnarchiveHabitsCommand(List<Habit> habits)
|
||||||
|
{
|
||||||
|
this.habits = habits;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void execute()
|
||||||
|
{
|
||||||
|
for(Habit h : habits)
|
||||||
|
h.unarchive();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void undo()
|
||||||
|
{
|
||||||
|
for(Habit h : habits)
|
||||||
|
h.archive();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getExecuteStringId()
|
||||||
|
{
|
||||||
|
return R.string.toast_habit_unarchived;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getUndoStringId()
|
||||||
|
{
|
||||||
|
return R.string.toast_habit_archived;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,305 +0,0 @@
|
|||||||
/* Copyright (C) 2016 Alinson Santos Xavier
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify it
|
|
||||||
* under the terms of the GNU General Public License as published by the Free
|
|
||||||
* Software Foundation, either version 3 of the License, or (at your option)
|
|
||||||
* any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
|
||||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
||||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
|
||||||
* more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.isoron.uhabits.dialogs;
|
|
||||||
|
|
||||||
import org.isoron.helpers.ColorHelper;
|
|
||||||
import org.isoron.helpers.Command;
|
|
||||||
import org.isoron.helpers.DialogHelper.OnSavedListener;
|
|
||||||
import org.isoron.uhabits.R;
|
|
||||||
import org.isoron.uhabits.models.Habit;
|
|
||||||
|
|
||||||
import android.app.DialogFragment;
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.graphics.Color;
|
|
||||||
import android.graphics.ColorMatrix;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.preference.PreferenceManager;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.View.OnClickListener;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.Button;
|
|
||||||
import android.widget.ImageButton;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import com.android.colorpicker.ColorPickerDialog;
|
|
||||||
import com.android.colorpicker.ColorPickerSwatch;
|
|
||||||
import com.android.datetimepicker.time.RadialPickerLayout;
|
|
||||||
import com.android.datetimepicker.time.TimePickerDialog;
|
|
||||||
import com.android.datetimepicker.time.TimePickerDialog.OnTimeSetListener;
|
|
||||||
|
|
||||||
public class EditHabitFragment extends DialogFragment implements OnClickListener
|
|
||||||
{
|
|
||||||
private int mode;
|
|
||||||
static final int EDIT_MODE = 0;
|
|
||||||
static final int CREATE_MODE = 1;
|
|
||||||
|
|
||||||
private OnSavedListener onSavedListener;
|
|
||||||
|
|
||||||
private Habit originalHabit, modified_habit;
|
|
||||||
private TextView tvName, tvDescription, tvFreqNum, tvFreqDen, tvInputReminder;
|
|
||||||
|
|
||||||
private SharedPreferences prefs;
|
|
||||||
|
|
||||||
static class SolidColorMatrix extends ColorMatrix
|
|
||||||
{
|
|
||||||
public SolidColorMatrix(int color)
|
|
||||||
{
|
|
||||||
float matrix[] = { 0.0f, 0.0f, 0.0f, 0.0f, Color.red(color), 0.0f, 0.0f, 0.0f, 0.0f,
|
|
||||||
Color.green(color), 0.0f, 0.0f, 0.0f, 0.0f, Color.blue(color), 0.0f, 0.0f,
|
|
||||||
0.0f, 1.0f, 0 };
|
|
||||||
set(matrix);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
|
||||||
* Factory *
|
|
||||||
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
|
||||||
|
|
||||||
static EditHabitFragment editSingleHabitFragment(long id)
|
|
||||||
{
|
|
||||||
EditHabitFragment frag = new EditHabitFragment();
|
|
||||||
Bundle args = new Bundle();
|
|
||||||
args.putLong("habitId", id);
|
|
||||||
args.putInt("editMode", EDIT_MODE);
|
|
||||||
frag.setArguments(args);
|
|
||||||
return frag;
|
|
||||||
}
|
|
||||||
|
|
||||||
static EditHabitFragment createHabitFragment()
|
|
||||||
{
|
|
||||||
EditHabitFragment frag = new EditHabitFragment();
|
|
||||||
Bundle args = new Bundle();
|
|
||||||
args.putInt("editMode", CREATE_MODE);
|
|
||||||
frag.setArguments(args);
|
|
||||||
return frag;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
|
||||||
* Creation *
|
|
||||||
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
|
||||||
Bundle savedInstanceState)
|
|
||||||
{
|
|
||||||
View view = inflater.inflate(R.layout.edit_habit, container, false);
|
|
||||||
tvName = (TextView) view.findViewById(R.id.input_name);
|
|
||||||
tvDescription = (TextView) view.findViewById(R.id.input_description);
|
|
||||||
tvFreqNum = (TextView) view.findViewById(R.id.input_freq_num);
|
|
||||||
tvFreqDen = (TextView) view.findViewById(R.id.input_freq_den);
|
|
||||||
tvInputReminder = (TextView) view.findViewById(R.id.input_reminder_time);
|
|
||||||
|
|
||||||
Button buttonSave = (Button) view.findViewById(R.id.buttonSave);
|
|
||||||
Button buttonDiscard = (Button) view.findViewById(R.id.buttonDiscard);
|
|
||||||
|
|
||||||
buttonSave.setOnClickListener(this);
|
|
||||||
buttonDiscard.setOnClickListener(this);
|
|
||||||
tvInputReminder.setOnClickListener(this);
|
|
||||||
|
|
||||||
ImageButton buttonPickColor = (ImageButton) view.findViewById(R.id.button_pick_color);
|
|
||||||
|
|
||||||
prefs = PreferenceManager.getDefaultSharedPreferences(getActivity());
|
|
||||||
|
|
||||||
Bundle args = getArguments();
|
|
||||||
mode = (Integer) args.get("editMode");
|
|
||||||
|
|
||||||
if(mode == CREATE_MODE)
|
|
||||||
{
|
|
||||||
getDialog().setTitle("Create habit");
|
|
||||||
modified_habit = new Habit();
|
|
||||||
|
|
||||||
int defaultNum = prefs.getInt("pref_default_habit_freq_num", modified_habit.freq_num);
|
|
||||||
int defaultDen = prefs.getInt("pref_default_habit_freq_den", modified_habit.freq_den);
|
|
||||||
int defaultColor = prefs.getInt("pref_default_habit_color", modified_habit.color);
|
|
||||||
|
|
||||||
modified_habit.color = defaultColor;
|
|
||||||
modified_habit.freq_num = defaultNum;
|
|
||||||
modified_habit.freq_den = defaultDen;
|
|
||||||
}
|
|
||||||
else if(mode == EDIT_MODE)
|
|
||||||
{
|
|
||||||
originalHabit = Habit.get((Long) args.get("habitId"));
|
|
||||||
modified_habit = new Habit(originalHabit);
|
|
||||||
|
|
||||||
getDialog().setTitle("Edit habit");
|
|
||||||
tvName.append(modified_habit.name);
|
|
||||||
tvDescription.append(modified_habit.description);
|
|
||||||
}
|
|
||||||
|
|
||||||
tvFreqNum.append(modified_habit.freq_num.toString());
|
|
||||||
tvFreqDen.append(modified_habit.freq_den.toString());
|
|
||||||
|
|
||||||
changeColor(modified_habit.color);
|
|
||||||
updateReminder();
|
|
||||||
|
|
||||||
buttonPickColor.setOnClickListener(new OnClickListener()
|
|
||||||
{
|
|
||||||
public void onClick(View view)
|
|
||||||
{
|
|
||||||
ColorPickerDialog picker = ColorPickerDialog.newInstance(
|
|
||||||
R.string.color_picker_default_title,
|
|
||||||
ColorHelper.palette, modified_habit.color, 4, ColorPickerDialog.SIZE_SMALL);
|
|
||||||
|
|
||||||
picker.setOnColorSelectedListener(new ColorPickerSwatch.OnColorSelectedListener()
|
|
||||||
{
|
|
||||||
public void onColorSelected(int color)
|
|
||||||
{
|
|
||||||
changeColor(color);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
picker.show(getFragmentManager(), "picker");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return view;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void changeColor(Integer color)
|
|
||||||
{
|
|
||||||
modified_habit.color = color;
|
|
||||||
tvName.setTextColor(color);
|
|
||||||
|
|
||||||
SharedPreferences.Editor editor = prefs.edit();
|
|
||||||
editor.putInt("pref_default_habit_color", color);
|
|
||||||
editor.apply();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateReminder()
|
|
||||||
{
|
|
||||||
if(modified_habit.reminder_hour != null)
|
|
||||||
{
|
|
||||||
tvInputReminder.setTextColor(Color.BLACK);
|
|
||||||
tvInputReminder.setText(String.format("%02d:%02d", modified_habit.reminder_hour,
|
|
||||||
modified_habit.reminder_min));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
tvInputReminder.setTextColor(Color.GRAY);
|
|
||||||
tvInputReminder.setText("Off");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setOnSavedListener(OnSavedListener onSavedListener)
|
|
||||||
{
|
|
||||||
this.onSavedListener = onSavedListener;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
|
||||||
* Callback *
|
|
||||||
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onClick(View v)
|
|
||||||
{
|
|
||||||
int id = v.getId();
|
|
||||||
|
|
||||||
/* Due date spinner */
|
|
||||||
if(id == R.id.input_reminder_time)
|
|
||||||
{
|
|
||||||
int default_hour = 8;
|
|
||||||
int default_min = 0;
|
|
||||||
|
|
||||||
if(modified_habit.reminder_hour != null) {
|
|
||||||
default_hour = modified_habit.reminder_hour;
|
|
||||||
default_min = modified_habit.reminder_min;
|
|
||||||
}
|
|
||||||
|
|
||||||
TimePickerDialog timePicker = TimePickerDialog.newInstance(new OnTimeSetListener()
|
|
||||||
{
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onTimeSet(RadialPickerLayout view, int hour, int minute)
|
|
||||||
{
|
|
||||||
modified_habit.reminder_hour = hour;
|
|
||||||
modified_habit.reminder_min = minute;
|
|
||||||
updateReminder();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onTimeCleared(RadialPickerLayout view)
|
|
||||||
{
|
|
||||||
modified_habit.reminder_hour = null;
|
|
||||||
modified_habit.reminder_min = null;
|
|
||||||
updateReminder();
|
|
||||||
}
|
|
||||||
}, default_hour, default_min, true);
|
|
||||||
timePicker.show(getFragmentManager(), "timePicker");
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Save button */
|
|
||||||
if(id == R.id.buttonSave)
|
|
||||||
{
|
|
||||||
Command command = null;
|
|
||||||
|
|
||||||
modified_habit.name = tvName.getText().toString().trim();
|
|
||||||
modified_habit.description = tvDescription.getText().toString().trim();
|
|
||||||
modified_habit.freq_num = Integer.parseInt(tvFreqNum.getText().toString());
|
|
||||||
modified_habit.freq_den = Integer.parseInt(tvFreqDen.getText().toString());
|
|
||||||
|
|
||||||
Boolean valid = true;
|
|
||||||
|
|
||||||
if(modified_habit.name.length() == 0)
|
|
||||||
{
|
|
||||||
tvName.setError("Name cannot be blank.");
|
|
||||||
valid = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(modified_habit.freq_den <= 0)
|
|
||||||
{
|
|
||||||
tvFreqNum.setError("Number must be positive.");
|
|
||||||
valid = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(modified_habit.freq_num > modified_habit.freq_den)
|
|
||||||
{
|
|
||||||
tvFreqNum.setError("You can have at most one repetition per day");
|
|
||||||
valid = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!valid)
|
|
||||||
return;
|
|
||||||
|
|
||||||
SharedPreferences.Editor editor = prefs.edit();
|
|
||||||
editor.putInt("pref_default_habit_freq_num", modified_habit.freq_num);
|
|
||||||
editor.putInt("pref_default_habit_freq_den", modified_habit.freq_den);
|
|
||||||
editor.apply();
|
|
||||||
|
|
||||||
Habit savedHabit = null;
|
|
||||||
|
|
||||||
if(mode == EDIT_MODE)
|
|
||||||
{
|
|
||||||
command = originalHabit.new EditCommand(modified_habit);
|
|
||||||
savedHabit = originalHabit;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(mode == CREATE_MODE)
|
|
||||||
command = new Habit.CreateCommand(modified_habit);
|
|
||||||
|
|
||||||
if(onSavedListener != null)
|
|
||||||
onSavedListener.onSaved(command, savedHabit);
|
|
||||||
|
|
||||||
dismiss();
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Discard button */
|
|
||||||
if(id == R.id.buttonDiscard)
|
|
||||||
{
|
|
||||||
dismiss();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,267 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||||
|
*
|
||||||
|
* This file is part of Loop Habit Tracker.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by the
|
||||||
|
* Free Software Foundation, either version 3 of the License, or (at your
|
||||||
|
* option) any later version.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
|
* more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along
|
||||||
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.isoron.uhabits.dialogs;
|
||||||
|
|
||||||
|
import android.app.AlertDialog;
|
||||||
|
import android.content.DialogInterface;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.AsyncTask;
|
||||||
|
import android.view.ActionMode;
|
||||||
|
import android.view.Menu;
|
||||||
|
import android.view.MenuItem;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.ProgressBar;
|
||||||
|
|
||||||
|
import com.android.colorpicker.ColorPickerDialog;
|
||||||
|
import com.android.colorpicker.ColorPickerSwatch;
|
||||||
|
|
||||||
|
import org.isoron.helpers.ColorHelper;
|
||||||
|
import org.isoron.helpers.DialogHelper;
|
||||||
|
import org.isoron.helpers.ReplayableActivity;
|
||||||
|
import org.isoron.uhabits.R;
|
||||||
|
import org.isoron.uhabits.commands.ArchiveHabitsCommand;
|
||||||
|
import org.isoron.uhabits.commands.ChangeHabitColorCommand;
|
||||||
|
import org.isoron.uhabits.commands.DeleteHabitsCommand;
|
||||||
|
import org.isoron.uhabits.commands.UnarchiveHabitsCommand;
|
||||||
|
import org.isoron.uhabits.fragments.EditHabitFragment;
|
||||||
|
import org.isoron.uhabits.io.CSVExporter;
|
||||||
|
import org.isoron.uhabits.loaders.HabitListLoader;
|
||||||
|
import org.isoron.uhabits.models.Habit;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class HabitSelectionCallback implements ActionMode.Callback
|
||||||
|
{
|
||||||
|
private HabitListLoader loader;
|
||||||
|
private List<Integer> selectedPositions;
|
||||||
|
private ReplayableActivity activity;
|
||||||
|
private Listener listener;
|
||||||
|
private DialogHelper.OnSavedListener onSavedListener;
|
||||||
|
private ProgressBar progressBar;
|
||||||
|
|
||||||
|
public interface Listener
|
||||||
|
{
|
||||||
|
void onActionModeDestroyed(ActionMode mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
public HabitSelectionCallback(ReplayableActivity activity, HabitListLoader loader)
|
||||||
|
{
|
||||||
|
this.activity = activity;
|
||||||
|
this.loader = loader;
|
||||||
|
selectedPositions = new LinkedList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setListener(Listener listener)
|
||||||
|
{
|
||||||
|
this.listener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setProgressBar(ProgressBar progressBar)
|
||||||
|
{
|
||||||
|
this.progressBar = progressBar;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOnSavedListener(DialogHelper.OnSavedListener onSavedListener)
|
||||||
|
{
|
||||||
|
this.onSavedListener = onSavedListener;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSelectedPositions(List<Integer> selectedPositions)
|
||||||
|
{
|
||||||
|
this.selectedPositions = selectedPositions;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onCreateActionMode(ActionMode mode, Menu menu)
|
||||||
|
{
|
||||||
|
activity.getMenuInflater().inflate(R.menu.list_habits_context, menu);
|
||||||
|
updateTitle(mode);
|
||||||
|
updateActions(menu);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onPrepareActionMode(ActionMode mode, Menu menu)
|
||||||
|
{
|
||||||
|
updateTitle(mode);
|
||||||
|
updateActions(menu);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateActions(Menu menu)
|
||||||
|
{
|
||||||
|
boolean showEdit = (selectedPositions.size() == 1);
|
||||||
|
boolean showArchive = true;
|
||||||
|
boolean showUnarchive = true;
|
||||||
|
for (int i : selectedPositions)
|
||||||
|
{
|
||||||
|
Habit h = loader.habitsList.get(i);
|
||||||
|
if (h.isArchived())
|
||||||
|
{
|
||||||
|
showArchive = false;
|
||||||
|
}
|
||||||
|
else showUnarchive = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
MenuItem itemEdit = menu.findItem(R.id.action_edit_habit);
|
||||||
|
MenuItem itemColor = menu.findItem(R.id.action_color);
|
||||||
|
MenuItem itemArchive = menu.findItem(R.id.action_archive_habit);
|
||||||
|
MenuItem itemUnarchive = menu.findItem(R.id.action_unarchive_habit);
|
||||||
|
|
||||||
|
itemColor.setVisible(true);
|
||||||
|
itemEdit.setVisible(showEdit);
|
||||||
|
itemArchive.setVisible(showArchive);
|
||||||
|
itemUnarchive.setVisible(showUnarchive);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateTitle(ActionMode mode)
|
||||||
|
{
|
||||||
|
mode.setTitle("" + selectedPositions.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onActionItemClicked(final ActionMode mode, MenuItem item)
|
||||||
|
{
|
||||||
|
final LinkedList<Habit> selectedHabits = new LinkedList<>();
|
||||||
|
for (int i : selectedPositions)
|
||||||
|
selectedHabits.add(loader.habitsList.get(i));
|
||||||
|
|
||||||
|
Habit firstHabit = selectedHabits.getFirst();
|
||||||
|
|
||||||
|
switch (item.getItemId())
|
||||||
|
{
|
||||||
|
case R.id.action_archive_habit:
|
||||||
|
activity.executeCommand(new ArchiveHabitsCommand(selectedHabits), null);
|
||||||
|
mode.finish();
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case R.id.action_unarchive_habit:
|
||||||
|
activity.executeCommand(new UnarchiveHabitsCommand(selectedHabits), null);
|
||||||
|
mode.finish();
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case R.id.action_edit_habit:
|
||||||
|
{
|
||||||
|
EditHabitFragment frag = EditHabitFragment.editSingleHabitFragment(firstHabit.getId());
|
||||||
|
frag.setOnSavedListener(onSavedListener);
|
||||||
|
frag.show(activity.getFragmentManager(), "editHabit");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
case R.id.action_color:
|
||||||
|
{
|
||||||
|
ColorPickerDialog picker = ColorPickerDialog.newInstance(R.string.color_picker_default_title,
|
||||||
|
ColorHelper.palette, firstHabit.color, 4, ColorPickerDialog.SIZE_SMALL);
|
||||||
|
|
||||||
|
picker.setOnColorSelectedListener(new ColorPickerSwatch.OnColorSelectedListener()
|
||||||
|
{
|
||||||
|
public void onColorSelected(int color)
|
||||||
|
{
|
||||||
|
activity.executeCommand(
|
||||||
|
new ChangeHabitColorCommand(selectedHabits, color), null);
|
||||||
|
mode.finish();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
picker.show(activity.getFragmentManager(), "picker");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
case R.id.action_delete:
|
||||||
|
{
|
||||||
|
new AlertDialog.Builder(activity).setTitle(R.string.delete_habits)
|
||||||
|
.setMessage(R.string.delete_habits_message)
|
||||||
|
.setPositiveButton(android.R.string.yes,
|
||||||
|
new DialogInterface.OnClickListener()
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
public void onClick(DialogInterface dialog, int which)
|
||||||
|
{
|
||||||
|
activity.executeCommand(
|
||||||
|
new DeleteHabitsCommand(selectedHabits), null);
|
||||||
|
mode.finish();
|
||||||
|
}
|
||||||
|
}).setNegativeButton(android.R.string.no, null)
|
||||||
|
.show();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
case R.id.action_export_csv:
|
||||||
|
{
|
||||||
|
onExportHabitsClick(selectedHabits);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroyActionMode(ActionMode mode)
|
||||||
|
{
|
||||||
|
if(listener != null) listener.onActionModeDestroyed(mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onExportHabitsClick(final LinkedList<Habit> selectedHabits)
|
||||||
|
{
|
||||||
|
new AsyncTask<Void, Void, Void>()
|
||||||
|
{
|
||||||
|
String filename;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPreExecute()
|
||||||
|
{
|
||||||
|
if(progressBar != null)
|
||||||
|
{
|
||||||
|
progressBar.setIndeterminate(true);
|
||||||
|
progressBar.setVisibility(View.VISIBLE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPostExecute(Void aVoid)
|
||||||
|
{
|
||||||
|
if(filename != null)
|
||||||
|
{
|
||||||
|
Intent intent = new Intent();
|
||||||
|
intent.setAction(Intent.ACTION_SEND);
|
||||||
|
intent.setType("application/zip");
|
||||||
|
intent.putExtra(Intent.EXTRA_STREAM, Uri.fromFile(new File(filename)));
|
||||||
|
|
||||||
|
activity.startActivity(intent);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(progressBar != null)
|
||||||
|
progressBar.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Void doInBackground(Void... params)
|
||||||
|
{
|
||||||
|
CSVExporter exporter = new CSVExporter(activity, selectedHabits);
|
||||||
|
filename = exporter.writeArchive();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}.execute();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||||
|
*
|
||||||
|
* This file is part of Loop Habit Tracker.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by the
|
||||||
|
* Free Software Foundation, either version 3 of the License, or (at your
|
||||||
|
* option) any later version.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
|
* more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along
|
||||||
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.isoron.uhabits.dialogs;
|
||||||
|
|
||||||
|
import android.animation.Animator;
|
||||||
|
import android.animation.AnimatorListenerAdapter;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
import android.preference.PreferenceManager;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import org.isoron.helpers.DateHelper;
|
||||||
|
import org.isoron.uhabits.R;
|
||||||
|
|
||||||
|
public class HintManager
|
||||||
|
{
|
||||||
|
private Context context;
|
||||||
|
private SharedPreferences prefs;
|
||||||
|
private View hintView;
|
||||||
|
|
||||||
|
public HintManager(Context context, View hintView)
|
||||||
|
{
|
||||||
|
this.context = context;
|
||||||
|
this.hintView = hintView;
|
||||||
|
prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void dismissHint()
|
||||||
|
{
|
||||||
|
hintView.animate().alpha(0f).setDuration(500).setListener(new AnimatorListenerAdapter()
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
public void onAnimationEnd(Animator animation)
|
||||||
|
{
|
||||||
|
hintView.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void showHintIfAppropriate()
|
||||||
|
{
|
||||||
|
Integer lastHintNumber = prefs.getInt("last_hint_number", -1);
|
||||||
|
Long lastHintTimestamp = prefs.getLong("last_hint_timestamp", -1);
|
||||||
|
|
||||||
|
if (DateHelper.getStartOfToday() > lastHintTimestamp) showHint(lastHintNumber + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showHint(int hintNumber)
|
||||||
|
{
|
||||||
|
String[] hints = context.getResources().getStringArray(R.array.hints);
|
||||||
|
if (hintNumber >= hints.length) return;
|
||||||
|
|
||||||
|
prefs.edit().putInt("last_hint_number", hintNumber).apply();
|
||||||
|
prefs.edit().putLong("last_hint_timestamp", DateHelper.getStartOfToday()).apply();
|
||||||
|
|
||||||
|
TextView tvContent = (TextView) hintView.findViewById(R.id.hintContent);
|
||||||
|
tvContent.setText(hints[hintNumber]);
|
||||||
|
|
||||||
|
hintView.setAlpha(0.0f);
|
||||||
|
hintView.setVisibility(View.VISIBLE);
|
||||||
|
hintView.animate().alpha(1f).setDuration(500);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||||
|
*
|
||||||
|
* This file is part of Loop Habit Tracker.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by the
|
||||||
|
* Free Software Foundation, either version 3 of the License, or (at your
|
||||||
|
* option) any later version.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
|
* more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along
|
||||||
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.isoron.uhabits.dialogs;
|
||||||
|
|
||||||
|
import android.app.AlertDialog;
|
||||||
|
import android.app.Dialog;
|
||||||
|
import android.app.DialogFragment;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.DialogInterface;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.util.DisplayMetrics;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import org.isoron.uhabits.R;
|
||||||
|
import org.isoron.uhabits.models.Habit;
|
||||||
|
import org.isoron.uhabits.views.HabitHistoryView;
|
||||||
|
|
||||||
|
public class HistoryEditorDialog extends DialogFragment
|
||||||
|
implements DialogInterface.OnClickListener
|
||||||
|
{
|
||||||
|
private Habit habit;
|
||||||
|
private Listener listener;
|
||||||
|
HabitHistoryView historyView;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Dialog onCreateDialog(Bundle savedInstanceState)
|
||||||
|
{
|
||||||
|
Context context = getActivity();
|
||||||
|
historyView = new HabitHistoryView(context, null);
|
||||||
|
int p = (int) getResources().getDimension(R.dimen.history_editor_padding);
|
||||||
|
|
||||||
|
if(savedInstanceState != null)
|
||||||
|
{
|
||||||
|
long id = savedInstanceState.getLong("habit", -1);
|
||||||
|
if(id > 0) this.habit = Habit.get(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
historyView.setPadding(p, 0, p, 0);
|
||||||
|
historyView.setHabit(habit);
|
||||||
|
historyView.setIsEditable(true);
|
||||||
|
|
||||||
|
AlertDialog.Builder builder = new AlertDialog.Builder(context);
|
||||||
|
builder.setTitle(R.string.history)
|
||||||
|
.setView(historyView)
|
||||||
|
.setPositiveButton(android.R.string.ok, this);
|
||||||
|
|
||||||
|
return builder.create();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onResume()
|
||||||
|
{
|
||||||
|
super.onResume();
|
||||||
|
|
||||||
|
DisplayMetrics metrics = getResources().getDisplayMetrics();
|
||||||
|
int maxHeight = getResources().getDimensionPixelSize(R.dimen.history_editor_max_height);
|
||||||
|
int width = metrics.widthPixels;
|
||||||
|
int height = Math.min(metrics.heightPixels, maxHeight);
|
||||||
|
|
||||||
|
Log.d("HistoryEditorDialog", String.format("h=%d max_h=%d", height, maxHeight));
|
||||||
|
|
||||||
|
getDialog().getWindow().setLayout(width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onClick(DialogInterface dialog, int which)
|
||||||
|
{
|
||||||
|
dismiss();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setHabit(Habit habit)
|
||||||
|
{
|
||||||
|
this.habit = habit;
|
||||||
|
if(historyView != null) historyView.setHabit(habit);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPause()
|
||||||
|
{
|
||||||
|
super.onPause();
|
||||||
|
if(listener != null) listener.onHistoryEditorClosed();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSaveInstanceState(Bundle outState)
|
||||||
|
{
|
||||||
|
outState.putLong("habit", habit.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setListener(Listener listener)
|
||||||
|
{
|
||||||
|
this.listener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface Listener {
|
||||||
|
void onHistoryEditorClosed();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,672 +0,0 @@
|
|||||||
/* Copyright (C) 2016 Alinson Santos Xavier
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify it
|
|
||||||
* under the terms of the GNU General Public License as published by the Free
|
|
||||||
* Software Foundation, either version 3 of the License, or (at your option)
|
|
||||||
* any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
|
||||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
||||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
|
||||||
* more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.isoron.uhabits.dialogs;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.app.Fragment;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.graphics.Color;
|
|
||||||
import android.graphics.Point;
|
|
||||||
import android.graphics.Typeface;
|
|
||||||
import android.os.AsyncTask;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.os.Handler;
|
|
||||||
import android.os.Vibrator;
|
|
||||||
import android.preference.PreferenceManager;
|
|
||||||
import android.util.DisplayMetrics;
|
|
||||||
import android.view.ContextMenu;
|
|
||||||
import android.view.ContextMenu.ContextMenuInfo;
|
|
||||||
import android.view.Display;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.Menu;
|
|
||||||
import android.view.MenuInflater;
|
|
||||||
import android.view.MenuItem;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.View.OnClickListener;
|
|
||||||
import android.view.View.OnLongClickListener;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.view.WindowManager;
|
|
||||||
import android.widget.AdapterView;
|
|
||||||
import android.widget.AdapterView.AdapterContextMenuInfo;
|
|
||||||
import android.widget.AdapterView.OnItemClickListener;
|
|
||||||
import android.widget.BaseAdapter;
|
|
||||||
import android.widget.Button;
|
|
||||||
import android.widget.LinearLayout;
|
|
||||||
import android.widget.LinearLayout.LayoutParams;
|
|
||||||
import android.widget.ProgressBar;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import com.mobeta.android.dslv.DragSortController;
|
|
||||||
import com.mobeta.android.dslv.DragSortListView;
|
|
||||||
import com.mobeta.android.dslv.DragSortListView.DropListener;
|
|
||||||
|
|
||||||
import org.isoron.helpers.ColorHelper;
|
|
||||||
import org.isoron.helpers.Command;
|
|
||||||
import org.isoron.helpers.DateHelper;
|
|
||||||
import org.isoron.helpers.DialogHelper.OnSavedListener;
|
|
||||||
import org.isoron.helpers.ReplayableActivity;
|
|
||||||
import org.isoron.uhabits.R;
|
|
||||||
import org.isoron.uhabits.ReminderHelper;
|
|
||||||
import org.isoron.uhabits.models.Habit;
|
|
||||||
|
|
||||||
import java.util.Date;
|
|
||||||
import java.util.GregorianCalendar;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.TimeZone;
|
|
||||||
|
|
||||||
public class ListHabitsFragment extends Fragment
|
|
||||||
implements OnSavedListener, OnItemClickListener, OnLongClickListener, DropListener,
|
|
||||||
OnClickListener
|
|
||||||
{
|
|
||||||
|
|
||||||
public static final int INACTIVE_COLOR = Color.rgb(230, 230, 230);
|
|
||||||
|
|
||||||
public interface OnHabitClickListener
|
|
||||||
{
|
|
||||||
void onHabitClicked(Habit habit);
|
|
||||||
}
|
|
||||||
|
|
||||||
ListHabitsAdapter adapter;
|
|
||||||
DragSortListView listView;
|
|
||||||
ReplayableActivity activity;
|
|
||||||
TextView tvNameHeader;
|
|
||||||
long lastLongClick = 0;
|
|
||||||
private int tvNameWidth;
|
|
||||||
private int button_count;
|
|
||||||
private View llEmpty;
|
|
||||||
private ProgressBar progressBar;
|
|
||||||
|
|
||||||
private OnHabitClickListener habitClickListener;
|
|
||||||
private boolean short_toggle_enabled;
|
|
||||||
|
|
||||||
private HashMap<Long, Habit> habits;
|
|
||||||
private HashMap<Integer, Habit> positionToHabit;
|
|
||||||
private HashMap<Long, int[]> checkmarks;
|
|
||||||
private HashMap<Long, Integer> scores;
|
|
||||||
|
|
||||||
private Long lastLoadedTimestamp = null;
|
|
||||||
private AsyncTask<Void, Integer, Void> currentFetchTask = null;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
|
||||||
Bundle savedInstanceState)
|
|
||||||
{
|
|
||||||
DisplayMetrics dm = getResources().getDisplayMetrics();
|
|
||||||
int width = (int) (dm.widthPixels / dm.density);
|
|
||||||
button_count = (int) ((width - 160) / 42);
|
|
||||||
tvNameWidth = (int) ((width - 30 - button_count * 42) * dm.density);
|
|
||||||
|
|
||||||
habits = new HashMap<>();
|
|
||||||
positionToHabit = new HashMap<>();
|
|
||||||
checkmarks = new HashMap<>();
|
|
||||||
scores = new HashMap<>();
|
|
||||||
|
|
||||||
View view = inflater.inflate(R.layout.list_habits_fragment, container, false);
|
|
||||||
tvNameHeader = (TextView) view.findViewById(R.id.tvNameHeader);
|
|
||||||
|
|
||||||
progressBar = (ProgressBar) view.findViewById(R.id.progressBar);
|
|
||||||
progressBar.setVisibility(View.INVISIBLE);
|
|
||||||
|
|
||||||
adapter = new ListHabitsAdapter(getActivity());
|
|
||||||
listView = (DragSortListView) view.findViewById(R.id.listView);
|
|
||||||
listView.setAdapter(adapter);
|
|
||||||
listView.setOnItemClickListener(this);
|
|
||||||
registerForContextMenu(listView);
|
|
||||||
listView.setDropListener(this);
|
|
||||||
|
|
||||||
DragSortController controller = new DragSortController(listView);
|
|
||||||
controller.setDragHandleId(R.id.tvStar);
|
|
||||||
controller.setRemoveEnabled(false);
|
|
||||||
controller.setSortEnabled(true);
|
|
||||||
controller.setDragInitMode(1);
|
|
||||||
|
|
||||||
listView.setFloatViewManager(controller);
|
|
||||||
listView.setOnTouchListener(controller);
|
|
||||||
listView.setDragEnabled(true);
|
|
||||||
|
|
||||||
Typeface fontawesome = Typeface.createFromAsset(getActivity().getAssets(),
|
|
||||||
"fontawesome-webfont.ttf");
|
|
||||||
((TextView) view.findViewById(R.id.tvStarEmpty)).setTypeface(fontawesome);
|
|
||||||
llEmpty = view.findViewById(R.id.llEmpty);
|
|
||||||
|
|
||||||
updateEmptyMessage();
|
|
||||||
setHasOptionsMenu(true);
|
|
||||||
return view;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onAttach(Activity activity)
|
|
||||||
{
|
|
||||||
super.onAttach(activity);
|
|
||||||
habitClickListener = (OnHabitClickListener) activity;
|
|
||||||
this.activity = (ReplayableActivity) activity;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onResume()
|
|
||||||
{
|
|
||||||
super.onResume();
|
|
||||||
if(lastLoadedTimestamp == null || lastLoadedTimestamp != DateHelper.getStartOfToday())
|
|
||||||
{
|
|
||||||
updateHeader();
|
|
||||||
fetchAllHabits();
|
|
||||||
updateEmptyMessage();
|
|
||||||
}
|
|
||||||
|
|
||||||
adapter.notifyDataSetChanged();
|
|
||||||
|
|
||||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(activity);
|
|
||||||
short_toggle_enabled = prefs.getBoolean("pref_short_toggle", false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateHeader()
|
|
||||||
{
|
|
||||||
LayoutInflater inflater = activity.getLayoutInflater();
|
|
||||||
View view = getView();
|
|
||||||
|
|
||||||
if (view == null) return;
|
|
||||||
|
|
||||||
GregorianCalendar day = new GregorianCalendar(TimeZone.getTimeZone("GMT"));
|
|
||||||
day.setTimeInMillis(DateHelper.getStartOfDay(DateHelper.getLocalTime()));
|
|
||||||
|
|
||||||
LinearLayout llButtonsHeader = (LinearLayout) view.findViewById(R.id.llButtonsHeader);
|
|
||||||
llButtonsHeader.removeAllViews();
|
|
||||||
|
|
||||||
for (int i = 0; i < button_count; i++)
|
|
||||||
{
|
|
||||||
View check = inflater.inflate(R.layout.list_habits_header_check, null);
|
|
||||||
Button btCheck = (Button) check.findViewById(R.id.tvCheck);
|
|
||||||
btCheck.setText(day.getDisplayName(GregorianCalendar.DAY_OF_WEEK,
|
|
||||||
GregorianCalendar.SHORT, Locale.US) + "\n" +
|
|
||||||
Integer.toString(day.get(GregorianCalendar.DAY_OF_MONTH)));
|
|
||||||
llButtonsHeader.addView(check);
|
|
||||||
|
|
||||||
day.add(GregorianCalendar.DAY_OF_MONTH, -1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void fetchAllHabits()
|
|
||||||
{
|
|
||||||
if(currentFetchTask != null) currentFetchTask.cancel(true);
|
|
||||||
|
|
||||||
currentFetchTask = new AsyncTask<Void, Integer, Void>()
|
|
||||||
{
|
|
||||||
HashMap<Long, Habit> newHabits = Habit.getAll();
|
|
||||||
HashMap<Integer, Habit> newPositionToHabit = new HashMap<>();
|
|
||||||
HashMap<Long, int[]> newCheckmarks = new HashMap<>();
|
|
||||||
HashMap<Long, Integer> newScores = new HashMap<>();
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Void doInBackground(Void... params)
|
|
||||||
{
|
|
||||||
long dateTo = DateHelper.getStartOfDay(DateHelper.getLocalTime());
|
|
||||||
long dateFrom = dateTo - (button_count - 1) * DateHelper.millisecondsInOneDay;
|
|
||||||
int[] empty = new int[button_count];
|
|
||||||
|
|
||||||
for(Habit h : newHabits.values())
|
|
||||||
{
|
|
||||||
newScores.put(h.getId(), 0);
|
|
||||||
newPositionToHabit.put(h.position, h);
|
|
||||||
newCheckmarks.put(h.getId(), empty);
|
|
||||||
}
|
|
||||||
|
|
||||||
int current = 0;
|
|
||||||
for(int i = 0; i < newHabits.size(); i++)
|
|
||||||
{
|
|
||||||
if(isCancelled()) return null;
|
|
||||||
|
|
||||||
Habit h = newPositionToHabit.get(i);
|
|
||||||
newScores.put(h.getId(), h.getScore());
|
|
||||||
newCheckmarks.put(h.getId(), h.getCheckmarks(dateFrom, dateTo));
|
|
||||||
|
|
||||||
publishProgress(current++, newHabits.size());
|
|
||||||
}
|
|
||||||
|
|
||||||
commit();
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void commit()
|
|
||||||
{
|
|
||||||
habits = newHabits;
|
|
||||||
positionToHabit = newPositionToHabit;
|
|
||||||
checkmarks = newCheckmarks;
|
|
||||||
scores = newScores;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onPreExecute()
|
|
||||||
{
|
|
||||||
progressBar.setIndeterminate(false);
|
|
||||||
progressBar.setProgress(0);
|
|
||||||
progressBar.setVisibility(View.VISIBLE);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onProgressUpdate(Integer... values)
|
|
||||||
{
|
|
||||||
progressBar.setMax(values[1]);
|
|
||||||
progressBar.setProgress(values[0]);
|
|
||||||
|
|
||||||
if(lastLoadedTimestamp == null)
|
|
||||||
{
|
|
||||||
commit();
|
|
||||||
adapter.notifyDataSetChanged();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onPostExecute(Void aVoid)
|
|
||||||
{
|
|
||||||
if(isCancelled()) return;
|
|
||||||
|
|
||||||
adapter.notifyDataSetChanged();
|
|
||||||
updateEmptyMessage();
|
|
||||||
|
|
||||||
progressBar.setVisibility(View.INVISIBLE);
|
|
||||||
currentFetchTask = null;
|
|
||||||
lastLoadedTimestamp = DateHelper.getStartOfToday();
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
currentFetchTask.execute();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void fetchHabit(final Long id)
|
|
||||||
{
|
|
||||||
new AsyncTask<Void, Void, Void>()
|
|
||||||
{
|
|
||||||
@Override
|
|
||||||
protected Void doInBackground(Void... params)
|
|
||||||
{
|
|
||||||
long dateTo = DateHelper.getStartOfDay(DateHelper.getLocalTime());
|
|
||||||
long dateFrom = dateTo - (button_count - 1) * DateHelper.millisecondsInOneDay;
|
|
||||||
|
|
||||||
Habit h = Habit.get(id);
|
|
||||||
habits.put(id, h);
|
|
||||||
scores.put(id, h.getScore());
|
|
||||||
checkmarks.put(id, h.getCheckmarks(dateFrom, dateTo));
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onPreExecute()
|
|
||||||
{
|
|
||||||
new Handler().postDelayed(new Runnable()
|
|
||||||
{
|
|
||||||
@Override
|
|
||||||
public void run()
|
|
||||||
{
|
|
||||||
if(getStatus() == Status.RUNNING)
|
|
||||||
{
|
|
||||||
progressBar.setIndeterminate(true);
|
|
||||||
progressBar.setVisibility(View.VISIBLE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, 500);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onPostExecute(Void aVoid)
|
|
||||||
{
|
|
||||||
progressBar.setVisibility(View.GONE);
|
|
||||||
adapter.notifyDataSetChanged();
|
|
||||||
}
|
|
||||||
}.execute();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater)
|
|
||||||
{
|
|
||||||
super.onCreateOptionsMenu(menu, inflater);
|
|
||||||
inflater.inflate(R.menu.list_habits_options, menu);
|
|
||||||
|
|
||||||
MenuItem showArchivedItem = menu.findItem(R.id.action_show_archived);
|
|
||||||
showArchivedItem.setChecked(Habit.isIncludeArchived());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfo)
|
|
||||||
{
|
|
||||||
super.onCreateContextMenu(menu, view, menuInfo);
|
|
||||||
getActivity().getMenuInflater().inflate(R.menu.list_habits_context, menu);
|
|
||||||
|
|
||||||
AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
|
|
||||||
final Habit habit = habits.get(info.id);
|
|
||||||
|
|
||||||
if(habit.isArchived())
|
|
||||||
menu.findItem(R.id.action_archive_habit).setVisible(false);
|
|
||||||
else
|
|
||||||
menu.findItem(R.id.action_unarchive_habit).setVisible(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onOptionsItemSelected(MenuItem item)
|
|
||||||
{
|
|
||||||
switch(item.getItemId())
|
|
||||||
{
|
|
||||||
case R.id.action_add:
|
|
||||||
{
|
|
||||||
EditHabitFragment frag = EditHabitFragment.createHabitFragment();
|
|
||||||
frag.setOnSavedListener(this);
|
|
||||||
frag.show(getFragmentManager(), "dialog");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
case R.id.action_show_archived:
|
|
||||||
{
|
|
||||||
Habit.setIncludeArchived(!Habit.isIncludeArchived());
|
|
||||||
fetchAllHabits();
|
|
||||||
activity.invalidateOptionsMenu();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
|
||||||
return super.onOptionsItemSelected(item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onContextItemSelected(MenuItem menuItem)
|
|
||||||
{
|
|
||||||
AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuItem.getMenuInfo();
|
|
||||||
final int id = menuItem.getItemId();
|
|
||||||
final Habit habit = habits.get(info.id);
|
|
||||||
|
|
||||||
if (id == R.id.action_edit_habit)
|
|
||||||
{
|
|
||||||
EditHabitFragment frag = EditHabitFragment.editSingleHabitFragment(habit.getId());
|
|
||||||
frag.setOnSavedListener(this);
|
|
||||||
frag.show(getFragmentManager(), "dialog");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
else if (id == R.id.action_archive_habit)
|
|
||||||
{
|
|
||||||
Command c = habit.new ArchiveCommand();
|
|
||||||
executeCommand(c, null);
|
|
||||||
}
|
|
||||||
else if (id == R.id.action_unarchive_habit)
|
|
||||||
{
|
|
||||||
Command c = habit.new UnarchiveCommand();
|
|
||||||
executeCommand(c, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
return super.onContextItemSelected(menuItem);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onItemClick(AdapterView<?> parent, View view, int position, long id)
|
|
||||||
{
|
|
||||||
if (new Date().getTime() - lastLongClick < 1000) return;
|
|
||||||
|
|
||||||
Habit habit = positionToHabit.get(position);
|
|
||||||
habitClickListener.onHabitClicked(habit);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onSaved(Command command, Object savedObject)
|
|
||||||
{
|
|
||||||
Habit h = (Habit) savedObject;
|
|
||||||
|
|
||||||
if(h == null) activity.executeCommand(command, null);
|
|
||||||
else activity.executeCommand(command, h.getId());
|
|
||||||
adapter.notifyDataSetChanged();
|
|
||||||
|
|
||||||
ReminderHelper.createReminderAlarms(activity);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateEmptyMessage()
|
|
||||||
{
|
|
||||||
if(lastLoadedTimestamp == null)
|
|
||||||
llEmpty.setVisibility(View.GONE);
|
|
||||||
else
|
|
||||||
llEmpty.setVisibility(habits.size() > 0 ? View.GONE : View.VISIBLE);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onLongClick(View v)
|
|
||||||
{
|
|
||||||
switch(v.getId())
|
|
||||||
{
|
|
||||||
case R.id.tvCheck:
|
|
||||||
{
|
|
||||||
lastLongClick = new Date().getTime();
|
|
||||||
if(!short_toggle_enabled)
|
|
||||||
{
|
|
||||||
toggleCheck(v);
|
|
||||||
Vibrator vb = (Vibrator) getActivity().getSystemService(Context.VIBRATOR_SERVICE);
|
|
||||||
vb.vibrate(100);
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void toggleCheck(View v)
|
|
||||||
{
|
|
||||||
Habit habit = habits.get((Long) v.getTag(R.string.habit_key));
|
|
||||||
|
|
||||||
int offset = (Integer) v.getTag(R.string.offset_key);
|
|
||||||
long timestamp = DateHelper.getStartOfDay(
|
|
||||||
DateHelper.getLocalTime() - offset * DateHelper.millisecondsInOneDay);
|
|
||||||
|
|
||||||
if(v.getTag(R.string.toggle_key).equals(2))
|
|
||||||
updateCheck(habit.color, (TextView) v, 0);
|
|
||||||
else
|
|
||||||
updateCheck(habit.color, (TextView) v, 2);
|
|
||||||
|
|
||||||
executeCommand(habit.new ToggleRepetitionCommand(timestamp), habit.getId());
|
|
||||||
}
|
|
||||||
|
|
||||||
private void executeCommand(Command c, Long refreshKey)
|
|
||||||
{
|
|
||||||
activity.executeCommand(c, refreshKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void drop(int from, int to)
|
|
||||||
{
|
|
||||||
Habit fromHabit = positionToHabit.get(from);
|
|
||||||
Habit toHabit = positionToHabit.get(to);
|
|
||||||
positionToHabit.put(to, fromHabit);
|
|
||||||
positionToHabit.put(from, toHabit);
|
|
||||||
adapter.notifyDataSetChanged();
|
|
||||||
|
|
||||||
Habit.reorder(from, to);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onClick(View v)
|
|
||||||
{
|
|
||||||
switch(v.getId())
|
|
||||||
{
|
|
||||||
case R.id.tvCheck:
|
|
||||||
if(short_toggle_enabled)
|
|
||||||
toggleCheck(v);
|
|
||||||
else
|
|
||||||
activity.showToast(R.string.long_press_to_toggle);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ListHabitsAdapter extends BaseAdapter
|
|
||||||
{
|
|
||||||
private Context context;
|
|
||||||
private LayoutInflater inflater;
|
|
||||||
private Typeface fontawesome;
|
|
||||||
|
|
||||||
public ListHabitsAdapter(Context context)
|
|
||||||
{
|
|
||||||
this.context = context;
|
|
||||||
|
|
||||||
inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
|
|
||||||
fontawesome = Typeface.createFromAsset(context.getAssets(), "fontawesome-webfont.ttf");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getCount()
|
|
||||||
{
|
|
||||||
return habits.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Object getItem(int position)
|
|
||||||
{
|
|
||||||
return positionToHabit.get(position);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public long getItemId(int position)
|
|
||||||
{
|
|
||||||
return ((Habit) getItem(position)).getId();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public View getView(int position, View view, ViewGroup parent)
|
|
||||||
{
|
|
||||||
final Habit habit = positionToHabit.get(position);
|
|
||||||
|
|
||||||
if (view == null || (Long) view.getTag(R.id.KEY_TIMESTAMP) !=
|
|
||||||
DateHelper.getStartOfToday())
|
|
||||||
{
|
|
||||||
view = inflater.inflate(R.layout.list_habits_item, null);
|
|
||||||
((TextView) view.findViewById(R.id.tvStar)).setTypeface(fontawesome);
|
|
||||||
|
|
||||||
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(tvNameWidth,
|
|
||||||
LayoutParams.WRAP_CONTENT, 1);
|
|
||||||
view.findViewById(R.id.tvName).setLayoutParams(params);
|
|
||||||
|
|
||||||
Display display = ((WindowManager) context.getSystemService(Context.WINDOW_SERVICE))
|
|
||||||
.getDefaultDisplay();
|
|
||||||
Point size = new Point();
|
|
||||||
display.getSize(size);
|
|
||||||
|
|
||||||
for (int i = 0; i < button_count; i++)
|
|
||||||
{
|
|
||||||
View check = inflater.inflate(R.layout.list_habits_item_check, null);
|
|
||||||
TextView btCheck = (TextView) check.findViewById(R.id.tvCheck);
|
|
||||||
btCheck.setTypeface(fontawesome);
|
|
||||||
btCheck.setOnLongClickListener(ListHabitsFragment.this);
|
|
||||||
btCheck.setOnClickListener(ListHabitsFragment.this);
|
|
||||||
((LinearLayout) view.findViewById(R.id.llButtons)).addView(check);
|
|
||||||
}
|
|
||||||
|
|
||||||
view.setTag(R.id.KEY_TIMESTAMP, DateHelper.getStartOfToday());
|
|
||||||
}
|
|
||||||
|
|
||||||
TextView tvStar = (TextView) view.findViewById(R.id.tvStar);
|
|
||||||
TextView tvName = (TextView) view.findViewById(R.id.tvName);
|
|
||||||
|
|
||||||
if (habit == null)
|
|
||||||
{
|
|
||||||
tvName.setText(null);
|
|
||||||
return view;
|
|
||||||
}
|
|
||||||
|
|
||||||
LinearLayout llInner = (LinearLayout) view.findViewById(R.id.llInner);
|
|
||||||
llInner.setTag(R.string.habit_key, habit.getId());
|
|
||||||
|
|
||||||
int activeColor = habit.color;
|
|
||||||
|
|
||||||
tvName.setText(habit.name);
|
|
||||||
tvName.setTextColor(activeColor);
|
|
||||||
|
|
||||||
if(habit.isArchived())
|
|
||||||
{
|
|
||||||
activeColor = ColorHelper.palette[12];
|
|
||||||
tvName.setTextColor(activeColor);
|
|
||||||
|
|
||||||
tvStar.setText(context.getString(R.string.fa_archive));
|
|
||||||
tvStar.setTextColor(activeColor);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
int score = scores.get(habit.getId());
|
|
||||||
|
|
||||||
if (score < Habit.HALF_STAR_CUTOFF)
|
|
||||||
{
|
|
||||||
tvStar.setText(context.getString(R.string.fa_star_o));
|
|
||||||
tvStar.setTextColor(INACTIVE_COLOR);
|
|
||||||
}
|
|
||||||
else if (score < Habit.FULL_STAR_CUTOFF)
|
|
||||||
{
|
|
||||||
tvStar.setText(context.getString(R.string.fa_star_half_o));
|
|
||||||
tvStar.setTextColor(INACTIVE_COLOR);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
tvStar.setText(context.getString(R.string.fa_star));
|
|
||||||
tvStar.setTextColor(activeColor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LinearLayout llButtons = (LinearLayout) view.findViewById(R.id.llButtons);
|
|
||||||
int m = llButtons.getChildCount();
|
|
||||||
|
|
||||||
int isChecked[] = checkmarks.get(habit.getId());
|
|
||||||
|
|
||||||
for (int i = 0; i < m; i++)
|
|
||||||
{
|
|
||||||
|
|
||||||
TextView tvCheck = (TextView) llButtons.getChildAt(i);
|
|
||||||
tvCheck.setTag(R.string.habit_key, habit.getId());
|
|
||||||
tvCheck.setTag(R.string.offset_key, i);
|
|
||||||
updateCheck(activeColor, tvCheck, isChecked[i]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return view;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateCheck(int activeColor, TextView tvCheck, int check)
|
|
||||||
{
|
|
||||||
switch (check)
|
|
||||||
{
|
|
||||||
case 2:
|
|
||||||
tvCheck.setText(R.string.fa_check);
|
|
||||||
tvCheck.setTextColor(activeColor);
|
|
||||||
tvCheck.setTag(R.string.toggle_key, 2);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 1:
|
|
||||||
tvCheck.setText(R.string.fa_check);
|
|
||||||
tvCheck.setTextColor(INACTIVE_COLOR);
|
|
||||||
tvCheck.setTag(R.string.toggle_key, 1);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 0:
|
|
||||||
tvCheck.setText(R.string.fa_times);
|
|
||||||
tvCheck.setTextColor(INACTIVE_COLOR);
|
|
||||||
tvCheck.setTag(R.string.toggle_key, 0);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onPostExecuteCommand(Long refreshKey)
|
|
||||||
{
|
|
||||||
if(refreshKey == null) fetchAllHabits();
|
|
||||||
else fetchHabit(refreshKey);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,139 +0,0 @@
|
|||||||
/* Copyright (C) 2016 Alinson Santos Xavier
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify it
|
|
||||||
* under the terms of the GNU General Public License as published by the Free
|
|
||||||
* Software Foundation, either version 3 of the License, or (at your option)
|
|
||||||
* any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
|
||||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
||||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
|
||||||
* more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.isoron.uhabits.dialogs;
|
|
||||||
|
|
||||||
import android.app.Fragment;
|
|
||||||
import android.graphics.Color;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.util.Log;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.Menu;
|
|
||||||
import android.view.MenuInflater;
|
|
||||||
import android.view.MenuItem;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.LinearLayout;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import org.isoron.helpers.ColorHelper;
|
|
||||||
import org.isoron.helpers.Command;
|
|
||||||
import org.isoron.helpers.DialogHelper;
|
|
||||||
import org.isoron.uhabits.R;
|
|
||||||
import org.isoron.uhabits.ReminderHelper;
|
|
||||||
import org.isoron.uhabits.ShowHabitActivity;
|
|
||||||
import org.isoron.uhabits.models.Habit;
|
|
||||||
import org.isoron.uhabits.views.HabitHistoryView;
|
|
||||||
import org.isoron.uhabits.views.HabitScoreView;
|
|
||||||
import org.isoron.uhabits.views.HabitStreakView;
|
|
||||||
import org.isoron.uhabits.views.RingView;
|
|
||||||
|
|
||||||
public class ShowHabitFragment extends Fragment implements DialogHelper.OnSavedListener
|
|
||||||
{
|
|
||||||
protected ShowHabitActivity activity;
|
|
||||||
private Habit habit;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onStart()
|
|
||||||
{
|
|
||||||
super.onStart();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
|
||||||
Bundle savedInstanceState)
|
|
||||||
{
|
|
||||||
Log.d("ShowHabitActivity", "Creating view...");
|
|
||||||
|
|
||||||
View view = inflater.inflate(R.layout.show_habit, container, false);
|
|
||||||
activity = (ShowHabitActivity) getActivity();
|
|
||||||
habit = activity.habit;
|
|
||||||
|
|
||||||
habit.updateCheckmarks();
|
|
||||||
|
|
||||||
if (android.os.Build.VERSION.SDK_INT >= 21)
|
|
||||||
{
|
|
||||||
int darkerHabitColor = ColorHelper.mixColors(habit.color, Color.BLACK, 0.75f);
|
|
||||||
activity.getWindow().setStatusBarColor(darkerHabitColor);
|
|
||||||
}
|
|
||||||
|
|
||||||
TextView tvHistory = (TextView) view.findViewById(R.id.tvHistory);
|
|
||||||
TextView tvOverview = (TextView) view.findViewById(R.id.tvOverview);
|
|
||||||
TextView tvStrength = (TextView) view.findViewById(R.id.tvStrength);
|
|
||||||
TextView tvStreaks = (TextView) view.findViewById(R.id.tvStreaks);
|
|
||||||
tvHistory.setTextColor(habit.color);
|
|
||||||
tvOverview.setTextColor(habit.color);
|
|
||||||
tvStrength.setTextColor(habit.color);
|
|
||||||
tvStreaks.setTextColor(habit.color);
|
|
||||||
|
|
||||||
LinearLayout llOverview = (LinearLayout) view.findViewById(R.id.llOverview);
|
|
||||||
llOverview.addView(new RingView(activity, (int) activity.getResources().getDimension(
|
|
||||||
R.dimen.small_square_size) * 4, habit.color,
|
|
||||||
((float) habit.getScore() / Habit.MAX_SCORE), "Habit strength"));
|
|
||||||
|
|
||||||
LinearLayout llStrength = (LinearLayout) view.findViewById(R.id.llStrength);
|
|
||||||
llStrength.addView(new HabitScoreView(activity, habit,
|
|
||||||
(int) activity.getResources().getDimension(R.dimen.small_square_size)));
|
|
||||||
|
|
||||||
LinearLayout llHistory = (LinearLayout) view.findViewById(R.id.llHistory);
|
|
||||||
HabitHistoryView hhv = new HabitHistoryView(activity, habit,
|
|
||||||
(int) activity.getResources().getDimension(R.dimen.small_square_size));
|
|
||||||
llHistory.addView(hhv);
|
|
||||||
|
|
||||||
LinearLayout llStreaks = (LinearLayout) view.findViewById(R.id.llStreaks);
|
|
||||||
HabitStreakView hsv = new HabitStreakView(activity, habit,
|
|
||||||
(int) activity.getResources().getDimension(R.dimen.small_square_size));
|
|
||||||
llStreaks.addView(hsv);
|
|
||||||
|
|
||||||
setHasOptionsMenu(true);
|
|
||||||
return view;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater)
|
|
||||||
{
|
|
||||||
inflater.inflate(R.menu.show_habit_fragment_menu, menu);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onOptionsItemSelected(MenuItem item)
|
|
||||||
{
|
|
||||||
switch (item.getItemId())
|
|
||||||
{
|
|
||||||
case R.id.action_edit_habit:
|
|
||||||
{
|
|
||||||
EditHabitFragment frag = EditHabitFragment.editSingleHabitFragment(habit.getId());
|
|
||||||
frag.setOnSavedListener(this);
|
|
||||||
frag.show(getFragmentManager(), "dialog");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onSaved(Command command, Object savedObject)
|
|
||||||
{
|
|
||||||
Habit h = (Habit) savedObject;
|
|
||||||
|
|
||||||
if(h == null) activity.executeCommand(command, null);
|
|
||||||
else activity.executeCommand(command, h.getId());
|
|
||||||
|
|
||||||
ReminderHelper.createReminderAlarms(activity);
|
|
||||||
activity.recreate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||||
|
*
|
||||||
|
* This file is part of Loop Habit Tracker.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by the
|
||||||
|
* Free Software Foundation, either version 3 of the License, or (at your
|
||||||
|
* option) any later version.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
|
* more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along
|
||||||
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.isoron.uhabits.dialogs;
|
||||||
|
|
||||||
|
import android.app.AlertDialog;
|
||||||
|
import android.app.Dialog;
|
||||||
|
import android.app.DialogFragment;
|
||||||
|
import android.content.DialogInterface;
|
||||||
|
import android.os.Bundle;
|
||||||
|
|
||||||
|
import org.isoron.helpers.DateHelper;
|
||||||
|
import org.isoron.uhabits.R;
|
||||||
|
|
||||||
|
public class WeekdayPickerDialog extends DialogFragment
|
||||||
|
implements DialogInterface.OnMultiChoiceClickListener, DialogInterface.OnClickListener
|
||||||
|
{
|
||||||
|
|
||||||
|
public interface OnWeekdaysPickedListener
|
||||||
|
{
|
||||||
|
void onWeekdaysPicked(boolean[] selectedDays);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean[] selectedDays;
|
||||||
|
private OnWeekdaysPickedListener listener;
|
||||||
|
|
||||||
|
public void setListener(OnWeekdaysPickedListener listener)
|
||||||
|
{
|
||||||
|
this.listener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSelectedDays(boolean[] selectedDays)
|
||||||
|
{
|
||||||
|
this.selectedDays = selectedDays;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Dialog onCreateDialog(Bundle savedInstanceState)
|
||||||
|
{
|
||||||
|
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
|
||||||
|
builder.setTitle(R.string.select_weekdays)
|
||||||
|
.setMultiChoiceItems(DateHelper.getLongDayNames(), selectedDays, this)
|
||||||
|
.setPositiveButton(android.R.string.yes, this)
|
||||||
|
.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener()
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
public void onClick(DialogInterface dialog, int which)
|
||||||
|
{
|
||||||
|
dismiss();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return builder.create();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onClick(DialogInterface dialog, int which, boolean isChecked)
|
||||||
|
{
|
||||||
|
selectedDays[which] = isChecked;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onClick(DialogInterface dialog, int which)
|
||||||
|
{
|
||||||
|
if(listener != null) listener.onWeekdaysPicked(selectedDays);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,372 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||||
|
*
|
||||||
|
* This file is part of Loop Habit Tracker.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by the
|
||||||
|
* Free Software Foundation, either version 3 of the License, or (at your
|
||||||
|
* option) any later version.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
|
* more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along
|
||||||
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.isoron.uhabits.fragments;
|
||||||
|
|
||||||
|
import android.app.DialogFragment;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
import android.graphics.Color;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.preference.PreferenceManager;
|
||||||
|
import android.text.format.DateFormat;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.View.OnClickListener;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.Button;
|
||||||
|
import android.widget.ImageButton;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import com.android.colorpicker.ColorPickerDialog;
|
||||||
|
import com.android.colorpicker.ColorPickerSwatch;
|
||||||
|
import com.android.datetimepicker.time.RadialPickerLayout;
|
||||||
|
import com.android.datetimepicker.time.TimePickerDialog;
|
||||||
|
|
||||||
|
import org.isoron.helpers.ColorHelper;
|
||||||
|
import org.isoron.helpers.Command;
|
||||||
|
import org.isoron.helpers.DateHelper;
|
||||||
|
import org.isoron.helpers.DialogHelper.OnSavedListener;
|
||||||
|
import org.isoron.uhabits.R;
|
||||||
|
import org.isoron.uhabits.commands.CreateHabitCommand;
|
||||||
|
import org.isoron.uhabits.commands.EditHabitCommand;
|
||||||
|
import org.isoron.uhabits.dialogs.WeekdayPickerDialog;
|
||||||
|
import org.isoron.uhabits.models.Habit;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
public class EditHabitFragment extends DialogFragment
|
||||||
|
implements OnClickListener, WeekdayPickerDialog.OnWeekdaysPickedListener,
|
||||||
|
TimePickerDialog.OnTimeSetListener
|
||||||
|
{
|
||||||
|
private Integer mode;
|
||||||
|
static final int EDIT_MODE = 0;
|
||||||
|
static final int CREATE_MODE = 1;
|
||||||
|
|
||||||
|
private OnSavedListener onSavedListener;
|
||||||
|
|
||||||
|
private Habit originalHabit;
|
||||||
|
private Habit modifiedHabit;
|
||||||
|
|
||||||
|
private TextView tvName;
|
||||||
|
private TextView tvDescription;
|
||||||
|
private TextView tvFreqNum;
|
||||||
|
private TextView tvFreqDen;
|
||||||
|
private TextView tvReminderTime;
|
||||||
|
private TextView tvReminderDays;
|
||||||
|
|
||||||
|
private SharedPreferences prefs;
|
||||||
|
private boolean is24HourMode;
|
||||||
|
|
||||||
|
public static EditHabitFragment editSingleHabitFragment(long id)
|
||||||
|
{
|
||||||
|
EditHabitFragment frag = new EditHabitFragment();
|
||||||
|
Bundle args = new Bundle();
|
||||||
|
args.putLong("habitId", id);
|
||||||
|
args.putInt("editMode", EDIT_MODE);
|
||||||
|
frag.setArguments(args);
|
||||||
|
return frag;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static EditHabitFragment createHabitFragment()
|
||||||
|
{
|
||||||
|
EditHabitFragment frag = new EditHabitFragment();
|
||||||
|
Bundle args = new Bundle();
|
||||||
|
args.putInt("editMode", CREATE_MODE);
|
||||||
|
frag.setArguments(args);
|
||||||
|
return frag;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||||
|
Bundle savedInstanceState)
|
||||||
|
{
|
||||||
|
View view = inflater.inflate(R.layout.edit_habit, container, false);
|
||||||
|
tvName = (TextView) view.findViewById(R.id.input_name);
|
||||||
|
tvDescription = (TextView) view.findViewById(R.id.input_description);
|
||||||
|
tvFreqNum = (TextView) view.findViewById(R.id.input_freq_num);
|
||||||
|
tvFreqDen = (TextView) view.findViewById(R.id.input_freq_den);
|
||||||
|
tvReminderTime = (TextView) view.findViewById(R.id.inputReminderTime);
|
||||||
|
tvReminderDays = (TextView) view.findViewById(R.id.inputReminderDays);
|
||||||
|
|
||||||
|
Button buttonSave = (Button) view.findViewById(R.id.buttonSave);
|
||||||
|
Button buttonDiscard = (Button) view.findViewById(R.id.buttonDiscard);
|
||||||
|
ImageButton buttonPickColor = (ImageButton) view.findViewById(R.id.buttonPickColor);
|
||||||
|
|
||||||
|
buttonSave.setOnClickListener(this);
|
||||||
|
buttonDiscard.setOnClickListener(this);
|
||||||
|
tvReminderTime.setOnClickListener(this);
|
||||||
|
tvReminderDays.setOnClickListener(this);
|
||||||
|
buttonPickColor.setOnClickListener(this);
|
||||||
|
|
||||||
|
prefs = PreferenceManager.getDefaultSharedPreferences(getActivity());
|
||||||
|
|
||||||
|
Bundle args = getArguments();
|
||||||
|
mode = (Integer) args.get("editMode");
|
||||||
|
|
||||||
|
is24HourMode = DateFormat.is24HourFormat(getActivity());
|
||||||
|
|
||||||
|
if (mode == CREATE_MODE)
|
||||||
|
{
|
||||||
|
getDialog().setTitle(R.string.create_habit);
|
||||||
|
modifiedHabit = new Habit();
|
||||||
|
|
||||||
|
int defaultNum = prefs.getInt("pref_default_habit_freq_num", modifiedHabit.freqNum);
|
||||||
|
int defaultDen = prefs.getInt("pref_default_habit_freq_den", modifiedHabit.freqDen);
|
||||||
|
int defaultColor = prefs.getInt("pref_default_habit_color", modifiedHabit.color);
|
||||||
|
|
||||||
|
modifiedHabit.color = defaultColor;
|
||||||
|
modifiedHabit.freqNum = defaultNum;
|
||||||
|
modifiedHabit.freqDen = defaultDen;
|
||||||
|
}
|
||||||
|
else if (mode == EDIT_MODE)
|
||||||
|
{
|
||||||
|
originalHabit = Habit.get((Long) args.get("habitId"));
|
||||||
|
modifiedHabit = new Habit(originalHabit);
|
||||||
|
|
||||||
|
getDialog().setTitle(R.string.edit_habit);
|
||||||
|
tvName.append(modifiedHabit.name);
|
||||||
|
tvDescription.append(modifiedHabit.description);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(savedInstanceState != null)
|
||||||
|
{
|
||||||
|
modifiedHabit.color = savedInstanceState.getInt("color", modifiedHabit.color);
|
||||||
|
modifiedHabit.reminderMin = savedInstanceState.getInt("reminderMin", -1);
|
||||||
|
modifiedHabit.reminderHour = savedInstanceState.getInt("reminderHour", -1);
|
||||||
|
modifiedHabit.reminderDays = savedInstanceState.getInt("reminderDays", -1);
|
||||||
|
|
||||||
|
if(modifiedHabit.reminderMin < 0)
|
||||||
|
{
|
||||||
|
modifiedHabit.reminderMin = null;
|
||||||
|
modifiedHabit.reminderHour = null;
|
||||||
|
modifiedHabit.reminderDays = 127;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tvFreqNum.append(modifiedHabit.freqNum.toString());
|
||||||
|
tvFreqDen.append(modifiedHabit.freqDen.toString());
|
||||||
|
|
||||||
|
changeColor(modifiedHabit.color);
|
||||||
|
updateReminder();
|
||||||
|
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void changeColor(Integer color)
|
||||||
|
{
|
||||||
|
modifiedHabit.color = color;
|
||||||
|
tvName.setTextColor(color);
|
||||||
|
|
||||||
|
SharedPreferences.Editor editor = prefs.edit();
|
||||||
|
editor.putInt("pref_default_habit_color", color);
|
||||||
|
editor.apply();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateReminder()
|
||||||
|
{
|
||||||
|
if (modifiedHabit.reminderHour != null)
|
||||||
|
{
|
||||||
|
tvReminderTime.setTextColor(Color.BLACK);
|
||||||
|
tvReminderTime.setText(DateHelper.formatTime(getActivity(), modifiedHabit.reminderHour,
|
||||||
|
modifiedHabit.reminderMin));
|
||||||
|
tvReminderDays.setVisibility(View.VISIBLE);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
tvReminderTime.setTextColor(Color.GRAY);
|
||||||
|
tvReminderTime.setText(R.string.reminder_off);
|
||||||
|
tvReminderDays.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean weekdays[] = DateHelper.unpackWeekdayList(modifiedHabit.reminderDays);
|
||||||
|
tvReminderDays.setText(DateHelper.formatWeekdayList(getActivity(), weekdays));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOnSavedListener(OnSavedListener onSavedListener)
|
||||||
|
{
|
||||||
|
this.onSavedListener = onSavedListener;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onClick(View v)
|
||||||
|
{
|
||||||
|
switch(v.getId())
|
||||||
|
{
|
||||||
|
case R.id.inputReminderTime:
|
||||||
|
onDateSpinnerClick();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case R.id.inputReminderDays:
|
||||||
|
onWeekdayClick();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case R.id.buttonSave:
|
||||||
|
onSaveButtonClick();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case R.id.buttonDiscard:
|
||||||
|
dismiss();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case R.id.buttonPickColor:
|
||||||
|
onColorButtonClick();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onColorButtonClick()
|
||||||
|
{
|
||||||
|
ColorPickerDialog picker = ColorPickerDialog.newInstance(
|
||||||
|
R.string.color_picker_default_title, ColorHelper.palette, modifiedHabit.color, 4,
|
||||||
|
ColorPickerDialog.SIZE_SMALL);
|
||||||
|
|
||||||
|
picker.setOnColorSelectedListener(new ColorPickerSwatch.OnColorSelectedListener()
|
||||||
|
{
|
||||||
|
public void onColorSelected(int color)
|
||||||
|
{
|
||||||
|
changeColor(color);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
picker.show(getFragmentManager(), "picker");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onSaveButtonClick()
|
||||||
|
{
|
||||||
|
modifiedHabit.name = tvName.getText().toString().trim();
|
||||||
|
modifiedHabit.description = tvDescription.getText().toString().trim();
|
||||||
|
modifiedHabit.freqNum = Integer.parseInt(tvFreqNum.getText().toString());
|
||||||
|
modifiedHabit.freqDen = Integer.parseInt(tvFreqDen.getText().toString());
|
||||||
|
|
||||||
|
if (!validate()) return;
|
||||||
|
|
||||||
|
SharedPreferences.Editor editor = prefs.edit();
|
||||||
|
editor.putInt("pref_default_habit_freq_num", modifiedHabit.freqNum);
|
||||||
|
editor.putInt("pref_default_habit_freq_den", modifiedHabit.freqDen);
|
||||||
|
editor.apply();
|
||||||
|
|
||||||
|
Command command = null;
|
||||||
|
Habit savedHabit = null;
|
||||||
|
|
||||||
|
if (mode == EDIT_MODE)
|
||||||
|
{
|
||||||
|
command = new EditHabitCommand(originalHabit, modifiedHabit);
|
||||||
|
savedHabit = originalHabit;
|
||||||
|
}
|
||||||
|
else if (mode == CREATE_MODE)
|
||||||
|
{
|
||||||
|
command = new CreateHabitCommand(modifiedHabit);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (onSavedListener != null) onSavedListener.onSaved(command, savedHabit);
|
||||||
|
|
||||||
|
dismiss();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean validate()
|
||||||
|
{
|
||||||
|
Boolean valid = true;
|
||||||
|
|
||||||
|
if (modifiedHabit.name.length() == 0)
|
||||||
|
{
|
||||||
|
tvName.setError(getString(R.string.validation_name_should_not_be_blank));
|
||||||
|
valid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (modifiedHabit.freqNum <= 0)
|
||||||
|
{
|
||||||
|
tvFreqNum.setError(getString(R.string.validation_number_should_be_positive));
|
||||||
|
valid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (modifiedHabit.freqNum > modifiedHabit.freqDen)
|
||||||
|
{
|
||||||
|
tvFreqNum.setError(getString(R.string.validation_at_most_one_rep_per_day));
|
||||||
|
valid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return valid;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onDateSpinnerClick()
|
||||||
|
{
|
||||||
|
int defaultHour = 8;
|
||||||
|
int defaultMin = 0;
|
||||||
|
|
||||||
|
if (modifiedHabit.reminderHour != null)
|
||||||
|
{
|
||||||
|
defaultHour = modifiedHabit.reminderHour;
|
||||||
|
defaultMin = modifiedHabit.reminderMin;
|
||||||
|
}
|
||||||
|
|
||||||
|
TimePickerDialog timePicker =
|
||||||
|
TimePickerDialog.newInstance(this, defaultHour, defaultMin, is24HourMode);
|
||||||
|
timePicker.show(getFragmentManager(), "timePicker");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onWeekdayClick()
|
||||||
|
{
|
||||||
|
WeekdayPickerDialog dialog = new WeekdayPickerDialog();
|
||||||
|
dialog.setListener(this);
|
||||||
|
dialog.setSelectedDays(DateHelper.unpackWeekdayList(modifiedHabit.reminderDays));
|
||||||
|
dialog.show(getFragmentManager(), "weekdayPicker");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onTimeSet(RadialPickerLayout view, int hour, int minute)
|
||||||
|
{
|
||||||
|
modifiedHabit.reminderHour = hour;
|
||||||
|
modifiedHabit.reminderMin = minute;
|
||||||
|
updateReminder();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onTimeCleared(RadialPickerLayout view)
|
||||||
|
{
|
||||||
|
modifiedHabit.reminderHour = null;
|
||||||
|
modifiedHabit.reminderMin = null;
|
||||||
|
updateReminder();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onWeekdaysPicked(boolean[] selectedDays)
|
||||||
|
{
|
||||||
|
int count = 0;
|
||||||
|
for(int i = 0; i < 7; i++)
|
||||||
|
if(selectedDays[i]) count++;
|
||||||
|
|
||||||
|
if(count == 0) Arrays.fill(selectedDays, true);
|
||||||
|
|
||||||
|
modifiedHabit.reminderDays = DateHelper.packWeekdayList(selectedDays);
|
||||||
|
updateReminder();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSaveInstanceState(Bundle outState)
|
||||||
|
{
|
||||||
|
super.onSaveInstanceState(outState);
|
||||||
|
|
||||||
|
outState.putInt("color", modifiedHabit.color);
|
||||||
|
if(modifiedHabit.reminderHour != null)
|
||||||
|
{
|
||||||
|
outState.putInt("reminderMin", modifiedHabit.reminderMin);
|
||||||
|
outState.putInt("reminderHour", modifiedHabit.reminderHour);
|
||||||
|
outState.putInt("reminderDays", modifiedHabit.reminderDays);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||||
|
*
|
||||||
|
* This file is part of Loop Habit Tracker.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by the
|
||||||
|
* Free Software Foundation, either version 3 of the License, or (at your
|
||||||
|
* option) any later version.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
|
* more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along
|
||||||
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.isoron.uhabits.fragments;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.BaseAdapter;
|
||||||
|
import android.widget.LinearLayout;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import org.isoron.helpers.DateHelper;
|
||||||
|
import org.isoron.uhabits.R;
|
||||||
|
import org.isoron.uhabits.helpers.ListHabitsHelper;
|
||||||
|
import org.isoron.uhabits.loaders.HabitListLoader;
|
||||||
|
import org.isoron.uhabits.models.Habit;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
class HabitListAdapter extends BaseAdapter
|
||||||
|
{
|
||||||
|
private LayoutInflater inflater;
|
||||||
|
private HabitListLoader loader;
|
||||||
|
private ListHabitsHelper helper;
|
||||||
|
private List selectedPositions;
|
||||||
|
private View.OnLongClickListener onCheckmarkLongClickListener;
|
||||||
|
private View.OnClickListener onCheckmarkClickListener;
|
||||||
|
|
||||||
|
public HabitListAdapter(Context context, HabitListLoader loader)
|
||||||
|
{
|
||||||
|
this.loader = loader;
|
||||||
|
|
||||||
|
inflater = LayoutInflater.from(context);
|
||||||
|
helper = new ListHabitsHelper(context, loader);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getCount()
|
||||||
|
{
|
||||||
|
return loader.habits.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Habit getItem(int position)
|
||||||
|
{
|
||||||
|
return loader.habitsList.get(position);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getItemId(int position)
|
||||||
|
{
|
||||||
|
return (getItem(position)).getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public View getView(int position, View view, ViewGroup parent)
|
||||||
|
{
|
||||||
|
final Habit habit = loader.habitsList.get(position);
|
||||||
|
|
||||||
|
if (view == null || (Long) view.getTag(R.id.timestamp_key) != DateHelper.getStartOfToday())
|
||||||
|
{
|
||||||
|
view = inflater.inflate(R.layout.list_habits_item, null);
|
||||||
|
helper.initializeLabelAndIcon(view);
|
||||||
|
helper.inflateCheckmarkButtons(view, onCheckmarkLongClickListener,
|
||||||
|
onCheckmarkClickListener, inflater);
|
||||||
|
}
|
||||||
|
|
||||||
|
TextView tvStar = ((TextView) view.findViewById(R.id.tvStar));
|
||||||
|
TextView tvName = (TextView) view.findViewById(R.id.label);
|
||||||
|
LinearLayout llInner = (LinearLayout) view.findViewById(R.id.llInner);
|
||||||
|
LinearLayout llButtons = (LinearLayout) view.findViewById(R.id.llButtons);
|
||||||
|
|
||||||
|
llInner.setTag(R.string.habit_key, habit.getId());
|
||||||
|
|
||||||
|
helper.updateNameAndIcon(habit, tvStar, tvName);
|
||||||
|
helper.updateCheckmarkButtons(habit, llButtons);
|
||||||
|
|
||||||
|
boolean selected = selectedPositions.contains(position);
|
||||||
|
helper.updateHabitBackground(llInner, selected);
|
||||||
|
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSelectedPositions(List selectedPositions)
|
||||||
|
{
|
||||||
|
this.selectedPositions = selectedPositions;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOnCheckmarkLongClickListener(View.OnLongClickListener listener)
|
||||||
|
{
|
||||||
|
this.onCheckmarkLongClickListener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOnCheckmarkClickListener(View.OnClickListener listener)
|
||||||
|
{
|
||||||
|
this.onCheckmarkClickListener = listener;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,418 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||||
|
*
|
||||||
|
* This file is part of Loop Habit Tracker.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by the
|
||||||
|
* Free Software Foundation, either version 3 of the License, or (at your
|
||||||
|
* option) any later version.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
|
* more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along
|
||||||
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.isoron.uhabits.fragments;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.app.Fragment;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.preference.PreferenceManager;
|
||||||
|
import android.view.ActionMode;
|
||||||
|
import android.view.ContextMenu;
|
||||||
|
import android.view.ContextMenu.ContextMenuInfo;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.Menu;
|
||||||
|
import android.view.MenuInflater;
|
||||||
|
import android.view.MenuItem;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.View.OnClickListener;
|
||||||
|
import android.view.View.OnLongClickListener;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.AdapterView;
|
||||||
|
import android.widget.AdapterView.AdapterContextMenuInfo;
|
||||||
|
import android.widget.AdapterView.OnItemClickListener;
|
||||||
|
import android.widget.LinearLayout;
|
||||||
|
import android.widget.ProgressBar;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import com.mobeta.android.dslv.DragSortController;
|
||||||
|
import com.mobeta.android.dslv.DragSortListView;
|
||||||
|
import com.mobeta.android.dslv.DragSortListView.DropListener;
|
||||||
|
|
||||||
|
import org.isoron.helpers.Command;
|
||||||
|
import org.isoron.helpers.DateHelper;
|
||||||
|
import org.isoron.helpers.DialogHelper;
|
||||||
|
import org.isoron.helpers.DialogHelper.OnSavedListener;
|
||||||
|
import org.isoron.helpers.ReplayableActivity;
|
||||||
|
import org.isoron.uhabits.R;
|
||||||
|
import org.isoron.uhabits.commands.ToggleRepetitionCommand;
|
||||||
|
import org.isoron.uhabits.dialogs.HabitSelectionCallback;
|
||||||
|
import org.isoron.uhabits.dialogs.HintManager;
|
||||||
|
import org.isoron.uhabits.helpers.ListHabitsHelper;
|
||||||
|
import org.isoron.uhabits.helpers.ReminderHelper;
|
||||||
|
import org.isoron.uhabits.loaders.HabitListLoader;
|
||||||
|
import org.isoron.uhabits.models.Habit;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class ListHabitsFragment extends Fragment
|
||||||
|
implements OnSavedListener, OnItemClickListener, OnLongClickListener, DropListener,
|
||||||
|
OnClickListener, HabitListLoader.Listener, AdapterView.OnItemLongClickListener,
|
||||||
|
HabitSelectionCallback.Listener
|
||||||
|
{
|
||||||
|
long lastLongClick = 0;
|
||||||
|
private boolean isShortToggleEnabled;
|
||||||
|
private boolean showArchived;
|
||||||
|
|
||||||
|
private ActionMode actionMode;
|
||||||
|
private HabitListAdapter adapter;
|
||||||
|
private HabitListLoader loader;
|
||||||
|
private HintManager hintManager;
|
||||||
|
private ListHabitsHelper helper;
|
||||||
|
private List<Integer> selectedPositions;
|
||||||
|
private OnHabitClickListener habitClickListener;
|
||||||
|
private ReplayableActivity activity;
|
||||||
|
private SharedPreferences prefs;
|
||||||
|
|
||||||
|
private DragSortListView listView;
|
||||||
|
private LinearLayout llButtonsHeader;
|
||||||
|
private ProgressBar progressBar;
|
||||||
|
private View llEmpty;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||||
|
Bundle savedInstanceState)
|
||||||
|
{
|
||||||
|
View view = inflater.inflate(R.layout.list_habits_fragment, container, false);
|
||||||
|
View llHint = view.findViewById(R.id.llHint);
|
||||||
|
TextView tvStarEmpty = (TextView) view.findViewById(R.id.tvStarEmpty);
|
||||||
|
listView = (DragSortListView) view.findViewById(R.id.listView);
|
||||||
|
llButtonsHeader = (LinearLayout) view.findViewById(R.id.llButtonsHeader);
|
||||||
|
llEmpty = view.findViewById(R.id.llEmpty);
|
||||||
|
progressBar = (ProgressBar) view.findViewById(R.id.progressBar);
|
||||||
|
|
||||||
|
selectedPositions = new LinkedList<>();
|
||||||
|
loader = new HabitListLoader();
|
||||||
|
helper = new ListHabitsHelper(activity, loader);
|
||||||
|
hintManager = new HintManager(activity, llHint);
|
||||||
|
|
||||||
|
loader.setListener(this);
|
||||||
|
loader.setCheckmarkCount(helper.getButtonCount());
|
||||||
|
loader.setProgressBar(progressBar);
|
||||||
|
|
||||||
|
llHint.setOnClickListener(this);
|
||||||
|
tvStarEmpty.setTypeface(helper.getFontawesome());
|
||||||
|
|
||||||
|
adapter = new HabitListAdapter(getActivity(), loader);
|
||||||
|
adapter.setSelectedPositions(selectedPositions);
|
||||||
|
adapter.setOnCheckmarkClickListener(this);
|
||||||
|
adapter.setOnCheckmarkLongClickListener(this);
|
||||||
|
|
||||||
|
DragSortListView.DragListener dragListener = new HabitsDragListener();
|
||||||
|
DragSortController dragSortController = new HabitsDragSortController();
|
||||||
|
|
||||||
|
listView.setAdapter(adapter);
|
||||||
|
listView.setOnItemClickListener(this);
|
||||||
|
listView.setOnItemLongClickListener(this);
|
||||||
|
listView.setDropListener(this);
|
||||||
|
listView.setDragListener(dragListener);
|
||||||
|
listView.setFloatViewManager(dragSortController);
|
||||||
|
listView.setDragEnabled(true);
|
||||||
|
listView.setLongClickable(true);
|
||||||
|
|
||||||
|
if(savedInstanceState != null)
|
||||||
|
{
|
||||||
|
EditHabitFragment frag = (EditHabitFragment) getFragmentManager()
|
||||||
|
.findFragmentByTag("editHabit");
|
||||||
|
if(frag != null) frag.setOnSavedListener(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
loader.updateAllHabits(true);
|
||||||
|
|
||||||
|
setHasOptionsMenu(true);
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
|
public void onAttach(Activity activity)
|
||||||
|
{
|
||||||
|
super.onAttach(activity);
|
||||||
|
this.activity = (ReplayableActivity) activity;
|
||||||
|
|
||||||
|
habitClickListener = (OnHabitClickListener) activity;
|
||||||
|
prefs = PreferenceManager.getDefaultSharedPreferences(activity);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onResume()
|
||||||
|
{
|
||||||
|
super.onResume();
|
||||||
|
Long timestamp = loader.getLastLoadTimestamp();
|
||||||
|
|
||||||
|
if (timestamp != null && timestamp != DateHelper.getStartOfToday())
|
||||||
|
loader.updateAllHabits(true);
|
||||||
|
|
||||||
|
helper.updateEmptyMessage(llEmpty);
|
||||||
|
helper.updateHeader(llButtonsHeader);
|
||||||
|
hintManager.showHintIfAppropriate();
|
||||||
|
|
||||||
|
adapter.notifyDataSetChanged();
|
||||||
|
isShortToggleEnabled = prefs.getBoolean("pref_short_toggle", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onLoadFinished()
|
||||||
|
{
|
||||||
|
adapter.notifyDataSetChanged();
|
||||||
|
helper.updateEmptyMessage(llEmpty);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater)
|
||||||
|
{
|
||||||
|
super.onCreateOptionsMenu(menu, inflater);
|
||||||
|
inflater.inflate(R.menu.list_habits_options, menu);
|
||||||
|
|
||||||
|
MenuItem showArchivedItem = menu.findItem(R.id.action_show_archived);
|
||||||
|
showArchivedItem.setChecked(showArchived);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfo)
|
||||||
|
{
|
||||||
|
super.onCreateContextMenu(menu, view, menuInfo);
|
||||||
|
getActivity().getMenuInflater().inflate(R.menu.list_habits_context, menu);
|
||||||
|
|
||||||
|
AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
|
||||||
|
final Habit habit = loader.habits.get(info.id);
|
||||||
|
|
||||||
|
if (habit.isArchived()) menu.findItem(R.id.action_archive_habit).setVisible(false);
|
||||||
|
else menu.findItem(R.id.action_unarchive_habit).setVisible(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onOptionsItemSelected(MenuItem item)
|
||||||
|
{
|
||||||
|
switch (item.getItemId())
|
||||||
|
{
|
||||||
|
case R.id.action_add:
|
||||||
|
{
|
||||||
|
EditHabitFragment frag = EditHabitFragment.createHabitFragment();
|
||||||
|
frag.setOnSavedListener(this);
|
||||||
|
frag.show(getFragmentManager(), "editHabit");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
case R.id.action_show_archived:
|
||||||
|
{
|
||||||
|
showArchived = !showArchived;
|
||||||
|
loader.setIncludeArchived(showArchived);
|
||||||
|
loader.updateAllHabits(true);
|
||||||
|
activity.invalidateOptionsMenu();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return super.onOptionsItemSelected(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onItemClick(AdapterView parent, View view, int position, long id)
|
||||||
|
{
|
||||||
|
if (new Date().getTime() - lastLongClick < 1000) return;
|
||||||
|
|
||||||
|
if(actionMode == null)
|
||||||
|
{
|
||||||
|
Habit habit = loader.habitsList.get(position);
|
||||||
|
habitClickListener.onHabitClicked(habit);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
int k = selectedPositions.indexOf(position);
|
||||||
|
if(k < 0)
|
||||||
|
selectedPositions.add(position);
|
||||||
|
else
|
||||||
|
selectedPositions.remove(k);
|
||||||
|
|
||||||
|
if(selectedPositions.isEmpty()) actionMode.finish();
|
||||||
|
else actionMode.invalidate();
|
||||||
|
|
||||||
|
adapter.notifyDataSetChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id)
|
||||||
|
{
|
||||||
|
selectItem(position);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void selectItem(int position)
|
||||||
|
{
|
||||||
|
if(!selectedPositions.contains(position))
|
||||||
|
selectedPositions.add(position);
|
||||||
|
|
||||||
|
adapter.notifyDataSetChanged();
|
||||||
|
|
||||||
|
if(actionMode == null)
|
||||||
|
{
|
||||||
|
HabitSelectionCallback callback = new HabitSelectionCallback(activity, loader);
|
||||||
|
callback.setSelectedPositions(selectedPositions);
|
||||||
|
callback.setProgressBar(progressBar);
|
||||||
|
callback.setOnSavedListener(this);
|
||||||
|
callback.setListener(this);
|
||||||
|
|
||||||
|
actionMode = getActivity().startActionMode(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(actionMode != null) actionMode.invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSaved(Command command, Object savedObject)
|
||||||
|
{
|
||||||
|
Habit h = (Habit) savedObject;
|
||||||
|
|
||||||
|
if (h == null) activity.executeCommand(command, null);
|
||||||
|
else activity.executeCommand(command, h.getId());
|
||||||
|
adapter.notifyDataSetChanged();
|
||||||
|
|
||||||
|
ReminderHelper.createReminderAlarms(activity);
|
||||||
|
|
||||||
|
if(actionMode != null) actionMode.finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onLongClick(View v)
|
||||||
|
{
|
||||||
|
lastLongClick = new Date().getTime();
|
||||||
|
|
||||||
|
switch (v.getId())
|
||||||
|
{
|
||||||
|
case R.id.tvCheck:
|
||||||
|
onCheckmarkLongClick(v);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onCheckmarkLongClick(View v)
|
||||||
|
{
|
||||||
|
if (isShortToggleEnabled) return;
|
||||||
|
|
||||||
|
toggleCheck(v);
|
||||||
|
DialogHelper.vibrate(activity, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void toggleCheck(View v)
|
||||||
|
{
|
||||||
|
Long tag = (Long) v.getTag(R.string.habit_key);
|
||||||
|
Integer offset = (Integer) v.getTag(R.string.offset_key);
|
||||||
|
long timestamp = DateHelper.getStartOfDay(
|
||||||
|
DateHelper.getLocalTime() - offset * DateHelper.millisecondsInOneDay);
|
||||||
|
|
||||||
|
Habit habit = loader.habits.get(tag);
|
||||||
|
if(habit == null) return;
|
||||||
|
|
||||||
|
helper.toggleCheckmarkView(v, habit);
|
||||||
|
executeCommand(new ToggleRepetitionCommand(habit, timestamp), habit.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void executeCommand(Command c, Long refreshKey)
|
||||||
|
{
|
||||||
|
activity.executeCommand(c, refreshKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void drop(int from, int to)
|
||||||
|
{
|
||||||
|
if(from == to) return;
|
||||||
|
if(actionMode != null) actionMode.finish();
|
||||||
|
|
||||||
|
loader.reorder(from, to);
|
||||||
|
adapter.notifyDataSetChanged();
|
||||||
|
loader.updateAllHabits(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onClick(View v)
|
||||||
|
{
|
||||||
|
switch (v.getId())
|
||||||
|
{
|
||||||
|
case R.id.tvCheck:
|
||||||
|
if (isShortToggleEnabled) toggleCheck(v);
|
||||||
|
else activity.showToast(R.string.long_press_to_toggle);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case R.id.llHint:
|
||||||
|
hintManager.dismissHint();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onPostExecuteCommand(Long refreshKey)
|
||||||
|
{
|
||||||
|
if (refreshKey == null) loader.updateAllHabits(true);
|
||||||
|
else loader.updateHabit(refreshKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onActionModeDestroyed(ActionMode mode)
|
||||||
|
{
|
||||||
|
actionMode = null;
|
||||||
|
selectedPositions.clear();
|
||||||
|
adapter.notifyDataSetChanged();
|
||||||
|
listView.setDragEnabled(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface OnHabitClickListener
|
||||||
|
{
|
||||||
|
void onHabitClicked(Habit habit);
|
||||||
|
}
|
||||||
|
|
||||||
|
private class HabitsDragSortController extends DragSortController
|
||||||
|
{
|
||||||
|
public HabitsDragSortController()
|
||||||
|
{
|
||||||
|
super(ListHabitsFragment.this.listView);
|
||||||
|
setRemoveEnabled(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public View onCreateFloatView(int position)
|
||||||
|
{
|
||||||
|
return adapter.getView(position, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroyFloatView(View floatView)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class HabitsDragListener implements DragSortListView.DragListener
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
public void drag(int from, int to)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void startDrag(int position)
|
||||||
|
{
|
||||||
|
selectItem(position);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,20 +1,23 @@
|
|||||||
/* Copyright (C) 2016 Alinson Santos Xavier
|
/*
|
||||||
|
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify it
|
* This file is part of Loop Habit Tracker.
|
||||||
* under the terms of the GNU General Public License as published by the Free
|
|
||||||
* Software Foundation, either version 3 of the License, or (at your option)
|
|
||||||
* any later version.
|
|
||||||
*
|
*
|
||||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
* it under the terms of the GNU General Public License as published by the
|
||||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
* Free Software Foundation, either version 3 of the License, or (at your
|
||||||
|
* option) any later version.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
* more details.
|
* more details.
|
||||||
*
|
*
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License along
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.isoron.uhabits.dialogs;
|
package org.isoron.uhabits.fragments;
|
||||||
|
|
||||||
import android.app.backup.BackupManager;
|
import android.app.backup.BackupManager;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
@@ -23,7 +26,8 @@ import android.preference.PreferenceFragment;
|
|||||||
|
|
||||||
import org.isoron.uhabits.R;
|
import org.isoron.uhabits.R;
|
||||||
|
|
||||||
public class SettingsFragment extends PreferenceFragment implements SharedPreferences.OnSharedPreferenceChangeListener
|
public class SettingsFragment extends PreferenceFragment
|
||||||
|
implements SharedPreferences.OnSharedPreferenceChangeListener
|
||||||
{
|
{
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(Bundle savedInstanceState)
|
public void onCreate(Bundle savedInstanceState)
|
||||||
@@ -0,0 +1,195 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||||
|
*
|
||||||
|
* This file is part of Loop Habit Tracker.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by the
|
||||||
|
* Free Software Foundation, either version 3 of the License, or (at your
|
||||||
|
* option) any later version.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
|
* more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along
|
||||||
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.isoron.uhabits.fragments;
|
||||||
|
|
||||||
|
import android.app.Fragment;
|
||||||
|
import android.graphics.Color;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.Menu;
|
||||||
|
import android.view.MenuInflater;
|
||||||
|
import android.view.MenuItem;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.Button;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import org.isoron.helpers.ColorHelper;
|
||||||
|
import org.isoron.helpers.Command;
|
||||||
|
import org.isoron.helpers.DialogHelper;
|
||||||
|
import org.isoron.uhabits.HabitBroadcastReceiver;
|
||||||
|
import org.isoron.uhabits.R;
|
||||||
|
import org.isoron.uhabits.ShowHabitActivity;
|
||||||
|
import org.isoron.uhabits.dialogs.HistoryEditorDialog;
|
||||||
|
import org.isoron.uhabits.helpers.ReminderHelper;
|
||||||
|
import org.isoron.uhabits.models.Habit;
|
||||||
|
import org.isoron.uhabits.models.Score;
|
||||||
|
import org.isoron.uhabits.views.HabitHistoryView;
|
||||||
|
import org.isoron.uhabits.views.HabitFrequencyView;
|
||||||
|
import org.isoron.uhabits.views.HabitScoreView;
|
||||||
|
import org.isoron.uhabits.views.HabitStreakView;
|
||||||
|
import org.isoron.uhabits.views.RingView;
|
||||||
|
|
||||||
|
public class ShowHabitFragment extends Fragment
|
||||||
|
implements DialogHelper.OnSavedListener, HistoryEditorDialog.Listener
|
||||||
|
{
|
||||||
|
protected ShowHabitActivity activity;
|
||||||
|
private Habit habit;
|
||||||
|
private HabitStreakView streakView;
|
||||||
|
private HabitScoreView scoreView;
|
||||||
|
private HabitHistoryView historyView;
|
||||||
|
private HabitFrequencyView punchcardView;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onStart()
|
||||||
|
{
|
||||||
|
super.onStart();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||||
|
Bundle savedInstanceState)
|
||||||
|
{
|
||||||
|
View view = inflater.inflate(R.layout.show_habit, container, false);
|
||||||
|
activity = (ShowHabitActivity) getActivity();
|
||||||
|
habit = activity.habit;
|
||||||
|
|
||||||
|
habit.checkmarks.rebuild();
|
||||||
|
|
||||||
|
Button btEditHistory = (Button) view.findViewById(R.id.btEditHistory);
|
||||||
|
streakView = (HabitStreakView) view.findViewById(R.id.streakView);
|
||||||
|
scoreView = (HabitScoreView) view.findViewById(R.id.scoreView);
|
||||||
|
historyView = (HabitHistoryView) view.findViewById(R.id.historyView);
|
||||||
|
punchcardView = (HabitFrequencyView) view.findViewById(R.id.punchcardView);
|
||||||
|
|
||||||
|
updateHeaders(view);
|
||||||
|
updateScoreRing(view);
|
||||||
|
|
||||||
|
streakView.setHabit(habit);
|
||||||
|
scoreView.setHabit(habit);
|
||||||
|
historyView.setHabit(habit);
|
||||||
|
punchcardView.setHabit(habit);
|
||||||
|
|
||||||
|
btEditHistory.setOnClickListener(new View.OnClickListener()
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
public void onClick(View v)
|
||||||
|
{
|
||||||
|
HistoryEditorDialog frag = new HistoryEditorDialog();
|
||||||
|
frag.setHabit(habit);
|
||||||
|
frag.setListener(ShowHabitFragment.this);
|
||||||
|
frag.show(getFragmentManager(), "historyEditor");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if(savedInstanceState != null)
|
||||||
|
{
|
||||||
|
EditHabitFragment fragEdit = (EditHabitFragment) getFragmentManager()
|
||||||
|
.findFragmentByTag("editHabit");
|
||||||
|
HistoryEditorDialog fragEditor = (HistoryEditorDialog) getFragmentManager()
|
||||||
|
.findFragmentByTag("historyEditor");
|
||||||
|
|
||||||
|
if(fragEdit != null) fragEdit.setOnSavedListener(this);
|
||||||
|
if(fragEditor != null) fragEditor.setListener(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
setHasOptionsMenu(true);
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateScoreRing(View view)
|
||||||
|
{
|
||||||
|
RingView scoreRing = (RingView) view.findViewById(R.id.scoreRing);
|
||||||
|
scoreRing.setColor(habit.color);
|
||||||
|
scoreRing.setPercentage((float) habit.scores.getNewestValue() / Score.MAX_SCORE);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateHeaders(View view)
|
||||||
|
{
|
||||||
|
if (android.os.Build.VERSION.SDK_INT >= 21)
|
||||||
|
{
|
||||||
|
int darkerHabitColor = ColorHelper.mixColors(habit.color, Color.BLACK, 0.75f);
|
||||||
|
activity.getWindow().setStatusBarColor(darkerHabitColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateColor(view, R.id.tvHistory);
|
||||||
|
updateColor(view, R.id.tvOverview);
|
||||||
|
updateColor(view, R.id.tvStrength);
|
||||||
|
updateColor(view, R.id.tvStreaks);
|
||||||
|
updateColor(view, R.id.tvWeekdayFreq);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateColor(View view, int viewId)
|
||||||
|
{
|
||||||
|
TextView textView = (TextView) view.findViewById(viewId);
|
||||||
|
textView.setTextColor(habit.color);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater)
|
||||||
|
{
|
||||||
|
inflater.inflate(R.menu.show_habit_fragment_menu, menu);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onOptionsItemSelected(MenuItem item)
|
||||||
|
{
|
||||||
|
switch (item.getItemId())
|
||||||
|
{
|
||||||
|
case R.id.action_edit_habit:
|
||||||
|
{
|
||||||
|
EditHabitFragment frag = EditHabitFragment.editSingleHabitFragment(habit.getId());
|
||||||
|
frag.setOnSavedListener(this);
|
||||||
|
frag.show(getFragmentManager(), "editHabit");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSaved(Command command, Object savedObject)
|
||||||
|
{
|
||||||
|
Habit h = (Habit) savedObject;
|
||||||
|
|
||||||
|
if (h == null) activity.executeCommand(command, null);
|
||||||
|
else activity.executeCommand(command, h.getId());
|
||||||
|
|
||||||
|
ReminderHelper.createReminderAlarms(activity);
|
||||||
|
activity.recreate();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onHistoryEditorClosed()
|
||||||
|
{
|
||||||
|
refreshData();
|
||||||
|
HabitBroadcastReceiver.sendRefreshBroadcast(getActivity());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void refreshData()
|
||||||
|
{
|
||||||
|
streakView.refreshData();
|
||||||
|
historyView.refreshData();
|
||||||
|
scoreView.refreshData();
|
||||||
|
punchcardView.refreshData();
|
||||||
|
updateScoreRing(getView());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,230 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||||
|
*
|
||||||
|
* This file is part of Loop Habit Tracker.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by the
|
||||||
|
* Free Software Foundation, either version 3 of the License, or (at your
|
||||||
|
* option) any later version.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
|
* more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along
|
||||||
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.isoron.uhabits.helpers;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.graphics.Color;
|
||||||
|
import android.graphics.Typeface;
|
||||||
|
import android.util.DisplayMetrics;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.Button;
|
||||||
|
import android.widget.LinearLayout;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import org.isoron.helpers.DateHelper;
|
||||||
|
import org.isoron.uhabits.R;
|
||||||
|
import org.isoron.uhabits.loaders.HabitListLoader;
|
||||||
|
import org.isoron.uhabits.models.Habit;
|
||||||
|
import org.isoron.uhabits.models.Score;
|
||||||
|
|
||||||
|
import java.util.GregorianCalendar;
|
||||||
|
|
||||||
|
public class ListHabitsHelper
|
||||||
|
{
|
||||||
|
public static final int INACTIVE_COLOR = Color.rgb(200, 200, 200);
|
||||||
|
public static final int INACTIVE_CHECKMARK_COLOR = Color.rgb(230, 230, 230);
|
||||||
|
|
||||||
|
private final Context context;
|
||||||
|
private final HabitListLoader loader;
|
||||||
|
private Typeface fontawesome;
|
||||||
|
|
||||||
|
public ListHabitsHelper(Context context, HabitListLoader loader)
|
||||||
|
{
|
||||||
|
this.context = context;
|
||||||
|
this.loader = loader;
|
||||||
|
|
||||||
|
fontawesome = Typeface.createFromAsset(context.getAssets(), "fontawesome-webfont.ttf");
|
||||||
|
}
|
||||||
|
|
||||||
|
public Typeface getFontawesome()
|
||||||
|
{
|
||||||
|
return fontawesome;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getButtonCount()
|
||||||
|
{
|
||||||
|
DisplayMetrics dm = context.getResources().getDisplayMetrics();
|
||||||
|
int width = (int) (dm.widthPixels / dm.density);
|
||||||
|
return Math.max(0, (int) ((width - 160) / 42.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getHabitNameWidth()
|
||||||
|
{
|
||||||
|
DisplayMetrics dm = context.getResources().getDisplayMetrics();
|
||||||
|
int width = (int) (dm.widthPixels / dm.density);
|
||||||
|
return (int) ((width - 30 - getButtonCount() * 42) * dm.density);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateCheckmarkButtons(Habit habit, LinearLayout llButtons)
|
||||||
|
{
|
||||||
|
int activeColor = getActiveColor(habit);
|
||||||
|
int m = llButtons.getChildCount();
|
||||||
|
Long habitId = habit.getId();
|
||||||
|
|
||||||
|
int isChecked[] = loader.checkmarks.get(habitId);
|
||||||
|
|
||||||
|
for (int i = 0; i < m; i++)
|
||||||
|
{
|
||||||
|
|
||||||
|
TextView tvCheck = (TextView) llButtons.getChildAt(i);
|
||||||
|
tvCheck.setTag(R.string.habit_key, habitId);
|
||||||
|
tvCheck.setTag(R.string.offset_key, i);
|
||||||
|
if(isChecked.length > i)
|
||||||
|
updateCheckmark(activeColor, tvCheck, isChecked[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getActiveColor(Habit habit)
|
||||||
|
{
|
||||||
|
int activeColor = habit.color;
|
||||||
|
if(habit.isArchived()) activeColor = INACTIVE_COLOR;
|
||||||
|
|
||||||
|
return activeColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void initializeLabelAndIcon(View itemView)
|
||||||
|
{
|
||||||
|
TextView tvStar = (TextView) itemView.findViewById(R.id.tvStar);
|
||||||
|
tvStar.setTypeface(getFontawesome());
|
||||||
|
|
||||||
|
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(getHabitNameWidth(),
|
||||||
|
LinearLayout.LayoutParams.WRAP_CONTENT, 1);
|
||||||
|
itemView.findViewById(R.id.label).setLayoutParams(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateNameAndIcon(Habit habit, TextView tvStar, TextView tvName)
|
||||||
|
{
|
||||||
|
int activeColor = getActiveColor(habit);
|
||||||
|
|
||||||
|
tvName.setText(habit.name);
|
||||||
|
tvName.setTextColor(activeColor);
|
||||||
|
|
||||||
|
if (habit.isArchived())
|
||||||
|
{
|
||||||
|
tvStar.setText(context.getString(R.string.fa_archive));
|
||||||
|
tvStar.setTextColor(activeColor);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
int score = loader.scores.get(habit.getId());
|
||||||
|
|
||||||
|
if (score < Score.HALF_STAR_CUTOFF)
|
||||||
|
{
|
||||||
|
tvStar.setText(context.getString(R.string.fa_star_o));
|
||||||
|
tvStar.setTextColor(INACTIVE_COLOR);
|
||||||
|
}
|
||||||
|
else if (score < Score.FULL_STAR_CUTOFF)
|
||||||
|
{
|
||||||
|
tvStar.setText(context.getString(R.string.fa_star_half_o));
|
||||||
|
tvStar.setTextColor(INACTIVE_COLOR);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
tvStar.setText(context.getString(R.string.fa_star));
|
||||||
|
tvStar.setTextColor(activeColor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateCheckmark(int activeColor, TextView tvCheck, int check)
|
||||||
|
{
|
||||||
|
switch (check)
|
||||||
|
{
|
||||||
|
case 2:
|
||||||
|
tvCheck.setText(R.string.fa_check);
|
||||||
|
tvCheck.setTextColor(activeColor);
|
||||||
|
tvCheck.setTag(R.string.toggle_key, 2);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 1:
|
||||||
|
tvCheck.setText(R.string.fa_check);
|
||||||
|
tvCheck.setTextColor(INACTIVE_CHECKMARK_COLOR);
|
||||||
|
tvCheck.setTag(R.string.toggle_key, 1);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 0:
|
||||||
|
tvCheck.setText(R.string.fa_times);
|
||||||
|
tvCheck.setTextColor(INACTIVE_CHECKMARK_COLOR);
|
||||||
|
tvCheck.setTag(R.string.toggle_key, 0);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateHabitBackground(View view, boolean isSelected)
|
||||||
|
{
|
||||||
|
if (isSelected)
|
||||||
|
view.setBackgroundResource(R.drawable.selected_box);
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (android.os.Build.VERSION.SDK_INT >= 21)
|
||||||
|
view.setBackgroundResource(R.drawable.ripple_white);
|
||||||
|
else view.setBackgroundResource(R.drawable.card_background);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void inflateCheckmarkButtons(View view, View.OnLongClickListener onLongClickListener,
|
||||||
|
View.OnClickListener onClickListener, LayoutInflater inflater)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < getButtonCount(); i++)
|
||||||
|
{
|
||||||
|
View check = inflater.inflate(R.layout.list_habits_item_check, null);
|
||||||
|
TextView btCheck = (TextView) check.findViewById(R.id.tvCheck);
|
||||||
|
btCheck.setTypeface(fontawesome);
|
||||||
|
btCheck.setOnLongClickListener(onLongClickListener);
|
||||||
|
btCheck.setOnClickListener(onClickListener);
|
||||||
|
((LinearLayout) view.findViewById(R.id.llButtons)).addView(check);
|
||||||
|
}
|
||||||
|
|
||||||
|
view.setTag(R.id.timestamp_key, DateHelper.getStartOfToday());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateHeader(ViewGroup header)
|
||||||
|
{
|
||||||
|
LayoutInflater inflater = LayoutInflater.from(context);
|
||||||
|
GregorianCalendar day = DateHelper.getStartOfTodayCalendar();
|
||||||
|
header.removeAllViews();
|
||||||
|
|
||||||
|
for (int i = 0; i < getButtonCount(); i++)
|
||||||
|
{
|
||||||
|
View tvDay = inflater.inflate(R.layout.list_habits_header_check, null);
|
||||||
|
Button btCheck = (Button) tvDay.findViewById(R.id.tvCheck);
|
||||||
|
btCheck.setText(DateHelper.formatHeaderDate(day));
|
||||||
|
header.addView(tvDay);
|
||||||
|
|
||||||
|
day.add(GregorianCalendar.DAY_OF_MONTH, -1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateEmptyMessage(View view)
|
||||||
|
{
|
||||||
|
if (loader.getLastLoadTimestamp() == null) view.setVisibility(View.GONE);
|
||||||
|
else view.setVisibility(loader.habits.size() > 0 ? View.GONE : View.VISIBLE);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void toggleCheckmarkView(View v, Habit habit)
|
||||||
|
{
|
||||||
|
if (v.getTag(R.string.toggle_key).equals(2))
|
||||||
|
updateCheckmark(habit.color, (TextView) v, 0);
|
||||||
|
else
|
||||||
|
updateCheckmark(habit.color, (TextView) v, 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||||
|
*
|
||||||
|
* This file is part of Loop Habit Tracker.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by the
|
||||||
|
* Free Software Foundation, either version 3 of the License, or (at your
|
||||||
|
* option) any later version.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
|
* more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along
|
||||||
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.isoron.uhabits.helpers;
|
||||||
|
|
||||||
|
import android.app.AlarmManager;
|
||||||
|
import android.app.PendingIntent;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import org.isoron.helpers.DateHelper;
|
||||||
|
import org.isoron.uhabits.HabitBroadcastReceiver;
|
||||||
|
import org.isoron.uhabits.models.Habit;
|
||||||
|
|
||||||
|
import java.text.DateFormat;
|
||||||
|
import java.util.Calendar;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
public class ReminderHelper
|
||||||
|
{
|
||||||
|
public static void createReminderAlarms(Context context)
|
||||||
|
{
|
||||||
|
for (Habit habit : Habit.getHabitsWithReminder())
|
||||||
|
createReminderAlarm(context, habit, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void createReminderAlarm(Context context, Habit habit, Long reminderTime)
|
||||||
|
{
|
||||||
|
if (reminderTime == null)
|
||||||
|
{
|
||||||
|
Calendar calendar = Calendar.getInstance();
|
||||||
|
calendar.setTimeInMillis(System.currentTimeMillis());
|
||||||
|
calendar.set(Calendar.HOUR_OF_DAY, habit.reminderHour);
|
||||||
|
calendar.set(Calendar.MINUTE, habit.reminderMin);
|
||||||
|
calendar.set(Calendar.SECOND, 0);
|
||||||
|
|
||||||
|
reminderTime = calendar.getTimeInMillis();
|
||||||
|
|
||||||
|
if (System.currentTimeMillis() > reminderTime)
|
||||||
|
reminderTime += AlarmManager.INTERVAL_DAY;
|
||||||
|
}
|
||||||
|
|
||||||
|
long timestamp = DateHelper.getStartOfDay(DateHelper.toLocalTime(reminderTime));
|
||||||
|
|
||||||
|
Uri uri = habit.getUri();
|
||||||
|
|
||||||
|
Intent alarmIntent = new Intent(context, HabitBroadcastReceiver.class);
|
||||||
|
alarmIntent.setAction(HabitBroadcastReceiver.ACTION_SHOW_REMINDER);
|
||||||
|
alarmIntent.setData(uri);
|
||||||
|
alarmIntent.putExtra("timestamp", timestamp);
|
||||||
|
alarmIntent.putExtra("reminderTime", reminderTime);
|
||||||
|
|
||||||
|
PendingIntent pendingIntent =
|
||||||
|
PendingIntent.getBroadcast(context, ((int) (habit.getId() % Integer.MAX_VALUE)) + 1,
|
||||||
|
alarmIntent, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||||
|
|
||||||
|
AlarmManager manager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
|
||||||
|
if (Build.VERSION.SDK_INT >= 19)
|
||||||
|
manager.setExact(AlarmManager.RTC_WAKEUP, reminderTime, pendingIntent);
|
||||||
|
else
|
||||||
|
manager.set(AlarmManager.RTC_WAKEUP, reminderTime, pendingIntent);
|
||||||
|
|
||||||
|
Log.d("ReminderHelper", String.format("Setting alarm (%s): %s",
|
||||||
|
DateFormat.getDateTimeInstance().format(new Date(reminderTime)), habit.name));
|
||||||
|
}
|
||||||
|
}
|
||||||
213
app/src/main/java/org/isoron/uhabits/io/CSVExporter.java
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||||
|
*
|
||||||
|
* This file is part of Loop Habit Tracker.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by the
|
||||||
|
* Free Software Foundation, either version 3 of the License, or (at your
|
||||||
|
* option) any later version.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
|
* more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along
|
||||||
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.isoron.uhabits.io;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.database.Cursor;
|
||||||
|
import android.database.sqlite.SQLiteDatabase;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import com.activeandroid.Cache;
|
||||||
|
|
||||||
|
import org.isoron.helpers.DateHelper;
|
||||||
|
import org.isoron.uhabits.models.Habit;
|
||||||
|
import org.isoron.uhabits.models.Score;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.FileWriter;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.TimeZone;
|
||||||
|
import java.util.zip.ZipEntry;
|
||||||
|
import java.util.zip.ZipOutputStream;
|
||||||
|
|
||||||
|
public class CSVExporter
|
||||||
|
{
|
||||||
|
private List<Habit> habits;
|
||||||
|
private Context context;
|
||||||
|
private java.text.DateFormat dateFormat;
|
||||||
|
|
||||||
|
private List<String> generateDirs;
|
||||||
|
private List<String> generateFilenames;
|
||||||
|
|
||||||
|
private String basePath;
|
||||||
|
|
||||||
|
public CSVExporter(Context context, List<Habit> habits)
|
||||||
|
{
|
||||||
|
this.habits = habits;
|
||||||
|
this.context = context;
|
||||||
|
generateDirs = new LinkedList<>();
|
||||||
|
generateFilenames = new LinkedList<>();
|
||||||
|
|
||||||
|
basePath = String.format("%s/export/", context.getFilesDir());
|
||||||
|
|
||||||
|
dateFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.US);
|
||||||
|
dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public String formatDate(long timestamp)
|
||||||
|
{
|
||||||
|
return dateFormat.format(new Date(timestamp));
|
||||||
|
}
|
||||||
|
|
||||||
|
public String formatScore(int score)
|
||||||
|
{
|
||||||
|
return String.format("%.2f", ((float) score) / Score.MAX_SCORE);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeScores(String dirPath, Habit habit) throws IOException
|
||||||
|
{
|
||||||
|
String path = dirPath + "scores.csv";
|
||||||
|
FileWriter out = new FileWriter(basePath + path);
|
||||||
|
generateFilenames.add(path);
|
||||||
|
|
||||||
|
String query = "select timestamp, score from score where habit = ? order by timestamp";
|
||||||
|
String params[] = { habit.getId().toString() };
|
||||||
|
|
||||||
|
SQLiteDatabase db = Cache.openDatabase();
|
||||||
|
Cursor cursor = db.rawQuery(query, params);
|
||||||
|
|
||||||
|
if(!cursor.moveToFirst()) return;
|
||||||
|
|
||||||
|
do
|
||||||
|
{
|
||||||
|
String timestamp = formatDate(cursor.getLong(0));
|
||||||
|
String score = formatScore(cursor.getInt(1));
|
||||||
|
out.write(String.format("%s,%s\n", timestamp, score));
|
||||||
|
|
||||||
|
} while(cursor.moveToNext());
|
||||||
|
|
||||||
|
out.close();
|
||||||
|
cursor.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeCheckmarks(String dirPath, Habit habit) throws IOException
|
||||||
|
{
|
||||||
|
String path = dirPath + "checkmarks.csv";
|
||||||
|
FileWriter out = new FileWriter(basePath + path);
|
||||||
|
generateFilenames.add(path);
|
||||||
|
|
||||||
|
String query = "select timestamp, value from checkmarks where habit = ? order by timestamp";
|
||||||
|
String params[] = { habit.getId().toString() };
|
||||||
|
|
||||||
|
SQLiteDatabase db = Cache.openDatabase();
|
||||||
|
Cursor cursor = db.rawQuery(query, params);
|
||||||
|
|
||||||
|
if(!cursor.moveToFirst()) return;
|
||||||
|
|
||||||
|
do
|
||||||
|
{
|
||||||
|
String timestamp = formatDate(cursor.getLong(0));
|
||||||
|
Integer value = cursor.getInt(1);
|
||||||
|
out.write(String.format("%s,%d\n", timestamp, value));
|
||||||
|
|
||||||
|
} while(cursor.moveToNext());
|
||||||
|
|
||||||
|
out.close();
|
||||||
|
cursor.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeFiles(Habit habit) throws IOException
|
||||||
|
{
|
||||||
|
String path = String.format("%s/", habit.name);
|
||||||
|
new File(basePath + path).mkdirs();
|
||||||
|
generateDirs.add(path);
|
||||||
|
|
||||||
|
writeScores(path, habit);
|
||||||
|
writeCheckmarks(path, habit);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeZipFile(String zipFilename) throws IOException
|
||||||
|
{
|
||||||
|
FileOutputStream fos = new FileOutputStream(zipFilename);
|
||||||
|
ZipOutputStream zos = new ZipOutputStream(fos);
|
||||||
|
|
||||||
|
for(String filename : generateFilenames)
|
||||||
|
addFileToZip(zos, filename);
|
||||||
|
|
||||||
|
zos.close();
|
||||||
|
fos.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addFileToZip(ZipOutputStream zos, String filename) throws IOException
|
||||||
|
{
|
||||||
|
FileInputStream fis = new FileInputStream(new File(basePath + filename));
|
||||||
|
ZipEntry ze = new ZipEntry(filename);
|
||||||
|
zos.putNextEntry(ze);
|
||||||
|
|
||||||
|
int length;
|
||||||
|
byte bytes[] = new byte[1024];
|
||||||
|
while((length = fis.read(bytes)) >= 0)
|
||||||
|
zos.write(bytes, 0, length);
|
||||||
|
|
||||||
|
zos.closeEntry();
|
||||||
|
fis.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void cleanup()
|
||||||
|
{
|
||||||
|
for(String filename : generateFilenames)
|
||||||
|
new File(basePath + filename).delete();
|
||||||
|
|
||||||
|
for(String filename : generateDirs)
|
||||||
|
new File(basePath + filename).delete();
|
||||||
|
|
||||||
|
new File(basePath).delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String writeArchive()
|
||||||
|
{
|
||||||
|
String date = formatDate(DateHelper.getStartOfToday());
|
||||||
|
|
||||||
|
File dir = context.getExternalCacheDir();
|
||||||
|
|
||||||
|
if(dir == null)
|
||||||
|
{
|
||||||
|
Log.e("CSVExporter", "No suitable directory found.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String zipFilename = String.format("%s/habits-%s.zip", dir, date);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
for (Habit h : habits)
|
||||||
|
writeFiles(h);
|
||||||
|
|
||||||
|
writeZipFile(zipFilename);
|
||||||
|
cleanup();
|
||||||
|
}
|
||||||
|
catch (IOException e)
|
||||||
|
{
|
||||||
|
e.printStackTrace();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return zipFilename;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,253 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||||
|
*
|
||||||
|
* This file is part of Loop Habit Tracker.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by the
|
||||||
|
* Free Software Foundation, either version 3 of the License, or (at your
|
||||||
|
* option) any later version.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
|
* more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along
|
||||||
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.isoron.uhabits.loaders;
|
||||||
|
|
||||||
|
import android.os.AsyncTask;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.ProgressBar;
|
||||||
|
|
||||||
|
import org.isoron.helpers.DateHelper;
|
||||||
|
import org.isoron.uhabits.models.Habit;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class HabitListLoader
|
||||||
|
{
|
||||||
|
public interface Listener
|
||||||
|
{
|
||||||
|
void onLoadFinished();
|
||||||
|
}
|
||||||
|
|
||||||
|
private AsyncTask<Void, Integer, Void> currentFetchTask;
|
||||||
|
private int checkmarkCount;
|
||||||
|
private ProgressBar progressBar;
|
||||||
|
|
||||||
|
private Listener listener;
|
||||||
|
private Long lastLoadTimestamp;
|
||||||
|
|
||||||
|
public HashMap<Long, Habit> habits;
|
||||||
|
public List<Habit> habitsList;
|
||||||
|
public HashMap<Long, int[]> checkmarks;
|
||||||
|
public HashMap<Long, Integer> scores;
|
||||||
|
|
||||||
|
boolean includeArchived;
|
||||||
|
|
||||||
|
public void setIncludeArchived(boolean includeArchived)
|
||||||
|
{
|
||||||
|
this.includeArchived = includeArchived;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setProgressBar(ProgressBar progressBar)
|
||||||
|
{
|
||||||
|
this.progressBar = progressBar;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCheckmarkCount(int checkmarkCount)
|
||||||
|
{
|
||||||
|
this.checkmarkCount = checkmarkCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setListener(Listener listener)
|
||||||
|
{
|
||||||
|
this.listener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getLastLoadTimestamp()
|
||||||
|
{
|
||||||
|
return lastLoadTimestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public HabitListLoader()
|
||||||
|
{
|
||||||
|
habits = new HashMap<>();
|
||||||
|
checkmarks = new HashMap<>();
|
||||||
|
scores = new HashMap<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void reorder(int from, int to)
|
||||||
|
{
|
||||||
|
Habit fromHabit = habitsList.get(from);
|
||||||
|
Habit toHabit = habitsList.get(to);
|
||||||
|
|
||||||
|
habitsList.remove(from);
|
||||||
|
habitsList.add(to, fromHabit);
|
||||||
|
|
||||||
|
Habit.reorder(fromHabit, toHabit);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateAllHabits(final boolean updateScoresAndCheckmarks)
|
||||||
|
{
|
||||||
|
if (currentFetchTask != null) currentFetchTask.cancel(true);
|
||||||
|
|
||||||
|
currentFetchTask = new AsyncTask<Void, Integer, Void>()
|
||||||
|
{
|
||||||
|
public HashMap<Long, Habit> newHabits;
|
||||||
|
public HashMap<Long, int[]> newCheckmarks;
|
||||||
|
public HashMap<Long, Integer> newScores;
|
||||||
|
public List<Habit> newHabitList;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Void doInBackground(Void... params)
|
||||||
|
{
|
||||||
|
newHabits = new HashMap<>();
|
||||||
|
newCheckmarks = new HashMap<>();
|
||||||
|
newScores = new HashMap<>();
|
||||||
|
newHabitList = Habit.getAll(includeArchived);
|
||||||
|
|
||||||
|
long dateTo = DateHelper.getStartOfDay(DateHelper.getLocalTime());
|
||||||
|
long dateFrom = dateTo - (checkmarkCount - 1) * DateHelper.millisecondsInOneDay;
|
||||||
|
int[] empty = new int[checkmarkCount];
|
||||||
|
|
||||||
|
for(Habit h : newHabitList)
|
||||||
|
{
|
||||||
|
Long id = h.getId();
|
||||||
|
|
||||||
|
newHabits.put(id, h);
|
||||||
|
|
||||||
|
if(checkmarks.containsKey(id))
|
||||||
|
newCheckmarks.put(id, checkmarks.get(id));
|
||||||
|
else
|
||||||
|
newCheckmarks.put(id, empty);
|
||||||
|
|
||||||
|
if(scores.containsKey(id))
|
||||||
|
newScores.put(id, scores.get(id));
|
||||||
|
else
|
||||||
|
newScores.put(id, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
commit();
|
||||||
|
|
||||||
|
if(!updateScoresAndCheckmarks) return null;
|
||||||
|
|
||||||
|
int current = 0;
|
||||||
|
for (Habit h : newHabitList)
|
||||||
|
{
|
||||||
|
if (isCancelled()) return null;
|
||||||
|
|
||||||
|
Long id = h.getId();
|
||||||
|
newScores.put(id, h.scores.getNewestValue());
|
||||||
|
newCheckmarks.put(id, h.checkmarks.getValues(dateFrom, dateTo));
|
||||||
|
|
||||||
|
publishProgress(current++, newHabits.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void commit()
|
||||||
|
{
|
||||||
|
habits = newHabits;
|
||||||
|
scores = newScores;
|
||||||
|
checkmarks = newCheckmarks;
|
||||||
|
habitsList = newHabitList;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPreExecute()
|
||||||
|
{
|
||||||
|
if(progressBar != null)
|
||||||
|
{
|
||||||
|
progressBar.setIndeterminate(false);
|
||||||
|
progressBar.setProgress(0);
|
||||||
|
progressBar.setVisibility(View.VISIBLE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onProgressUpdate(Integer... values)
|
||||||
|
{
|
||||||
|
if(progressBar != null)
|
||||||
|
{
|
||||||
|
progressBar.setMax(values[1]);
|
||||||
|
progressBar.setProgress(values[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(listener != null) listener.onLoadFinished();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPostExecute(Void aVoid)
|
||||||
|
{
|
||||||
|
if (isCancelled()) return;
|
||||||
|
|
||||||
|
if(progressBar != null) progressBar.setVisibility(View.INVISIBLE);
|
||||||
|
lastLoadTimestamp = DateHelper.getStartOfToday();
|
||||||
|
currentFetchTask = null;
|
||||||
|
|
||||||
|
if(listener != null) listener.onLoadFinished();
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
currentFetchTask.execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateHabit(final Long id)
|
||||||
|
{
|
||||||
|
new AsyncTask<Void, Void, Void>()
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
protected Void doInBackground(Void... params)
|
||||||
|
{
|
||||||
|
long dateTo = DateHelper.getStartOfDay(DateHelper.getLocalTime());
|
||||||
|
long dateFrom = dateTo - (checkmarkCount - 1) * DateHelper.millisecondsInOneDay;
|
||||||
|
|
||||||
|
Habit h = Habit.get(id);
|
||||||
|
habits.put(id, h);
|
||||||
|
scores.put(id, h.scores.getNewestValue());
|
||||||
|
checkmarks.put(id, h.checkmarks.getValues(dateFrom, dateTo));
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPreExecute()
|
||||||
|
{
|
||||||
|
new Handler().postDelayed(new Runnable()
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
public void run()
|
||||||
|
{
|
||||||
|
if (getStatus() == Status.RUNNING)
|
||||||
|
{
|
||||||
|
if(progressBar != null)
|
||||||
|
{
|
||||||
|
progressBar.setIndeterminate(true);
|
||||||
|
progressBar.setVisibility(View.VISIBLE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPostExecute(Void aVoid)
|
||||||
|
{
|
||||||
|
if(progressBar != null) progressBar.setVisibility(View.GONE);
|
||||||
|
|
||||||
|
if(listener != null)
|
||||||
|
listener.onLoadFinished();
|
||||||
|
|
||||||
|
}
|
||||||
|
}.execute();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,17 +1,20 @@
|
|||||||
/* Copyright (C) 2016 Alinson Santos Xavier
|
/*
|
||||||
|
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify it
|
* This file is part of Loop Habit Tracker.
|
||||||
* under the terms of the GNU General Public License as published by the Free
|
|
||||||
* Software Foundation, either version 3 of the License, or (at your option)
|
|
||||||
* any later version.
|
|
||||||
*
|
*
|
||||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
* it under the terms of the GNU General Public License as published by the
|
||||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
* Free Software Foundation, either version 3 of the License, or (at your
|
||||||
|
* option) any later version.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
* more details.
|
* more details.
|
||||||
*
|
*
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License along
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.isoron.uhabits.models;
|
package org.isoron.uhabits.models;
|
||||||
|
|||||||
178
app/src/main/java/org/isoron/uhabits/models/CheckmarkList.java
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||||
|
*
|
||||||
|
* This file is part of Loop Habit Tracker.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by the
|
||||||
|
* Free Software Foundation, either version 3 of the License, or (at your
|
||||||
|
* option) any later version.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
|
* more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along
|
||||||
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.isoron.uhabits.models;
|
||||||
|
|
||||||
|
import android.database.Cursor;
|
||||||
|
import android.database.sqlite.SQLiteDatabase;
|
||||||
|
|
||||||
|
import com.activeandroid.ActiveAndroid;
|
||||||
|
import com.activeandroid.Cache;
|
||||||
|
import com.activeandroid.query.Delete;
|
||||||
|
import com.activeandroid.query.Select;
|
||||||
|
|
||||||
|
import org.isoron.helpers.DateHelper;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class CheckmarkList
|
||||||
|
{
|
||||||
|
private Habit habit;
|
||||||
|
|
||||||
|
public CheckmarkList(Habit habit)
|
||||||
|
{
|
||||||
|
this.habit = habit;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void deleteNewerThan(long timestamp)
|
||||||
|
{
|
||||||
|
new Delete().from(Checkmark.class)
|
||||||
|
.where("habit = ?", habit.getId())
|
||||||
|
.and("timestamp >= ?", timestamp)
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int[] getValues(Long fromTimestamp, Long toTimestamp)
|
||||||
|
{
|
||||||
|
rebuild();
|
||||||
|
|
||||||
|
if(fromTimestamp > toTimestamp) return new int[0];
|
||||||
|
|
||||||
|
String query = "select value, timestamp from Checkmarks where " +
|
||||||
|
"habit = ? and timestamp >= ? and timestamp <= ?";
|
||||||
|
|
||||||
|
SQLiteDatabase db = Cache.openDatabase();
|
||||||
|
String args[] = { habit.getId().toString(), fromTimestamp.toString(),
|
||||||
|
toTimestamp.toString() };
|
||||||
|
Cursor cursor = db.rawQuery(query, args);
|
||||||
|
|
||||||
|
long day = DateHelper.millisecondsInOneDay;
|
||||||
|
int nDays = (int) ((toTimestamp - fromTimestamp) / day) + 1;
|
||||||
|
int[] checks = new int[nDays];
|
||||||
|
|
||||||
|
if (cursor.moveToFirst())
|
||||||
|
{
|
||||||
|
do
|
||||||
|
{
|
||||||
|
long timestamp = cursor.getLong(1);
|
||||||
|
int offset = (int) ((timestamp - fromTimestamp) / day);
|
||||||
|
checks[nDays - offset - 1] = cursor.getInt(0);
|
||||||
|
|
||||||
|
} while (cursor.moveToNext());
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor.close();
|
||||||
|
return checks;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int[] getAllValues()
|
||||||
|
{
|
||||||
|
Repetition oldestRep = habit.repetitions.getOldest();
|
||||||
|
if(oldestRep == null) return new int[0];
|
||||||
|
|
||||||
|
Long toTimestamp = DateHelper.getStartOfToday();
|
||||||
|
Long fromTimestamp = oldestRep.timestamp;
|
||||||
|
return getValues(fromTimestamp, toTimestamp);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void rebuild()
|
||||||
|
{
|
||||||
|
long beginning;
|
||||||
|
long today = DateHelper.getStartOfToday();
|
||||||
|
long day = DateHelper.millisecondsInOneDay;
|
||||||
|
|
||||||
|
Checkmark newestCheckmark = getNewest();
|
||||||
|
if (newestCheckmark == null)
|
||||||
|
{
|
||||||
|
Repetition oldestRep = habit.repetitions.getOldest();
|
||||||
|
if (oldestRep == null) return;
|
||||||
|
|
||||||
|
beginning = oldestRep.timestamp;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
beginning = newestCheckmark.timestamp + day;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (beginning > today) return;
|
||||||
|
|
||||||
|
long beginningExtended = beginning - (long) (habit.freqDen) * day;
|
||||||
|
List<Repetition> reps = habit.repetitions.selectFromTo(beginningExtended, today).execute();
|
||||||
|
|
||||||
|
int nDays = (int) ((today - beginning) / day) + 1;
|
||||||
|
int nDaysExtended = (int) ((today - beginningExtended) / day) + 1;
|
||||||
|
|
||||||
|
int checks[] = new int[nDaysExtended];
|
||||||
|
|
||||||
|
// explicit checks
|
||||||
|
for (Repetition rep : reps)
|
||||||
|
{
|
||||||
|
int offset = (int) ((rep.timestamp - beginningExtended) / day);
|
||||||
|
checks[nDaysExtended - offset - 1] = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// implicit checks
|
||||||
|
for (int i = 0; i < nDays; i++)
|
||||||
|
{
|
||||||
|
int counter = 0;
|
||||||
|
|
||||||
|
for (int j = 0; j < habit.freqDen; j++)
|
||||||
|
if (checks[i + j] == 2) counter++;
|
||||||
|
|
||||||
|
if (counter >= habit.freqNum) checks[i] = Math.max(checks[i], 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
ActiveAndroid.beginTransaction();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
for (int i = 0; i < nDays; i++)
|
||||||
|
{
|
||||||
|
Checkmark c = new Checkmark();
|
||||||
|
c.habit = habit;
|
||||||
|
c.timestamp = today - i * day;
|
||||||
|
c.value = checks[i];
|
||||||
|
c.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
ActiveAndroid.setTransactionSuccessful();
|
||||||
|
} finally
|
||||||
|
{
|
||||||
|
ActiveAndroid.endTransaction();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Checkmark getNewest()
|
||||||
|
{
|
||||||
|
return new Select().from(Checkmark.class)
|
||||||
|
.where("habit = ?", habit.getId())
|
||||||
|
.orderBy("timestamp desc")
|
||||||
|
.limit(1)
|
||||||
|
.executeSingle();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getCurrentValue()
|
||||||
|
{
|
||||||
|
rebuild();
|
||||||
|
Checkmark c = getNewest();
|
||||||
|
|
||||||
|
if(c != null) return c.value;
|
||||||
|
else return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,27 +1,28 @@
|
|||||||
/* Copyright (C) 2016 Alinson Santos Xavier
|
/*
|
||||||
|
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify it
|
* This file is part of Loop Habit Tracker.
|
||||||
* under the terms of the GNU General Public License as published by the Free
|
|
||||||
* Software Foundation, either version 3 of the License, or (at your option)
|
|
||||||
* any later version.
|
|
||||||
*
|
*
|
||||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
* it under the terms of the GNU General Public License as published by the
|
||||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
* Free Software Foundation, either version 3 of the License, or (at your
|
||||||
|
* option) any later version.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
* more details.
|
* more details.
|
||||||
*
|
*
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License along
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.isoron.uhabits.models;
|
package org.isoron.uhabits.models;
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.database.Cursor;
|
import android.net.Uri;
|
||||||
import android.database.sqlite.SQLiteDatabase;
|
|
||||||
|
|
||||||
import com.activeandroid.ActiveAndroid;
|
import com.activeandroid.ActiveAndroid;
|
||||||
import com.activeandroid.Cache;
|
|
||||||
import com.activeandroid.Model;
|
import com.activeandroid.Model;
|
||||||
import com.activeandroid.annotation.Column;
|
import com.activeandroid.annotation.Column;
|
||||||
import com.activeandroid.annotation.Table;
|
import com.activeandroid.annotation.Table;
|
||||||
@@ -32,24 +33,12 @@ import com.activeandroid.query.Update;
|
|||||||
import com.activeandroid.util.SQLiteUtils;
|
import com.activeandroid.util.SQLiteUtils;
|
||||||
|
|
||||||
import org.isoron.helpers.ColorHelper;
|
import org.isoron.helpers.ColorHelper;
|
||||||
import org.isoron.helpers.Command;
|
|
||||||
import org.isoron.helpers.DateHelper;
|
|
||||||
import org.isoron.uhabits.R;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@Table(name = "Habits")
|
@Table(name = "Habits")
|
||||||
public class Habit extends Model
|
public class Habit extends Model
|
||||||
{
|
{
|
||||||
|
|
||||||
public static final int HALF_STAR_CUTOFF = 5999000;
|
|
||||||
public static final int FULL_STAR_CUTOFF = 12973000;
|
|
||||||
public static final int MAX_SCORE = 19259500;
|
|
||||||
|
|
||||||
private static boolean includeArchived = false;
|
|
||||||
|
|
||||||
@Column(name = "name")
|
@Column(name = "name")
|
||||||
public String name;
|
public String name;
|
||||||
|
|
||||||
@@ -57,10 +46,10 @@ public class Habit extends Model
|
|||||||
public String description;
|
public String description;
|
||||||
|
|
||||||
@Column(name = "freq_num")
|
@Column(name = "freq_num")
|
||||||
public Integer freq_num;
|
public Integer freqNum;
|
||||||
|
|
||||||
@Column(name = "freq_den")
|
@Column(name = "freq_den")
|
||||||
public Integer freq_den;
|
public Integer freqDen;
|
||||||
|
|
||||||
@Column(name = "color")
|
@Column(name = "color")
|
||||||
public Integer color;
|
public Integer color;
|
||||||
@@ -69,10 +58,13 @@ public class Habit extends Model
|
|||||||
public Integer position;
|
public Integer position;
|
||||||
|
|
||||||
@Column(name = "reminder_hour")
|
@Column(name = "reminder_hour")
|
||||||
public Integer reminder_hour;
|
public Integer reminderHour;
|
||||||
|
|
||||||
@Column(name = "reminder_min")
|
@Column(name = "reminder_min")
|
||||||
public Integer reminder_min;
|
public Integer reminderMin;
|
||||||
|
|
||||||
|
@Column(name = "reminder_days")
|
||||||
|
public Integer reminderDays;
|
||||||
|
|
||||||
@Column(name = "highlight")
|
@Column(name = "highlight")
|
||||||
public Integer highlight;
|
public Integer highlight;
|
||||||
@@ -80,19 +72,35 @@ public class Habit extends Model
|
|||||||
@Column(name = "archived")
|
@Column(name = "archived")
|
||||||
public Integer archived;
|
public Integer archived;
|
||||||
|
|
||||||
|
public StreakList streaks;
|
||||||
|
public ScoreList scores;
|
||||||
|
public RepetitionList repetitions;
|
||||||
|
public CheckmarkList checkmarks;
|
||||||
|
|
||||||
public Habit(Habit model)
|
public Habit(Habit model)
|
||||||
{
|
{
|
||||||
copyAttributes(model);
|
copyAttributes(model);
|
||||||
|
initializeLists();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Habit()
|
public Habit()
|
||||||
{
|
{
|
||||||
this.color = ColorHelper.palette[5];
|
this.color = ColorHelper.palette[5];
|
||||||
this.position = Habit.getCount();
|
this.position = Habit.countWithArchived();
|
||||||
this.highlight = 0;
|
this.highlight = 0;
|
||||||
this.archived = 0;
|
this.archived = 0;
|
||||||
this.freq_den = 7;
|
this.freqDen = 7;
|
||||||
this.freq_num = 3;
|
this.freqNum = 3;
|
||||||
|
this.reminderDays = 127;
|
||||||
|
initializeLists();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initializeLists()
|
||||||
|
{
|
||||||
|
streaks = new StreakList(this);
|
||||||
|
scores = new ScoreList(this);
|
||||||
|
repetitions = new RepetitionList(this);
|
||||||
|
checkmarks = new CheckmarkList(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Habit get(Long id)
|
public static Habit get(Long id)
|
||||||
@@ -100,17 +108,10 @@ public class Habit extends Model
|
|||||||
return Habit.load(Habit.class, id);
|
return Habit.load(Habit.class, id);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static HashMap<Long, Habit> getAll()
|
public static List<Habit> getAll(boolean includeArchive)
|
||||||
{
|
{
|
||||||
List<Habit> habits = select().execute();
|
if(includeArchive) return selectWithArchived().execute();
|
||||||
HashMap<Long, Habit> map = new HashMap<>();
|
else return select().execute();
|
||||||
|
|
||||||
for(Habit h : habits)
|
|
||||||
{
|
|
||||||
map.put(h.getId(), h);
|
|
||||||
}
|
|
||||||
|
|
||||||
return map;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("DefaultLocale")
|
@SuppressLint("DefaultLocale")
|
||||||
@@ -121,41 +122,28 @@ public class Habit extends Model
|
|||||||
|
|
||||||
protected static From select()
|
protected static From select()
|
||||||
{
|
{
|
||||||
if(includeArchived)
|
return new Select().from(Habit.class).where("archived = 0").orderBy("position");
|
||||||
return new Select().from(Habit.class).orderBy("position");
|
|
||||||
else
|
|
||||||
return new Select().from(Habit.class).where("archived = 0").orderBy("position");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void setIncludeArchived(boolean includeArchived)
|
public static From selectWithArchived()
|
||||||
{
|
{
|
||||||
Habit.includeArchived = includeArchived;
|
return new Select().from(Habit.class).orderBy("position");
|
||||||
rebuildOrder();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean isIncludeArchived()
|
public static int count()
|
||||||
{
|
|
||||||
return Habit.includeArchived;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static int getCount()
|
|
||||||
{
|
{
|
||||||
return select().count();
|
return select().count();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Habit getByPosition(int position)
|
public static int countWithArchived()
|
||||||
{
|
{
|
||||||
return select().offset(position).executeSingle();
|
return selectWithArchived().count();
|
||||||
}
|
|
||||||
|
|
||||||
public static java.util.List<Habit> getHabits()
|
|
||||||
{
|
|
||||||
return select().execute();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static java.util.List<Habit> getHighlightedHabits()
|
public static java.util.List<Habit> getHighlightedHabits()
|
||||||
{
|
{
|
||||||
return select().where("highlight = 1").orderBy("reminder_hour desc, reminder_min desc")
|
return select().where("highlight = 1")
|
||||||
|
.orderBy("reminder_hour desc, reminder_min desc")
|
||||||
.execute();
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,23 +152,30 @@ public class Habit extends Model
|
|||||||
return select().where("reminder_hour is not null").execute();
|
return select().where("reminder_hour is not null").execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void reorder(int from, int to)
|
public static void reorder(Habit from, Habit to)
|
||||||
{
|
{
|
||||||
if (from == to) return;
|
if(from == to) return;
|
||||||
|
|
||||||
Habit h = Habit.getByPosition(from);
|
if (to.position < from.position)
|
||||||
if (to < from) new Update(Habit.class).set("position = position + 1")
|
{
|
||||||
.where("position >= ? and position < ?", to, from).execute();
|
new Update(Habit.class).set("position = position + 1")
|
||||||
else new Update(Habit.class).set("position = position - 1")
|
.where("position >= ? and position < ?", to.position, from.position)
|
||||||
.where("position > ? and position <= ?", from, to).execute();
|
.execute();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
new Update(Habit.class).set("position = position - 1")
|
||||||
|
.where("position > ? and position <= ?", from.position, to.position)
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
|
||||||
h.position = to;
|
from.position = to.position;
|
||||||
h.save();
|
from.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void rebuildOrder()
|
public static void rebuildOrder()
|
||||||
{
|
{
|
||||||
List<Habit> habits = select().execute();
|
List<Habit> habits = selectWithArchived().execute();
|
||||||
|
|
||||||
ActiveAndroid.beginTransaction();
|
ActiveAndroid.beginTransaction();
|
||||||
try
|
try
|
||||||
@@ -201,43 +196,17 @@ public class Habit extends Model
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void roundTimestamps()
|
|
||||||
{
|
|
||||||
List<Repetition> reps = new Select().from(Repetition.class).execute();
|
|
||||||
for (Repetition r : reps)
|
|
||||||
{
|
|
||||||
r.timestamp = DateHelper.getStartOfDay(r.timestamp);
|
|
||||||
r.save();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void recomputeAllScores()
|
|
||||||
{
|
|
||||||
for (Habit habit : getHabits())
|
|
||||||
{
|
|
||||||
habit.deleteScoresNewerThan(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static int getStarCount()
|
|
||||||
{
|
|
||||||
String args[] = {};
|
|
||||||
return SQLiteUtils.intQuery("select count(*) from (select score, max(timestamp) from " +
|
|
||||||
"score group by habit) as scores where scores.score >= " +
|
|
||||||
Integer.toString(12973000), args);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public void copyAttributes(Habit model)
|
public void copyAttributes(Habit model)
|
||||||
{
|
{
|
||||||
this.name = model.name;
|
this.name = model.name;
|
||||||
this.description = model.description;
|
this.description = model.description;
|
||||||
this.freq_num = model.freq_num;
|
this.freqNum = model.freqNum;
|
||||||
this.freq_den = model.freq_den;
|
this.freqDen = model.freqDen;
|
||||||
this.color = model.color;
|
this.color = model.color;
|
||||||
this.position = model.position;
|
this.position = model.position;
|
||||||
this.reminder_hour = model.reminder_hour;
|
this.reminderHour = model.reminderHour;
|
||||||
this.reminder_min = model.reminder_min;
|
this.reminderMin = model.reminderMin;
|
||||||
|
this.reminderDays = model.reminderDays;
|
||||||
this.highlight = model.highlight;
|
this.highlight = model.highlight;
|
||||||
this.archived = model.archived;
|
this.archived = model.archived;
|
||||||
}
|
}
|
||||||
@@ -248,138 +217,18 @@ public class Habit extends Model
|
|||||||
Habit.updateId(getId(), id);
|
Habit.updateId(getId(), id);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected From selectReps()
|
public void cascadeDelete()
|
||||||
{
|
{
|
||||||
return new Select().from(Repetition.class).where("habit = ?", getId()).orderBy("timestamp");
|
Long id = getId();
|
||||||
}
|
|
||||||
|
|
||||||
protected From selectRepsFromTo(long timeFrom, long timeTo)
|
|
||||||
{
|
|
||||||
return selectReps().and("timestamp >= ?", timeFrom).and("timestamp <= ?", timeTo);
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean hasRep(long timestamp)
|
|
||||||
{
|
|
||||||
int count = selectReps().where("timestamp = ?", timestamp).count();
|
|
||||||
return (count > 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean hasRepToday()
|
|
||||||
{
|
|
||||||
return hasRep(DateHelper.getStartOfToday());
|
|
||||||
}
|
|
||||||
|
|
||||||
public void deleteReps(long timestamp)
|
|
||||||
{
|
|
||||||
new Delete().from(Repetition.class).where("habit = ?", getId())
|
|
||||||
.and("timestamp = ?", timestamp).execute();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void deleteCheckmarksNewerThan(long timestamp)
|
|
||||||
{
|
|
||||||
new Delete().from(Checkmark.class)
|
|
||||||
.where("habit = ?", getId())
|
|
||||||
.and("timestamp >= ?", timestamp)
|
|
||||||
.execute();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void deleteStreaksNewerThan(long timestamp)
|
|
||||||
{
|
|
||||||
new Delete().from(Streak.class)
|
|
||||||
.where("habit = ?", getId())
|
|
||||||
.and("end >= ?", timestamp - DateHelper.millisecondsInOneDay)
|
|
||||||
.execute();
|
|
||||||
}
|
|
||||||
|
|
||||||
public int[] getCheckmarks(Long fromTimestamp, Long toTimestamp)
|
|
||||||
{
|
|
||||||
updateCheckmarks();
|
|
||||||
|
|
||||||
String query = "select value, timestamp from Checkmarks where " +
|
|
||||||
"habit = ? and timestamp >= ? and timestamp <= ?";
|
|
||||||
|
|
||||||
SQLiteDatabase db = Cache.openDatabase();
|
|
||||||
String args[] = {getId().toString(), fromTimestamp.toString(), toTimestamp.toString()};
|
|
||||||
Cursor cursor = db.rawQuery(query, args);
|
|
||||||
|
|
||||||
long day = DateHelper.millisecondsInOneDay;
|
|
||||||
int nDays = (int) ((toTimestamp - fromTimestamp) / day) + 1;
|
|
||||||
int[] checks = new int[nDays];
|
|
||||||
|
|
||||||
if(cursor.moveToFirst())
|
|
||||||
{
|
|
||||||
do
|
|
||||||
{
|
|
||||||
long timestamp = cursor.getLong(1);
|
|
||||||
int offset = (int) ((timestamp - fromTimestamp) / day);
|
|
||||||
checks[nDays - offset - 1] = cursor.getInt(0);
|
|
||||||
|
|
||||||
} while (cursor.moveToNext());
|
|
||||||
}
|
|
||||||
|
|
||||||
return checks;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void updateCheckmarks()
|
|
||||||
{
|
|
||||||
long beginning;
|
|
||||||
long today = DateHelper.getStartOfToday();
|
|
||||||
long day = DateHelper.millisecondsInOneDay;
|
|
||||||
|
|
||||||
Checkmark newestCheckmark = getNewestCheckmark();
|
|
||||||
if(newestCheckmark == null)
|
|
||||||
{
|
|
||||||
Repetition oldestRep = getOldestRep();
|
|
||||||
if (oldestRep == null) return;
|
|
||||||
|
|
||||||
beginning = oldestRep.timestamp;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
beginning = newestCheckmark.timestamp + day;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(beginning > today)
|
|
||||||
return;
|
|
||||||
|
|
||||||
long beginningExtended = beginning - (long) (freq_den) * day;
|
|
||||||
List<Repetition> reps = selectRepsFromTo(beginningExtended, today).execute();
|
|
||||||
|
|
||||||
int nDays = (int) ((today - beginning) / day) + 1;
|
|
||||||
int nDaysExtended = (int) ((today - beginningExtended) / day) + 1;
|
|
||||||
|
|
||||||
int checks[] = new int[nDaysExtended];
|
|
||||||
|
|
||||||
// explicit checks
|
|
||||||
for (Repetition rep : reps)
|
|
||||||
{
|
|
||||||
int offset = (int) ((rep.timestamp - beginningExtended) / day);
|
|
||||||
checks[nDaysExtended - offset - 1] = 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
// implicit checks
|
|
||||||
for (int i = 0; i < nDays; i++)
|
|
||||||
{
|
|
||||||
int counter = 0;
|
|
||||||
|
|
||||||
for (int j = 0; j < freq_den; j++)
|
|
||||||
if (checks[i + j] == 2) counter++;
|
|
||||||
|
|
||||||
if (counter >= freq_num) checks[i] = Math.max(checks[i], 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
ActiveAndroid.beginTransaction();
|
ActiveAndroid.beginTransaction();
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
for (int i = 0; i < nDays; i++)
|
new Delete().from(Checkmark.class).where("habit = ?", id).execute();
|
||||||
{
|
new Delete().from(Repetition.class).where("habit = ?", id).execute();
|
||||||
Checkmark c = new Checkmark();
|
new Delete().from(Score.class).where("habit = ?", id).execute();
|
||||||
c.habit = this;
|
new Delete().from(Streak.class).where("habit = ?", id).execute();
|
||||||
c.timestamp = today - i * day;
|
delete();
|
||||||
c.value = checks[i];
|
|
||||||
c.save();
|
|
||||||
}
|
|
||||||
|
|
||||||
ActiveAndroid.setTransactionSuccessful();
|
ActiveAndroid.setTransactionSuccessful();
|
||||||
}
|
}
|
||||||
@@ -389,68 +238,15 @@ public class Habit extends Model
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Checkmark getNewestCheckmark()
|
public Uri getUri()
|
||||||
{
|
{
|
||||||
return new Select().from(Checkmark.class)
|
return Uri.parse(String.format("content://org.isoron.uhabits/habit/%d", getId()));
|
||||||
.where("habit = ?", getId())
|
|
||||||
.orderBy("timestamp desc")
|
|
||||||
.limit(1)
|
|
||||||
.executeSingle();
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getRepsCount(int days)
|
|
||||||
{
|
|
||||||
long timeTo = DateHelper.getStartOfToday();
|
|
||||||
long timeFrom = timeTo - DateHelper.millisecondsInOneDay * days;
|
|
||||||
return selectRepsFromTo(timeFrom, timeTo).count();
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean hasImplicitRepToday()
|
|
||||||
{
|
|
||||||
long today = DateHelper.getStartOfToday();
|
|
||||||
int reps[] = getCheckmarks(today - DateHelper.millisecondsInOneDay, today);
|
|
||||||
return (reps[0] > 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Repetition getOldestRep()
|
|
||||||
{
|
|
||||||
return (Repetition) selectReps().limit(1).executeSingle();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Repetition getOldestRepNewerThan(long timestamp)
|
|
||||||
{
|
|
||||||
return selectReps()
|
|
||||||
.where("timestamp > ?", timestamp)
|
|
||||||
.limit(1)
|
|
||||||
.executeSingle();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void toggleRepetition(long timestamp)
|
|
||||||
{
|
|
||||||
if (hasRep(timestamp))
|
|
||||||
{
|
|
||||||
deleteReps(timestamp);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Repetition rep = new Repetition();
|
|
||||||
rep.habit = this;
|
|
||||||
rep.timestamp = timestamp;
|
|
||||||
rep.save();
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteScoresNewerThan(timestamp);
|
|
||||||
deleteCheckmarksNewerThan(timestamp);
|
|
||||||
deleteStreaksNewerThan(timestamp);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void archive()
|
public void archive()
|
||||||
{
|
{
|
||||||
archived = 1;
|
archived = 1;
|
||||||
position = 9999;
|
|
||||||
save();
|
save();
|
||||||
|
|
||||||
Habit.rebuildOrder();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void unarchive()
|
public void unarchive()
|
||||||
@@ -463,349 +259,4 @@ public class Habit extends Model
|
|||||||
{
|
{
|
||||||
return archived != 0;
|
return archived != 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void toggleRepetitionToday()
|
|
||||||
{
|
|
||||||
toggleRepetition(DateHelper.getStartOfToday());
|
|
||||||
}
|
|
||||||
|
|
||||||
public Score getNewestScore()
|
|
||||||
{
|
|
||||||
return new Select().from(Score.class).where("habit = ?", getId()).orderBy("timestamp desc")
|
|
||||||
.limit(1).executeSingle();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void deleteScoresNewerThan(long timestamp)
|
|
||||||
{
|
|
||||||
new Delete().from(Score.class).where("habit = ?", getId()).and("timestamp >= ?", timestamp)
|
|
||||||
.execute();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Integer getScore()
|
|
||||||
{
|
|
||||||
int beginningScore;
|
|
||||||
long beginningTime;
|
|
||||||
|
|
||||||
long today = DateHelper.getStartOfDay(DateHelper.getLocalTime());
|
|
||||||
long day = DateHelper.millisecondsInOneDay;
|
|
||||||
|
|
||||||
double freq = ((double) freq_num) / freq_den;
|
|
||||||
double multiplier = Math.pow(0.5, 1.0 / (14.0 / freq - 1));
|
|
||||||
|
|
||||||
Score newestScore = getNewestScore();
|
|
||||||
if (newestScore == null)
|
|
||||||
{
|
|
||||||
Repetition oldestRep = getOldestRep();
|
|
||||||
if (oldestRep == null) return 0;
|
|
||||||
beginningTime = oldestRep.timestamp;
|
|
||||||
beginningScore = 0;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
beginningTime = newestScore.timestamp + day;
|
|
||||||
beginningScore = newestScore.score;
|
|
||||||
}
|
|
||||||
|
|
||||||
long nDays = (today - beginningTime) / day;
|
|
||||||
if (nDays < 0) return newestScore.score;
|
|
||||||
|
|
||||||
int reps[] = getCheckmarks(beginningTime, today);
|
|
||||||
|
|
||||||
ActiveAndroid.beginTransaction();
|
|
||||||
int lastScore = beginningScore;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
for (int i = 0; i < reps.length; i++)
|
|
||||||
{
|
|
||||||
Score s = new Score();
|
|
||||||
s.habit = this;
|
|
||||||
s.timestamp = beginningTime + day * i;
|
|
||||||
s.score = (int) (lastScore * multiplier);
|
|
||||||
if (reps[reps.length - i - 1] == 2)
|
|
||||||
{
|
|
||||||
s.score += 1000000;
|
|
||||||
s.score = Math.min(s.score, 19259500);
|
|
||||||
}
|
|
||||||
s.save();
|
|
||||||
|
|
||||||
lastScore = s.score;
|
|
||||||
}
|
|
||||||
|
|
||||||
ActiveAndroid.setTransactionSuccessful();
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
ActiveAndroid.endTransaction();
|
|
||||||
}
|
|
||||||
|
|
||||||
return lastScore;
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<Score> getScores(long fromTimestamp, long toTimestamp)
|
|
||||||
{
|
|
||||||
return getScores(fromTimestamp, toTimestamp, 1, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<Score> getScores(long fromTimestamp, long toTimestamp, int divisor, long offset)
|
|
||||||
{
|
|
||||||
return new Select().from(Score.class).where("habit = ? and timestamp > ? and " +
|
|
||||||
"timestamp <= ? and (timestamp - ?) % ? = 0", getId(), fromTimestamp, toTimestamp,
|
|
||||||
offset, divisor).execute();
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<Streak> getStreaks()
|
|
||||||
{
|
|
||||||
updateStreaks();
|
|
||||||
|
|
||||||
return new Select()
|
|
||||||
.from(Streak.class)
|
|
||||||
.where("habit = ?", getId())
|
|
||||||
.orderBy("end asc")
|
|
||||||
.execute();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Streak getNewestStreak()
|
|
||||||
{
|
|
||||||
return new Select()
|
|
||||||
.from(Streak.class)
|
|
||||||
.where("habit = ?", getId())
|
|
||||||
.orderBy("end desc")
|
|
||||||
.limit(1)
|
|
||||||
.executeSingle();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void updateStreaks()
|
|
||||||
{
|
|
||||||
long beginning;
|
|
||||||
long today = DateHelper.getStartOfToday();
|
|
||||||
long day = DateHelper.millisecondsInOneDay;
|
|
||||||
|
|
||||||
Streak newestStreak = getNewestStreak();
|
|
||||||
if(newestStreak == null)
|
|
||||||
{
|
|
||||||
Repetition oldestRep = getOldestRep();
|
|
||||||
if (oldestRep == null) return;
|
|
||||||
|
|
||||||
beginning = oldestRep.timestamp;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Repetition oldestRep = getOldestRepNewerThan(newestStreak.end);
|
|
||||||
if (oldestRep == null) return;
|
|
||||||
|
|
||||||
beginning = oldestRep.timestamp;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(beginning > today) return;
|
|
||||||
|
|
||||||
int checks[] = getCheckmarks(beginning, today);
|
|
||||||
ArrayList<Long> list = new ArrayList<>();
|
|
||||||
|
|
||||||
long current = beginning;
|
|
||||||
list.add(current);
|
|
||||||
|
|
||||||
for(int i = 1; i < checks.length; i++)
|
|
||||||
{
|
|
||||||
current += day;
|
|
||||||
int j = checks.length - i - 1;
|
|
||||||
|
|
||||||
if((checks[j + 1] == 0 && checks[j] > 0)) list.add(current);
|
|
||||||
if((checks[j + 1] > 0 && checks[j] == 0)) list.add(current - day);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(list.size() % 2 == 1)
|
|
||||||
list.add(current);
|
|
||||||
|
|
||||||
ActiveAndroid.beginTransaction();
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
for (int i = 0; i < list.size(); i += 2)
|
|
||||||
{
|
|
||||||
Streak streak = new Streak();
|
|
||||||
streak.habit = this;
|
|
||||||
streak.start = list.get(i);
|
|
||||||
streak.end = list.get(i + 1);
|
|
||||||
streak.length = (streak.end - streak.start) / day + 1;
|
|
||||||
streak.save();
|
|
||||||
}
|
|
||||||
|
|
||||||
ActiveAndroid.setTransactionSuccessful();
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
ActiveAndroid.endTransaction();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class CreateCommand extends Command
|
|
||||||
{
|
|
||||||
private Habit model;
|
|
||||||
private Long savedId;
|
|
||||||
|
|
||||||
public CreateCommand(Habit model)
|
|
||||||
{
|
|
||||||
this.model = model;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void execute()
|
|
||||||
{
|
|
||||||
Habit savedHabit = new Habit(model);
|
|
||||||
if (savedId == null)
|
|
||||||
{
|
|
||||||
savedHabit.save();
|
|
||||||
savedId = savedHabit.getId();
|
|
||||||
} else
|
|
||||||
{
|
|
||||||
savedHabit.save(savedId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void undo()
|
|
||||||
{
|
|
||||||
Habit.get(savedId).delete();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Integer getExecuteStringId()
|
|
||||||
{
|
|
||||||
return R.string.toast_habit_created;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Integer getUndoStringId()
|
|
||||||
{
|
|
||||||
return R.string.toast_habit_deleted;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public class EditCommand extends Command
|
|
||||||
{
|
|
||||||
private Habit original;
|
|
||||||
private Habit modified;
|
|
||||||
private long savedId;
|
|
||||||
private boolean hasIntervalChanged;
|
|
||||||
|
|
||||||
public EditCommand(Habit modified)
|
|
||||||
{
|
|
||||||
this.savedId = getId();
|
|
||||||
this.modified = new Habit(modified);
|
|
||||||
this.original = new Habit(Habit.this);
|
|
||||||
|
|
||||||
hasIntervalChanged = (this.original.freq_den != this.modified.freq_den ||
|
|
||||||
this.original.freq_num != this.modified.freq_num);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void execute()
|
|
||||||
{
|
|
||||||
Habit habit = Habit.get(savedId);
|
|
||||||
habit.copyAttributes(modified);
|
|
||||||
habit.save();
|
|
||||||
if (hasIntervalChanged)
|
|
||||||
{
|
|
||||||
habit.deleteCheckmarksNewerThan(0);
|
|
||||||
habit.deleteStreaksNewerThan(0);
|
|
||||||
habit.deleteScoresNewerThan(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void undo()
|
|
||||||
{
|
|
||||||
Habit habit = Habit.get(savedId);
|
|
||||||
habit.copyAttributes(original);
|
|
||||||
habit.save();
|
|
||||||
if (hasIntervalChanged)
|
|
||||||
{
|
|
||||||
habit.deleteCheckmarksNewerThan(0);
|
|
||||||
habit.deleteStreaksNewerThan(0);
|
|
||||||
habit.deleteScoresNewerThan(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public Integer getExecuteStringId()
|
|
||||||
{
|
|
||||||
return R.string.toast_habit_changed;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Integer getUndoStringId()
|
|
||||||
{
|
|
||||||
return R.string.toast_habit_changed_back;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class ToggleRepetitionCommand extends Command
|
|
||||||
{
|
|
||||||
private Long offset;
|
|
||||||
|
|
||||||
public ToggleRepetitionCommand(long offset)
|
|
||||||
{
|
|
||||||
this.offset = offset;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void execute()
|
|
||||||
{
|
|
||||||
toggleRepetition(offset);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void undo()
|
|
||||||
{
|
|
||||||
execute();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class ArchiveCommand extends Command
|
|
||||||
{
|
|
||||||
@Override
|
|
||||||
public void execute()
|
|
||||||
{
|
|
||||||
archive();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void undo()
|
|
||||||
{
|
|
||||||
unarchive();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Integer getExecuteStringId()
|
|
||||||
{
|
|
||||||
return R.string.toast_habit_archived;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Integer getUndoStringId()
|
|
||||||
{
|
|
||||||
return R.string.toast_habit_unarchived;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class UnarchiveCommand extends Command
|
|
||||||
{
|
|
||||||
@Override
|
|
||||||
public void execute()
|
|
||||||
{
|
|
||||||
unarchive();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void undo()
|
|
||||||
{
|
|
||||||
archive();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Integer getExecuteStringId()
|
|
||||||
{
|
|
||||||
return R.string.toast_habit_unarchived;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Integer getUndoStringId()
|
|
||||||
{
|
|
||||||
return R.string.toast_habit_archived;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,20 @@
|
|||||||
/* Copyright (C) 2016 Alinson Santos Xavier
|
/*
|
||||||
|
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify it
|
* This file is part of Loop Habit Tracker.
|
||||||
* under the terms of the GNU General Public License as published by the Free
|
|
||||||
* Software Foundation, either version 3 of the License, or (at your option)
|
|
||||||
* any later version.
|
|
||||||
*
|
*
|
||||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
* it under the terms of the GNU General Public License as published by the
|
||||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
* Free Software Foundation, either version 3 of the License, or (at your
|
||||||
|
* option) any later version.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
* more details.
|
* more details.
|
||||||
*
|
*
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License along
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.isoron.uhabits.models;
|
package org.isoron.uhabits.models;
|
||||||
@@ -21,11 +24,12 @@ import com.activeandroid.annotation.Column;
|
|||||||
import com.activeandroid.annotation.Table;
|
import com.activeandroid.annotation.Table;
|
||||||
|
|
||||||
@Table(name = "Repetitions")
|
@Table(name = "Repetitions")
|
||||||
public class Repetition extends Model {
|
public class Repetition extends Model
|
||||||
|
{
|
||||||
|
|
||||||
@Column(name = "habit")
|
@Column(name = "habit")
|
||||||
public Habit habit;
|
public Habit habit;
|
||||||
|
|
||||||
@Column(name = "timestamp")
|
@Column(name = "timestamp")
|
||||||
public Long timestamp;
|
public Long timestamp;
|
||||||
}
|
}
|
||||||
|
|||||||
159
app/src/main/java/org/isoron/uhabits/models/RepetitionList.java
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||||
|
*
|
||||||
|
* This file is part of Loop Habit Tracker.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by the
|
||||||
|
* Free Software Foundation, either version 3 of the License, or (at your
|
||||||
|
* option) any later version.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
|
* more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along
|
||||||
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.isoron.uhabits.models;
|
||||||
|
|
||||||
|
import android.database.Cursor;
|
||||||
|
import android.database.sqlite.SQLiteDatabase;
|
||||||
|
|
||||||
|
import com.activeandroid.Cache;
|
||||||
|
import com.activeandroid.query.Delete;
|
||||||
|
import com.activeandroid.query.From;
|
||||||
|
import com.activeandroid.query.Select;
|
||||||
|
|
||||||
|
import org.isoron.helpers.DateHelper;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Calendar;
|
||||||
|
import java.util.GregorianCalendar;
|
||||||
|
import java.util.HashMap;
|
||||||
|
|
||||||
|
public class RepetitionList
|
||||||
|
{
|
||||||
|
|
||||||
|
private Habit habit;
|
||||||
|
|
||||||
|
public RepetitionList(Habit habit)
|
||||||
|
{
|
||||||
|
this.habit = habit;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected From select()
|
||||||
|
{
|
||||||
|
return new Select().from(Repetition.class)
|
||||||
|
.where("habit = ?", habit.getId())
|
||||||
|
.orderBy("timestamp");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected From selectFromTo(long timeFrom, long timeTo)
|
||||||
|
{
|
||||||
|
return select().and("timestamp >= ?", timeFrom).and("timestamp <= ?", timeTo);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean contains(long timestamp)
|
||||||
|
{
|
||||||
|
int count = select().where("timestamp = ?", timestamp).count();
|
||||||
|
return (count > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void delete(long timestamp)
|
||||||
|
{
|
||||||
|
new Delete().from(Repetition.class)
|
||||||
|
.where("habit = ?", habit.getId())
|
||||||
|
.and("timestamp = ?", timestamp)
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Repetition getOldestNewerThan(long timestamp)
|
||||||
|
{
|
||||||
|
return select().where("timestamp > ?", timestamp).limit(1).executeSingle();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void toggle(long timestamp)
|
||||||
|
{
|
||||||
|
timestamp = DateHelper.getStartOfDay(timestamp);
|
||||||
|
|
||||||
|
if (contains(timestamp))
|
||||||
|
{
|
||||||
|
delete(timestamp);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Repetition rep = new Repetition();
|
||||||
|
rep.habit = habit;
|
||||||
|
rep.timestamp = timestamp;
|
||||||
|
rep.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
habit.scores.deleteNewerThan(timestamp);
|
||||||
|
habit.checkmarks.deleteNewerThan(timestamp);
|
||||||
|
habit.streaks.deleteNewerThan(timestamp);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Repetition getOldest()
|
||||||
|
{
|
||||||
|
return (Repetition) select().limit(1).executeSingle();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasImplicitRepToday()
|
||||||
|
{
|
||||||
|
long today = DateHelper.getStartOfToday();
|
||||||
|
int reps[] = habit.checkmarks.getValues(today - DateHelper.millisecondsInOneDay, today);
|
||||||
|
return (reps[0] > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public HashMap<Long, Integer[]> getWeekdayFrequency()
|
||||||
|
{
|
||||||
|
Repetition oldestRep = getOldest();
|
||||||
|
if(oldestRep == null) return new HashMap<>();
|
||||||
|
|
||||||
|
String query = "select strftime('%Y', timestamp / 1000, 'unixepoch') as year," +
|
||||||
|
"strftime('%m', timestamp / 1000, 'unixepoch') as month," +
|
||||||
|
"strftime('%w', timestamp / 1000, 'unixepoch') as weekday, " +
|
||||||
|
"count(*) from repetitions " +
|
||||||
|
"where habit = ? " +
|
||||||
|
"group by year, month, weekday";
|
||||||
|
|
||||||
|
String[] params = { habit.getId().toString() };
|
||||||
|
|
||||||
|
SQLiteDatabase db = Cache.openDatabase();
|
||||||
|
Cursor cursor = db.rawQuery(query, params);
|
||||||
|
|
||||||
|
if(!cursor.moveToFirst()) return new HashMap<>();
|
||||||
|
|
||||||
|
HashMap <Long, Integer[]> map = new HashMap<>();
|
||||||
|
GregorianCalendar date = DateHelper.getStartOfTodayCalendar();
|
||||||
|
|
||||||
|
do
|
||||||
|
{
|
||||||
|
int year = Integer.parseInt(cursor.getString(0));
|
||||||
|
int month = Integer.parseInt(cursor.getString(1));
|
||||||
|
int weekday = (Integer.parseInt(cursor.getString(2)) + 1) % 7;
|
||||||
|
int count = cursor.getInt(3);
|
||||||
|
|
||||||
|
date.set(year, month - 1, 1);
|
||||||
|
long timestamp = date.getTimeInMillis();
|
||||||
|
|
||||||
|
Integer[] list = map.get(timestamp);
|
||||||
|
|
||||||
|
if(list == null)
|
||||||
|
{
|
||||||
|
list = new Integer[7];
|
||||||
|
Arrays.fill(list, 0);
|
||||||
|
map.put(timestamp, list);
|
||||||
|
}
|
||||||
|
|
||||||
|
list[weekday] = count;
|
||||||
|
}
|
||||||
|
while (cursor.moveToNext());
|
||||||
|
cursor.close();
|
||||||
|
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,17 +1,20 @@
|
|||||||
/* Copyright (C) 2016 Alinson Santos Xavier
|
/*
|
||||||
|
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify it
|
* This file is part of Loop Habit Tracker.
|
||||||
* under the terms of the GNU General Public License as published by the Free
|
|
||||||
* Software Foundation, either version 3 of the License, or (at your option)
|
|
||||||
* any later version.
|
|
||||||
*
|
*
|
||||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
* it under the terms of the GNU General Public License as published by the
|
||||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
* Free Software Foundation, either version 3 of the License, or (at your
|
||||||
|
* option) any later version.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
* more details.
|
* more details.
|
||||||
*
|
*
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License along
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.isoron.uhabits.models;
|
package org.isoron.uhabits.models;
|
||||||
@@ -23,12 +26,16 @@ import com.activeandroid.annotation.Table;
|
|||||||
@Table(name = "Score")
|
@Table(name = "Score")
|
||||||
public class Score extends Model
|
public class Score extends Model
|
||||||
{
|
{
|
||||||
@Column(name = "habit")
|
public static final int HALF_STAR_CUTOFF = 9629750;
|
||||||
public Habit habit;
|
public static final int FULL_STAR_CUTOFF = 15407600;
|
||||||
|
public static final int MAX_SCORE = 19259500;
|
||||||
@Column(name = "timestamp")
|
|
||||||
public Long timestamp;
|
@Column(name = "habit")
|
||||||
|
public Habit habit;
|
||||||
@Column(name = "score")
|
|
||||||
public Integer score;
|
@Column(name = "timestamp")
|
||||||
|
public Long timestamp;
|
||||||
|
|
||||||
|
@Column(name = "score")
|
||||||
|
public Integer score;
|
||||||
}
|
}
|
||||||
|
|||||||
169
app/src/main/java/org/isoron/uhabits/models/ScoreList.java
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||||
|
*
|
||||||
|
* This file is part of Loop Habit Tracker.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by the
|
||||||
|
* Free Software Foundation, either version 3 of the License, or (at your
|
||||||
|
* option) any later version.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
|
* more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along
|
||||||
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.isoron.uhabits.models;
|
||||||
|
|
||||||
|
import android.database.Cursor;
|
||||||
|
import android.database.sqlite.SQLiteDatabase;
|
||||||
|
|
||||||
|
import com.activeandroid.ActiveAndroid;
|
||||||
|
import com.activeandroid.Cache;
|
||||||
|
import com.activeandroid.query.Delete;
|
||||||
|
import com.activeandroid.query.Select;
|
||||||
|
|
||||||
|
import org.isoron.helpers.DateHelper;
|
||||||
|
|
||||||
|
public class ScoreList
|
||||||
|
{
|
||||||
|
private Habit habit;
|
||||||
|
|
||||||
|
public ScoreList(Habit habit)
|
||||||
|
{
|
||||||
|
this.habit = habit;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getCurrentStarStatus()
|
||||||
|
{
|
||||||
|
int score = getNewestValue();
|
||||||
|
|
||||||
|
if(score >= Score.FULL_STAR_CUTOFF) return 2;
|
||||||
|
else if(score >= Score.HALF_STAR_CUTOFF) return 1;
|
||||||
|
else return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Score getNewest()
|
||||||
|
{
|
||||||
|
return new Select().from(Score.class)
|
||||||
|
.where("habit = ?", habit.getId())
|
||||||
|
.orderBy("timestamp desc")
|
||||||
|
.limit(1)
|
||||||
|
.executeSingle();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void deleteNewerThan(long timestamp)
|
||||||
|
{
|
||||||
|
new Delete().from(Score.class)
|
||||||
|
.where("habit = ?", habit.getId())
|
||||||
|
.and("timestamp >= ?", timestamp)
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getNewestValue()
|
||||||
|
{
|
||||||
|
int beginningScore;
|
||||||
|
long beginningTime;
|
||||||
|
|
||||||
|
long today = DateHelper.getStartOfDay(DateHelper.getLocalTime());
|
||||||
|
long day = DateHelper.millisecondsInOneDay;
|
||||||
|
|
||||||
|
double freq = ((double) habit.freqNum) / habit.freqDen;
|
||||||
|
double multiplier = Math.pow(0.5, 1.0 / (14.0 / freq - 1));
|
||||||
|
|
||||||
|
Score newestScore = getNewest();
|
||||||
|
if (newestScore == null)
|
||||||
|
{
|
||||||
|
Repetition oldestRep = habit.repetitions.getOldest();
|
||||||
|
if (oldestRep == null) return 0;
|
||||||
|
beginningTime = oldestRep.timestamp;
|
||||||
|
beginningScore = 0;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
beginningTime = newestScore.timestamp + day;
|
||||||
|
beginningScore = newestScore.score;
|
||||||
|
}
|
||||||
|
|
||||||
|
long nDays = (today - beginningTime) / day;
|
||||||
|
if (nDays < 0) return newestScore.score;
|
||||||
|
|
||||||
|
int reps[] = habit.checkmarks.getValues(beginningTime, today);
|
||||||
|
|
||||||
|
ActiveAndroid.beginTransaction();
|
||||||
|
int lastScore = beginningScore;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
for (int i = 0; i < reps.length; i++)
|
||||||
|
{
|
||||||
|
Score s = new Score();
|
||||||
|
s.habit = habit;
|
||||||
|
s.timestamp = beginningTime + day * i;
|
||||||
|
s.score = (int) (lastScore * multiplier);
|
||||||
|
if (reps[reps.length - i - 1] == 2)
|
||||||
|
{
|
||||||
|
s.score += 1000000;
|
||||||
|
s.score = Math.min(s.score, Score.MAX_SCORE);
|
||||||
|
}
|
||||||
|
s.save();
|
||||||
|
|
||||||
|
lastScore = s.score;
|
||||||
|
}
|
||||||
|
|
||||||
|
ActiveAndroid.setTransactionSuccessful();
|
||||||
|
} finally
|
||||||
|
{
|
||||||
|
ActiveAndroid.endTransaction();
|
||||||
|
}
|
||||||
|
|
||||||
|
return lastScore;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int[] getAllValues(Long fromTimestamp, Long toTimestamp, Integer divisor)
|
||||||
|
{
|
||||||
|
// Force rebuild of the score table
|
||||||
|
getNewestValue();
|
||||||
|
|
||||||
|
Long offset = toTimestamp - (divisor - 1) * DateHelper.millisecondsInOneDay;
|
||||||
|
|
||||||
|
String query = "select ((timestamp - ?) / ?) as time, avg(score) from Score " +
|
||||||
|
"where habit = ? and timestamp > ? and timestamp <= ? " +
|
||||||
|
"group by time order by time desc";
|
||||||
|
|
||||||
|
String params[] = { offset.toString(), divisor.toString(), habit.getId().toString(),
|
||||||
|
fromTimestamp.toString(), toTimestamp.toString()};
|
||||||
|
|
||||||
|
SQLiteDatabase db = Cache.openDatabase();
|
||||||
|
Cursor cursor = db.rawQuery(query, params);
|
||||||
|
|
||||||
|
if(!cursor.moveToFirst()) return new int[0];
|
||||||
|
|
||||||
|
int k = 0;
|
||||||
|
int[] scores = new int[cursor.getCount()];
|
||||||
|
|
||||||
|
do
|
||||||
|
{
|
||||||
|
scores[k++] = (int) cursor.getLong(1);
|
||||||
|
}
|
||||||
|
while (cursor.moveToNext());
|
||||||
|
|
||||||
|
cursor.close();
|
||||||
|
return scores;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public int[] getAllValues(int divisor)
|
||||||
|
{
|
||||||
|
Repetition oldestRep = habit.repetitions.getOldest();
|
||||||
|
if(oldestRep == null) return new int[0];
|
||||||
|
|
||||||
|
long fromTimestamp = oldestRep.timestamp;
|
||||||
|
long toTimestamp = DateHelper.getStartOfToday();
|
||||||
|
return getAllValues(fromTimestamp, toTimestamp, divisor);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,17 +1,20 @@
|
|||||||
/* Copyright (C) 2016 Alinson Santos Xavier
|
/*
|
||||||
|
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify it
|
* This file is part of Loop Habit Tracker.
|
||||||
* under the terms of the GNU General Public License as published by the Free
|
|
||||||
* Software Foundation, either version 3 of the License, or (at your option)
|
|
||||||
* any later version.
|
|
||||||
*
|
*
|
||||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
* it under the terms of the GNU General Public License as published by the
|
||||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
* Free Software Foundation, either version 3 of the License, or (at your
|
||||||
|
* option) any later version.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
* more details.
|
* more details.
|
||||||
*
|
*
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License along
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.isoron.uhabits.models;
|
package org.isoron.uhabits.models;
|
||||||
|
|||||||
129
app/src/main/java/org/isoron/uhabits/models/StreakList.java
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||||
|
*
|
||||||
|
* This file is part of Loop Habit Tracker.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by the
|
||||||
|
* Free Software Foundation, either version 3 of the License, or (at your
|
||||||
|
* option) any later version.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
|
* more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along
|
||||||
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.isoron.uhabits.models;
|
||||||
|
|
||||||
|
import com.activeandroid.ActiveAndroid;
|
||||||
|
import com.activeandroid.query.Delete;
|
||||||
|
import com.activeandroid.query.Select;
|
||||||
|
|
||||||
|
import org.isoron.helpers.DateHelper;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class StreakList
|
||||||
|
{
|
||||||
|
private Habit habit;
|
||||||
|
|
||||||
|
public StreakList(Habit habit)
|
||||||
|
{
|
||||||
|
this.habit = habit;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Streak> getAll()
|
||||||
|
{
|
||||||
|
rebuild();
|
||||||
|
|
||||||
|
return new Select().from(Streak.class)
|
||||||
|
.where("habit = ?", habit.getId())
|
||||||
|
.orderBy("end asc")
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Streak getNewest()
|
||||||
|
{
|
||||||
|
return new Select().from(Streak.class)
|
||||||
|
.where("habit = ?", habit.getId())
|
||||||
|
.orderBy("end desc")
|
||||||
|
.limit(1)
|
||||||
|
.executeSingle();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void rebuild()
|
||||||
|
{
|
||||||
|
long beginning;
|
||||||
|
long today = DateHelper.getStartOfToday();
|
||||||
|
long day = DateHelper.millisecondsInOneDay;
|
||||||
|
|
||||||
|
Streak newestStreak = getNewest();
|
||||||
|
if (newestStreak != null)
|
||||||
|
{
|
||||||
|
beginning = newestStreak.start;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Repetition oldestRep = habit.repetitions.getOldest();
|
||||||
|
if (oldestRep == null) return;
|
||||||
|
|
||||||
|
beginning = oldestRep.timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (beginning > today) return;
|
||||||
|
|
||||||
|
int checks[] = habit.checkmarks.getValues(beginning, today);
|
||||||
|
ArrayList<Long> list = new ArrayList<>();
|
||||||
|
|
||||||
|
long current = beginning;
|
||||||
|
list.add(current);
|
||||||
|
|
||||||
|
for (int i = 1; i < checks.length; i++)
|
||||||
|
{
|
||||||
|
current += day;
|
||||||
|
int j = checks.length - i - 1;
|
||||||
|
|
||||||
|
if ((checks[j + 1] == 0 && checks[j] > 0)) list.add(current);
|
||||||
|
if ((checks[j + 1] > 0 && checks[j] == 0)) list.add(current - day);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (list.size() % 2 == 1) list.add(current);
|
||||||
|
|
||||||
|
ActiveAndroid.beginTransaction();
|
||||||
|
|
||||||
|
if(newestStreak != null) newestStreak.delete();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
for (int i = 0; i < list.size(); i += 2)
|
||||||
|
{
|
||||||
|
Streak streak = new Streak();
|
||||||
|
streak.habit = habit;
|
||||||
|
streak.start = list.get(i);
|
||||||
|
streak.end = list.get(i + 1);
|
||||||
|
streak.length = (streak.end - streak.start) / day + 1;
|
||||||
|
streak.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
ActiveAndroid.setTransactionSuccessful();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
ActiveAndroid.endTransaction();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void deleteNewerThan(long timestamp)
|
||||||
|
{
|
||||||
|
new Delete().from(Streak.class)
|
||||||
|
.where("habit = ?", habit.getId())
|
||||||
|
.and("end >= ?", timestamp - DateHelper.millisecondsInOneDay)
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
}
|
||||||
206
app/src/main/java/org/isoron/uhabits/views/CheckmarkView.java
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||||
|
*
|
||||||
|
* This file is part of Loop Habit Tracker.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by the
|
||||||
|
* Free Software Foundation, either version 3 of the License, or (at your
|
||||||
|
* option) any later version.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
|
* more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along
|
||||||
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.isoron.uhabits.views;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.graphics.Canvas;
|
||||||
|
import android.graphics.Color;
|
||||||
|
import android.graphics.Paint;
|
||||||
|
import android.graphics.Rect;
|
||||||
|
import android.graphics.Typeface;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.text.Layout;
|
||||||
|
import android.text.StaticLayout;
|
||||||
|
import android.text.TextPaint;
|
||||||
|
import android.util.AttributeSet;
|
||||||
|
import android.view.View;
|
||||||
|
|
||||||
|
import org.isoron.helpers.ColorHelper;
|
||||||
|
import org.isoron.uhabits.R;
|
||||||
|
import org.isoron.uhabits.models.Habit;
|
||||||
|
|
||||||
|
public class CheckmarkView extends View
|
||||||
|
{
|
||||||
|
private Paint pCard;
|
||||||
|
private Paint pIcon;
|
||||||
|
|
||||||
|
private int primaryColor;
|
||||||
|
private int backgroundColor;
|
||||||
|
private int timesColor;
|
||||||
|
private int darkGrey;
|
||||||
|
|
||||||
|
private int width;
|
||||||
|
private int height;
|
||||||
|
private int leftMargin;
|
||||||
|
private int topMargin;
|
||||||
|
private int padding;
|
||||||
|
private String label;
|
||||||
|
|
||||||
|
private String fa_check;
|
||||||
|
private String fa_times;
|
||||||
|
private String fa_full_star;
|
||||||
|
private String fa_half_star;
|
||||||
|
private String fa_empty_star;
|
||||||
|
|
||||||
|
private int check_status;
|
||||||
|
private int star_status;
|
||||||
|
|
||||||
|
private Rect rect;
|
||||||
|
private TextPaint textPaint;
|
||||||
|
private StaticLayout labelLayout;
|
||||||
|
|
||||||
|
public CheckmarkView(Context context)
|
||||||
|
{
|
||||||
|
super(context);
|
||||||
|
init(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
public CheckmarkView(Context context, AttributeSet attrs)
|
||||||
|
{
|
||||||
|
super(context, attrs);
|
||||||
|
init(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void init(Context context)
|
||||||
|
{
|
||||||
|
Typeface fontawesome =
|
||||||
|
Typeface.createFromAsset(context.getAssets(), "fontawesome-webfont.ttf");
|
||||||
|
|
||||||
|
pCard = new Paint();
|
||||||
|
pCard.setAntiAlias(true);
|
||||||
|
|
||||||
|
pIcon = new Paint();
|
||||||
|
pIcon.setAntiAlias(true);
|
||||||
|
pIcon.setTypeface(fontawesome);
|
||||||
|
pIcon.setTextAlign(Paint.Align.CENTER);
|
||||||
|
|
||||||
|
textPaint = new TextPaint();
|
||||||
|
textPaint.setColor(Color.WHITE);
|
||||||
|
textPaint.setAntiAlias(true);
|
||||||
|
|
||||||
|
fa_check = context.getString(R.string.fa_check);
|
||||||
|
fa_times = context.getString(R.string.fa_times);
|
||||||
|
fa_empty_star = context.getString(R.string.fa_star_o);
|
||||||
|
fa_half_star = context.getString(R.string.fa_star_half_o);
|
||||||
|
fa_full_star = context.getString(R.string.fa_star);
|
||||||
|
|
||||||
|
primaryColor = ColorHelper.palette[10];
|
||||||
|
backgroundColor = Color.argb(255, 255, 255, 255);
|
||||||
|
timesColor = Color.argb(128, 255, 255, 255);
|
||||||
|
darkGrey = Color.argb(64, 0, 0, 0);
|
||||||
|
|
||||||
|
rect = new Rect();
|
||||||
|
check_status = 2;
|
||||||
|
star_status = 0;
|
||||||
|
label = "Wake up early";
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setHabit(Habit habit)
|
||||||
|
{
|
||||||
|
this.check_status = habit.checkmarks.getCurrentValue();
|
||||||
|
this.star_status = habit.scores.getCurrentStarStatus();
|
||||||
|
this.primaryColor = Color.argb(230, Color.red(habit.color), Color.green(habit.color), Color.blue(habit.color));
|
||||||
|
this.label = habit.name;
|
||||||
|
updateLabel();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDraw(Canvas canvas)
|
||||||
|
{
|
||||||
|
super.onDraw(canvas);
|
||||||
|
|
||||||
|
drawBackground(canvas);
|
||||||
|
drawCheckmark(canvas);
|
||||||
|
drawLabel(canvas);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void drawBackground(Canvas canvas)
|
||||||
|
{
|
||||||
|
int color = (check_status == 2 ? primaryColor : darkGrey);
|
||||||
|
|
||||||
|
pCard.setColor(color);
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
|
||||||
|
canvas.drawRoundRect(leftMargin, topMargin, width - leftMargin, height - topMargin, padding,
|
||||||
|
padding, pCard);
|
||||||
|
else
|
||||||
|
canvas.drawRect(leftMargin, topMargin, width - leftMargin, height - topMargin, pCard);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void drawCheckmark(Canvas canvas)
|
||||||
|
{
|
||||||
|
String text = (check_status == 0 ? fa_times : fa_check);
|
||||||
|
int color = (check_status == 2 ? Color.WHITE : timesColor);
|
||||||
|
|
||||||
|
pIcon.setColor(color);
|
||||||
|
pIcon.setTextSize(width * 0.5f);
|
||||||
|
pIcon.getTextBounds(text, 0, 1, rect);
|
||||||
|
|
||||||
|
// canvas.drawLine(0, 0.67f * height, width, 0.67f * height, pIcon);
|
||||||
|
|
||||||
|
int y = (int) ((0.67f * height - rect.bottom - rect.top) / 2);
|
||||||
|
canvas.drawText(text, width / 2, y, pIcon);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void drawLabel(Canvas canvas)
|
||||||
|
{
|
||||||
|
canvas.save();
|
||||||
|
float y;
|
||||||
|
int nLines = labelLayout.getLineCount();
|
||||||
|
|
||||||
|
if(nLines == 1)
|
||||||
|
y = height * 0.8f - padding;
|
||||||
|
else
|
||||||
|
y = height * 0.7f - padding;
|
||||||
|
|
||||||
|
canvas.translate(leftMargin + padding, y);
|
||||||
|
|
||||||
|
labelLayout.draw(canvas);
|
||||||
|
canvas.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
|
||||||
|
{
|
||||||
|
int width = MeasureSpec.getSize(widthMeasureSpec);
|
||||||
|
setMeasuredDimension(width, (int) (width * 1.25));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight)
|
||||||
|
{
|
||||||
|
this.width = getMeasuredWidth();
|
||||||
|
this.height = getMeasuredHeight();
|
||||||
|
|
||||||
|
leftMargin = (int) (width * 0.015);
|
||||||
|
topMargin = (int) (height * 0.015);
|
||||||
|
padding = 8 * leftMargin;
|
||||||
|
textPaint.setTextSize(0.15f * width);
|
||||||
|
|
||||||
|
updateLabel();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateLabel()
|
||||||
|
{
|
||||||
|
textPaint.setColor(Color.WHITE);
|
||||||
|
labelLayout = new StaticLayout(label, textPaint, width - 2 * leftMargin - 2 * padding,
|
||||||
|
Layout.Alignment.ALIGN_CENTER, 1.0f, 0.0f, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,295 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||||
|
*
|
||||||
|
* This file is part of Loop Habit Tracker.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by the
|
||||||
|
* Free Software Foundation, either version 3 of the License, or (at your
|
||||||
|
* option) any later version.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
|
* more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along
|
||||||
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.isoron.uhabits.views;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.graphics.Canvas;
|
||||||
|
import android.graphics.Color;
|
||||||
|
import android.graphics.Paint;
|
||||||
|
import android.graphics.RectF;
|
||||||
|
import android.util.AttributeSet;
|
||||||
|
|
||||||
|
import org.isoron.helpers.ColorHelper;
|
||||||
|
import org.isoron.helpers.DateHelper;
|
||||||
|
import org.isoron.uhabits.models.Habit;
|
||||||
|
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.Calendar;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.GregorianCalendar;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Random;
|
||||||
|
import java.util.TimeZone;
|
||||||
|
|
||||||
|
public class HabitFrequencyView extends ScrollableDataView
|
||||||
|
{
|
||||||
|
|
||||||
|
private Paint pGrid;
|
||||||
|
private float em;
|
||||||
|
private Habit habit;
|
||||||
|
private SimpleDateFormat dfMonth;
|
||||||
|
private SimpleDateFormat dfYear;
|
||||||
|
|
||||||
|
private Paint pText, pGraph;
|
||||||
|
private RectF rect, prevRect;
|
||||||
|
private int baseSize;
|
||||||
|
private int paddingTop;
|
||||||
|
|
||||||
|
private int columnWidth;
|
||||||
|
private int columnHeight;
|
||||||
|
private int nColumns;
|
||||||
|
|
||||||
|
private int textColor;
|
||||||
|
private int dimmedTextColor;
|
||||||
|
private int[] colors;
|
||||||
|
private int primaryColor;
|
||||||
|
private boolean isBackgroundTransparent;
|
||||||
|
|
||||||
|
private HashMap<Long, Integer[]> frequency;
|
||||||
|
private String wdays[];
|
||||||
|
|
||||||
|
public HabitFrequencyView(Context context, AttributeSet attrs)
|
||||||
|
{
|
||||||
|
super(context, attrs);
|
||||||
|
this.primaryColor = ColorHelper.palette[7];
|
||||||
|
this.frequency = new HashMap<>();
|
||||||
|
wdays = DateHelper.getShortDayNames();
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setHabit(Habit habit)
|
||||||
|
{
|
||||||
|
this.habit = habit;
|
||||||
|
createColors();
|
||||||
|
refreshData();
|
||||||
|
postInvalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void init()
|
||||||
|
{
|
||||||
|
refreshData();
|
||||||
|
createPaints();
|
||||||
|
createColors();
|
||||||
|
|
||||||
|
dfMonth = new SimpleDateFormat("MMM", Locale.getDefault());
|
||||||
|
dfYear = new SimpleDateFormat("yyyy", Locale.getDefault());
|
||||||
|
|
||||||
|
dfMonth.setTimeZone(TimeZone.getTimeZone("GMT"));
|
||||||
|
dfYear.setTimeZone(TimeZone.getTimeZone("GMT"));
|
||||||
|
|
||||||
|
rect = new RectF();
|
||||||
|
prevRect = new RectF();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createColors()
|
||||||
|
{
|
||||||
|
if(habit != null)
|
||||||
|
this.primaryColor = habit.color;
|
||||||
|
|
||||||
|
if (isBackgroundTransparent)
|
||||||
|
{
|
||||||
|
primaryColor = ColorHelper.setSaturation(primaryColor, 0.75f);
|
||||||
|
primaryColor = ColorHelper.setValue(primaryColor, 1.0f);
|
||||||
|
|
||||||
|
textColor = Color.argb(192, 255, 255, 255);
|
||||||
|
dimmedTextColor = Color.argb(128, 255, 255, 255);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
textColor = Color.argb(64, 0, 0, 0);
|
||||||
|
dimmedTextColor = Color.argb(16, 0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
colors = new int[4];
|
||||||
|
|
||||||
|
colors[0] = Color.rgb(230, 230, 230);
|
||||||
|
colors[3] = primaryColor;
|
||||||
|
colors[1] = ColorHelper.mixColors(colors[0], colors[3], 0.66f);
|
||||||
|
colors[2] = ColorHelper.mixColors(colors[0], colors[3], 0.33f);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void createPaints()
|
||||||
|
{
|
||||||
|
pText = new Paint();
|
||||||
|
pText.setAntiAlias(true);
|
||||||
|
|
||||||
|
pGraph = new Paint();
|
||||||
|
pGraph.setTextAlign(Paint.Align.CENTER);
|
||||||
|
pGraph.setAntiAlias(true);
|
||||||
|
|
||||||
|
pGrid = new Paint();
|
||||||
|
pGrid.setAntiAlias(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
|
||||||
|
{
|
||||||
|
int width = MeasureSpec.getSize(widthMeasureSpec);
|
||||||
|
int height = MeasureSpec.getSize(heightMeasureSpec);
|
||||||
|
setMeasuredDimension(width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight)
|
||||||
|
{
|
||||||
|
if(height < 9) height = 200;
|
||||||
|
|
||||||
|
baseSize = height / 8;
|
||||||
|
setScrollerBucketSize(baseSize);
|
||||||
|
|
||||||
|
columnWidth = baseSize;
|
||||||
|
columnHeight = 8 * baseSize;
|
||||||
|
nColumns = width / baseSize;
|
||||||
|
paddingTop = 0;
|
||||||
|
|
||||||
|
pText.setTextSize(baseSize * 0.4f);
|
||||||
|
pGraph.setTextSize(baseSize * 0.4f);
|
||||||
|
pGraph.setStrokeWidth(baseSize * 0.1f);
|
||||||
|
pGrid.setStrokeWidth(baseSize * 0.05f);
|
||||||
|
em = pText.getFontSpacing();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void refreshData()
|
||||||
|
{
|
||||||
|
if(isInEditMode())
|
||||||
|
generateRandomData();
|
||||||
|
else if(habit != null)
|
||||||
|
frequency = habit.repetitions.getWeekdayFrequency();
|
||||||
|
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void generateRandomData()
|
||||||
|
{
|
||||||
|
GregorianCalendar date = DateHelper.getStartOfTodayCalendar();
|
||||||
|
date.set(Calendar.DAY_OF_MONTH, 1);
|
||||||
|
Random rand = new Random();
|
||||||
|
frequency.clear();
|
||||||
|
|
||||||
|
for(int i = 0; i < 40; i++)
|
||||||
|
{
|
||||||
|
Integer values[] = new Integer[7];
|
||||||
|
for(int j = 0; j < 7; j++)
|
||||||
|
values[j] = rand.nextInt(5);
|
||||||
|
|
||||||
|
frequency.put(date.getTimeInMillis(), values);
|
||||||
|
date.add(Calendar.MONTH, -1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDraw(Canvas canvas)
|
||||||
|
{
|
||||||
|
super.onDraw(canvas);
|
||||||
|
|
||||||
|
rect.set(0, 0, nColumns * columnWidth, columnHeight);
|
||||||
|
rect.offset(0, paddingTop);
|
||||||
|
|
||||||
|
drawGrid(canvas, rect);
|
||||||
|
|
||||||
|
pText.setTextAlign(Paint.Align.CENTER);
|
||||||
|
pText.setColor(textColor);
|
||||||
|
pGraph.setColor(primaryColor);
|
||||||
|
prevRect.setEmpty();
|
||||||
|
|
||||||
|
GregorianCalendar currentDate = DateHelper.getStartOfTodayCalendar();
|
||||||
|
|
||||||
|
currentDate.set(Calendar.DAY_OF_MONTH, 1);
|
||||||
|
currentDate.add(Calendar.MONTH, -nColumns + 2 - getDataOffset());
|
||||||
|
|
||||||
|
for(int i = 0; i < nColumns - 1; i++)
|
||||||
|
{
|
||||||
|
rect.set(0, 0, columnWidth, columnHeight);
|
||||||
|
rect.offset(i * columnWidth, 0);
|
||||||
|
|
||||||
|
drawColumn(canvas, rect, currentDate);
|
||||||
|
currentDate.add(Calendar.MONTH, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void drawColumn(Canvas canvas, RectF rect, GregorianCalendar date)
|
||||||
|
{
|
||||||
|
Integer values[] = frequency.get(date.getTimeInMillis());
|
||||||
|
float rowHeight = rect.height() / 8.0f;
|
||||||
|
prevRect.set(rect);
|
||||||
|
|
||||||
|
for (int i = 0; i < 7; i++)
|
||||||
|
{
|
||||||
|
rect.set(0, 0, baseSize, baseSize);
|
||||||
|
rect.offset(prevRect.left, prevRect.top + columnWidth * i);
|
||||||
|
|
||||||
|
if(values != null)
|
||||||
|
drawMarker(canvas, rect, values[i]);
|
||||||
|
|
||||||
|
rect.offset(0, rowHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
drawFooter(canvas, rect, date);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void drawFooter(Canvas canvas, RectF rect, GregorianCalendar date)
|
||||||
|
{
|
||||||
|
Date time = date.getTime();
|
||||||
|
|
||||||
|
canvas.drawText(dfMonth.format(time), rect.centerX(), rect.centerY() - 0.1f * em, pText);
|
||||||
|
|
||||||
|
if(date.get(Calendar.MONTH) == 1)
|
||||||
|
canvas.drawText(dfYear.format(time), rect.centerX(), rect.centerY() + 0.9f * em, pText);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void drawMarker(Canvas canvas, RectF rect, Integer value)
|
||||||
|
{
|
||||||
|
float padding = rect.height() * 0.2f;
|
||||||
|
float radius = (rect.height() - 2 * padding) / 2.0f / 4.0f * Math.min(value, 4);
|
||||||
|
|
||||||
|
pGraph.setColor(colors[Math.min(3, Math.max(0, value - 1))]);
|
||||||
|
canvas.drawCircle(rect.centerX(), rect.centerY(), radius, pGraph);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void drawGrid(Canvas canvas, RectF rGrid)
|
||||||
|
{
|
||||||
|
int nRows = 7;
|
||||||
|
float rowHeight = rGrid.height() / (nRows + 1);
|
||||||
|
|
||||||
|
pText.setTextAlign(Paint.Align.LEFT);
|
||||||
|
pText.setColor(textColor);
|
||||||
|
pGrid.setColor(dimmedTextColor);
|
||||||
|
|
||||||
|
for (int i = 0; i < nRows; i++)
|
||||||
|
{
|
||||||
|
canvas.drawText(wdays[i], rGrid.right - columnWidth,
|
||||||
|
rGrid.top + rowHeight / 2 + 0.25f * em, pText);
|
||||||
|
|
||||||
|
pGrid.setStrokeWidth(1f);
|
||||||
|
canvas.drawLine(rGrid.left, rGrid.top, rGrid.right, rGrid.top, pGrid);
|
||||||
|
|
||||||
|
rGrid.offset(0, rowHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas.drawLine(rGrid.left, rGrid.top, rGrid.right, rGrid.top, pGrid);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setIsBackgroundTransparent(boolean isBackgroundTransparent)
|
||||||
|
{
|
||||||
|
this.isBackgroundTransparent = isBackgroundTransparent;
|
||||||
|
createColors();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,17 +1,20 @@
|
|||||||
/* Copyright (C) 2016 Alinson Santos Xavier
|
/*
|
||||||
|
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify it
|
* This file is part of Loop Habit Tracker.
|
||||||
* under the terms of the GNU General Public License as published by the Free
|
|
||||||
* Software Foundation, either version 3 of the License, or (at your option)
|
|
||||||
* any later version.
|
|
||||||
*
|
*
|
||||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
* it under the terms of the GNU General Public License as published by the
|
||||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
* Free Software Foundation, either version 3 of the License, or (at your
|
||||||
|
* option) any later version.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
* more details.
|
* more details.
|
||||||
*
|
*
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License along
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.isoron.uhabits.views;
|
package org.isoron.uhabits.views;
|
||||||
@@ -22,232 +25,386 @@ import android.graphics.Color;
|
|||||||
import android.graphics.Paint;
|
import android.graphics.Paint;
|
||||||
import android.graphics.Paint.Align;
|
import android.graphics.Paint.Align;
|
||||||
import android.graphics.Rect;
|
import android.graphics.Rect;
|
||||||
import android.support.v4.view.MotionEventCompat;
|
import android.os.AsyncTask;
|
||||||
|
import android.util.AttributeSet;
|
||||||
import android.view.MotionEvent;
|
import android.view.MotionEvent;
|
||||||
import android.view.View;
|
|
||||||
|
|
||||||
import org.isoron.helpers.ColorHelper;
|
import org.isoron.helpers.ColorHelper;
|
||||||
import org.isoron.helpers.DateHelper;
|
import org.isoron.helpers.DateHelper;
|
||||||
import org.isoron.uhabits.R;
|
import org.isoron.uhabits.R;
|
||||||
import org.isoron.uhabits.models.Checkmark;
|
|
||||||
import org.isoron.uhabits.models.Habit;
|
import org.isoron.uhabits.models.Habit;
|
||||||
|
|
||||||
import java.text.SimpleDateFormat;
|
import java.text.SimpleDateFormat;
|
||||||
import java.util.Calendar;
|
import java.util.Calendar;
|
||||||
import java.util.GregorianCalendar;
|
import java.util.GregorianCalendar;
|
||||||
import java.util.List;
|
import java.util.Locale;
|
||||||
|
import java.util.Random;
|
||||||
|
|
||||||
public class HabitHistoryView extends View
|
public class HabitHistoryView extends ScrollableDataView
|
||||||
{
|
{
|
||||||
|
|
||||||
private Habit habit;
|
private Habit habit;
|
||||||
private int[] checks;
|
private int[] checkmarks;
|
||||||
|
|
||||||
private Context context;
|
|
||||||
private Paint pSquareBg, pSquareFg, pTextHeader;
|
private Paint pSquareBg, pSquareFg, pTextHeader;
|
||||||
|
private int squareSpacing;
|
||||||
|
|
||||||
private int squareSize, squareSpacing;
|
private float squareTextOffset;
|
||||||
private int nColumns, offsetWeeks;
|
private float headerTextOffset;
|
||||||
|
|
||||||
private int colorPrimary, colorPrimaryBrighter, grey;
|
private int columnWidth;
|
||||||
private Float prevX, prevY;
|
private int columnHeight;
|
||||||
|
private int nColumns;
|
||||||
|
|
||||||
public HabitHistoryView(Context context, Habit habit, int squareSize)
|
private String wdays[];
|
||||||
|
private SimpleDateFormat dfMonth;
|
||||||
|
private SimpleDateFormat dfYear;
|
||||||
|
|
||||||
|
private Calendar baseDate;
|
||||||
|
private int nDays;
|
||||||
|
private int todayWeekday;
|
||||||
|
private int colors[];
|
||||||
|
private Rect baseLocation;
|
||||||
|
private int primaryColor;
|
||||||
|
|
||||||
|
private boolean isBackgroundTransparent;
|
||||||
|
private int textColor;
|
||||||
|
private boolean isEditable;
|
||||||
|
|
||||||
|
public HabitHistoryView(Context context, AttributeSet attrs)
|
||||||
|
{
|
||||||
|
super(context, attrs);
|
||||||
|
this.primaryColor = ColorHelper.palette[7];
|
||||||
|
this.checkmarks = new int[0];
|
||||||
|
this.isEditable = false;
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setHabit(Habit habit)
|
||||||
{
|
{
|
||||||
super(context);
|
|
||||||
this.habit = habit;
|
this.habit = habit;
|
||||||
this.context = context;
|
createColors();
|
||||||
this.squareSize = squareSize;
|
refreshData();
|
||||||
|
postInvalidate();
|
||||||
|
}
|
||||||
|
|
||||||
colorPrimary = habit.color;
|
private void init()
|
||||||
colorPrimaryBrighter = ColorHelper.mixColors(colorPrimary, Color.WHITE, 0.5f);
|
{
|
||||||
grey = Color.rgb(230, 230, 230);
|
refreshData();
|
||||||
squareSpacing = 2;
|
createPaints();
|
||||||
|
createColors();
|
||||||
|
|
||||||
pTextHeader = new Paint();
|
wdays = DateHelper.getShortDayNames();
|
||||||
pTextHeader.setColor(Color.LTGRAY);
|
dfMonth = new SimpleDateFormat("MMM", Locale.getDefault());
|
||||||
pTextHeader.setTextAlign(Align.LEFT);
|
dfYear = new SimpleDateFormat("yyyy", Locale.getDefault());
|
||||||
pTextHeader.setTextSize(squareSize * 0.5f);
|
|
||||||
pTextHeader.setAntiAlias(true);
|
|
||||||
|
|
||||||
pSquareBg = new Paint();
|
baseLocation = new Rect();
|
||||||
pSquareBg.setColor(habit.color);
|
}
|
||||||
|
|
||||||
pSquareFg = new Paint();
|
private void updateDate()
|
||||||
pSquareFg.setColor(Color.WHITE);
|
{
|
||||||
pSquareFg.setAntiAlias(true);
|
baseDate = new GregorianCalendar();
|
||||||
pSquareFg.setTextSize(squareSize * 0.5f);
|
baseDate.setTimeInMillis(DateHelper.getLocalTime());
|
||||||
pSquareFg.setTextAlign(Align.CENTER);
|
baseDate.add(Calendar.DAY_OF_YEAR, -(getDataOffset() - 1) * 7);
|
||||||
|
|
||||||
|
nDays = (nColumns - 1) * 7;
|
||||||
|
todayWeekday = new GregorianCalendar().get(Calendar.DAY_OF_WEEK) % 7;
|
||||||
|
|
||||||
|
baseDate.add(Calendar.DAY_OF_YEAR, -nDays);
|
||||||
|
baseDate.add(Calendar.DAY_OF_YEAR, -todayWeekday);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
|
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
|
||||||
{
|
{
|
||||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
int width = MeasureSpec.getSize(widthMeasureSpec);
|
||||||
setMeasuredDimension(getMeasuredWidth(), 8 * squareSize);
|
int height = MeasureSpec.getSize(heightMeasureSpec);
|
||||||
|
setMeasuredDimension(width, height);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onSizeChanged(int w, int h, int oldw, int oldh)
|
protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight)
|
||||||
{
|
{
|
||||||
nColumns = (w / squareSize) - 1;
|
if(height < 8) height = 200;
|
||||||
fetchReps();
|
int baseSize = height / 8;
|
||||||
|
setScrollerBucketSize(baseSize);
|
||||||
|
|
||||||
|
squareSpacing = (int) Math.floor(baseSize / 15.0);
|
||||||
|
int maxTextSize = getResources().getDimensionPixelSize(R.dimen.history_max_font_size);
|
||||||
|
float textSize = Math.min(baseSize * 0.5f, maxTextSize);
|
||||||
|
|
||||||
|
pSquareFg.setTextSize(textSize);
|
||||||
|
pTextHeader.setTextSize(textSize);
|
||||||
|
squareTextOffset = pSquareFg.getFontSpacing() * 0.4f;
|
||||||
|
headerTextOffset = pTextHeader.getFontSpacing() * 0.3f;
|
||||||
|
|
||||||
|
int rightLabelWidth = getWeekdayLabelWidth();
|
||||||
|
int horizontalPadding = getPaddingRight() + getPaddingLeft();
|
||||||
|
|
||||||
|
columnWidth = baseSize;
|
||||||
|
columnHeight = 8 * baseSize;
|
||||||
|
nColumns = (width - rightLabelWidth - horizontalPadding) / baseSize + 1;
|
||||||
|
|
||||||
|
updateDate();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void fetchReps()
|
private int getWeekdayLabelWidth()
|
||||||
{
|
{
|
||||||
Calendar currentDate = new GregorianCalendar();
|
int width = 0;
|
||||||
currentDate.add(Calendar.DAY_OF_YEAR, -offsetWeeks * 7);
|
Rect bounds = new Rect();
|
||||||
int dayOfWeek = currentDate.get(Calendar.DAY_OF_WEEK) % 7;
|
|
||||||
|
|
||||||
long dateTo = DateHelper.getStartOfToday();
|
for(String w : wdays)
|
||||||
for (int i = 0; i < 7 - dayOfWeek; i++)
|
{
|
||||||
dateTo += DateHelper.millisecondsInOneDay;
|
pSquareFg.getTextBounds(w, 0, w.length(), bounds);
|
||||||
|
width = Math.max(width, bounds.right);
|
||||||
|
}
|
||||||
|
|
||||||
for (int i = 0; i < offsetWeeks * 7; i++)
|
return width;
|
||||||
dateTo -= DateHelper.millisecondsInOneDay;
|
|
||||||
|
|
||||||
long dateFrom = dateTo;
|
|
||||||
for (int i = 0; i < nColumns * 7; i++)
|
|
||||||
dateFrom -= DateHelper.millisecondsInOneDay;
|
|
||||||
|
|
||||||
checks = habit.getCheckmarks(dateFrom, dateTo);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void createColors()
|
||||||
|
{
|
||||||
|
if(habit != null)
|
||||||
|
this.primaryColor = habit.color;
|
||||||
|
|
||||||
|
if(isBackgroundTransparent)
|
||||||
|
primaryColor = ColorHelper.setMinValue(primaryColor, 0.75f);
|
||||||
|
|
||||||
|
int red = Color.red(primaryColor);
|
||||||
|
int green = Color.green(primaryColor);
|
||||||
|
int blue = Color.blue(primaryColor);
|
||||||
|
|
||||||
|
if(isBackgroundTransparent)
|
||||||
|
{
|
||||||
|
colors = new int[3];
|
||||||
|
colors[0] = Color.argb(16, 255, 255, 255);
|
||||||
|
colors[1] = Color.argb(128, red, green, blue);
|
||||||
|
colors[2] = primaryColor;
|
||||||
|
textColor = Color.rgb(255, 255, 255);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
colors = new int[3];
|
||||||
|
colors[0] = Color.argb(25, 0, 0, 0);
|
||||||
|
colors[1] = Color.argb(127, red, green, blue);
|
||||||
|
colors[2] = primaryColor;
|
||||||
|
textColor = Color.argb(64, 0, 0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void createPaints()
|
||||||
|
{
|
||||||
|
pTextHeader = new Paint();
|
||||||
|
pTextHeader.setTextAlign(Align.LEFT);
|
||||||
|
pTextHeader.setAntiAlias(true);
|
||||||
|
|
||||||
|
pSquareBg = new Paint();
|
||||||
|
pSquareBg.setColor(primaryColor);
|
||||||
|
|
||||||
|
pSquareFg = new Paint();
|
||||||
|
pSquareFg.setColor(Color.WHITE);
|
||||||
|
pSquareFg.setAntiAlias(true);
|
||||||
|
pSquareFg.setTextAlign(Align.CENTER);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void refreshData()
|
||||||
|
{
|
||||||
|
if(isInEditMode())
|
||||||
|
generateRandomData();
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if(habit == null) return;
|
||||||
|
checkmarks = habit.checkmarks.getAllValues();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateDate();
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void generateRandomData()
|
||||||
|
{
|
||||||
|
Random random = new Random();
|
||||||
|
checkmarks = new int[100];
|
||||||
|
|
||||||
|
for(int i = 0; i < 100; i++)
|
||||||
|
if(random.nextFloat() < 0.3) checkmarks[i] = 2;
|
||||||
|
|
||||||
|
for(int i = 0; i < 100 - 7; i++)
|
||||||
|
{
|
||||||
|
int count = 0;
|
||||||
|
for (int j = 0; j < 7; j++)
|
||||||
|
if(checkmarks[i + j] != 0)
|
||||||
|
count++;
|
||||||
|
|
||||||
|
if(count >= 3) checkmarks[i] = Math.max(checkmarks[i], 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String previousMonth;
|
||||||
|
private String previousYear;
|
||||||
|
private boolean justPrintedYear;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onDraw(Canvas canvas)
|
protected void onDraw(Canvas canvas)
|
||||||
{
|
{
|
||||||
super.onDraw(canvas);
|
super.onDraw(canvas);
|
||||||
|
|
||||||
Rect square = new Rect(0, 0, squareSize - squareSpacing, squareSize - squareSpacing);
|
baseLocation.set(0, 0, columnWidth - squareSpacing, columnWidth - squareSpacing);
|
||||||
|
baseLocation.offset(getPaddingLeft(), getPaddingTop());
|
||||||
|
|
||||||
Calendar currentDate = new GregorianCalendar();
|
previousMonth = "";
|
||||||
currentDate.add(Calendar.DAY_OF_YEAR, - (offsetWeeks - 1) * 7);
|
previousYear = "";
|
||||||
|
justPrintedYear = false;
|
||||||
|
|
||||||
int nDays = nColumns * 7;
|
pTextHeader.setColor(textColor);
|
||||||
int todayWeekday = new GregorianCalendar().get(Calendar.DAY_OF_WEEK) % 7;
|
|
||||||
|
|
||||||
currentDate.add(Calendar.DAY_OF_YEAR, -nDays);
|
updateDate();
|
||||||
currentDate.add(Calendar.DAY_OF_YEAR, -todayWeekday);
|
GregorianCalendar currentDate = (GregorianCalendar) baseDate.clone();
|
||||||
|
|
||||||
SimpleDateFormat dfMonth = new SimpleDateFormat("MMM");
|
for (int column = 0; column < nColumns - 1; column++)
|
||||||
SimpleDateFormat dfYear = new SimpleDateFormat("yyyy");
|
|
||||||
|
|
||||||
String previousMonth = "";
|
|
||||||
String previousYear = "";
|
|
||||||
|
|
||||||
int colors[] = {grey, colorPrimaryBrighter, colorPrimary};
|
|
||||||
String markers[] =
|
|
||||||
{context.getString(R.string.fa_times), context.getString(R.string.fa_check),
|
|
||||||
context.getString(R.string.fa_check)};
|
|
||||||
|
|
||||||
float squareTextOffset = pSquareFg.getFontSpacing() * 0.4f;
|
|
||||||
float headerTextOffset = pTextHeader.getFontSpacing() * 0.3f;
|
|
||||||
boolean justPrintedYear = false;
|
|
||||||
|
|
||||||
int k = nDays;
|
|
||||||
for (int i = 0; i < nColumns; i++)
|
|
||||||
{
|
{
|
||||||
String month = dfMonth.format(currentDate.getTime());
|
drawColumn(canvas, baseLocation, currentDate, column);
|
||||||
String year = dfYear.format(currentDate.getTime());
|
baseLocation.offset(columnWidth, - columnHeight);
|
||||||
|
|
||||||
if (!month.equals(previousMonth))
|
|
||||||
{
|
|
||||||
int offset = 0;
|
|
||||||
if (justPrintedYear) offset += squareSize;
|
|
||||||
|
|
||||||
canvas.drawText(month, square.left + offset, square.bottom - headerTextOffset,
|
|
||||||
pTextHeader);
|
|
||||||
previousMonth = month;
|
|
||||||
justPrintedYear = false;
|
|
||||||
} else if (!year.equals(previousYear))
|
|
||||||
{
|
|
||||||
canvas.drawText(year, square.left, square.bottom - headerTextOffset, pTextHeader);
|
|
||||||
previousYear = year;
|
|
||||||
justPrintedYear = true;
|
|
||||||
} else
|
|
||||||
{
|
|
||||||
justPrintedYear = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
square.offset(0, squareSize);
|
|
||||||
|
|
||||||
for (int j = 0; j < 7; j++)
|
|
||||||
{
|
|
||||||
if (!(i == nColumns - 1 && offsetWeeks == 0 && j > todayWeekday))
|
|
||||||
{
|
|
||||||
if(k >= checks.length) pSquareBg.setColor(colors[0]);
|
|
||||||
else pSquareBg.setColor(colors[checks[k]]);
|
|
||||||
|
|
||||||
canvas.drawRect(square, pSquareBg);
|
|
||||||
canvas.drawText(Integer.toString(currentDate.get(Calendar.DAY_OF_MONTH)),
|
|
||||||
square.centerX(), square.centerY() + squareTextOffset, pSquareFg);
|
|
||||||
}
|
|
||||||
|
|
||||||
currentDate.add(Calendar.DAY_OF_MONTH, 1);
|
|
||||||
square.offset(0, squareSize);
|
|
||||||
k--;
|
|
||||||
}
|
|
||||||
|
|
||||||
square.offset(squareSize, -8 * squareSize);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
String wdays[] = {"Sat", "Sun", "Mon", "Tue", "Wed", "Thu", "Fri"};
|
drawAxis(canvas, baseLocation);
|
||||||
|
}
|
||||||
|
|
||||||
for (int i = 0; i < 7; i++)
|
private void drawColumn(Canvas canvas, Rect location, GregorianCalendar date, int column)
|
||||||
|
{
|
||||||
|
drawColumnHeader(canvas, location, date);
|
||||||
|
location.offset(0, columnWidth);
|
||||||
|
|
||||||
|
for (int j = 0; j < 7; j++)
|
||||||
{
|
{
|
||||||
square.offset(0, squareSize);
|
if (!(column == nColumns - 2 && getDataOffset() == 0 && j > todayWeekday))
|
||||||
canvas.drawText(wdays[i], square.left + headerTextOffset,
|
{
|
||||||
square.bottom - headerTextOffset, pTextHeader);
|
int checkmarkOffset = getDataOffset() * 7 + nDays - 7 * (column + 1) + todayWeekday - j;
|
||||||
|
drawSquare(canvas, location, date, checkmarkOffset);
|
||||||
|
}
|
||||||
|
|
||||||
|
date.add(Calendar.DAY_OF_MONTH, 1);
|
||||||
|
location.offset(0, columnWidth);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
private void drawSquare(Canvas canvas, Rect location, GregorianCalendar date,
|
||||||
public boolean onTouchEvent(MotionEvent event)
|
int checkmarkOffset)
|
||||||
{
|
{
|
||||||
int action = event.getAction();
|
if (checkmarkOffset >= checkmarks.length) pSquareBg.setColor(colors[0]);
|
||||||
|
else pSquareBg.setColor(colors[checkmarks[checkmarkOffset]]);
|
||||||
|
|
||||||
int pointerIndex = MotionEventCompat.getActionIndex(event);
|
canvas.drawRect(location, pSquareBg);
|
||||||
final float x = MotionEventCompat.getX(event, pointerIndex);
|
String text = Integer.toString(date.get(Calendar.DAY_OF_MONTH));
|
||||||
final float y = MotionEventCompat.getY(event, pointerIndex);
|
canvas.drawText(text, location.centerX(), location.centerY() + squareTextOffset, pSquareFg);
|
||||||
|
}
|
||||||
|
|
||||||
if (action == MotionEvent.ACTION_DOWN)
|
private void drawAxis(Canvas canvas, Rect location)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < 7; i++)
|
||||||
{
|
{
|
||||||
prevX = x;
|
location.offset(0, columnWidth);
|
||||||
prevY = y;
|
canvas.drawText(wdays[i], location.left + headerTextOffset,
|
||||||
|
location.bottom - headerTextOffset, pTextHeader);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (action == MotionEvent.ACTION_MOVE)
|
private boolean justSkippedColumn = false;
|
||||||
|
|
||||||
|
private void drawColumnHeader(Canvas canvas, Rect location, GregorianCalendar date)
|
||||||
|
{
|
||||||
|
String month = dfMonth.format(date.getTime());
|
||||||
|
String year = dfYear.format(date.getTime());
|
||||||
|
|
||||||
|
if (!month.equals(previousMonth))
|
||||||
{
|
{
|
||||||
float dx = x - prevX;
|
int offset = 0;
|
||||||
float dy = y - prevY;
|
if (justPrintedYear)
|
||||||
|
|
||||||
if (Math.abs(dy) > Math.abs(dx)) return false;
|
|
||||||
getParent().requestDisallowInterceptTouchEvent(true);
|
|
||||||
if(move(dx))
|
|
||||||
{
|
{
|
||||||
prevX = x;
|
offset += columnWidth;
|
||||||
prevY = y;
|
justSkippedColumn = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
canvas.drawText(month, location.left + offset, location.bottom - headerTextOffset,
|
||||||
|
pTextHeader);
|
||||||
|
|
||||||
|
previousMonth = month;
|
||||||
|
justPrintedYear = false;
|
||||||
}
|
}
|
||||||
|
else if (!year.equals(previousYear))
|
||||||
|
{
|
||||||
|
if(!justSkippedColumn)
|
||||||
|
{
|
||||||
|
canvas.drawText(year, location.left, location.bottom - headerTextOffset, pTextHeader);
|
||||||
|
previousYear = year;
|
||||||
|
justPrintedYear = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
justSkippedColumn = false;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
justSkippedColumn = false;
|
||||||
|
justPrintedYear = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setIsBackgroundTransparent(boolean isBackgroundTransparent)
|
||||||
|
{
|
||||||
|
this.isBackgroundTransparent = isBackgroundTransparent;
|
||||||
|
createColors();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onSingleTapUp(MotionEvent e)
|
||||||
|
{
|
||||||
|
if(!isEditable) return false;
|
||||||
|
|
||||||
|
int pointerId = e.getPointerId(0);
|
||||||
|
float x = e.getX(pointerId);
|
||||||
|
float y = e.getY(pointerId);
|
||||||
|
|
||||||
|
final Long timestamp = positionToTimestamp(x, y);
|
||||||
|
if(timestamp == null) return false;
|
||||||
|
|
||||||
|
new AsyncTask<Void, Void, Void>()
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
protected Void doInBackground(Void... params)
|
||||||
|
{
|
||||||
|
habit.repetitions.toggle(timestamp);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPostExecute(Void aVoid)
|
||||||
|
{
|
||||||
|
refreshData();
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
}.execute();
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean move(float dx)
|
private Long positionToTimestamp(float x, float y)
|
||||||
{
|
{
|
||||||
int newOffsetWeeks = offsetWeeks + (int) (dx / squareSize);
|
int col = (int) (x / columnWidth);
|
||||||
newOffsetWeeks = Math.max(0, newOffsetWeeks);
|
int row = (int) (y / columnWidth);
|
||||||
|
|
||||||
if (newOffsetWeeks != offsetWeeks)
|
if(row == 0) return null;
|
||||||
{
|
if(col == nColumns - 1) return null;
|
||||||
offsetWeeks = newOffsetWeeks;
|
|
||||||
fetchReps();
|
int offset = col * 7 + (row - 1);
|
||||||
invalidate();
|
Calendar date = (Calendar) baseDate.clone();
|
||||||
return true;
|
date.add(Calendar.DAY_OF_YEAR, offset);
|
||||||
}
|
|
||||||
else
|
if(DateHelper.getStartOfDay(date.getTimeInMillis()) > DateHelper.getStartOfToday())
|
||||||
return false;
|
return null;
|
||||||
|
|
||||||
|
return date.getTimeInMillis();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setIsEditable(boolean isEditable)
|
||||||
|
{
|
||||||
|
this.isEditable = isEditable;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,20 @@
|
|||||||
/* Copyright (C) 2016 Alinson Santos Xavier
|
/*
|
||||||
|
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify it
|
* This file is part of Loop Habit Tracker.
|
||||||
* under the terms of the GNU General Public License as published by the Free
|
|
||||||
* Software Foundation, either version 3 of the License, or (at your option)
|
|
||||||
* any later version.
|
|
||||||
*
|
*
|
||||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
* it under the terms of the GNU General Public License as published by the
|
||||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
* Free Software Foundation, either version 3 of the License, or (at your
|
||||||
|
* option) any later version.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
* more details.
|
* more details.
|
||||||
*
|
*
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License along
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.isoron.uhabits.views;
|
package org.isoron.uhabits.views;
|
||||||
@@ -20,10 +23,10 @@ import android.content.Context;
|
|||||||
import android.graphics.Canvas;
|
import android.graphics.Canvas;
|
||||||
import android.graphics.Color;
|
import android.graphics.Color;
|
||||||
import android.graphics.Paint;
|
import android.graphics.Paint;
|
||||||
|
import android.graphics.PorterDuff;
|
||||||
|
import android.graphics.PorterDuffXfermode;
|
||||||
import android.graphics.RectF;
|
import android.graphics.RectF;
|
||||||
import android.support.v4.view.MotionEventCompat;
|
import android.util.AttributeSet;
|
||||||
import android.view.MotionEvent;
|
|
||||||
import android.view.View;
|
|
||||||
|
|
||||||
import org.isoron.helpers.ColorHelper;
|
import org.isoron.helpers.ColorHelper;
|
||||||
import org.isoron.helpers.DateHelper;
|
import org.isoron.helpers.DateHelper;
|
||||||
@@ -31,90 +34,161 @@ import org.isoron.uhabits.models.Habit;
|
|||||||
import org.isoron.uhabits.models.Score;
|
import org.isoron.uhabits.models.Score;
|
||||||
|
|
||||||
import java.text.SimpleDateFormat;
|
import java.text.SimpleDateFormat;
|
||||||
import java.util.List;
|
import java.util.Locale;
|
||||||
|
import java.util.Random;
|
||||||
|
|
||||||
public class HabitScoreView extends View
|
public class HabitScoreView extends ScrollableDataView
|
||||||
{
|
{
|
||||||
public static final int BUCKET_SIZE = 7;
|
public static final int BUCKET_SIZE = 7;
|
||||||
|
public static final PorterDuffXfermode XFERMODE_CLEAR =
|
||||||
|
new PorterDuffXfermode(PorterDuff.Mode.CLEAR);
|
||||||
|
public static final PorterDuffXfermode XFERMODE_SRC =
|
||||||
|
new PorterDuffXfermode(PorterDuff.Mode.SRC);
|
||||||
|
|
||||||
private final Paint pGrid;
|
private Paint pGrid;
|
||||||
private final float em;
|
private float em;
|
||||||
private Habit habit;
|
private Habit habit;
|
||||||
private int columnWidth, columnHeight, nColumns;
|
private SimpleDateFormat dfMonth;
|
||||||
|
private SimpleDateFormat dfDay;
|
||||||
|
|
||||||
private Paint pText, pGraph;
|
private Paint pText, pGraph;
|
||||||
private int dataOffset;
|
private RectF rect, prevRect;
|
||||||
|
private int baseSize;
|
||||||
|
private int paddingTop;
|
||||||
|
|
||||||
private int barHeaderHeight;
|
private int columnWidth;
|
||||||
|
private int columnHeight;
|
||||||
|
private int nColumns;
|
||||||
|
|
||||||
|
private int textColor;
|
||||||
|
private int dimmedTextColor;
|
||||||
private int[] colors;
|
private int[] colors;
|
||||||
private float prevX;
|
private int[] scores;
|
||||||
private float prevY;
|
private int primaryColor;
|
||||||
private List<Score> scores;
|
private boolean isBackgroundTransparent;
|
||||||
|
|
||||||
public HabitScoreView(Context context, Habit habit, int columnWidth)
|
public HabitScoreView(Context context, AttributeSet attrs)
|
||||||
|
{
|
||||||
|
super(context, attrs);
|
||||||
|
this.primaryColor = ColorHelper.palette[7];
|
||||||
|
this.scores = new int[0];
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setHabit(Habit habit)
|
||||||
{
|
{
|
||||||
super(context);
|
|
||||||
this.habit = habit;
|
this.habit = habit;
|
||||||
this.columnWidth = columnWidth;
|
createColors();
|
||||||
|
refreshData();
|
||||||
|
postInvalidate();
|
||||||
|
}
|
||||||
|
|
||||||
pText = new Paint();
|
private void init()
|
||||||
pText.setColor(Color.LTGRAY);
|
{
|
||||||
pText.setTextAlign(Paint.Align.LEFT);
|
refreshData();
|
||||||
pText.setTextSize(columnWidth * 0.5f);
|
createPaints();
|
||||||
pText.setAntiAlias(true);
|
createColors();
|
||||||
|
|
||||||
pGraph = new Paint();
|
dfMonth = new SimpleDateFormat("MMM", Locale.getDefault());
|
||||||
pGraph.setTextAlign(Paint.Align.CENTER);
|
dfDay = new SimpleDateFormat("d", Locale.getDefault());
|
||||||
pGraph.setTextSize(columnWidth * 0.5f);
|
|
||||||
pGraph.setAntiAlias(true);
|
|
||||||
pGraph.setStrokeWidth(columnWidth * 0.1f);
|
|
||||||
|
|
||||||
pGrid = new Paint();
|
rect = new RectF();
|
||||||
pGrid.setColor(Color.LTGRAY);
|
prevRect = new RectF();
|
||||||
pGrid.setAntiAlias(true);
|
}
|
||||||
pGrid.setStrokeWidth(columnWidth * 0.05f);
|
|
||||||
|
|
||||||
columnHeight = 8 * columnWidth;
|
private void createColors()
|
||||||
barHeaderHeight = columnWidth;
|
{
|
||||||
em = pText.getFontSpacing();
|
if(habit != null)
|
||||||
|
this.primaryColor = habit.color;
|
||||||
|
|
||||||
|
if (isBackgroundTransparent)
|
||||||
|
{
|
||||||
|
primaryColor = ColorHelper.setSaturation(primaryColor, 0.75f);
|
||||||
|
primaryColor = ColorHelper.setValue(primaryColor, 1.0f);
|
||||||
|
|
||||||
|
textColor = Color.argb(192, 255, 255, 255);
|
||||||
|
dimmedTextColor = Color.argb(128, 255, 255, 255);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
textColor = Color.argb(64, 0, 0, 0);
|
||||||
|
dimmedTextColor = Color.argb(16, 0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
colors = new int[4];
|
colors = new int[4];
|
||||||
|
|
||||||
colors[0] = Color.rgb(230, 230, 230);
|
colors[0] = Color.rgb(230, 230, 230);
|
||||||
colors[3] = habit.color;
|
colors[3] = primaryColor;
|
||||||
colors[1] = ColorHelper.mixColors(colors[0], colors[3], 0.66f);
|
colors[1] = ColorHelper.mixColors(colors[0], colors[3], 0.66f);
|
||||||
colors[2] = ColorHelper.mixColors(colors[0], colors[3], 0.33f);
|
colors[2] = ColorHelper.mixColors(colors[0], colors[3], 0.33f);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void fetchScores()
|
protected void createPaints()
|
||||||
{
|
{
|
||||||
|
pText = new Paint();
|
||||||
|
pText.setAntiAlias(true);
|
||||||
|
|
||||||
long toTimestamp = DateHelper.getStartOfToday();
|
pGraph = new Paint();
|
||||||
for (int i = 0; i < dataOffset * BUCKET_SIZE; i++)
|
pGraph.setTextAlign(Paint.Align.CENTER);
|
||||||
toTimestamp -= DateHelper.millisecondsInOneDay;
|
pGraph.setAntiAlias(true);
|
||||||
|
|
||||||
long fromTimestamp = toTimestamp;
|
pGrid = new Paint();
|
||||||
for (int i = 0; i < nColumns * BUCKET_SIZE; i++)
|
pGrid.setAntiAlias(true);
|
||||||
fromTimestamp -= DateHelper.millisecondsInOneDay;
|
|
||||||
|
|
||||||
scores = habit.getScores(fromTimestamp, toTimestamp, BUCKET_SIZE * DateHelper.millisecondsInOneDay,
|
|
||||||
toTimestamp);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
|
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
|
||||||
{
|
{
|
||||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
int width = MeasureSpec.getSize(widthMeasureSpec);
|
||||||
setMeasuredDimension(getMeasuredWidth(), columnHeight + 2 * barHeaderHeight);
|
int height = MeasureSpec.getSize(heightMeasureSpec);
|
||||||
|
setMeasuredDimension(width, height);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onSizeChanged(int w, int h, int oldw, int oldh)
|
protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight)
|
||||||
{
|
{
|
||||||
super.onSizeChanged(w, h, oldw, oldh);
|
if(height < 9) height = 200;
|
||||||
nColumns = w / columnWidth;
|
|
||||||
fetchScores();
|
baseSize = height / 9;
|
||||||
|
setScrollerBucketSize(baseSize);
|
||||||
|
|
||||||
|
columnWidth = baseSize;
|
||||||
|
columnHeight = 8 * baseSize;
|
||||||
|
nColumns = width / baseSize;
|
||||||
|
paddingTop = (int) (baseSize * 0.15f);
|
||||||
|
|
||||||
|
pText.setTextSize(baseSize * 0.5f);
|
||||||
|
pGraph.setTextSize(baseSize * 0.5f);
|
||||||
|
pGraph.setStrokeWidth(baseSize * 0.1f);
|
||||||
|
pGrid.setStrokeWidth(baseSize * 0.05f);
|
||||||
|
em = pText.getFontSpacing();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void refreshData()
|
||||||
|
{
|
||||||
|
if(isInEditMode())
|
||||||
|
generateRandomData();
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (habit == null) return;
|
||||||
|
scores = habit.scores.getAllValues(BUCKET_SIZE * DateHelper.millisecondsInOneDay);
|
||||||
|
}
|
||||||
|
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void generateRandomData()
|
||||||
|
{
|
||||||
|
Random random = new Random();
|
||||||
|
scores = new int[100];
|
||||||
|
scores[0] = Score.MAX_SCORE / 2;
|
||||||
|
|
||||||
|
for(int i = 1; i < 100; i++)
|
||||||
|
{
|
||||||
|
int step = Score.MAX_SCORE / 10;
|
||||||
|
scores[i] = scores[i - 1] + random.nextInt(step * 2) - step;
|
||||||
|
scores[i] = Math.max(0, Math.min(Score.MAX_SCORE, scores[i]));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -123,70 +197,71 @@ public class HabitScoreView extends View
|
|||||||
super.onDraw(canvas);
|
super.onDraw(canvas);
|
||||||
|
|
||||||
float lineHeight = pText.getFontSpacing();
|
float lineHeight = pText.getFontSpacing();
|
||||||
float barHeaderOffset = lineHeight * 0.4f;
|
|
||||||
|
|
||||||
RectF rGrid = new RectF(0, 0, nColumns * columnWidth, columnHeight);
|
rect.set(0, 0, nColumns * columnWidth, columnHeight);
|
||||||
rGrid.offset(0, barHeaderHeight);
|
rect.offset(0, paddingTop);
|
||||||
drawGrid(canvas, rGrid);
|
|
||||||
|
|
||||||
SimpleDateFormat dfMonth = new SimpleDateFormat("MMM");
|
drawGrid(canvas, rect);
|
||||||
SimpleDateFormat dfDay = new SimpleDateFormat("d");
|
|
||||||
|
|
||||||
String previousMonth = "";
|
String previousMonth = "";
|
||||||
|
|
||||||
pGraph.setColor(habit.color);
|
pText.setTextAlign(Paint.Align.CENTER);
|
||||||
RectF prevR = null;
|
pText.setColor(textColor);
|
||||||
|
pGraph.setColor(primaryColor);
|
||||||
|
prevRect.setEmpty();
|
||||||
|
|
||||||
for (int offset = nColumns - scores.size(); offset < nColumns; offset++)
|
long currentDate = DateHelper.getStartOfToday();
|
||||||
|
|
||||||
|
for(int k = 0; k < nColumns + getDataOffset() - 1; k++)
|
||||||
|
currentDate -= 7 * DateHelper.millisecondsInOneDay;
|
||||||
|
|
||||||
|
for (int k = 0; k < nColumns; k++)
|
||||||
{
|
{
|
||||||
Score score = scores.get(offset - nColumns + scores.size());
|
String month = dfMonth.format(currentDate);
|
||||||
String month = dfMonth.format(score.timestamp);
|
String day = dfDay.format(currentDate);
|
||||||
String day = dfDay.format(score.timestamp);
|
|
||||||
|
|
||||||
long s = score.score;
|
int score = 0;
|
||||||
double sRelative = ((double) s) / Habit.MAX_SCORE;
|
int offset = nColumns - k - 1 + getDataOffset();
|
||||||
|
if(offset < scores.length) score = scores[offset];
|
||||||
|
|
||||||
|
double sRelative = ((double) score) / Score.MAX_SCORE;
|
||||||
int height = (int) (columnHeight * sRelative);
|
int height = (int) (columnHeight * sRelative);
|
||||||
|
|
||||||
RectF r = new RectF(0, 0, columnWidth, columnWidth);
|
rect.set(0, 0, baseSize, baseSize);
|
||||||
r.offset(offset * columnWidth,
|
rect.offset(k * columnWidth, paddingTop + columnHeight - height - columnWidth / 2);
|
||||||
barHeaderHeight + columnHeight - height - columnWidth / 2);
|
|
||||||
|
|
||||||
if (prevR != null)
|
if (!prevRect.isEmpty())
|
||||||
{
|
{
|
||||||
drawLine(canvas, prevR, r);
|
drawLine(canvas, prevRect, rect);
|
||||||
drawMarker(canvas, prevR);
|
drawMarker(canvas, prevRect);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (offset == nColumns - 1) drawMarker(canvas, r);
|
if (k == nColumns - 1) drawMarker(canvas, rect);
|
||||||
|
|
||||||
prevR = r;
|
prevRect.set(rect);
|
||||||
|
|
||||||
|
rect.set(0, 0, columnWidth, columnHeight);
|
||||||
|
rect.offset(k * columnWidth, paddingTop);
|
||||||
|
|
||||||
r = new RectF(0, 0, columnWidth, columnHeight);
|
|
||||||
r.offset(offset * columnWidth, barHeaderHeight);
|
|
||||||
if (!month.equals(previousMonth))
|
if (!month.equals(previousMonth))
|
||||||
{
|
canvas.drawText(month, rect.centerX(), rect.bottom + lineHeight * 1.2f, pText);
|
||||||
canvas.drawText(month, r.centerX(), r.bottom + lineHeight * 1.2f, pText);
|
else
|
||||||
} else
|
canvas.drawText(day, rect.centerX(), rect.bottom + lineHeight * 1.2f, pText);
|
||||||
{
|
|
||||||
canvas.drawText(day, r.centerX(), r.bottom + lineHeight * 1.2f, pText);
|
|
||||||
}
|
|
||||||
|
|
||||||
previousMonth = month;
|
previousMonth = month;
|
||||||
|
currentDate += 7 * DateHelper.millisecondsInOneDay;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void drawGrid(Canvas canvas, RectF rGrid)
|
private void drawGrid(Canvas canvas, RectF rGrid)
|
||||||
{
|
{
|
||||||
// pGrid.setColor(Color.rgb(230, 230, 230));
|
|
||||||
// pGrid.setStyle(Paint.Style.STROKE);
|
|
||||||
// canvas.drawRect(rGrid, pGrid);
|
|
||||||
|
|
||||||
int nRows = 5;
|
int nRows = 5;
|
||||||
float rowHeight = rGrid.height() / nRows;
|
float rowHeight = rGrid.height() / nRows;
|
||||||
|
|
||||||
pGrid.setColor(Color.rgb(240, 240, 240));
|
pText.setTextAlign(Paint.Align.LEFT);
|
||||||
|
pText.setColor(textColor);
|
||||||
|
pGrid.setColor(dimmedTextColor);
|
||||||
|
|
||||||
for (int i = 0; i < nRows; i++)
|
for (int i = 0; i < nRows; i++)
|
||||||
{
|
{
|
||||||
canvas.drawText(String.format("%d%%", (100 - i * 100 / nRows)), rGrid.left + 0.5f * em,
|
canvas.drawText(String.format("%d%%", (100 - i * 100 / nRows)), rGrid.left + 0.5f * em,
|
||||||
@@ -200,7 +275,7 @@ public class HabitScoreView extends View
|
|||||||
|
|
||||||
private void drawLine(Canvas canvas, RectF rectFrom, RectF rectTo)
|
private void drawLine(Canvas canvas, RectF rectFrom, RectF rectTo)
|
||||||
{
|
{
|
||||||
pGraph.setColor(habit.color);
|
pGraph.setColor(primaryColor);
|
||||||
canvas.drawLine(rectFrom.centerX(), rectFrom.centerY(), rectTo.centerX(), rectTo.centerY(),
|
canvas.drawLine(rectFrom.centerX(), rectFrom.centerY(), rectTo.centerX(), rectTo.centerY(),
|
||||||
pGraph);
|
pGraph);
|
||||||
}
|
}
|
||||||
@@ -208,61 +283,32 @@ public class HabitScoreView extends View
|
|||||||
private void drawMarker(Canvas canvas, RectF rect)
|
private void drawMarker(Canvas canvas, RectF rect)
|
||||||
{
|
{
|
||||||
rect.inset(columnWidth * 0.15f, columnWidth * 0.15f);
|
rect.inset(columnWidth * 0.15f, columnWidth * 0.15f);
|
||||||
pGraph.setColor(Color.WHITE);
|
setModeOrColor(pGraph, XFERMODE_CLEAR, Color.WHITE);
|
||||||
canvas.drawOval(rect, pGraph);
|
canvas.drawOval(rect, pGraph);
|
||||||
|
|
||||||
rect.inset(columnWidth * 0.1f, columnWidth * 0.1f);
|
rect.inset(columnWidth * 0.1f, columnWidth * 0.1f);
|
||||||
pGraph.setColor(habit.color);
|
setModeOrColor(pGraph, XFERMODE_SRC, primaryColor);
|
||||||
canvas.drawOval(rect, pGraph);
|
canvas.drawOval(rect, pGraph);
|
||||||
|
|
||||||
rect.inset(columnWidth * 0.1f, columnWidth * 0.1f);
|
rect.inset(columnWidth * 0.1f, columnWidth * 0.1f);
|
||||||
pGraph.setColor(Color.WHITE);
|
setModeOrColor(pGraph, XFERMODE_CLEAR, Color.WHITE);
|
||||||
canvas.drawOval(rect, pGraph);
|
canvas.drawOval(rect, pGraph);
|
||||||
|
|
||||||
|
if(isBackgroundTransparent)
|
||||||
|
pGraph.setXfermode(XFERMODE_SRC);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
public void setIsBackgroundTransparent(boolean isBackgroundTransparent)
|
||||||
public boolean onTouchEvent(MotionEvent event)
|
|
||||||
{
|
{
|
||||||
int action = event.getAction();
|
this.isBackgroundTransparent = isBackgroundTransparent;
|
||||||
|
createColors();
|
||||||
int pointerIndex = MotionEventCompat.getActionIndex(event);
|
|
||||||
final float x = MotionEventCompat.getX(event, pointerIndex);
|
|
||||||
final float y = MotionEventCompat.getY(event, pointerIndex);
|
|
||||||
|
|
||||||
if (action == MotionEvent.ACTION_DOWN)
|
|
||||||
{
|
|
||||||
prevX = x;
|
|
||||||
prevY = y;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (action == MotionEvent.ACTION_MOVE)
|
|
||||||
{
|
|
||||||
float dx = x - prevX;
|
|
||||||
float dy = y - prevY;
|
|
||||||
|
|
||||||
if (Math.abs(dy) > Math.abs(dx)) return false;
|
|
||||||
getParent().requestDisallowInterceptTouchEvent(true);
|
|
||||||
if (move(dx))
|
|
||||||
{
|
|
||||||
prevX = x;
|
|
||||||
prevY = y;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean move(float dx)
|
private void setModeOrColor(Paint p, PorterDuffXfermode mode, int color)
|
||||||
{
|
{
|
||||||
int newDataOffset = dataOffset + (int) (dx / columnWidth);
|
if(isBackgroundTransparent)
|
||||||
newDataOffset = Math.max(0, newDataOffset);
|
p.setXfermode(mode);
|
||||||
|
else
|
||||||
if (newDataOffset != dataOffset)
|
p.setColor(color);
|
||||||
{
|
|
||||||
dataOffset = newDataOffset;
|
|
||||||
fetchScores();
|
|
||||||
invalidate();
|
|
||||||
return true;
|
|
||||||
} else return false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,20 @@
|
|||||||
/* Copyright (C) 2016 Alinson Santos Xavier
|
/*
|
||||||
|
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify it
|
* This file is part of Loop Habit Tracker.
|
||||||
* under the terms of the GNU General Public License as published by the Free
|
|
||||||
* Software Foundation, either version 3 of the License, or (at your option)
|
|
||||||
* any later version.
|
|
||||||
*
|
*
|
||||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
* it under the terms of the GNU General Public License as published by the
|
||||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
* Free Software Foundation, either version 3 of the License, or (at your
|
||||||
|
* option) any later version.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
* more details.
|
* more details.
|
||||||
*
|
*
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License along
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.isoron.uhabits.views;
|
package org.isoron.uhabits.views;
|
||||||
@@ -21,9 +24,7 @@ import android.graphics.Canvas;
|
|||||||
import android.graphics.Color;
|
import android.graphics.Color;
|
||||||
import android.graphics.Paint;
|
import android.graphics.Paint;
|
||||||
import android.graphics.Rect;
|
import android.graphics.Rect;
|
||||||
import android.support.v4.view.MotionEventCompat;
|
import android.util.AttributeSet;
|
||||||
import android.view.MotionEvent;
|
|
||||||
import android.view.View;
|
|
||||||
|
|
||||||
import org.isoron.helpers.ColorHelper;
|
import org.isoron.helpers.ColorHelper;
|
||||||
import org.isoron.helpers.DateHelper;
|
import org.isoron.helpers.DateHelper;
|
||||||
@@ -32,74 +33,184 @@ import org.isoron.uhabits.models.Streak;
|
|||||||
|
|
||||||
import java.text.SimpleDateFormat;
|
import java.text.SimpleDateFormat;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Random;
|
||||||
|
|
||||||
public class HabitStreakView extends View
|
public class HabitStreakView extends ScrollableDataView
|
||||||
{
|
{
|
||||||
private Habit habit;
|
private Habit habit;
|
||||||
private int columnWidth, columnHeight, nColumns;
|
|
||||||
|
|
||||||
private Paint pText, pBar;
|
private Paint pText, pBar;
|
||||||
private List<Streak> streaks;
|
|
||||||
private int dataOffset;
|
private long[] startTimes;
|
||||||
|
private long[] endTimes;
|
||||||
|
private long[] lengths;
|
||||||
|
|
||||||
|
private int columnWidth;
|
||||||
|
private int columnHeight;
|
||||||
|
private int headerHeight;
|
||||||
|
private int nColumns;
|
||||||
|
|
||||||
private long maxStreakLength;
|
private long maxStreakLength;
|
||||||
|
|
||||||
private int barHeaderHeight;
|
|
||||||
|
|
||||||
private int[] colors;
|
private int[] colors;
|
||||||
private float prevX;
|
private SimpleDateFormat dfMonth;
|
||||||
private float prevY;
|
private Rect rect;
|
||||||
|
private int baseSize;
|
||||||
|
private int primaryColor;
|
||||||
|
|
||||||
public HabitStreakView(Context context, Habit habit, int columnWidth)
|
private boolean isBackgroundTransparent;
|
||||||
|
private int textColor;
|
||||||
|
private Paint pBarText;
|
||||||
|
|
||||||
|
public HabitStreakView(Context context, AttributeSet attrs)
|
||||||
{
|
{
|
||||||
super(context);
|
super(context, attrs);
|
||||||
this.habit = habit;
|
this.primaryColor = ColorHelper.palette[7];
|
||||||
this.columnWidth = columnWidth;
|
startTimes = endTimes = lengths = new long[0];
|
||||||
|
init();
|
||||||
pText = new Paint();
|
|
||||||
pText.setColor(Color.LTGRAY);
|
|
||||||
pText.setTextAlign(Paint.Align.CENTER);
|
|
||||||
pText.setTextSize(columnWidth * 0.5f);
|
|
||||||
pText.setAntiAlias(true);
|
|
||||||
|
|
||||||
pBar = new Paint();
|
|
||||||
pBar.setTextAlign(Paint.Align.CENTER);
|
|
||||||
pBar.setTextSize(columnWidth * 0.5f);
|
|
||||||
pBar.setAntiAlias(true);
|
|
||||||
|
|
||||||
columnHeight = 8 * columnWidth;
|
|
||||||
barHeaderHeight = columnWidth;
|
|
||||||
|
|
||||||
colors = new int[4];
|
|
||||||
|
|
||||||
colors[0] = Color.rgb(230, 230, 230);
|
|
||||||
colors[3] = habit.color;
|
|
||||||
colors[1] = ColorHelper.mixColors(colors[0], colors[3], 0.66f);
|
|
||||||
colors[2] = ColorHelper.mixColors(colors[0], colors[3], 0.33f);
|
|
||||||
|
|
||||||
fetchStreaks();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void fetchStreaks()
|
public void setHabit(Habit habit)
|
||||||
{
|
{
|
||||||
streaks = habit.getStreaks();
|
this.habit = habit;
|
||||||
|
|
||||||
for(Streak s : streaks)
|
createColors();
|
||||||
maxStreakLength = Math.max(maxStreakLength, s.length);
|
refreshData();
|
||||||
|
postInvalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void init()
|
||||||
|
{
|
||||||
|
refreshData();
|
||||||
|
createPaints();
|
||||||
|
createColors();
|
||||||
|
|
||||||
|
dfMonth = new SimpleDateFormat("MMM", Locale.getDefault());
|
||||||
|
rect = new Rect();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
|
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
|
||||||
{
|
{
|
||||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
int width = MeasureSpec.getSize(widthMeasureSpec);
|
||||||
setMeasuredDimension(getMeasuredWidth(), columnHeight + 2*barHeaderHeight);
|
int height = MeasureSpec.getSize(heightMeasureSpec);
|
||||||
|
setMeasuredDimension(width, height);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onSizeChanged(int w, int h, int oldw, int oldh)
|
protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight)
|
||||||
{
|
{
|
||||||
super.onSizeChanged(w, h, oldw, oldh);
|
baseSize = height / 10;
|
||||||
nColumns = w / columnWidth;
|
setScrollerBucketSize(baseSize);
|
||||||
|
|
||||||
|
columnWidth = baseSize;
|
||||||
|
columnHeight = 8 * baseSize;
|
||||||
|
headerHeight = baseSize;
|
||||||
|
nColumns = width / baseSize - 1;
|
||||||
|
|
||||||
|
pText.setTextSize(baseSize * 0.5f);
|
||||||
|
pBar.setTextSize(baseSize * 0.5f);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createColors()
|
||||||
|
{
|
||||||
|
if(habit != null)
|
||||||
|
this.primaryColor = habit.color;
|
||||||
|
|
||||||
|
if(isBackgroundTransparent)
|
||||||
|
{
|
||||||
|
primaryColor = ColorHelper.setSaturation(primaryColor, 0.75f);
|
||||||
|
primaryColor = ColorHelper.setValue(primaryColor, 1.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
int red = Color.red(primaryColor);
|
||||||
|
int green = Color.green(primaryColor);
|
||||||
|
int blue = Color.blue(primaryColor);
|
||||||
|
|
||||||
|
if(isBackgroundTransparent)
|
||||||
|
{
|
||||||
|
colors = new int[4];
|
||||||
|
colors[3] = primaryColor;
|
||||||
|
colors[2] = Color.argb(213, red, green, blue);
|
||||||
|
colors[1] = Color.argb(170, red, green, blue);
|
||||||
|
colors[0] = Color.argb(128, red, green, blue);
|
||||||
|
textColor = Color.rgb(255, 255, 255);
|
||||||
|
pBarText = pText;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
colors = new int[4];
|
||||||
|
colors[3] = primaryColor;
|
||||||
|
colors[2] = Color.argb(192, red, green, blue);
|
||||||
|
colors[1] = Color.argb(96, red, green, blue);
|
||||||
|
colors[0] = Color.argb(32, 0, 0, 0);
|
||||||
|
textColor = Color.argb(64, 0, 0, 0);
|
||||||
|
pBarText = pBar;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void createPaints()
|
||||||
|
{
|
||||||
|
pText = new Paint();
|
||||||
|
pText.setTextAlign(Paint.Align.CENTER);
|
||||||
|
pText.setAntiAlias(true);
|
||||||
|
|
||||||
|
pBar = new Paint();
|
||||||
|
pBar.setTextAlign(Paint.Align.CENTER);
|
||||||
|
pBar.setAntiAlias(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void refreshData()
|
||||||
|
{
|
||||||
|
if(isInEditMode())
|
||||||
|
generateRandomData();
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if(habit == null) return;
|
||||||
|
|
||||||
|
List<Streak> streaks = habit.streaks.getAll();
|
||||||
|
int size = streaks.size();
|
||||||
|
|
||||||
|
startTimes = new long[size];
|
||||||
|
endTimes = new long[size];
|
||||||
|
lengths = new long[size];
|
||||||
|
|
||||||
|
int k = 0;
|
||||||
|
for (Streak s : streaks)
|
||||||
|
{
|
||||||
|
startTimes[k] = s.start;
|
||||||
|
endTimes[k] = s.end;
|
||||||
|
lengths[k] = s.length;
|
||||||
|
k++;
|
||||||
|
|
||||||
|
maxStreakLength = Math.max(maxStreakLength, s.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void generateRandomData()
|
||||||
|
{
|
||||||
|
int size = 30;
|
||||||
|
|
||||||
|
startTimes = new long[size];
|
||||||
|
endTimes = new long[size];
|
||||||
|
lengths = new long[size];
|
||||||
|
|
||||||
|
Random random = new Random();
|
||||||
|
Long date = DateHelper.getStartOfToday();
|
||||||
|
|
||||||
|
for(int i = 0; i < size; i++)
|
||||||
|
{
|
||||||
|
int l = (int) Math.pow(2, random.nextFloat() * 5 + 1);
|
||||||
|
|
||||||
|
endTimes[i] = date;
|
||||||
|
date -= l * DateHelper.millisecondsInOneDay;
|
||||||
|
lengths[i] = (long) l;
|
||||||
|
startTimes[i] = date;
|
||||||
|
|
||||||
|
maxStreakLength = Math.max(maxStreakLength, l);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -110,79 +221,40 @@ public class HabitStreakView extends View
|
|||||||
float lineHeight = pText.getFontSpacing();
|
float lineHeight = pText.getFontSpacing();
|
||||||
float barHeaderOffset = lineHeight * 0.4f;
|
float barHeaderOffset = lineHeight * 0.4f;
|
||||||
|
|
||||||
int nStreaks = streaks.size();
|
int nStreaks = startTimes.length;
|
||||||
int start = Math.max(0, nStreaks - nColumns - dataOffset);
|
int start = nStreaks - nColumns - getDataOffset();
|
||||||
SimpleDateFormat dfMonth = new SimpleDateFormat("MMM");
|
|
||||||
|
pText.setColor(textColor);
|
||||||
|
|
||||||
String previousMonth = "";
|
String previousMonth = "";
|
||||||
|
|
||||||
for (int offset = 0; offset < nColumns && start+offset < nStreaks; offset++)
|
for (int offset = 0; offset < nColumns && start + offset < nStreaks; offset++)
|
||||||
{
|
{
|
||||||
String month = dfMonth.format(streaks.get(start+offset).start);
|
if(start + offset < 0) continue;
|
||||||
|
String month = dfMonth.format(startTimes[start + offset]);
|
||||||
|
|
||||||
long l = streaks.get(offset+start).length;
|
long l = lengths[offset + start];
|
||||||
double lRelative = ((double) l) / maxStreakLength;
|
double lRelative = ((double) l) / maxStreakLength;
|
||||||
|
|
||||||
pBar.setColor(colors[(int) Math.floor(lRelative*3)]);
|
pBar.setColor(colors[(int) Math.floor(lRelative * 3)]);
|
||||||
|
|
||||||
int height = (int) (columnHeight * lRelative);
|
int height = (int) (columnHeight * lRelative);
|
||||||
Rect r = new Rect(0,0,columnWidth-2, height);
|
rect.set(0, 0, columnWidth - 2, height);
|
||||||
r.offset(offset * columnWidth, barHeaderHeight + columnHeight - height);
|
rect.offset(offset * columnWidth, headerHeight + columnHeight - height);
|
||||||
|
|
||||||
canvas.drawRect(r, pBar);
|
canvas.drawRect(rect, pBar);
|
||||||
canvas.drawText(Long.toString(l), r.centerX(), r.top - barHeaderOffset, pBar);
|
canvas.drawText(Long.toString(l), rect.centerX(), rect.top - barHeaderOffset, pBarText);
|
||||||
|
|
||||||
if(!month.equals(previousMonth))
|
if (!month.equals(previousMonth))
|
||||||
canvas.drawText(month, r.centerX(), r.bottom + lineHeight * 1.2f, pText);
|
canvas.drawText(month, rect.centerX(), rect.bottom + lineHeight * 1.2f, pText);
|
||||||
|
|
||||||
previousMonth = month;
|
previousMonth = month;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
public void setIsBackgroundTransparent(boolean isBackgroundTransparent)
|
||||||
public boolean onTouchEvent(MotionEvent event)
|
|
||||||
{
|
{
|
||||||
int action = event.getAction();
|
this.isBackgroundTransparent = isBackgroundTransparent;
|
||||||
|
createColors();
|
||||||
int pointerIndex = MotionEventCompat.getActionIndex(event);
|
|
||||||
final float x = MotionEventCompat.getX(event, pointerIndex);
|
|
||||||
final float y = MotionEventCompat.getY(event, pointerIndex);
|
|
||||||
|
|
||||||
if (action == MotionEvent.ACTION_DOWN)
|
|
||||||
{
|
|
||||||
prevX = x;
|
|
||||||
prevY = y;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (action == MotionEvent.ACTION_MOVE)
|
|
||||||
{
|
|
||||||
float dx = x - prevX;
|
|
||||||
float dy = y - prevY;
|
|
||||||
|
|
||||||
if (Math.abs(dy) > Math.abs(dx)) return false;
|
|
||||||
getParent().requestDisallowInterceptTouchEvent(true);
|
|
||||||
if(move(dx))
|
|
||||||
{
|
|
||||||
prevX = x;
|
|
||||||
prevY = y;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean move(float dx)
|
|
||||||
{
|
|
||||||
int newDataOffset = dataOffset + (int) (dx / columnWidth);
|
|
||||||
newDataOffset = Math.max(0, Math.min(streaks.size() - nColumns, newDataOffset));
|
|
||||||
|
|
||||||
if (newDataOffset != dataOffset)
|
|
||||||
{
|
|
||||||
dataOffset = newDataOffset;
|
|
||||||
invalidate();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,20 @@
|
|||||||
/* Copyright (C) 2016 Alinson Santos Xavier
|
/*
|
||||||
|
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify it
|
* This file is part of Loop Habit Tracker.
|
||||||
* under the terms of the GNU General Public License as published by the Free
|
|
||||||
* Software Foundation, either version 3 of the License, or (at your option)
|
|
||||||
* any later version.
|
|
||||||
*
|
*
|
||||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
* it under the terms of the GNU General Public License as published by the
|
||||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
* Free Software Foundation, either version 3 of the License, or (at your
|
||||||
|
* option) any later version.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
* more details.
|
* more details.
|
||||||
*
|
*
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License along
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.isoron.uhabits.views;
|
package org.isoron.uhabits.views;
|
||||||
@@ -21,38 +24,64 @@ import android.graphics.Canvas;
|
|||||||
import android.graphics.Color;
|
import android.graphics.Color;
|
||||||
import android.graphics.Paint;
|
import android.graphics.Paint;
|
||||||
import android.graphics.RectF;
|
import android.graphics.RectF;
|
||||||
|
import android.util.AttributeSet;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
|
||||||
|
import org.isoron.helpers.ColorHelper;
|
||||||
|
import org.isoron.helpers.DialogHelper;
|
||||||
|
import org.isoron.uhabits.R;
|
||||||
|
|
||||||
public class RingView extends View
|
public class RingView extends View
|
||||||
{
|
{
|
||||||
|
|
||||||
private int size;
|
private int size;
|
||||||
private int color;
|
private int color;
|
||||||
private float perc;
|
private float percentage;
|
||||||
private Paint pRing;
|
private Paint pRing;
|
||||||
private float lineHeight;
|
private float lineHeight;
|
||||||
private String label;
|
private String label;
|
||||||
|
private RectF rect;
|
||||||
|
|
||||||
public RingView(Context context, int size, int color, float perc, String label)
|
public RingView(Context context, AttributeSet attrs)
|
||||||
|
{
|
||||||
|
super(context, attrs);
|
||||||
|
|
||||||
|
this.size = (int) context.getResources().getDimension(R.dimen.small_square_size) * 4;
|
||||||
|
this.label = DialogHelper.getAttribute(context, attrs, "label");
|
||||||
|
this.color = ColorHelper.palette[7];
|
||||||
|
this.percentage = 0.75f;
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setColor(int color)
|
||||||
{
|
{
|
||||||
super(context);
|
|
||||||
this.size = size;
|
|
||||||
this.color = color;
|
this.color = color;
|
||||||
this.perc = perc;
|
|
||||||
|
|
||||||
pRing = new Paint();
|
|
||||||
pRing.setColor(color);
|
pRing.setColor(color);
|
||||||
pRing.setAntiAlias(true);
|
postInvalidate();
|
||||||
pRing.setTextAlign(Paint.Align.CENTER);
|
}
|
||||||
|
|
||||||
this.label = label;
|
public void setPercentage(float percentage)
|
||||||
|
{
|
||||||
|
this.percentage = percentage;
|
||||||
|
postInvalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void init()
|
||||||
|
{
|
||||||
|
pRing = new Paint();
|
||||||
|
pRing.setAntiAlias(true);
|
||||||
|
pRing.setColor(color);
|
||||||
|
pRing.setTextAlign(Paint.Align.CENTER);
|
||||||
|
pRing.setTextSize(size * 0.2f);
|
||||||
|
lineHeight = pRing.getFontSpacing();
|
||||||
|
rect = new RectF();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
|
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
|
||||||
{
|
{
|
||||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||||
setMeasuredDimension(size, size + (int) (2*lineHeight));
|
setMeasuredDimension(size, size + (int) (2 * lineHeight));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -62,22 +91,22 @@ public class RingView extends View
|
|||||||
float thickness = size * 0.15f;
|
float thickness = size * 0.15f;
|
||||||
|
|
||||||
pRing.setColor(color);
|
pRing.setColor(color);
|
||||||
RectF r = new RectF(0, 0, size, size);
|
rect.set(0, 0, size, size);
|
||||||
canvas.drawArc(r, -90, 360 * perc, true, pRing);
|
canvas.drawArc(rect, -90, 360 * percentage, true, pRing);
|
||||||
|
|
||||||
pRing.setColor(Color.rgb(230, 230, 230));
|
pRing.setColor(Color.rgb(230, 230, 230));
|
||||||
canvas.drawArc(r, 360 * perc - 90 + 2, 360 * (1 - perc) - 4, true, pRing);
|
canvas.drawArc(rect, 360 * percentage - 90 + 2, 360 * (1 - percentage) - 4, true, pRing);
|
||||||
|
|
||||||
pRing.setColor(Color.WHITE);
|
pRing.setColor(Color.WHITE);
|
||||||
r.inset(thickness, thickness);
|
rect.inset(thickness, thickness);
|
||||||
canvas.drawArc(r, -90, 360, true, pRing);
|
canvas.drawArc(rect, -90, 360, true, pRing);
|
||||||
|
|
||||||
pRing.setColor(Color.GRAY);
|
pRing.setColor(Color.GRAY);
|
||||||
pRing.setTextSize(size * 0.2f);
|
pRing.setTextSize(size * 0.2f);
|
||||||
lineHeight = pRing.getFontSpacing();
|
canvas.drawText(String.format("%.0f%%", percentage * 100), rect.centerX(),
|
||||||
canvas.drawText(String.format("%.0f%%", perc * 100), r.centerX(), r.centerY()+lineHeight/3, pRing);
|
rect.centerY() + lineHeight / 3, pRing);
|
||||||
|
|
||||||
pRing.setTextSize(size * 0.15f);
|
pRing.setTextSize(size * 0.15f);
|
||||||
canvas.drawText(label, size/2, size + lineHeight * 1.2f, pRing);
|
canvas.drawText(label, size / 2, size + lineHeight * 1.2f, pRing);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,145 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||||
|
*
|
||||||
|
* This file is part of Loop Habit Tracker.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by the
|
||||||
|
* Free Software Foundation, either version 3 of the License, or (at your
|
||||||
|
* option) any later version.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
|
* more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along
|
||||||
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.isoron.uhabits.views;
|
||||||
|
|
||||||
|
import android.animation.ValueAnimator;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.util.AttributeSet;
|
||||||
|
import android.view.GestureDetector;
|
||||||
|
import android.view.MotionEvent;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.Scroller;
|
||||||
|
|
||||||
|
public abstract class ScrollableDataView extends View implements GestureDetector.OnGestureListener,
|
||||||
|
ValueAnimator.AnimatorUpdateListener
|
||||||
|
{
|
||||||
|
|
||||||
|
private int dataOffset;
|
||||||
|
private int scrollerBucketSize;
|
||||||
|
|
||||||
|
private GestureDetector detector;
|
||||||
|
private Scroller scroller;
|
||||||
|
private ValueAnimator scrollAnimator;
|
||||||
|
|
||||||
|
public ScrollableDataView(Context context)
|
||||||
|
{
|
||||||
|
super(context);
|
||||||
|
init(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ScrollableDataView(Context context, AttributeSet attrs)
|
||||||
|
{
|
||||||
|
super(context, attrs);
|
||||||
|
init(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void init(Context context)
|
||||||
|
{
|
||||||
|
detector = new GestureDetector(context, this);
|
||||||
|
scroller = new Scroller(context, null, true);
|
||||||
|
scrollAnimator = ValueAnimator.ofFloat(0, 1);
|
||||||
|
scrollAnimator.addUpdateListener(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onTouchEvent(MotionEvent event)
|
||||||
|
{
|
||||||
|
return detector.onTouchEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onDown(MotionEvent e)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onShowPress(MotionEvent e)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onSingleTapUp(MotionEvent e)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onScroll(MotionEvent e1, MotionEvent e2, float dx, float dy)
|
||||||
|
{
|
||||||
|
if(scrollerBucketSize == 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if(Math.abs(dx) > Math.abs(dy))
|
||||||
|
getParent().requestDisallowInterceptTouchEvent(true);
|
||||||
|
|
||||||
|
scroller.startScroll(scroller.getCurrX(), scroller.getCurrY(), (int) -dx, (int) dy, 0);
|
||||||
|
scroller.computeScrollOffset();
|
||||||
|
dataOffset = Math.max(0, scroller.getCurrX() / scrollerBucketSize);
|
||||||
|
postInvalidate();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onLongPress(MotionEvent e)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY)
|
||||||
|
{
|
||||||
|
scroller.fling(scroller.getCurrX(), scroller.getCurrY(), (int) velocityX / 2, 0, 0, 100000,
|
||||||
|
0, 0);
|
||||||
|
invalidate();
|
||||||
|
|
||||||
|
scrollAnimator.setDuration(scroller.getDuration());
|
||||||
|
scrollAnimator.start();
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAnimationUpdate(ValueAnimator animation)
|
||||||
|
{
|
||||||
|
if (!scroller.isFinished())
|
||||||
|
{
|
||||||
|
scroller.computeScrollOffset();
|
||||||
|
dataOffset = Math.max(0, scroller.getCurrX() / scrollerBucketSize);
|
||||||
|
postInvalidate();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
scrollAnimator.cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getDataOffset()
|
||||||
|
{
|
||||||
|
return dataOffset;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setScrollerBucketSize(int scrollerBucketSize)
|
||||||
|
{
|
||||||
|
this.scrollerBucketSize = scrollerBucketSize;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,201 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||||
|
*
|
||||||
|
* This file is part of Loop Habit Tracker.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by the
|
||||||
|
* Free Software Foundation, either version 3 of the License, or (at your
|
||||||
|
* option) any later version.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
|
* more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along
|
||||||
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.isoron.uhabits.widgets;
|
||||||
|
|
||||||
|
import android.app.PendingIntent;
|
||||||
|
import android.appwidget.AppWidgetManager;
|
||||||
|
import android.appwidget.AppWidgetProvider;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
import android.media.Image;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.preference.PreferenceManager;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.ImageView;
|
||||||
|
import android.widget.LinearLayout;
|
||||||
|
import android.widget.RemoteViews;
|
||||||
|
|
||||||
|
import org.isoron.helpers.DialogHelper;
|
||||||
|
import org.isoron.uhabits.R;
|
||||||
|
import org.isoron.uhabits.models.Habit;
|
||||||
|
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
public abstract class BaseWidgetProvider extends AppWidgetProvider
|
||||||
|
{
|
||||||
|
|
||||||
|
private int width, height;
|
||||||
|
|
||||||
|
protected abstract int getDefaultHeight();
|
||||||
|
|
||||||
|
protected abstract int getDefaultWidth();
|
||||||
|
|
||||||
|
protected abstract PendingIntent getOnClickPendingIntent(Context context, Habit habit);
|
||||||
|
|
||||||
|
protected abstract int getLayoutId();
|
||||||
|
|
||||||
|
protected abstract View buildCustomView(Context context, Habit habit);
|
||||||
|
|
||||||
|
public static String getHabitIdKey(long widgetId)
|
||||||
|
{
|
||||||
|
return String.format("widget-%06d-habit", widgetId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDeleted(Context context, int[] appWidgetIds)
|
||||||
|
{
|
||||||
|
Context appContext = context.getApplicationContext();
|
||||||
|
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(appContext);
|
||||||
|
|
||||||
|
for(Integer id : appWidgetIds)
|
||||||
|
prefs.edit().remove(getHabitIdKey(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAppWidgetOptionsChanged(Context context, AppWidgetManager appWidgetManager,
|
||||||
|
int appWidgetId, Bundle newOptions)
|
||||||
|
{
|
||||||
|
updateWidget(context, appWidgetManager, appWidgetId, newOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onUpdate(Context context, AppWidgetManager manager, int[] appWidgetIds)
|
||||||
|
{
|
||||||
|
for(int id : appWidgetIds)
|
||||||
|
{
|
||||||
|
Bundle options = null;
|
||||||
|
|
||||||
|
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN)
|
||||||
|
options = manager.getAppWidgetOptions(id);
|
||||||
|
|
||||||
|
updateWidget(context, manager, id, options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateWidget(Context context, AppWidgetManager manager, int widgetId, Bundle options)
|
||||||
|
{
|
||||||
|
updateWidgetSize(context, options);
|
||||||
|
|
||||||
|
Context appContext = context.getApplicationContext();
|
||||||
|
RemoteViews remoteViews = new RemoteViews(context.getPackageName(), getLayoutId());
|
||||||
|
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(appContext);
|
||||||
|
|
||||||
|
Long habitId = prefs.getLong(getHabitIdKey(widgetId), -1L);
|
||||||
|
if(habitId < 0) return;
|
||||||
|
|
||||||
|
Habit habit = Habit.get(habitId);
|
||||||
|
View widgetView = buildCustomView(context, habit);
|
||||||
|
measureCustomView(context, width, height, widgetView);
|
||||||
|
|
||||||
|
widgetView.setDrawingCacheEnabled(true);
|
||||||
|
widgetView.buildDrawingCache(true);
|
||||||
|
Bitmap drawingCache = widgetView.getDrawingCache();
|
||||||
|
|
||||||
|
remoteViews.setTextViewText(R.id.label, habit.name);
|
||||||
|
remoteViews.setImageViewBitmap(R.id.imageView, drawingCache);
|
||||||
|
|
||||||
|
//savePreview(context, widgetId, drawingCache);
|
||||||
|
|
||||||
|
PendingIntent onClickIntent = getOnClickPendingIntent(context, habit);
|
||||||
|
if(onClickIntent != null) remoteViews.setOnClickPendingIntent(R.id.imageView, onClickIntent);
|
||||||
|
|
||||||
|
manager.updateAppWidget(widgetId, remoteViews);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void savePreview(Context context, int widgetId, Bitmap widgetCache)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
LayoutInflater inflater = LayoutInflater.from(context);
|
||||||
|
View view = inflater.inflate(getLayoutId(), null);
|
||||||
|
|
||||||
|
ImageView iv = (ImageView) view.findViewById(R.id.imageView);
|
||||||
|
iv.setImageBitmap(widgetCache);
|
||||||
|
|
||||||
|
view.measure(width, height);
|
||||||
|
view.layout(0, 0, view.getMeasuredWidth(), view.getMeasuredHeight());
|
||||||
|
view.setDrawingCacheEnabled(true);
|
||||||
|
view.buildDrawingCache();
|
||||||
|
Bitmap previewCache = view.getDrawingCache();
|
||||||
|
|
||||||
|
String filename = String.format("%s/%d.png", context.getExternalCacheDir(), widgetId);
|
||||||
|
Log.d("BaseWidgetProvider", String.format("Writing %s", filename));
|
||||||
|
FileOutputStream out = new FileOutputStream(filename);
|
||||||
|
|
||||||
|
if(previewCache != null)
|
||||||
|
previewCache.compress(Bitmap.CompressFormat.PNG, 100, out);
|
||||||
|
|
||||||
|
out.close();
|
||||||
|
}
|
||||||
|
catch (IOException e)
|
||||||
|
{
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateWidgetSize(Context context, Bundle options)
|
||||||
|
{
|
||||||
|
int maxWidth = getDefaultWidth();
|
||||||
|
int minWidth = getDefaultWidth();
|
||||||
|
int maxHeight = getDefaultHeight();
|
||||||
|
int minHeight = getDefaultHeight();
|
||||||
|
|
||||||
|
if (options != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN)
|
||||||
|
{
|
||||||
|
maxWidth = (int) DialogHelper.dpToPixels(context,
|
||||||
|
options.getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH));
|
||||||
|
maxHeight = (int) DialogHelper.dpToPixels(context,
|
||||||
|
options.getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT));
|
||||||
|
minWidth = (int) DialogHelper.dpToPixels(context,
|
||||||
|
options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH));
|
||||||
|
minHeight = (int) DialogHelper.dpToPixels(context,
|
||||||
|
options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT));
|
||||||
|
}
|
||||||
|
|
||||||
|
width = maxWidth;
|
||||||
|
height = maxHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void measureCustomView(Context context, int w, int h, View customView)
|
||||||
|
{
|
||||||
|
LayoutInflater inflater = LayoutInflater.from(context);
|
||||||
|
View entireView = inflater.inflate(getLayoutId(), null);
|
||||||
|
|
||||||
|
int specWidth = View.MeasureSpec.makeMeasureSpec(w, View.MeasureSpec.EXACTLY);
|
||||||
|
int specHeight = View.MeasureSpec.makeMeasureSpec(h, View.MeasureSpec.EXACTLY);
|
||||||
|
|
||||||
|
entireView.measure(specWidth, specHeight);
|
||||||
|
entireView.layout(0, 0, entireView.getMeasuredWidth(), entireView.getMeasuredHeight());
|
||||||
|
|
||||||
|
View imageView = entireView.findViewById(R.id.imageView);
|
||||||
|
w = imageView.getMeasuredWidth();
|
||||||
|
h = imageView.getMeasuredHeight();
|
||||||
|
|
||||||
|
specWidth = View.MeasureSpec.makeMeasureSpec(w, View.MeasureSpec.EXACTLY);
|
||||||
|
specHeight = View.MeasureSpec.makeMeasureSpec(h, View.MeasureSpec.EXACTLY);
|
||||||
|
customView.measure(specWidth, specHeight);
|
||||||
|
customView.layout(0, 0, customView.getMeasuredWidth(), customView.getMeasuredHeight());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||||
|
*
|
||||||
|
* This file is part of Loop Habit Tracker.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by the
|
||||||
|
* Free Software Foundation, either version 3 of the License, or (at your
|
||||||
|
* option) any later version.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
|
* more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along
|
||||||
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package org.isoron.uhabits.widgets;
|
||||||
|
|
||||||
|
import android.app.PendingIntent;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.view.View;
|
||||||
|
|
||||||
|
import org.isoron.uhabits.HabitBroadcastReceiver;
|
||||||
|
import org.isoron.uhabits.R;
|
||||||
|
import org.isoron.uhabits.models.Habit;
|
||||||
|
import org.isoron.uhabits.views.CheckmarkView;
|
||||||
|
|
||||||
|
public class CheckmarkWidgetProvider extends BaseWidgetProvider
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
protected View buildCustomView(Context context, Habit habit)
|
||||||
|
{
|
||||||
|
CheckmarkView view = new CheckmarkView(context);
|
||||||
|
view.setHabit(habit);
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected PendingIntent getOnClickPendingIntent(Context context, Habit habit)
|
||||||
|
{
|
||||||
|
return HabitBroadcastReceiver.buildCheckIntent(context, habit, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected int getDefaultHeight()
|
||||||
|
{
|
||||||
|
return 200;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected int getDefaultWidth()
|
||||||
|
{
|
||||||
|
return 200;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected int getLayoutId()
|
||||||
|
{
|
||||||
|
return R.layout.widget_checkmark;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||||
|
*
|
||||||
|
* This file is part of Loop Habit Tracker.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by the
|
||||||
|
* Free Software Foundation, either version 3 of the License, or (at your
|
||||||
|
* option) any later version.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
|
* more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along
|
||||||
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.isoron.uhabits.widgets;
|
||||||
|
|
||||||
|
import android.app.PendingIntent;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.view.View;
|
||||||
|
|
||||||
|
import org.isoron.uhabits.R;
|
||||||
|
import org.isoron.uhabits.models.Habit;
|
||||||
|
import org.isoron.uhabits.views.HabitFrequencyView;
|
||||||
|
|
||||||
|
public class FrequencyWidgetProvider extends BaseWidgetProvider
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
protected View buildCustomView(Context context, Habit habit)
|
||||||
|
{
|
||||||
|
HabitFrequencyView view = new HabitFrequencyView(context, null);
|
||||||
|
view.setIsBackgroundTransparent(true);
|
||||||
|
view.setHabit(habit);
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected PendingIntent getOnClickPendingIntent(Context context, Habit habit)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected int getDefaultHeight()
|
||||||
|
{
|
||||||
|
return 200;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected int getDefaultWidth()
|
||||||
|
{
|
||||||
|
return 200;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected int getLayoutId()
|
||||||
|
{
|
||||||
|
return R.layout.widget_graph;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||||
|
*
|
||||||
|
* This file is part of Loop Habit Tracker.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by the
|
||||||
|
* Free Software Foundation, either version 3 of the License, or (at your
|
||||||
|
* option) any later version.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
|
* more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along
|
||||||
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.isoron.uhabits.widgets;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.appwidget.AppWidgetManager;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.preference.PreferenceManager;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.AdapterView;
|
||||||
|
import android.widget.ArrayAdapter;
|
||||||
|
import android.widget.ListView;
|
||||||
|
|
||||||
|
import org.isoron.uhabits.MainActivity;
|
||||||
|
import org.isoron.uhabits.R;
|
||||||
|
import org.isoron.uhabits.models.Habit;
|
||||||
|
import org.isoron.uhabits.widgets.BaseWidgetProvider;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class HabitPickerDialog extends Activity implements AdapterView.OnItemClickListener
|
||||||
|
{
|
||||||
|
|
||||||
|
private Integer widgetId;
|
||||||
|
private ArrayList<Long> habitIds;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState)
|
||||||
|
{
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setContentView(R.layout.widget_configure_activity);
|
||||||
|
|
||||||
|
Intent intent = getIntent();
|
||||||
|
Bundle extras = intent.getExtras();
|
||||||
|
|
||||||
|
if (extras != null) widgetId = extras.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID,
|
||||||
|
AppWidgetManager.INVALID_APPWIDGET_ID);
|
||||||
|
|
||||||
|
ListView listView = (ListView) findViewById(R.id.listView);
|
||||||
|
|
||||||
|
habitIds = new ArrayList<>();
|
||||||
|
ArrayList<String> habitNames = new ArrayList<>();
|
||||||
|
|
||||||
|
List<Habit> habits = Habit.getAll(false);
|
||||||
|
for(Habit h : habits)
|
||||||
|
{
|
||||||
|
habitIds.add(h.getId());
|
||||||
|
habitNames.add(h.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
ArrayAdapter<String> adapter =
|
||||||
|
new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, habitNames);
|
||||||
|
listView.setAdapter(adapter);
|
||||||
|
listView.setOnItemClickListener(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onItemClick(AdapterView<?> parent, View view, int position, long id)
|
||||||
|
{
|
||||||
|
Long habitId = habitIds.get(position);
|
||||||
|
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(
|
||||||
|
getApplicationContext());
|
||||||
|
prefs.edit().putLong(BaseWidgetProvider.getHabitIdKey(widgetId), habitId).commit();
|
||||||
|
|
||||||
|
MainActivity.updateWidgets(this);
|
||||||
|
|
||||||
|
Intent resultValue = new Intent();
|
||||||
|
resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, widgetId);
|
||||||
|
setResult(RESULT_OK, resultValue);
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||||
|
*
|
||||||
|
* This file is part of Loop Habit Tracker.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by the
|
||||||
|
* Free Software Foundation, either version 3 of the License, or (at your
|
||||||
|
* option) any later version.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
|
* more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along
|
||||||
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package org.isoron.uhabits.widgets;
|
||||||
|
|
||||||
|
import android.app.PendingIntent;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.view.View;
|
||||||
|
|
||||||
|
import org.isoron.uhabits.R;
|
||||||
|
import org.isoron.uhabits.models.Habit;
|
||||||
|
import org.isoron.uhabits.views.HabitHistoryView;
|
||||||
|
|
||||||
|
public class HistoryWidgetProvider extends BaseWidgetProvider
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
protected View buildCustomView(Context context, Habit habit)
|
||||||
|
{
|
||||||
|
HabitHistoryView view = new HabitHistoryView(context, null);
|
||||||
|
view.setHabit(habit);
|
||||||
|
view.setIsBackgroundTransparent(true);
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected PendingIntent getOnClickPendingIntent(Context context, Habit habit)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected int getDefaultHeight()
|
||||||
|
{
|
||||||
|
return 200;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected int getDefaultWidth()
|
||||||
|
{
|
||||||
|
return 200;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected int getLayoutId()
|
||||||
|
{
|
||||||
|
return R.layout.widget_graph;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||||
|
*
|
||||||
|
* This file is part of Loop Habit Tracker.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by the
|
||||||
|
* Free Software Foundation, either version 3 of the License, or (at your
|
||||||
|
* option) any later version.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
|
* more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along
|
||||||
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package org.isoron.uhabits.widgets;
|
||||||
|
|
||||||
|
import android.app.PendingIntent;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.view.View;
|
||||||
|
|
||||||
|
import org.isoron.uhabits.R;
|
||||||
|
import org.isoron.uhabits.models.Habit;
|
||||||
|
import org.isoron.uhabits.views.HabitScoreView;
|
||||||
|
|
||||||
|
public class ScoreWidgetProvider extends BaseWidgetProvider
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
protected View buildCustomView(Context context, Habit habit)
|
||||||
|
{
|
||||||
|
HabitScoreView view = new HabitScoreView(context, null);
|
||||||
|
view.setIsBackgroundTransparent(true);
|
||||||
|
view.setHabit(habit);
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected PendingIntent getOnClickPendingIntent(Context context, Habit habit)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected int getDefaultHeight()
|
||||||
|
{
|
||||||
|
return 200;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected int getDefaultWidth()
|
||||||
|
{
|
||||||
|
return 200;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected int getLayoutId()
|
||||||
|
{
|
||||||
|
return R.layout.widget_graph;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||||
|
*
|
||||||
|
* This file is part of Loop Habit Tracker.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by the
|
||||||
|
* Free Software Foundation, either version 3 of the License, or (at your
|
||||||
|
* option) any later version.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
|
* more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along
|
||||||
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package org.isoron.uhabits.widgets;
|
||||||
|
|
||||||
|
import android.app.PendingIntent;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.view.View;
|
||||||
|
|
||||||
|
import org.isoron.uhabits.R;
|
||||||
|
import org.isoron.uhabits.models.Habit;
|
||||||
|
import org.isoron.uhabits.views.HabitStreakView;
|
||||||
|
|
||||||
|
public class StreakWidgetProvider extends BaseWidgetProvider
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
protected View buildCustomView(Context context, Habit habit)
|
||||||
|
{
|
||||||
|
HabitStreakView view = new HabitStreakView(context, null);
|
||||||
|
view.setIsBackgroundTransparent(true);
|
||||||
|
view.setHabit(habit);
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected PendingIntent getOnClickPendingIntent(Context context, Habit habit)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected int getDefaultHeight()
|
||||||
|
{
|
||||||
|
return 200;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected int getDefaultWidth()
|
||||||
|
{
|
||||||
|
return 200;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected int getLayoutId()
|
||||||
|
{
|
||||||
|
return R.layout.widget_graph;
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
app/src/main/res/drawable-hdpi/ic_action_archive_dark.png
Normal file
|
After Width: | Height: | Size: 338 B |
BIN
app/src/main/res/drawable-hdpi/ic_action_archive_light.png
Normal file
|
After Width: | Height: | Size: 393 B |
BIN
app/src/main/res/drawable-hdpi/ic_action_color_dark.png
Normal file
|
After Width: | Height: | Size: 492 B |
BIN
app/src/main/res/drawable-hdpi/ic_action_color_light.png
Normal file
|
After Width: | Height: | Size: 563 B |
|
Before Width: | Height: | Size: 244 B |
|
Before Width: | Height: | Size: 822 B After Width: | Height: | Size: 822 B |
|
Before Width: | Height: | Size: 552 B |
BIN
app/src/main/res/drawable-hdpi/ic_action_unarchive_dark.png
Normal file
|
After Width: | Height: | Size: 337 B |
BIN
app/src/main/res/drawable-hdpi/ic_action_unarchive_light.png
Normal file
|
After Width: | Height: | Size: 410 B |
BIN
app/src/main/res/drawable-mdpi/ic_action_archive_dark.png
Normal file
|
After Width: | Height: | Size: 214 B |
BIN
app/src/main/res/drawable-mdpi/ic_action_archive_light.png
Normal file
|
After Width: | Height: | Size: 244 B |
BIN
app/src/main/res/drawable-mdpi/ic_action_color_dark.png
Normal file
|
After Width: | Height: | Size: 289 B |
BIN
app/src/main/res/drawable-mdpi/ic_action_color_light.png
Normal file
|
After Width: | Height: | Size: 319 B |
|
Before Width: | Height: | Size: 192 B |
|
Before Width: | Height: | Size: 554 B After Width: | Height: | Size: 554 B |