Merge remote-tracking branch 'upstream/dev' into dev

pull/643/head
Christoph Hennemann 5 years ago
commit fba006f488

@ -0,0 +1,31 @@
name: Build & Test
on:
push:
branches:
- dev
pull_request:
branches:
- dev
jobs:
build:
runs-on: macOS-latest
steps:
- uses: actions/checkout@v1
- name: Install Java Development Kit 1.8
uses: actions/setup-java@v1
with:
java-version: 1.8
- name: Build APK & Run small tests
run: android/build.sh build
- name: Run medium tests
uses: ReactiveCircus/android-emulator-runner@v2.2.0
with:
api-level: 29
script: android/build.sh medium-tests
- name: Upload artifacts
uses: actions/upload-artifact@v1
with:
name: Build
path: android/uhabits-android/build/outputs/

@ -0,0 +1,40 @@
name: Build, Test & Publish
on:
push:
branches:
- master
jobs:
build:
runs-on: macOS-latest
steps:
- uses: actions/checkout@v1
- name: Install GPG
uses: olafurpg/setup-gpg@v2
- name: Decrypt secrets
env:
GPG_PASSWORD: ${{ secrets.GPG_PASSWORD }}
run: .secret/decrypt.sh
- name: Install Java Development Kit 1.8
uses: actions/setup-java@v1
with:
java-version: 1.8
- name: Build APK & Run small tests
env:
RELEASE: 1
run: android/build.sh build
- name: Run medium tests
uses: ReactiveCircus/android-emulator-runner@v2.2.0
env:
RELEASE: 1
with:
api-level: 29
script: android/build.sh medium-tests
- name: Upload build to GitHub
uses: actions/upload-artifact@v1
with:
name: Build
path: android/uhabits-android/build/outputs/
- name: Upload APK to Google Play
run: cd android && ./gradlew publishReleaseApk

7
.gitignore vendored

@ -4,13 +4,20 @@
*.perspectivev3 *.perspectivev3
*.swp *.swp
*~.nib *~.nib
*.hprof
.DS_Store .DS_Store
._.DS_Store
.externalNativeBuild .externalNativeBuild
.gradle .gradle
.idea .idea
.secret
build build
build/ build/
captures captures
local.properties local.properties
node_modules node_modules
*xcuserdata* *xcuserdata*
*.sketch
/design
/releases
/screenshots

@ -0,0 +1,16 @@
#!/bin/sh
cd "$(dirname "$0")"
if [ -z "$GPG_PASSWORD" ]; then
echo Env variable GPG_PASSWORD must be defined
exit 1
fi
gpg \
--quiet \
--batch \
--yes \
--decrypt \
--passphrase="$GPG_PASSWORD" \
--output secret.tar.gz \
secret
tar -xzf secret.tar.gz
rm secret.tar.gz

Binary file not shown.

@ -1,5 +1,22 @@
# Changelog # Changelog
### 1.8.8 (June 21, 2020)
* Make small changes to the habit scheduling algorithm, so that "1 time every x days" habits work more predictably.
* Fix crash when saving habit
### 1.8.0 (Jan 1, 2020)
* New bar chart showing number of repetitions performed in each week, month, quarter or year.
* Improved calculation of streaks for non-daily habits: performing habits on irregular weekdays will no longer break your streak.
* Many more colors to choose from (now 20 in total).
* Ability to customize how transparent the widgets are on your home screen.
* Ability to customize the first day of the week.
* Yes/No buttons on notifications, instead of just "Check".
* Automatic dark theme according to phone settings (Android 10).
* Smaller APK and backup files.
* Many other internal code changes improving performance and stability.
### 1.7.11 (Aug 10, 2019) ### 1.7.11 (Aug 10, 2019)
* Fix bug that produced corrupted CSV files in some countries * Fix bug that produced corrupted CSV files in some countries

@ -7,7 +7,7 @@ source.
<p align="center"> <p align="center">
<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="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> <a href="http://f-droid.org/app/org.isoron.uhabits"><img alt="Get it on F-Droid" src="http://i.imgur.com/baSPE7X.png" height="75px"/></a>
</p> </p>
## Screenshots ## Screenshots
@ -21,34 +21,32 @@ source.
## Features ## Features
* **Simple, beautiful and modern interface.** Loop has a minimalistic interface * <b>Beautiful, minimalistic and lightweight interface.</b>
that is easy to use and follows the material design guidelines. Loop has an elegant and minimalistic interface that is very easy to use, even for first-time users. Highly optimized for speed, the app works well even on older phones.
* **Habit score.** In addition to showing your current streak, Loop has an * <b>Habit score.</b>
advanced algorithm for calculating the strength of your habits. Every Loop has an advanced formula 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 progress, unlike many other don't-break-the-chain apps.
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.
* **Detailed graphs and statistics.** Clearly see how your habits improved over * <b>Flexible schedules.</b>
time with beautiful and detailed graphs. Scroll back to see the complete In addition to daily habits, Loop supports habits with more complex schedules, such as 3 times per week or every other day.
history of your habits.
* **Flexible schedules.** Supports both daily habits and habits with more * <b>Reminders.</b>
complex schedules, such as 3 times every week; one time every other week; or Schedule notifications to remind you of your habits. Each habit can have its own reminder, at a chosen time of the day. Easily check or dismiss your habit directly from the notification.
every other day.
* **Reminders.** Create an individual reminder for each habit, at a chosen hour * <b>Widgets.</b>
of the day. Easily check, dismiss or snooze your habit directly from the Be reminded of your habits whenever you unlock your phone. Colorful widgets allow you to track your habits directly from your home screen, without even opening the app.
notification, without opening the app.
* **Optimized for smartwatches.** Reminders can be checked, snoozed or * <b>Take control of your data.</b>
dismissed directly from your Android Wear watch. If you want to further analyze your data, or move it to another service, Loop allows you to export it to spreadsheets (CSV) or to a database file (SQLite). For power users, checkmarks can be added through other apps, such as Tasker.
* **Completely ad-free and open source.** There are absolutely no * <b>No limitations.</b>
advertisements, annoying notifications or intrusive permissions in this app, Track as many habits as you wish. Loop imposes no artificial limits on how many habits you can have. All features are available to all users. There are no in-app purchases.
and there will never be. The complete source code is available under the
GPLv3. * <b>Completely ad-free and open source.</b>
There are no advertisements, annoying notifications or intrusive permissions in this app, and there will never be. The app is completely open-source (GPLv3).
* <b>Works offline and respects your privacy.</b>
Loop doesn't require an Internet connection or online account registration. Your confidential data is never sent to anyone. Neither the developers nor any third-parties have access to it.
## Installing ## Installing
@ -85,33 +83,33 @@ contribute, even if you are not a software developer.
<img align="right" src="https://www.gnu.org/graphics/gplv3-88x31.png"> <img align="right" src="https://www.gnu.org/graphics/gplv3-88x31.png">
Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com> Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
Loop Habit Tracker is free software: you can redistribute it and/or modify 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 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 Free Software Foundation, either version 3 of the License, or (at your
option) any later version. option) any later version.
Loop Habit Tracker is distributed in the hope that it will be useful, but Loop Habit Tracker is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for 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 along You should have received a copy of the GNU General Public License along
with this program. If not, see <http://www.gnu.org/licenses/>. with this program. If not, see <http://www.gnu.org/licenses/>.
[screen1]: screenshots/original/uhabits1.png [screen1]: screenshots/uhabits1.png
[screen2]: screenshots/original/uhabits2.png [screen2]: screenshots/uhabits2.png
[screen3]: screenshots/original/uhabits3.png [screen3]: screenshots/uhabits3.png
[screen4]: screenshots/original/uhabits4.png [screen4]: screenshots/uhabits4.png
[screen5]: screenshots/original/uhabits5.png [screen5]: screenshots/uhabits5.png
[screen6]: screenshots/original/uhabits6.png [screen6]: screenshots/uhabits6.png
[screen1th]: screenshots/thumbs/uhabits1.png [screen1th]: screenshots/uhabits1_th.png
[screen2th]: screenshots/thumbs/uhabits2.png [screen2th]: screenshots/uhabits2_th.png
[screen3th]: screenshots/thumbs/uhabits3.png [screen3th]: screenshots/uhabits3_th.png
[screen4th]: screenshots/thumbs/uhabits4.png [screen4th]: screenshots/uhabits4_th.png
[screen5th]: screenshots/thumbs/uhabits5.png [screen5th]: screenshots/uhabits5_th.png
[screen6th]: screenshots/thumbs/uhabits6.png [screen6th]: screenshots/uhabits6_th.png
[poedit]: http://translate.loophabits.org [poedit]: http://translate.loophabits.org
[playstore]: https://play.google.com/store/apps/details?id=org.isoron.uhabits [playstore]: https://play.google.com/store/apps/details?id=org.isoron.uhabits
[releases]: https://github.com/iSoron/uhabits/releases [releases]: https://github.com/iSoron/uhabits/releases

@ -13,6 +13,7 @@
.gradle .gradle
.idea .idea
.project .project
.secret
Thumbs.db Thumbs.db
art/ art/
bin/ bin/

@ -1,4 +1,5 @@
apply plugin: 'com.android.library' apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
android { android {
compileSdkVersion COMPILE_SDK_VERSION as Integer compileSdkVersion COMPILE_SDK_VERSION as Integer
@ -6,17 +7,8 @@ android {
defaultConfig { defaultConfig {
minSdkVersion MIN_SDK_VERSION as Integer minSdkVersion MIN_SDK_VERSION as Integer
targetSdkVersion TARGET_SDK_VERSION as Integer targetSdkVersion TARGET_SDK_VERSION as Integer
versionCode 1 versionCode VERSION_CODE as Integer
versionName "1.0" versionName "$VERSION_NAME"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
} }
compileOptions { compileOptions {
@ -28,24 +20,14 @@ android {
checkReleaseBuilds false checkReleaseBuilds false
abortOnError false abortOnError false
} }
} }
dependencies { dependencies {
implementation "com.google.dagger:dagger:$DAGGER_VERSION" implementation "com.google.dagger:dagger:$DAGGER_VERSION"
implementation "com.android.support:design:$SUPPORT_LIBRARY_VERSION" implementation 'com.google.android.material:material:1.0.0'
implementation "com.android.support:appcompat-v7:$SUPPORT_LIBRARY_VERSION" implementation 'androidx.appcompat:appcompat:1.0.0'
implementation "org.apache.commons:commons-lang3:3.5" implementation "org.apache.commons:commons-lang3:3.5"
annotationProcessor "com.google.dagger:dagger-compiler:$DAGGER_VERSION" annotationProcessor "com.google.dagger:dagger-compiler:$DAGGER_VERSION"
androidTestAnnotationProcessor "com.google.dagger:dagger-compiler:$DAGGER_VERSION" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$KOTLIN_VERSION"
androidTestImplementation "com.google.dagger:dagger:$DAGGER_VERSION"
testAnnotationProcessor "com.google.dagger:dagger-compiler:$DAGGER_VERSION"
testImplementation "junit:junit:4.12"
androidTestImplementation('com.android.support.test.espresso:espresso-core:2.2.2', {
exclude group: 'com.android.support', module: 'support-annotations'
})
} }

@ -1,155 +0,0 @@
/*
* Copyright (C) 2017 Á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.androidbase;
import android.content.*;
import android.os.*;
import android.support.annotation.*;
import android.view.*;
import java.io.*;
import java.text.*;
import java.util.*;
import javax.inject.*;
public class AndroidBugReporter
{
private final Context context;
@Inject
public AndroidBugReporter(@NonNull @AppContext Context context)
{
this.context = context;
}
/**
* Captures and returns a bug report. The bug report contains some device
* information and the logcat.
*
* @return a String containing the bug report.
* @throws IOException when any I/O error occur.
*/
@NonNull
public String getBugReport() throws IOException
{
String logcat = getLogcat();
String deviceInfo = getDeviceInfo();
String log = "---------- BUG REPORT BEGINS ----------\n";
log += deviceInfo + "\n" + logcat;
log += "---------- BUG REPORT ENDS ------------\n";
return log;
}
public String getDeviceInfo()
{
if (context == null) return "null context\n";
WindowManager wm =
(WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
return
String.format("App Version Name: %s\n", BuildConfig.VERSION_NAME) +
String.format("App Version Code: %s\n", BuildConfig.VERSION_CODE) +
String.format("OS Version: %s (%s)\n",
System.getProperty("os.version"), Build.VERSION.INCREMENTAL) +
String.format("OS API Level: %s\n", Build.VERSION.SDK) +
String.format("Device: %s\n", Build.DEVICE) +
String.format("Model (Product): %s (%s)\n", Build.MODEL,
Build.PRODUCT) +
String.format("Manufacturer: %s\n", Build.MANUFACTURER) +
String.format("Other tags: %s\n", Build.TAGS) +
String.format("Screen Width: %s\n",
wm.getDefaultDisplay().getWidth()) +
String.format("Screen Height: %s\n",
wm.getDefaultDisplay().getHeight()) +
String.format("External storage state: %s\n\n",
Environment.getExternalStorageState());
}
public String getLogcat() throws IOException
{
int maxLineCount = 250;
StringBuilder builder = new StringBuilder();
String[] command = new String[]{ "logcat", "-d" };
java.lang.Process process = Runtime.getRuntime().exec(command);
InputStreamReader in = new InputStreamReader(process.getInputStream());
BufferedReader bufferedReader = new BufferedReader(in);
LinkedList<String> log = new LinkedList<>();
String line;
while ((line = bufferedReader.readLine()) != null)
{
log.addLast(line);
if (log.size() > maxLineCount) log.removeFirst();
}
for (String l : log)
{
builder.append(l);
builder.append('\n');
}
return builder.toString();
}
/**
* Captures a bug report and saves it to a file in the SD card.
* <p>
* The contents of the file are generated by the method {@link
* #getBugReport()}. The file is saved in the apps's external private
* storage.
*
* @return the generated file.
* @throws IOException when I/O errors occur.
*/
@NonNull
public void dumpBugReportToFile()
{
try
{
String date =
new SimpleDateFormat("yyyy-MM-dd HHmmss", Locale.US).format(
new Date());
if (context == null) throw new IllegalStateException();
File dir = new AndroidDirFinder(context).getFilesDir("Logs");
if (dir == null)
throw new IOException("log dir should not be null");
File logFile =
new File(String.format("%s/Log %s.txt", dir.getPath(), date));
FileWriter output = new FileWriter(logFile);
output.write(getBugReport());
output.close();
}
catch (IOException e)
{
e.printStackTrace();
}
}
}

@ -0,0 +1,110 @@
/*
* Copyright (C) 2017 Á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.androidbase
import android.content.Context
import android.os.Build
import android.os.Environment
import android.view.WindowManager
import java.io.*
import java.text.SimpleDateFormat
import java.util.*
import javax.inject.Inject
open class AndroidBugReporter @Inject constructor(@AppContext private val context: Context) {
/**
* Captures and returns a bug report. The bug report contains some device
* information and the logcat.
*
* @return a String containing the bug report.
* @throws IOException when any I/O error occur.
*/
@Throws(IOException::class)
fun getBugReport(): String {
var log = "---------- BUG REPORT BEGINS ----------\n"
log += "${getLogcat()}\n"
log += "${getDeviceInfo()}\n"
log += "---------- BUG REPORT ENDS ------------\n"
return log
}
@Throws(IOException::class)
fun getLogcat(): String {
val maxLineCount = 250
val builder = StringBuilder()
val process = Runtime.getRuntime().exec(arrayOf("logcat", "-d"))
val inputReader = InputStreamReader(process.inputStream)
val bufferedReader = BufferedReader(inputReader)
val log = LinkedList<String>()
var line: String?
while (true) {
line = bufferedReader.readLine()
if (line == null) break;
log.addLast(line)
if (log.size > maxLineCount) log.removeFirst()
}
for (l in log) {
builder.appendln(l)
}
return builder.toString()
}
/**
* Captures a bug report and saves it to a file in the SD card.
*
* The contents of the file are generated by the method [ ][.getBugReport]. The file is saved
* in the apps's external private storage.
*
* @return the generated file.
* @throws IOException when I/O errors occur.
*/
fun dumpBugReportToFile() {
try {
val date = SimpleDateFormat("yyyy-MM-dd HHmmss", Locale.US).format(Date())
val dir = AndroidDirFinder(context).getFilesDir("Logs")
?: throw IOException("log dir should not be null")
val logFile = File(String.format("%s/Log %s.txt", dir.path, date))
val output = FileWriter(logFile)
output.write(getBugReport())
output.close()
} catch (e: IOException) {
e.printStackTrace()
}
}
private fun getDeviceInfo(): String {
val wm = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
return buildString {
appendln("App Version Name: ${BuildConfig.VERSION_NAME}")
appendln("App Version Code: ${BuildConfig.VERSION_CODE}")
appendln("OS Version: ${System.getProperty("os.version")} (${Build.VERSION.INCREMENTAL})")
appendln("OS API Level: ${Build.VERSION.SDK}")
appendln("Device: ${Build.DEVICE}")
appendln("Model (Product): ${Build.MODEL} (${Build.PRODUCT})")
appendln("Manufacturer: ${Build.MANUFACTURER}")
appendln("Other tags: ${Build.TAGS}")
appendln("Screen Width: ${wm.defaultDisplay.width}")
appendln("Screen Height: ${wm.defaultDisplay.height}")
appendln("External storage state: ${Environment.getExternalStorageState()}")
appendln()
}
}
}

@ -1,58 +0,0 @@
/*
* Copyright (C) 2017 Á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.androidbase;
import android.content.*;
import android.support.annotation.*;
import android.support.v4.content.*;
import android.util.*;
import org.isoron.androidbase.utils.*;
import java.io.*;
import javax.inject.*;
public class AndroidDirFinder
{
@NonNull
private Context context;
@Inject
public AndroidDirFinder(@NonNull @AppContext Context context)
{
this.context = context;
}
@Nullable
public File getFilesDir(@Nullable String relativePath)
{
File externalFilesDirs[] =
ContextCompat.getExternalFilesDirs(context, null);
if (externalFilesDirs == null)
{
Log.e("BaseSystem",
"getFilesDir: getExternalFilesDirs returned null");
return null;
}
return FileUtils.getDir(externalFilesDirs, relativePath);
}
}

@ -0,0 +1,34 @@
/*
* Copyright (C) 2017 Á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.androidbase
import android.content.Context
import androidx.core.content.ContextCompat
import org.isoron.androidbase.utils.FileUtils
import java.io.File
import javax.inject.Inject
class AndroidDirFinder @Inject constructor(@param:AppContext private val context: Context) {
fun getFilesDir(relativePath: String): File? {
return FileUtils.getDir(
ContextCompat.getExternalFilesDirs(context, null),
relativePath
)
}
}

@ -16,16 +16,14 @@
* You should have received a copy of the GNU General Public License along * You should have received a copy of the GNU General Public License 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.androidbase
package org.isoron.androidbase.activities; import java.lang.annotation.Documented
import java.lang.annotation.Retention
import java.lang.annotation.*; import java.lang.annotation.RetentionPolicy
import javax.inject.Qualifier
import javax.inject.*;
@Qualifier @Qualifier
@Documented @Documented
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
public @interface ActivityContext annotation class AppContext
{
}

@ -1,67 +0,0 @@
/*
* Copyright (C) 2017 Á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.androidbase;
import android.support.annotation.*;
import org.isoron.androidbase.activities.*;
public class BaseExceptionHandler implements Thread.UncaughtExceptionHandler
{
@Nullable
private Thread.UncaughtExceptionHandler originalHandler;
@NonNull
private BaseActivity activity;
public BaseExceptionHandler(@NonNull BaseActivity activity)
{
this.activity = activity;
originalHandler = Thread.getDefaultUncaughtExceptionHandler();
}
@Override
public void uncaughtException(@Nullable Thread thread,
@Nullable Throwable ex)
{
if (ex == null) return;
try
{
ex.printStackTrace();
new AndroidBugReporter(activity).dumpBugReportToFile();
}
catch (Exception e)
{
e.printStackTrace();
}
// if (ex.getCause() instanceof InconsistentDatabaseException)
// {
// HabitsApplication app = (HabitsApplication) activity.getApplication();
// HabitList habits = app.getComponent().getHabitList();
// habits.repair();
// System.exit(0);
// }
if (originalHandler != null)
originalHandler.uncaughtException(thread, ex);
}
}

@ -0,0 +1,39 @@
/*
* Copyright (C) 2017 Á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.androidbase
import org.isoron.androidbase.activities.BaseActivity
class BaseExceptionHandler(private val activity: BaseActivity) : Thread.UncaughtExceptionHandler {
private val originalHandler: Thread.UncaughtExceptionHandler? =
Thread.getDefaultUncaughtExceptionHandler()
override fun uncaughtException(thread: Thread?, ex: Throwable?) {
if (ex == null) return
if (thread == null) return
try {
ex.printStackTrace()
AndroidBugReporter(activity).dumpBugReportToFile()
} catch (e: Exception) {
e.printStackTrace()
}
originalHandler?.uncaughtException(thread, ex)
}
}

@ -1,71 +0,0 @@
/*
* Copyright (C) 2017 Á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.androidbase;
import android.content.*;
import android.support.annotation.*;
import org.isoron.androidbase.*;
import java.io.*;
import java.security.*;
import java.security.cert.Certificate;
import java.security.cert.*;
import javax.inject.*;
import javax.net.ssl.*;
public class SSLContextProvider
{
private Context context;
@Inject
public SSLContextProvider(@NonNull @AppContext Context context)
{
this.context = context;
}
public SSLContext getCACertSSLContext()
{
try
{
CertificateFactory cf = CertificateFactory.getInstance("X.509");
InputStream caInput = context.getAssets().open("cacert.pem");
Certificate ca = cf.generateCertificate(caInput);
KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
ks.load(null, null);
ks.setCertificateEntry("ca", ca);
TrustManagerFactory tmf = TrustManagerFactory.getInstance(
TrustManagerFactory.getDefaultAlgorithm());
tmf.init(ks);
SSLContext ctx = SSLContext.getInstance("TLS");
ctx.init(null, tmf.getTrustManagers(), null);
return ctx;
}
catch (Exception e)
{
throw new RuntimeException(e);
}
}
}

@ -0,0 +1,48 @@
/*
* Copyright (C) 2017 Á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.androidbase
import android.content.Context
import java.security.KeyStore
import java.security.cert.CertificateFactory
import javax.inject.Inject
import javax.net.ssl.SSLContext
import javax.net.ssl.TrustManagerFactory
class SSLContextProvider @Inject constructor(@param:AppContext private val context: Context) {
fun getCACertSSLContext(): SSLContext {
try {
val cf = CertificateFactory.getInstance("X.509")
val ca = cf.generateCertificate(context.assets.open("cacert.pem"))
val ks = KeyStore.getInstance(KeyStore.getDefaultType()).apply {
load(null, null)
setCertificateEntry("ca", ca)
}
val alg = TrustManagerFactory.getDefaultAlgorithm()
val tmf = TrustManagerFactory.getInstance(alg).apply {
init(ks)
}
return SSLContext.getInstance("TLS").apply {
init(null, tmf.trustManagers, null)
}
} catch (e: Exception) {
throw RuntimeException(e)
}
}
}

@ -16,16 +16,11 @@
* You should have received a copy of the GNU General Public License along * You should have received a copy of the GNU General Public License 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.androidbase.activities
package org.isoron.androidbase; import javax.inject.*
import java.lang.annotation.*;
import javax.inject.*;
@Qualifier @Qualifier
@Documented @MustBeDocumented
@Retention(RetentionPolicy.RUNTIME) @kotlin.annotation.Retention(AnnotationRetention.RUNTIME)
public @interface AppContext annotation class ActivityContext
{
}

@ -16,13 +16,12 @@
* You should have received a copy of the GNU General Public License along * You should have received a copy of the GNU General Public License 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.androidbase.activities
package org.isoron.androidbase.activities; import javax.inject.*
import javax.inject.*;
/** /**
* Scope used by objects that live as long as the activity is alive. * Scope used by objects that live as long as the activity is alive.
*/ */
@Scope @Scope
public @interface ActivityScope { } annotation class ActivityScope

@ -21,8 +21,9 @@ package org.isoron.androidbase.activities;
import android.content.*; import android.content.*;
import android.os.*; import android.os.*;
import android.support.annotation.*;
import android.support.v7.app.*; import androidx.annotation.Nullable;
import androidx.appcompat.app.*;
import android.view.*; import android.view.*;
import org.isoron.androidbase.*; import org.isoron.androidbase.*;

@ -19,9 +19,11 @@
package org.isoron.androidbase.activities; package org.isoron.androidbase.activities;
import android.support.annotation.*;
import android.view.*; import android.view.*;
import androidx.annotation.MenuRes;
import androidx.annotation.NonNull;
/** /**
* Base class for all the menus in the application. * Base class for all the menus in the application.
* <p> * <p>

@ -20,8 +20,10 @@
package org.isoron.androidbase.activities; package org.isoron.androidbase.activities;
import android.content.*; import android.content.*;
import android.support.annotation.*;
import android.support.v7.widget.Toolbar; import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.Toolbar;
import android.view.*; import android.view.*;
import android.widget.*; import android.widget.*;

@ -24,15 +24,19 @@ import android.graphics.*;
import android.graphics.drawable.*; import android.graphics.drawable.*;
import android.net.*; import android.net.*;
import android.os.*; import android.os.*;
import android.support.annotation.*;
import android.support.design.widget.*; import androidx.annotation.NonNull;
import android.support.v4.content.res.*; import androidx.annotation.Nullable;
import android.support.v7.app.*; import androidx.annotation.StringRes;
import android.support.v7.view.ActionMode; import androidx.core.content.res.*;
import android.support.v7.widget.Toolbar; import androidx.appcompat.app.*;
import androidx.appcompat.view.ActionMode;
import androidx.appcompat.widget.Toolbar;
import android.view.*; import android.view.*;
import android.widget.*; import android.widget.*;
import com.google.android.material.snackbar.Snackbar;
import org.isoron.androidbase.*; import org.isoron.androidbase.*;
import org.isoron.androidbase.utils.*; import org.isoron.androidbase.utils.*;
@ -40,7 +44,7 @@ import java.io.*;
import static android.os.Build.VERSION.SDK_INT; import static android.os.Build.VERSION.SDK_INT;
import static android.os.Build.VERSION_CODES.LOLLIPOP; import static android.os.Build.VERSION_CODES.LOLLIPOP;
import static android.support.v4.content.FileProvider.getUriForFile; import static androidx.core.content.FileProvider.getUriForFile;
/** /**
* Base class for all screens in the application. * Base class for all screens in the application.
@ -214,7 +218,7 @@ public class BaseScreen
if (snackbar == null) if (snackbar == null)
{ {
snackbar = Snackbar.make(rootView, stringId, Snackbar.LENGTH_SHORT); snackbar = Snackbar.make(rootView, stringId, Snackbar.LENGTH_SHORT);
int tvId = android.support.design.R.id.snackbar_text; int tvId = R.id.snackbar_text;
TextView tv = (TextView) snackbar.getView().findViewById(tvId); TextView tv = (TextView) snackbar.getView().findViewById(tvId);
tv.setTextColor(Color.WHITE); tv.setTextColor(Color.WHITE);
} }

@ -19,8 +19,9 @@
package org.isoron.androidbase.activities; package org.isoron.androidbase.activities;
import android.support.annotation.*; import androidx.annotation.NonNull;
import android.support.v7.view.ActionMode; import androidx.annotation.Nullable;
import androidx.appcompat.view.ActionMode;
import android.view.*; import android.view.*;
/** /**

@ -1,66 +0,0 @@
/*
* 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.androidbase.utils;
import android.graphics.*;
public abstract class ColorUtils
{
public static int mixColors(int color1, int color2, float amount)
{
final byte ALPHA_CHANNEL = 24;
final byte RED_CHANNEL = 16;
final byte GREEN_CHANNEL = 8;
final byte BLUE_CHANNEL = 0;
final float inverseAmount = 1.0f - amount;
int a = ((int) (((float) (color1 >> ALPHA_CHANNEL & 0xff) * amount) +
((float) (color2 >> ALPHA_CHANNEL & 0xff) *
inverseAmount))) & 0xff;
int r = ((int) (((float) (color1 >> RED_CHANNEL & 0xff) * amount) +
((float) (color2 >> RED_CHANNEL & 0xff) *
inverseAmount))) & 0xff;
int g = ((int) (((float) (color1 >> GREEN_CHANNEL & 0xff) * amount) +
((float) (color2 >> GREEN_CHANNEL & 0xff) *
inverseAmount))) & 0xff;
int b = ((int) (((float) (color1 & 0xff) * amount) +
((float) (color2 & 0xff) * inverseAmount))) & 0xff;
return a << ALPHA_CHANNEL | r << RED_CHANNEL | g << GREEN_CHANNEL |
b << BLUE_CHANNEL;
}
public static int setAlpha(int color, float newAlpha)
{
int intAlpha = (int) (newAlpha * 255);
return Color.argb(intAlpha, Color.red(color), Color.green(color),
Color.blue(color));
}
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);
}
}

@ -0,0 +1,58 @@
/*
* 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.androidbase.utils
import android.graphics.Color
import kotlin.math.max
object ColorUtils {
private const val ALPHA_CHANNEL = 24
private const val RED_CHANNEL = 16
private const val GREEN_CHANNEL = 8
private const val BLUE_CHANNEL = 0
@JvmStatic
fun mixColors(color1: Int, color2: Int, amount: Float): Int {
val a = mixColorChannel(color1, color2, amount, ALPHA_CHANNEL)
val r = mixColorChannel(color1, color2, amount, RED_CHANNEL)
val g = mixColorChannel(color1, color2, amount, GREEN_CHANNEL)
val b = mixColorChannel(color1, color2, amount, BLUE_CHANNEL)
return a or r or g or b
}
@JvmStatic
fun setAlpha(color: Int, newAlpha: Float): Int {
val intAlpha = (newAlpha * 255).toInt()
return Color.argb(intAlpha, Color.red(color), Color.green(color), Color.blue(color))
}
@JvmStatic
fun setMinValue(color: Int, newValue: Float): Int {
val hsv = FloatArray(3)
Color.colorToHSV(color, hsv)
hsv[2] = max(hsv[2], newValue)
return Color.HSVToColor(hsv)
}
private fun mixColorChannel(color1: Int, color2: Int, amount: Float, channel: Int): Int {
val fl = (color1 shr channel and 0xff).toFloat() * amount
val f2 = (color2 shr channel and 0xff).toFloat() * (1.0f - amount)
return (fl + f2).toInt() and 0xff shl channel
}
}

@ -1,92 +0,0 @@
/*
* Copyright (C) 2017 Á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.androidbase.utils;
import android.os.*;
import android.support.annotation.*;
import android.util.*;
import java.io.*;
public abstract class FileUtils
{
public static void copy(File src, File dst) throws IOException
{
FileInputStream inStream = new FileInputStream(src);
FileOutputStream outStream = new FileOutputStream(dst);
copy(inStream, outStream);
}
public static void copy(InputStream inStream, File dst) throws IOException
{
FileOutputStream outStream = new FileOutputStream(dst);
copy(inStream, outStream);
}
public static void copy(InputStream in, OutputStream out) throws IOException
{
int numBytes;
byte[] buffer = new byte[1024];
while ((numBytes = in.read(buffer)) != -1)
out.write(buffer, 0, numBytes);
}
@Nullable
public static File getDir(@NonNull File potentialParentDirs[],
@Nullable String relativePath)
{
if (relativePath == null) relativePath = "";
File chosenDir = null;
for (File dir : potentialParentDirs)
{
if (dir == null || !dir.canWrite()) continue;
chosenDir = dir;
break;
}
if (chosenDir == null)
{
Log.e("FileUtils",
"getDir: all potential parents are null or non-writable");
return null;
}
File dir = new File(
String.format("%s/%s/", chosenDir.getAbsolutePath(), relativePath));
if (!dir.exists() && !dir.mkdirs())
{
Log.e("FileUtils",
"getDir: chosen dir does not exist and cannot be created");
return null;
}
return dir;
}
@Nullable
public static File getSDCardDir(@Nullable String relativePath)
{
File parents[] =
new File[]{ Environment.getExternalStorageDirectory() };
return getDir(parents, relativePath);
}
}

@ -0,0 +1,66 @@
/*
* Copyright (C) 2017 Á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.androidbase.utils
import android.os.Environment
import android.util.Log
import java.io.*
fun File.copyTo(dst: File) {
val inStream = FileInputStream(this)
val outStream = FileOutputStream(dst)
inStream.copyTo(outStream)
}
fun InputStream.copyTo(dst: File) {
val outStream = FileOutputStream(dst)
this.copyTo(outStream)
}
fun InputStream.copyTo(out: OutputStream) {
var numBytes: Int
val buffer = ByteArray(1024)
while (this.read(buffer).also { numBytes = it } != -1) {
out.write(buffer, 0, numBytes)
}
}
object FileUtils {
@JvmStatic
fun getDir(potentialParentDirs: Array<File>, relativePath: String): File? {
val chosenDir: File? = potentialParentDirs.firstOrNull { dir -> dir.canWrite() }
if (chosenDir == null) {
Log.e("FileUtils", "getDir: all potential parents are null or non-writable")
return null
}
val dir = File("${chosenDir.absolutePath}/${relativePath}/")
if (!dir.exists() && !dir.mkdirs()) {
Log.e("FileUtils", "getDir: chosen dir does not exist and cannot be created")
return null
}
return dir
}
@JvmStatic
fun getSDCardDir(relativePath: String): File? {
val parents = arrayOf(Environment.getExternalStorageDirectory())
return getDir(parents, relativePath)
}
}

@ -1,102 +0,0 @@
/*
* 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.androidbase.utils;
import android.content.*;
import android.content.res.*;
import android.graphics.*;
import android.support.annotation.*;
import android.support.v4.view.*;
import android.util.*;
import android.view.*;
import android.widget.*;
public abstract class InterfaceUtils
{
private static Typeface fontAwesome;
@Nullable
private static Float fixedResolution = null;
public static void setFixedResolution(@NonNull Float f)
{
fixedResolution = f;
}
public static Typeface getFontAwesome(Context context)
{
if(fontAwesome == null) fontAwesome =
Typeface.createFromAsset(context.getAssets(),
"fontawesome-webfont.ttf");
return fontAwesome;
}
public static float dpToPixels(Context context, float dp)
{
if(fixedResolution != null) return dp * fixedResolution;
Resources resources = context.getResources();
DisplayMetrics metrics = resources.getDisplayMetrics();
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, metrics);
}
public static float spToPixels(Context context, float sp)
{
if(fixedResolution != null) return sp * fixedResolution;
Resources resources = context.getResources();
DisplayMetrics metrics = resources.getDisplayMetrics();
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp, metrics);
}
public static float getDimension(Context context, int id)
{
float dim = context.getResources().getDimension(id);
if (fixedResolution == null) return dim;
else
{
DisplayMetrics dm = context.getResources().getDisplayMetrics();
float actualDensity = dm.density;
return dim / actualDensity * fixedResolution;
}
}
public static void setupEditorAction(@NonNull ViewGroup parent,
@NonNull TextView.OnEditorActionListener listener)
{
for (int i = 0; i < parent.getChildCount(); i++)
{
View child = parent.getChildAt(i);
if (child instanceof ViewGroup)
setupEditorAction((ViewGroup) child, listener);
if (child instanceof TextView)
((TextView) child).setOnEditorActionListener(listener);
}
}
public static boolean isLayoutRtl(View view)
{
return ViewCompat.getLayoutDirection(view) ==
ViewCompat.LAYOUT_DIRECTION_RTL;
}
}

@ -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.androidbase.utils
import android.content.*
import android.graphics.*
import android.util.*
import android.view.*
import android.widget.*
import android.widget.TextView.*
import androidx.core.view.*
object InterfaceUtils {
private var fontAwesome: Typeface? = null
private var fixedResolution: Float? = null
@JvmStatic
fun setFixedResolution(f: Float) {
fixedResolution = f
}
@JvmStatic
fun getFontAwesome(context: Context): Typeface? {
if (fontAwesome == null) {
fontAwesome = Typeface.createFromAsset(context.assets, "fontawesome-webfont.ttf")
}
return fontAwesome
}
@JvmStatic
fun dpToPixels(context: Context, dp: Float): Float {
if (fixedResolution != null) return dp * fixedResolution!!
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
dp,
context.resources.displayMetrics)
}
@JvmStatic
fun spToPixels(context: Context, sp: Float): Float {
if (fixedResolution != null) return sp * fixedResolution!!
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,
sp,
context.resources.displayMetrics)
}
@JvmStatic
fun getDimension(context: Context, id: Int): Float {
val dim = context.resources.getDimension(id)
if (fixedResolution != null) {
val actualDensity = context.resources.displayMetrics.density
return dim / actualDensity * fixedResolution!!
}
return dim
}
fun setupEditorAction(parent: ViewGroup,
listener: OnEditorActionListener) {
for (i in 0 until parent.childCount) {
val child = parent.getChildAt(i)
if (child is ViewGroup) setupEditorAction(child, listener)
if (child is TextView) child.setOnEditorActionListener(listener)
}
}
fun isLayoutRtl(view: View?): Boolean {
return ViewCompat.getLayoutDirection(view!!) ==
ViewCompat.LAYOUT_DIRECTION_RTL
}
}

@ -1,116 +0,0 @@
/*
* 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.androidbase.utils;
import android.content.*;
import android.content.res.*;
import android.graphics.drawable.*;
import android.support.annotation.*;
import org.isoron.androidbase.*;
public class StyledResources
{
private static Integer fixedTheme;
private final Context context;
public StyledResources(@NonNull Context context)
{
this.context = context;
}
public static void setFixedTheme(Integer theme)
{
fixedTheme = theme;
}
public boolean getBoolean(@AttrRes int attrId)
{
TypedArray ta = getTypedArray(attrId);
boolean bool = ta.getBoolean(0, false);
ta.recycle();
return bool;
}
public int getDimension(@AttrRes int attrId)
{
TypedArray ta = getTypedArray(attrId);
int dim = ta.getDimensionPixelSize(0, 0);
ta.recycle();
return dim;
}
public int getColor(@AttrRes int attrId)
{
TypedArray ta = getTypedArray(attrId);
int color = ta.getColor(0, 0);
ta.recycle();
return color;
}
public Drawable getDrawable(@AttrRes int attrId)
{
TypedArray ta = getTypedArray(attrId);
Drawable drawable = ta.getDrawable(0);
ta.recycle();
return drawable;
}
public float getFloat(@AttrRes int attrId)
{
TypedArray ta = getTypedArray(attrId);
float f = ta.getFloat(0, 0);
ta.recycle();
return f;
}
public int[] getPalette()
{
int resourceId = getResource(R.attr.palette);
if (resourceId < 0) throw new RuntimeException("resource not found");
return context.getResources().getIntArray(resourceId);
}
public int getResource(@AttrRes int attrId)
{
TypedArray ta = getTypedArray(attrId);
int resourceId = ta.getResourceId(0, -1);
ta.recycle();
return resourceId;
}
private TypedArray getTypedArray(@AttrRes int attrId)
{
int[] attrs = new int[]{ attrId };
if (fixedTheme != null)
return context.getTheme().obtainStyledAttributes(fixedTheme, attrs);
return context.obtainStyledAttributes(attrs);
}
}

@ -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.androidbase.utils
import android.content.Context
import android.content.res.TypedArray
import android.graphics.drawable.Drawable
import androidx.annotation.AttrRes
import org.isoron.androidbase.R
class StyledResources(private val context: Context) {
fun getBoolean(@AttrRes attrId: Int): Boolean {
val ta = getTypedArray(attrId)
val bool = ta.getBoolean(0, false)
ta.recycle()
return bool
}
fun getDimension(@AttrRes attrId: Int): Int {
val ta = getTypedArray(attrId)
val dim = ta.getDimensionPixelSize(0, 0)
ta.recycle()
return dim
}
fun getColor(@AttrRes attrId: Int): Int {
val ta = getTypedArray(attrId)
val color = ta.getColor(0, 0)
ta.recycle()
return color
}
fun getDrawable(@AttrRes attrId: Int): Drawable? {
val ta = getTypedArray(attrId)
val drawable = ta.getDrawable(0)
ta.recycle()
return drawable
}
fun getFloat(@AttrRes attrId: Int): Float {
val ta = getTypedArray(attrId)
val f = ta.getFloat(0, 0f)
ta.recycle()
return f
}
fun getPalette(): IntArray {
val resourceId = getResource(R.attr.palette)
if (resourceId < 0) throw RuntimeException("palette resource not found")
return context.resources.getIntArray(resourceId)
}
fun getResource(@AttrRes attrId: Int): Int {
val ta = getTypedArray(attrId)
val resourceId = ta.getResourceId(0, -1)
ta.recycle()
return resourceId
}
private fun getTypedArray(@AttrRes attrId: Int): TypedArray {
val attrs = intArrayOf(attrId)
if (fixedTheme != null) {
return context.theme.obtainStyledAttributes(fixedTheme!!, attrs)
}
return context.obtainStyledAttributes(attrs)
}
companion object {
private var fixedTheme: Int? = null
@JvmStatic
fun setFixedTheme(theme: Int?) {
fixedTheme = theme
}
}
}

@ -6,16 +6,6 @@ android {
defaultConfig { defaultConfig {
minSdkVersion MIN_SDK_VERSION as Integer minSdkVersion MIN_SDK_VERSION as Integer
targetSdkVersion TARGET_SDK_VERSION as Integer targetSdkVersion TARGET_SDK_VERSION as Integer
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
} }
compileOptions { compileOptions {
@ -30,5 +20,5 @@ android {
} }
dependencies { dependencies {
implementation "com.android.support:appcompat-v7:$SUPPORT_LIBRARY_VERSION" implementation 'androidx.appcompat:appcompat:1.0.0'
} }

@ -18,8 +18,8 @@ package com.android.colorpicker;
import android.app.*; import android.app.*;
import android.os.*; import android.os.*;
import android.support.v7.app.AlertDialog; import androidx.appcompat.app.AlertDialog;
import android.support.v7.app.*; import androidx.appcompat.app.*;
import android.view.*; import android.view.*;
import android.widget.*; import android.widget.*;

@ -21,13 +21,15 @@ import android.content.res.*;
import android.graphics.*; import android.graphics.*;
import android.graphics.Paint.*; import android.graphics.Paint.*;
import android.os.*; import android.os.*;
import android.support.v4.view.*; import androidx.core.view.*;
import android.support.v4.view.accessibility.*; import androidx.core.view.accessibility.*;
import android.support.v4.widget.*; import androidx.core.widget.*;
import android.text.format.*; import android.text.format.*;
import android.view.*; import android.view.*;
import android.view.accessibility.*; import android.view.accessibility.*;
import androidx.customview.widget.ExploreByTouchHelper;
import com.android.*; import com.android.*;
import com.android.datetimepicker.*; import com.android.datetimepicker.*;
import com.android.datetimepicker.date.MonthAdapter.*; import com.android.datetimepicker.date.MonthAdapter.*;

@ -41,8 +41,8 @@ public class AmPmCirclesView extends View {
private final Paint mPaint = new Paint(); private final Paint mPaint = new Paint();
private int mSelectedAlpha; private int mSelectedAlpha;
private int mUnselectedColor; private int mUnselectedColor;
private int mAmPmTextColor; protected int mAmPmTextColor = Color.WHITE;
private int mSelectedColor; protected int mSelectedColor = Color.BLUE;
private float mCircleRadiusMultiplier; private float mCircleRadiusMultiplier;
private float mAmPmCircleRadiusMultiplier; private float mAmPmCircleRadiusMultiplier;
private String mAmText; private String mAmText;
@ -73,8 +73,8 @@ public class AmPmCirclesView extends View {
Resources res = context.getResources(); Resources res = context.getResources();
mUnselectedColor = res.getColor(R.color.white); mUnselectedColor = res.getColor(R.color.white);
mSelectedColor = res.getColor(R.color.blue); //mSelectedColor = res.getColor(R.color.blue);
mAmPmTextColor = res.getColor(R.color.ampm_text_color); //mAmPmTextColor = res.getColor(R.color.ampm_text_color);
mSelectedAlpha = SELECTED_ALPHA; mSelectedAlpha = SELECTED_ALPHA;
String typefaceFamily = res.getString(R.string.sans_serif); String typefaceFamily = res.getString(R.string.sans_serif);
Typeface tf = Typeface.create(typefaceFamily, Typeface.NORMAL); Typeface tf = Typeface.create(typefaceFamily, Typeface.NORMAL);
@ -105,8 +105,8 @@ public class AmPmCirclesView extends View {
mSelectedAlpha = SELECTED_ALPHA_THEME_DARK; mSelectedAlpha = SELECTED_ALPHA_THEME_DARK;
} else { } else {
mUnselectedColor = res.getColor(R.color.white); mUnselectedColor = res.getColor(R.color.white);
mSelectedColor = res.getColor(R.color.blue); //mSelectedColor = res.getColor(R.color.blue);
mAmPmTextColor = res.getColor(R.color.ampm_text_color); //mAmPmTextColor = res.getColor(R.color.ampm_text_color);
mSelectedAlpha = SELECTED_ALPHA; mSelectedAlpha = SELECTED_ALPHA;
} }
} }

@ -84,6 +84,14 @@ public class RadialPickerLayout extends FrameLayout implements OnTouchListener {
private AnimatorSet mTransition; private AnimatorSet mTransition;
private Handler mHandler = new Handler(); private Handler mHandler = new Handler();
public void setColor(int selectedColor)
{
mHourRadialSelectorView.mPaint.setColor(selectedColor);
mMinuteRadialSelectorView.mPaint.setColor(selectedColor);
mAmPmCirclesView.mSelectedColor = selectedColor;
mAmPmCirclesView.mAmPmTextColor = selectedColor;
}
public interface OnValueSelectedListener { public interface OnValueSelectedListener {
void onValueSelected(int pickerIndex, int newValue, boolean autoAdvance); void onValueSelected(int pickerIndex, int newValue, boolean autoAdvance);
} }

@ -40,7 +40,7 @@ public class RadialSelectorView extends View {
// Alpha level for the line. // Alpha level for the line.
private static final int FULL_ALPHA = Utils.FULL_ALPHA; private static final int FULL_ALPHA = Utils.FULL_ALPHA;
private final Paint mPaint = new Paint(); protected final Paint mPaint = new Paint();
private boolean mIsInitialized; private boolean mIsInitialized;
private boolean mDrawValuesReady; private boolean mDrawValuesReady;
@ -96,8 +96,6 @@ public class RadialSelectorView extends View {
Resources res = context.getResources(); Resources res = context.getResources();
int blue = res.getColor(R.color.blue);
mPaint.setColor(blue);
mPaint.setAntiAlias(true); mPaint.setAntiAlias(true);
mSelectionAlpha = SELECTED_ALPHA; mSelectionAlpha = SELECTED_ALPHA;
@ -139,15 +137,11 @@ public class RadialSelectorView extends View {
/* package */ void setTheme(Context context, boolean themeDark) { /* package */ void setTheme(Context context, boolean themeDark) {
Resources res = context.getResources(); Resources res = context.getResources();
int color;
if (themeDark) { if (themeDark) {
color = res.getColor(R.color.red);
mSelectionAlpha = SELECTED_ALPHA_THEME_DARK; mSelectionAlpha = SELECTED_ALPHA_THEME_DARK;
} else { } else {
color = res.getColor(R.color.blue);
mSelectionAlpha = SELECTED_ALPHA; mSelectionAlpha = SELECTED_ALPHA;
} }
mPaint.setColor(color);
} }
/** /**

@ -23,7 +23,9 @@ import android.app.*;
import android.content.*; import android.content.*;
import android.content.res.*; import android.content.res.*;
import android.os.*; import android.os.*;
import android.support.v7.app.*;
import androidx.appcompat.app.*;
import android.util.*; import android.util.*;
import android.view.*; import android.view.*;
import android.view.View.*; import android.view.View.*;
@ -39,7 +41,8 @@ import java.util.*;
/** /**
* Dialog to set a time. * Dialog to set a time.
*/ */
public class TimePickerDialog extends AppCompatDialogFragment implements OnValueSelectedListener{ public class TimePickerDialog extends AppCompatDialogFragment implements OnValueSelectedListener
{
private static final String TAG = "TimePickerDialog"; private static final String TAG = "TimePickerDialog";
private static final String KEY_HOUR_OF_DAY = "hour_of_day"; private static final String KEY_HOUR_OF_DAY = "hour_of_day";
@ -49,6 +52,7 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
private static final String KEY_IN_KB_MODE = "in_kb_mode"; private static final String KEY_IN_KB_MODE = "in_kb_mode";
private static final String KEY_TYPED_TIMES = "typed_times"; private static final String KEY_TYPED_TIMES = "typed_times";
private static final String KEY_DARK_THEME = "dark_theme"; private static final String KEY_DARK_THEME = "dark_theme";
private static final String KEY_SELECTED_COLOR = "selected_color";
public static final int HOUR_INDEX = 0; public static final int HOUR_INDEX = 0;
public static final int MINUTE_INDEX = 1; public static final int MINUTE_INDEX = 1;
@ -108,37 +112,50 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
* The callback interface used to indicate the user is done filling in * The callback interface used to indicate the user is done filling in
* the time (they clicked on the 'Set' button). * the time (they clicked on the 'Set' button).
*/ */
public interface OnTimeSetListener { public interface OnTimeSetListener
{
/** /**
* @param view The view associated with this listener. * @param view The view associated with this listener.
* @param hourOfDay The hour that was set. * @param hourOfDay The hour that was set.
* @param minute The minute that was set. * @param minute The minute that was set.
*/ */
void onTimeSet(RadialPickerLayout view, int hourOfDay, int minute); void onTimeSet(RadialPickerLayout view, int hourOfDay, int minute);
default void onTimeCleared(RadialPickerLayout view) {} default void onTimeCleared(RadialPickerLayout view)
{
}
} }
public TimePickerDialog() { public TimePickerDialog()
{
// Empty constructor required for dialog fragment. // Empty constructor required for dialog fragment.
} }
@SuppressLint("Java") @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.
} }
public static TimePickerDialog newInstance(OnTimeSetListener callback, public static TimePickerDialog newInstance(OnTimeSetListener callback,
int hourOfDay, int minute, boolean is24HourMode) { int hourOfDay,
int minute,
boolean is24HourMode,
int color)
{
TimePickerDialog ret = new TimePickerDialog(); TimePickerDialog ret = new TimePickerDialog();
ret.initialize(callback, hourOfDay, minute, is24HourMode); ret.initialize(callback, hourOfDay, minute, is24HourMode, color);
return ret; return ret;
} }
public void initialize(OnTimeSetListener callback, public void initialize(OnTimeSetListener callback,
int hourOfDay, int minute, boolean is24HourMode) { int hourOfDay,
int minute,
boolean is24HourMode,
int color)
{
mCallback = callback; mCallback = callback;
mInitialHourOfDay = hourOfDay; mInitialHourOfDay = hourOfDay;
@ -146,40 +163,47 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
mIs24HourMode = is24HourMode; mIs24HourMode = is24HourMode;
mInKbMode = false; mInKbMode = false;
mThemeDark = false; mThemeDark = false;
mSelectedColor = color;
} }
/** /**
* Set a dark or light theme. NOTE: this will only take effect for the next onCreateView. * Set a dark or light theme. NOTE: this will only take effect for the next onCreateView.
*/ */
public void setThemeDark(boolean dark) { public void setThemeDark(boolean dark)
{
mThemeDark = dark; mThemeDark = dark;
} }
public boolean isThemeDark() { public boolean isThemeDark()
{
return mThemeDark; return mThemeDark;
} }
public void setOnTimeSetListener(OnTimeSetListener callback) { public void setOnTimeSetListener(OnTimeSetListener callback)
{
mCallback = callback; mCallback = callback;
} }
public void setStartTime(int hourOfDay, int minute) { public void setStartTime(int hourOfDay, int minute)
{
mInitialHourOfDay = hourOfDay; mInitialHourOfDay = hourOfDay;
mInitialMinute = minute; mInitialMinute = minute;
mInKbMode = false; mInKbMode = false;
} }
@Override @Override
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
if (savedInstanceState != null && savedInstanceState.containsKey(KEY_HOUR_OF_DAY) if (savedInstanceState != null && savedInstanceState.containsKey(KEY_HOUR_OF_DAY)
&& savedInstanceState.containsKey(KEY_MINUTE) && savedInstanceState.containsKey(KEY_MINUTE)
&& savedInstanceState.containsKey(KEY_IS_24_HOUR_VIEW)) { && savedInstanceState.containsKey(KEY_IS_24_HOUR_VIEW)) {
mInitialHourOfDay = savedInstanceState.getInt(KEY_HOUR_OF_DAY); mInitialHourOfDay = savedInstanceState.getInt(KEY_HOUR_OF_DAY);
mInitialMinute = savedInstanceState.getInt(KEY_MINUTE); mInitialMinute = savedInstanceState.getInt(KEY_MINUTE);
mIs24HourMode = savedInstanceState.getBoolean(KEY_IS_24_HOUR_VIEW); mIs24HourMode = savedInstanceState.getBoolean(KEY_IS_24_HOUR_VIEW);
mInKbMode = savedInstanceState.getBoolean(KEY_IN_KB_MODE); mInKbMode = savedInstanceState.getBoolean(KEY_IN_KB_MODE);
mThemeDark = savedInstanceState.getBoolean(KEY_DARK_THEME); mThemeDark = savedInstanceState.getBoolean(KEY_DARK_THEME);
mSelectedColor = savedInstanceState.getInt(KEY_SELECTED_COLOR);
} }
} }
@ -191,7 +215,8 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
@Override @Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) { Bundle savedInstanceState)
{
getDialog().requestWindowFeature(Window.FEATURE_NO_TITLE); getDialog().requestWindowFeature(Window.FEATURE_NO_TITLE);
View view = inflater.inflate(R.layout.time_picker_dialog, null); View view = inflater.inflate(R.layout.time_picker_dialog, null);
@ -203,8 +228,8 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
mSelectHours = res.getString(R.string.select_hours); mSelectHours = res.getString(R.string.select_hours);
mMinutePickerDescription = res.getString(R.string.minute_picker_description); mMinutePickerDescription = res.getString(R.string.minute_picker_description);
mSelectMinutes = res.getString(R.string.select_minutes); mSelectMinutes = res.getString(R.string.select_minutes);
mSelectedColor = res.getColor(mThemeDark? R.color.red : R.color.blue); //mSelectedColor = res.getColor(mThemeDark ? R.color.red : R.color.blue);
mUnselectedColor = res.getColor(mThemeDark? R.color.white : R.color.numbers_text_color); mUnselectedColor = res.getColor(mThemeDark ? R.color.white : R.color.numbers_text_color);
mHourView = (TextView) view.findViewById(R.id.hours); mHourView = (TextView) view.findViewById(R.id.hours);
mHourView.setOnKeyListener(keyboardListener); mHourView.setOnKeyListener(keyboardListener);
@ -223,8 +248,9 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
mTimePicker = (RadialPickerLayout) view.findViewById(R.id.time_picker); mTimePicker = (RadialPickerLayout) view.findViewById(R.id.time_picker);
mTimePicker.setOnValueSelectedListener(this); mTimePicker.setOnValueSelectedListener(this);
mTimePicker.setOnKeyListener(keyboardListener); mTimePicker.setOnKeyListener(keyboardListener);
mTimePicker.setColor(mSelectedColor);
mTimePicker.initialize(getActivity(), mHapticFeedbackController, mInitialHourOfDay, mTimePicker.initialize(getActivity(), mHapticFeedbackController, mInitialHourOfDay,
mInitialMinute, mIs24HourMode); mInitialMinute, mIs24HourMode);
int currentItemShowing = HOUR_INDEX; int currentItemShowing = HOUR_INDEX;
if (savedInstanceState != null && if (savedInstanceState != null &&
@ -234,25 +260,31 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
setCurrentItemShowing(currentItemShowing, false, true, true); setCurrentItemShowing(currentItemShowing, false, true, true);
mTimePicker.invalidate(); mTimePicker.invalidate();
mHourView.setOnClickListener(new OnClickListener() { mHourView.setOnClickListener(new OnClickListener()
{
@Override @Override
public void onClick(View v) { public void onClick(View v)
{
setCurrentItemShowing(HOUR_INDEX, true, false, true); setCurrentItemShowing(HOUR_INDEX, true, false, true);
tryVibrate(); tryVibrate();
} }
}); });
mMinuteView.setOnClickListener(new OnClickListener() { mMinuteView.setOnClickListener(new OnClickListener()
{
@Override @Override
public void onClick(View v) { public void onClick(View v)
{
setCurrentItemShowing(MINUTE_INDEX, true, false, true); setCurrentItemShowing(MINUTE_INDEX, true, false, true);
tryVibrate(); tryVibrate();
} }
}); });
mDoneButton = (TextView) view.findViewById(R.id.done_button); mDoneButton = (TextView) view.findViewById(R.id.done_button);
mDoneButton.setOnClickListener(new OnClickListener() { mDoneButton.setOnClickListener(new OnClickListener()
{
@Override @Override
public void onClick(View v) { public void onClick(View v)
{
if (mInKbMode && isTypedTimeFullyLegal()) { if (mInKbMode && isTypedTimeFullyLegal()) {
finishKbMode(false); finishKbMode(false);
} else { } else {
@ -260,25 +292,25 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
} }
if (mCallback != null) { if (mCallback != null) {
mCallback.onTimeSet(mTimePicker, mCallback.onTimeSet(mTimePicker,
mTimePicker.getHours(), mTimePicker.getMinutes()); mTimePicker.getHours(), mTimePicker.getMinutes());
} }
dismiss(); dismiss();
} }
}); });
mDoneButton.setOnKeyListener(keyboardListener); mDoneButton.setOnKeyListener(keyboardListener);
mClearButton = (TextView) view.findViewById(R.id.clear_button); mClearButton = (TextView) view.findViewById(R.id.clear_button);
mClearButton.setOnClickListener(new OnClickListener() mClearButton.setOnClickListener(new OnClickListener()
{ {
@Override @Override
public void onClick(View v) public void onClick(View v)
{ {
if(mCallback != null) { if (mCallback != null) {
mCallback.onTimeCleared(mTimePicker); mCallback.onTimeCleared(mTimePicker);
} }
dismiss(); dismiss();
} }
}); });
mClearButton.setOnKeyListener(keyboardListener); mClearButton.setOnKeyListener(keyboardListener);
// Enable or disable the AM/PM view. // Enable or disable the AM/PM view.
@ -293,15 +325,17 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
separatorView.setLayoutParams(paramsSeparator); separatorView.setLayoutParams(paramsSeparator);
} else { } else {
mAmPmTextView.setVisibility(View.VISIBLE); mAmPmTextView.setVisibility(View.VISIBLE);
updateAmPmDisplay(mInitialHourOfDay < 12? AM : PM); updateAmPmDisplay(mInitialHourOfDay < 12 ? AM : PM);
mAmPmHitspace.setOnClickListener(new OnClickListener() { mAmPmHitspace.setOnClickListener(new OnClickListener()
{
@Override @Override
public void onClick(View v) { public void onClick(View v)
{
tryVibrate(); tryVibrate();
int amOrPm = mTimePicker.getIsCurrentlyAmOrPm(); int amOrPm = mTimePicker.getIsCurrentlyAmOrPm();
if (amOrPm == AM) { if (amOrPm == AM) {
amOrPm = PM; amOrPm = PM;
} else if (amOrPm == PM){ } else if (amOrPm == PM) {
amOrPm = AM; amOrPm = AM;
} }
updateAmPmDisplay(amOrPm); updateAmPmDisplay(amOrPm);
@ -328,56 +362,61 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
mTypedTimes = new ArrayList<Integer>(); mTypedTimes = new ArrayList<Integer>();
} }
// Set the theme at the end so that the initialize()s above don't counteract the theme.
mTimePicker.setTheme(getActivity().getApplicationContext(), mThemeDark);
// Prepare some palette to use.
int white = res.getColor(R.color.white);
int circleBackground = res.getColor(R.color.circle_background);
int line = res.getColor(R.color.line_background);
int timeDisplay = res.getColor(R.color.numbers_text_color);
ColorStateList doneTextColor = res.getColorStateList(R.color.done_text_color);
int doneBackground = R.drawable.done_background_color;
int darkGray = res.getColor(R.color.dark_gray); // // Set the theme at the end so that the initialize()s above don't counteract the theme.
int lightGray = res.getColor(R.color.light_gray); // mTimePicker.setTheme(getActivity().getApplicationContext(), mThemeDark);
int darkLine = res.getColor(R.color.line_dark); // // Prepare some palette to use.
ColorStateList darkDoneTextColor = res.getColorStateList(R.color.done_text_color_dark); // int white = res.getColor(R.color.white);
int darkDoneBackground = R.drawable.done_background_color_dark; // int circleBackground = res.getColor(R.color.circle_background);
// int line = res.getColor(R.color.line_background);
// int timeDisplay = res.getColor(R.color.numbers_text_color);
// ColorStateList doneTextColor = res.getColorStateList(R.color.done_text_color);
// int doneBackground = R.drawable.done_background_color;
//
// int darkGray = res.getColor(R.color.dark_gray);
// int lightGray = res.getColor(R.color.light_gray);
// int darkLine = res.getColor(R.color.line_dark);
// ColorStateList darkDoneTextColor = res.getColorStateList(R.color.done_text_color_dark);
// int darkDoneBackground = R.drawable.done_background_color_dark;
// Set the palette for each view based on the theme. // Set the palette for each view based on the theme.
view.findViewById(R.id.time_display_background).setBackgroundColor(mThemeDark? darkGray : white); // view.findViewById(R.id.time_display_background).setBackgroundColor(mThemeDark? darkGray : white);
view.findViewById(R.id.time_display).setBackgroundColor(mThemeDark? darkGray : white); // view.findViewById(R.id.time_display).setBackgroundColor(mThemeDark? darkGray : white);
((TextView) view.findViewById(R.id.separator)).setTextColor(mThemeDark? white : timeDisplay); // ((TextView) view.findViewById(R.id.separator)).setTextColor(mThemeDark? white : timeDisplay);
((TextView) view.findViewById(R.id.ampm_label)).setTextColor(mThemeDark? white : timeDisplay); // ((TextView) view.findViewById(R.id.ampm_label)).setTextColor(mThemeDark? white : timeDisplay);
view.findViewById(R.id.line).setBackgroundColor(mThemeDark? darkLine : line); // view.findViewById(R.id.line).setBackgroundColor(mThemeDark? darkLine : line);
mDoneButton.setTextColor(mThemeDark? darkDoneTextColor : doneTextColor); // mDoneButton.setTextColor(mThemeDark? darkDoneTextColor : doneTextColor);
mTimePicker.setBackgroundColor(mThemeDark? lightGray : circleBackground); // mTimePicker.setBackgroundColor(mThemeDark? lightGray : circleBackground);
mDoneButton.setBackgroundResource(mThemeDark? darkDoneBackground : doneBackground); // mDoneButton.setBackgroundResource(mThemeDark? darkDoneBackground : doneBackground);
return view; return view;
} }
@Override @Override
public void onResume() { public void onResume()
{
super.onResume(); super.onResume();
mHapticFeedbackController.start(); mHapticFeedbackController.start();
} }
@Override @Override
public void onPause() { public void onPause()
{
super.onPause(); super.onPause();
mHapticFeedbackController.stop(); mHapticFeedbackController.stop();
} }
public void tryVibrate() { public void tryVibrate()
{
mHapticFeedbackController.tryVibrate(); mHapticFeedbackController.tryVibrate();
} }
private void updateAmPmDisplay(int amOrPm) { private void updateAmPmDisplay(int amOrPm)
{
if (amOrPm == AM) { if (amOrPm == AM) {
mAmPmTextView.setText(mAmText); mAmPmTextView.setText(mAmText);
Utils.tryAccessibilityAnnounce(mTimePicker, mAmText); Utils.tryAccessibilityAnnounce(mTimePicker, mAmText);
mAmPmHitspace.setContentDescription(mAmText); mAmPmHitspace.setContentDescription(mAmText);
} else if (amOrPm == PM){ } else if (amOrPm == PM) {
mAmPmTextView.setText(mPmText); mAmPmTextView.setText(mPmText);
Utils.tryAccessibilityAnnounce(mTimePicker, mPmText); Utils.tryAccessibilityAnnounce(mTimePicker, mPmText);
mAmPmHitspace.setContentDescription(mPmText); mAmPmHitspace.setContentDescription(mPmText);
@ -387,7 +426,8 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
} }
@Override @Override
public void onSaveInstanceState(Bundle outState) { public void onSaveInstanceState(Bundle outState)
{
if (mTimePicker != null) { if (mTimePicker != null) {
outState.putInt(KEY_HOUR_OF_DAY, mTimePicker.getHours()); outState.putInt(KEY_HOUR_OF_DAY, mTimePicker.getHours());
outState.putInt(KEY_MINUTE, mTimePicker.getMinutes()); outState.putInt(KEY_MINUTE, mTimePicker.getMinutes());
@ -398,6 +438,7 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
outState.putIntegerArrayList(KEY_TYPED_TIMES, mTypedTimes); outState.putIntegerArrayList(KEY_TYPED_TIMES, mTypedTimes);
} }
outState.putBoolean(KEY_DARK_THEME, mThemeDark); outState.putBoolean(KEY_DARK_THEME, mThemeDark);
outState.putInt(KEY_SELECTED_COLOR, mSelectedColor);
} }
} }
@ -405,7 +446,8 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
* Called by the picker for updating the header display. * Called by the picker for updating the header display.
*/ */
@Override @Override
public void onValueSelected(int pickerIndex, int newValue, boolean autoAdvance) { public void onValueSelected(int pickerIndex, int newValue, boolean autoAdvance)
{
if (pickerIndex == HOUR_INDEX) { if (pickerIndex == HOUR_INDEX) {
setHour(newValue, false); setHour(newValue, false);
String announcement = String.format("%d", newValue); String announcement = String.format("%d", newValue);
@ -417,7 +459,7 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
} }
Utils.tryAccessibilityAnnounce(mTimePicker, announcement); Utils.tryAccessibilityAnnounce(mTimePicker, announcement);
} else if (pickerIndex == MINUTE_INDEX){ } else if (pickerIndex == MINUTE_INDEX) {
setMinute(newValue); setMinute(newValue);
mTimePicker.setContentDescription(mMinutePickerDescription + ": " + newValue); mTimePicker.setContentDescription(mMinutePickerDescription + ": " + newValue);
} else if (pickerIndex == AMPM_INDEX) { } else if (pickerIndex == AMPM_INDEX) {
@ -430,7 +472,8 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
} }
} }
private void setHour(int value, boolean announce) { private void setHour(int value, boolean announce)
{
String format; String format;
if (mIs24HourMode) { if (mIs24HourMode) {
format = "%02d"; format = "%02d";
@ -450,7 +493,8 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
} }
} }
private void setMinute(int value) { private void setMinute(int value)
{
if (value == 60) { if (value == 60) {
value = 0; value = 0;
} }
@ -462,7 +506,8 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
// Show either Hours or Minutes. // Show either Hours or Minutes.
private void setCurrentItemShowing(int index, boolean animateCircle, boolean delayLabelAnimate, private void setCurrentItemShowing(int index, boolean animateCircle, boolean delayLabelAnimate,
boolean announce) { boolean announce)
{
mTimePicker.setCurrentItemShowing(index, animateCircle); mTimePicker.setCurrentItemShowing(index, animateCircle);
TextView labelToAnimate; TextView labelToAnimate;
@ -485,8 +530,8 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
labelToAnimate = mMinuteView; labelToAnimate = mMinuteView;
} }
int hourColor = (index == HOUR_INDEX)? mSelectedColor : mUnselectedColor; int hourColor = (index == HOUR_INDEX) ? mSelectedColor : mUnselectedColor;
int minuteColor = (index == MINUTE_INDEX)? mSelectedColor : mUnselectedColor; int minuteColor = (index == MINUTE_INDEX) ? mSelectedColor : mUnselectedColor;
mHourView.setTextColor(hourColor); mHourView.setTextColor(hourColor);
mMinuteView.setTextColor(minuteColor); mMinuteView.setTextColor(minuteColor);
@ -499,15 +544,17 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
/** /**
* For keyboard mode, processes key events. * For keyboard mode, processes key events.
*
* @param keyCode the pressed key. * @param keyCode the pressed key.
* @return true if the key was successfully processed, false otherwise. * @return true if the key was successfully processed, false otherwise.
*/ */
private boolean processKeyUp(int keyCode) { private boolean processKeyUp(int keyCode)
{
if (keyCode == KeyEvent.KEYCODE_ESCAPE || keyCode == KeyEvent.KEYCODE_BACK) { if (keyCode == KeyEvent.KEYCODE_ESCAPE || keyCode == KeyEvent.KEYCODE_BACK) {
dismiss(); dismiss();
return true; return true;
} else if (keyCode == KeyEvent.KEYCODE_TAB) { } else if (keyCode == KeyEvent.KEYCODE_TAB) {
if(mInKbMode) { if (mInKbMode) {
if (isTypedTimeFullyLegal()) { if (isTypedTimeFullyLegal()) {
finishKbMode(true); finishKbMode(true);
} }
@ -522,7 +569,7 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
} }
if (mCallback != null) { if (mCallback != null) {
mCallback.onTimeSet(mTimePicker, mCallback.onTimeSet(mTimePicker,
mTimePicker.getHours(), mTimePicker.getMinutes()); mTimePicker.getHours(), mTimePicker.getMinutes());
} }
dismiss(); dismiss();
return true; return true;
@ -539,7 +586,7 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
deletedKeyStr = String.format("%d", getValFromKeyCode(deleted)); deletedKeyStr = String.format("%d", getValFromKeyCode(deleted));
} }
Utils.tryAccessibilityAnnounce(mTimePicker, Utils.tryAccessibilityAnnounce(mTimePicker,
String.format(mDeletedKeyFormat, deletedKeyStr)); String.format(mDeletedKeyFormat, deletedKeyStr));
updateDisplay(true); updateDisplay(true);
} }
} }
@ -549,7 +596,7 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
|| keyCode == KeyEvent.KEYCODE_6 || keyCode == KeyEvent.KEYCODE_7 || keyCode == KeyEvent.KEYCODE_6 || keyCode == KeyEvent.KEYCODE_7
|| keyCode == KeyEvent.KEYCODE_8 || keyCode == KeyEvent.KEYCODE_9 || keyCode == KeyEvent.KEYCODE_8 || keyCode == KeyEvent.KEYCODE_9
|| (!mIs24HourMode && || (!mIs24HourMode &&
(keyCode == getAmOrPmKeyCode(AM) || keyCode == getAmOrPmKeyCode(PM)))) { (keyCode == getAmOrPmKeyCode(AM) || keyCode == getAmOrPmKeyCode(PM)))) {
if (!mInKbMode) { if (!mInKbMode) {
if (mTimePicker == null) { if (mTimePicker == null) {
// Something's wrong, because time picker should definitely not be null. // Something's wrong, because time picker should definitely not be null.
@ -572,11 +619,13 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
/** /**
* Try to start keyboard mode with the specified key, as long as the timepicker is not in the * Try to start keyboard mode with the specified key, as long as the timepicker is not in the
* middle of a touch-event. * middle of a touch-event.
*
* @param keyCode The key to use as the first press. Keyboard mode will not be started if the * @param keyCode The key to use as the first press. Keyboard mode will not be started if the
* key is not legal to start with. Or, pass in -1 to get into keyboard mode without a starting * key is not legal to start with. Or, pass in -1 to get into keyboard mode without a starting
* key. * key.
*/ */
private void tryStartingKbMode(int keyCode) { private void tryStartingKbMode(int keyCode)
{
if (mTimePicker.trySettingInputEnabled(false) && if (mTimePicker.trySettingInputEnabled(false) &&
(keyCode == -1 || addKeyIfLegal(keyCode))) { (keyCode == -1 || addKeyIfLegal(keyCode))) {
mInKbMode = true; mInKbMode = true;
@ -585,7 +634,8 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
} }
} }
private boolean addKeyIfLegal(int keyCode) { private boolean addKeyIfLegal(int keyCode)
{
// If we're in 24hour mode, we'll need to check if the input is full. If in AM/PM mode, // If we're in 24hour mode, we'll need to check if the input is full. If in AM/PM mode,
// we'll need to see if AM/PM have been typed. // we'll need to see if AM/PM have been typed.
if ((mIs24HourMode && mTypedTimes.size() == 4) || if ((mIs24HourMode && mTypedTimes.size() == 4) ||
@ -617,7 +667,8 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
* Traverse the tree to see if the keys that have been typed so far are legal as is, * Traverse the tree to see if the keys that have been typed so far are legal as is,
* or may become legal as more keys are typed (excluding backspace). * or may become legal as more keys are typed (excluding backspace).
*/ */
private boolean isTypedTimeLegalSoFar() { private boolean isTypedTimeLegalSoFar()
{
Node node = mLegalTimesTree; Node node = mLegalTimesTree;
for (int keyCode : mTypedTimes) { for (int keyCode : mTypedTimes) {
node = node.canReach(keyCode); node = node.canReach(keyCode);
@ -631,7 +682,8 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
/** /**
* Check if the time that has been typed so far is completely legal, as is. * Check if the time that has been typed so far is completely legal, as is.
*/ */
private boolean isTypedTimeFullyLegal() { private boolean isTypedTimeFullyLegal()
{
if (mIs24HourMode) { if (mIs24HourMode) {
// For 24-hour mode, the time is legal if the hours and minutes are each legal. Note: // For 24-hour mode, the time is legal if the hours and minutes are each legal. Note:
// getEnteredTime() will ONLY call isTypedTimeFullyLegal() when NOT in 24hour mode. // getEnteredTime() will ONLY call isTypedTimeFullyLegal() when NOT in 24hour mode.
@ -645,7 +697,8 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
} }
} }
private int deleteLastTypedKey() { private int deleteLastTypedKey()
{
int deleted = mTypedTimes.remove(mTypedTimes.size() - 1); int deleted = mTypedTimes.remove(mTypedTimes.size() - 1);
if (!isTypedTimeFullyLegal()) { if (!isTypedTimeFullyLegal()) {
mDoneButton.setEnabled(false); mDoneButton.setEnabled(false);
@ -655,9 +708,11 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
/** /**
* Get out of keyboard mode. If there is nothing in typedTimes, revert to TimePicker's time. * Get out of keyboard mode. If there is nothing in typedTimes, revert to TimePicker's time.
*
* @param changeDisplays If true, update the displays with the relevant time. * @param changeDisplays If true, update the displays with the relevant time.
*/ */
private void finishKbMode(boolean updateDisplays) { private void finishKbMode(boolean updateDisplays)
{
mInKbMode = false; mInKbMode = false;
if (!mTypedTimes.isEmpty()) { if (!mTypedTimes.isEmpty()) {
int values[] = getEnteredTime(null); int values[] = getEnteredTime(null);
@ -677,29 +732,31 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
* Update the hours, minutes, and AM/PM displays with the typed times. If the typedTimes is * Update the hours, minutes, and AM/PM displays with the typed times. If the typedTimes is
* empty, either show an empty display (filled with the placeholder text), or update from the * empty, either show an empty display (filled with the placeholder text), or update from the
* timepicker's values. * timepicker's values.
*
* @param allowEmptyDisplay if true, then if the typedTimes is empty, use the placeholder text. * @param allowEmptyDisplay if true, then if the typedTimes is empty, use the placeholder text.
* Otherwise, revert to the timepicker's values. * Otherwise, revert to the timepicker's values.
*/ */
private void updateDisplay(boolean allowEmptyDisplay) { private void updateDisplay(boolean allowEmptyDisplay)
{
if (!allowEmptyDisplay && mTypedTimes.isEmpty()) { if (!allowEmptyDisplay && mTypedTimes.isEmpty()) {
int hour = mTimePicker.getHours(); int hour = mTimePicker.getHours();
int minute = mTimePicker.getMinutes(); int minute = mTimePicker.getMinutes();
setHour(hour, true); setHour(hour, true);
setMinute(minute); setMinute(minute);
if (!mIs24HourMode) { if (!mIs24HourMode) {
updateAmPmDisplay(hour < 12? AM : PM); updateAmPmDisplay(hour < 12 ? AM : PM);
} }
setCurrentItemShowing(mTimePicker.getCurrentItemShowing(), true, true, true); setCurrentItemShowing(mTimePicker.getCurrentItemShowing(), true, true, true);
mDoneButton.setEnabled(true); mDoneButton.setEnabled(true);
} else { } else {
Boolean[] enteredZeros = {false, false}; Boolean[] enteredZeros = {false, false};
int[] values = getEnteredTime(enteredZeros); int[] values = getEnteredTime(enteredZeros);
String hourFormat = enteredZeros[0]? "%02d" : "%2d"; String hourFormat = enteredZeros[0] ? "%02d" : "%2d";
String minuteFormat = (enteredZeros[1])? "%02d" : "%2d"; String minuteFormat = (enteredZeros[1]) ? "%02d" : "%2d";
String hourStr = (values[0] == -1)? mDoublePlaceholderText : String hourStr = (values[0] == -1) ? mDoublePlaceholderText :
String.format(hourFormat, values[0]).replace(' ', mPlaceholderText); String.format(hourFormat, values[0]).replace(' ', mPlaceholderText);
String minuteStr = (values[1] == -1)? mDoublePlaceholderText : String minuteStr = (values[1] == -1) ? mDoublePlaceholderText :
String.format(minuteFormat, values[1]).replace(' ', mPlaceholderText); String.format(minuteFormat, values[1]).replace(' ', mPlaceholderText);
mHourView.setText(hourStr); mHourView.setText(hourStr);
mHourSpaceView.setText(hourStr); mHourSpaceView.setText(hourStr);
mHourView.setTextColor(mUnselectedColor); mHourView.setTextColor(mUnselectedColor);
@ -712,7 +769,8 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
} }
} }
private static int getValFromKeyCode(int keyCode) { private static int getValFromKeyCode(int keyCode)
{
switch (keyCode) { switch (keyCode) {
case KeyEvent.KEYCODE_0: case KeyEvent.KEYCODE_0:
return 0; return 0;
@ -741,20 +799,22 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
/** /**
* Get the currently-entered time, as integer values of the hours and minutes typed. * Get the currently-entered time, as integer values of the hours and minutes typed.
*
* @param enteredZeros A size-2 boolean array, which the caller should initialize, and which * @param enteredZeros A size-2 boolean array, which the caller should initialize, and which
* may then be used for the caller to know whether zeros had been explicitly entered as either * may then be used for the caller to know whether zeros had been explicitly entered as either
* hours of minutes. This is helpful for deciding whether to show the dashes, or actual 0's. * hours of minutes. This is helpful for deciding whether to show the dashes, or actual 0's.
* @return A size-3 int array. The first value will be the hours, the second value will be the * @return A size-3 int array. The first value will be the hours, the second value will be the
* minutes, and the third will be either TimePickerDialog.AM or TimePickerDialog.PM. * minutes, and the third will be either TimePickerDialog.AM or TimePickerDialog.PM.
*/ */
private int[] getEnteredTime(Boolean[] enteredZeros) { private int[] getEnteredTime(Boolean[] enteredZeros)
{
int amOrPm = -1; int amOrPm = -1;
int startIndex = 1; int startIndex = 1;
if (!mIs24HourMode && isTypedTimeFullyLegal()) { if (!mIs24HourMode && isTypedTimeFullyLegal()) {
int keyCode = mTypedTimes.get(mTypedTimes.size() - 1); int keyCode = mTypedTimes.get(mTypedTimes.size() - 1);
if (keyCode == getAmOrPmKeyCode(AM)) { if (keyCode == getAmOrPmKeyCode(AM)) {
amOrPm = AM; amOrPm = AM;
} else if (keyCode == getAmOrPmKeyCode(PM)){ } else if (keyCode == getAmOrPmKeyCode(PM)) {
amOrPm = PM; amOrPm = PM;
} }
startIndex = 2; startIndex = 2;
@ -765,15 +825,15 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
int val = getValFromKeyCode(mTypedTimes.get(mTypedTimes.size() - i)); int val = getValFromKeyCode(mTypedTimes.get(mTypedTimes.size() - i));
if (i == startIndex) { if (i == startIndex) {
minute = val; minute = val;
} else if (i == startIndex+1) { } else if (i == startIndex + 1) {
minute += 10*val; minute += 10 * val;
if (enteredZeros != null && val == 0) { if (enteredZeros != null && val == 0) {
enteredZeros[1] = true; enteredZeros[1] = true;
} }
} else if (i == startIndex+2) { } else if (i == startIndex + 2) {
hour = val; hour = val;
} else if (i == startIndex+3) { } else if (i == startIndex + 3) {
hour += 10*val; hour += 10 * val;
if (enteredZeros != null && val == 0) { if (enteredZeros != null && val == 0) {
enteredZeros[0] = true; enteredZeros[0] = true;
} }
@ -787,7 +847,8 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
/** /**
* Get the keycode value for AM and PM in the current language. * Get the keycode value for AM and PM in the current language.
*/ */
private int getAmOrPmKeyCode(int amOrPm) { private int getAmOrPmKeyCode(int amOrPm)
{
// Cache the codes. // Cache the codes.
if (mAmKeyCode == -1 || mPmKeyCode == -1) { if (mAmKeyCode == -1 || mPmKeyCode == -1) {
// Find the first character in the AM/PM text that is unique. // Find the first character in the AM/PM text that is unique.
@ -822,7 +883,8 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
/** /**
* Create a tree for deciding what keys can legally be typed. * Create a tree for deciding what keys can legally be typed.
*/ */
private void generateLegalTimesTree() { private void generateLegalTimesTree()
{
// Create a quick cache of numbers to their keycodes. // Create a quick cache of numbers to their keycodes.
int k0 = KeyEvent.KEYCODE_0; int k0 = KeyEvent.KEYCODE_0;
int k1 = KeyEvent.KEYCODE_1; int k1 = KeyEvent.KEYCODE_1;
@ -878,7 +940,7 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
// When the first digit is 2, the second digit may be 4-5. // When the first digit is 2, the second digit may be 4-5.
secondDigit = new Node(k4, k5); secondDigit = new Node(k4, k5);
firstDigit.addChild(secondDigit); firstDigit.addChild(secondDigit);
// We must now be followd by the last minute digit. E.g. 2:40, 2:53. // We must now be followed by the last minute digit. E.g. 2:40, 2:53.
secondDigit.addChild(minuteSecondDigit); secondDigit.addChild(minuteSecondDigit);
// The first digit may be 3-9. // The first digit may be 3-9.
@ -955,20 +1017,24 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
* mLegalKeys represents the keys that can be typed to get to the node. * mLegalKeys represents the keys that can be typed to get to the node.
* mChildren are the children that can be reached from this node. * mChildren are the children that can be reached from this node.
*/ */
private class Node { private class Node
{
private int[] mLegalKeys; private int[] mLegalKeys;
private ArrayList<Node> mChildren; private ArrayList<Node> mChildren;
public Node(int... legalKeys) { public Node(int... legalKeys)
{
mLegalKeys = legalKeys; mLegalKeys = legalKeys;
mChildren = new ArrayList<Node>(); mChildren = new ArrayList<Node>();
} }
public void addChild(Node child) { public void addChild(Node child)
{
mChildren.add(child); mChildren.add(child);
} }
public boolean containsKey(int key) { public boolean containsKey(int key)
{
for (int i = 0; i < mLegalKeys.length; i++) { for (int i = 0; i < mLegalKeys.length; i++) {
if (mLegalKeys[i] == key) { if (mLegalKeys[i] == key) {
return true; return true;
@ -977,7 +1043,8 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
return false; return false;
} }
public Node canReach(int key) { public Node canReach(int key)
{
if (mChildren == null) { if (mChildren == null) {
return null; return null;
} }
@ -990,9 +1057,11 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
} }
} }
private class KeyboardListener implements OnKeyListener { private class KeyboardListener implements OnKeyListener
{
@Override @Override
public boolean onKey(View v, int keyCode, KeyEvent event) { public boolean onKey(View v, int keyCode, KeyEvent event)
{
if (event.getAction() == KeyEvent.ACTION_UP) { if (event.getAction() == KeyEvent.ACTION_UP) {
return processKeyUp(keyCode); return processKeyUp(keyCode);
} }
@ -1000,14 +1069,16 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
} }
} }
public void setDismissListener( DialogInterface.OnDismissListener listener ) { public void setDismissListener(DialogInterface.OnDismissListener listener)
{
dismissListener = listener; dismissListener = listener;
} }
@Override @Override
public void onDismiss(DialogInterface dialog) { public void onDismiss(DialogInterface dialog)
{
super.onDismiss(dialog); super.onDismiss(dialog);
if( dismissListener != null ) if (dismissListener != null)
dismissListener.onDismiss(dialog); dismissListener.onDismiss(dialog);
} }
} }

@ -49,33 +49,33 @@
android:layout_height="1dip" android:layout_height="1dip"
android:background="@color/line_background" /> android:background="@color/line_background" />
<LinearLayout <androidx.appcompat.widget.LinearLayoutCompat
style="?android:attr/buttonBarStyle" style="?android:attr/buttonBarStyle"
android:layout_width="@dimen/date_picker_component_width" android:layout_width="@dimen/date_picker_component_width"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal" > android:orientation="horizontal" >
<Button <androidx.appcompat.widget.AppCompatButton
style="?android:attr/buttonBarButtonStyle"
android:id="@+id/clear_button" android:id="@+id/clear_button"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1" android:layout_weight="1"
android:background="@drawable/done_background_color"
android:minHeight="48dp" android:minHeight="48dp"
android:textColor="#333"
android:text="@string/clear_label" android:text="@string/clear_label"
android:textColor="@color/done_text_color"
android:textSize="@dimen/done_label_size" /> android:textSize="@dimen/done_label_size" />
<Button <androidx.appcompat.widget.AppCompatButton
style="?android:attr/buttonBarButtonStyle"
android:id="@+id/done_button" android:id="@+id/done_button"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1" android:layout_weight="1"
android:background="@drawable/done_background_color"
android:minHeight="48dp" android:minHeight="48dp"
android:textColor="#333"
android:text="@string/done_label" android:text="@string/done_label"
android:textColor="@color/done_text_color"
android:textSize="@dimen/done_label_size" /> android:textSize="@dimen/done_label_size" />
</LinearLayout> </androidx.appcompat.widget.LinearLayoutCompat>
</LinearLayout> </LinearLayout>

@ -15,305 +15,266 @@
# You should have received a copy of the GNU General Public License along # You should have received a copy of the GNU General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>. # with this program. If not, see <http://www.gnu.org/licenses/>.
cd "$(dirname "$0")"
ADB="${ANDROID_HOME}/platform-tools/adb" ADB="${ANDROID_HOME}/platform-tools/adb"
EMULATOR="${ANDROID_HOME}/tools/emulator" EMULATOR="${ANDROID_HOME}/tools/emulator"
GRADLE="./gradlew --stacktrace" GRADLE="./gradlew --stacktrace"
PACKAGE_NAME=org.isoron.uhabits PACKAGE_NAME=org.isoron.uhabits
OUTPUTS_DIR=uhabits-android/build/outputs OUTPUTS_DIR=uhabits-android/build/outputs
VERSION=$(cat gradle.properties | grep VERSION_NAME | sed -e 's/.*=//g;s/ //g')
if [ ! -f "${ANDROID_HOME}/platform-tools/adb" ]; then if [ ! -f "${ANDROID_HOME}/platform-tools/adb" ]; then
echo "Error: ANDROID_HOME is not set correctly" echo "Error: ANDROID_HOME is not set correctly"
exit 1 exit 1
fi fi
log_error() { log_error() {
if [ ! -z "$TEAMCITY_VERSION" ]; then if [ ! -z "$TEAMCITY_VERSION" ]; then
echo "###teamcity[progressMessage '$1']" echo "###teamcity[progressMessage '$1']"
else else
local COLOR='\033[1;31m' local COLOR='\033[1;31m'
local NC='\033[0m' local NC='\033[0m'
echo -e "$COLOR>>> $1 $NC" echo -e "$COLOR>>> $1 $NC"
fi fi
} }
log_info() { log_info() {
if [ ! -z "$TEAMCITY_VERSION" ]; then if [ ! -z "$TEAMCITY_VERSION" ]; then
echo "###teamcity[progressMessage '$1']" echo "###teamcity[progressMessage '$1']"
else else
local COLOR='\033[1;32m' local COLOR='\033[1;32m'
local NC='\033[0m' local NC='\033[0m'
echo -e "$COLOR>>> $1 $NC" echo -e "$COLOR>>> $1 $NC"
fi fi
} }
fail() { fail() {
if [ ! -z ${AVD_NAME} ]; then log_error "BUILD FAILED"
stop_emulator exit 1
stop_gradle_daemon
fi
log_error "BUILD FAILED"
exit 1
}
start_emulator() {
log_info "Starting emulator ($AVD_NAME)"
$EMULATOR -avd ${AVD_NAME} -port ${AVD_SERIAL} -no-audio -no-window &
$ADB wait-for-device shell 'while [[ -z $(getprop sys.boot_completed) ]]; do sleep 1; done; input keyevent 82'
} }
stop_emulator() { if [ ! -z $RELEASE ]; then
log_info "Stopping emulator" log_info "Reading secret env variables from ../.secret/env"
$ADB emu kill source ../.secret/env || fail
} fi
stop_gradle_daemon() {
log_info "Stopping gradle daemon"
$GRADLE --stop
}
run_adb_as_root() { run_adb_as_root() {
log_info "Running adb as root" log_info "Running adb as root"
$ADB root $ADB root
} }
build_apk() { build_apk() {
if [ ! -z $RELEASE ]; then log_info "Removing old APKs..."
if [ -z "$KEY_FILE" -o -z "$STORE_PASSWORD" -o -z "$KEY_ALIAS" -o -z "$KEY_PASSWORD" ]; then rm -vf build/*.apk
log_error "Environment variables KEY_FILE, KEY_ALIAS, KEY_PASSWORD and STORE_PASSWORD must be defined"
exit 1 if [ ! -z $RELEASE ]; then
fi log_info "Building release APK"
log_info "Building release APK" ./gradlew assembleRelease
./gradlew assembleRelease \ cp -v uhabits-android/build/outputs/apk/release/uhabits-android-release.apk build/loop-$VERSION-release.apk
-Pandroid.injected.signing.store.file=$KEY_FILE \ fi
-Pandroid.injected.signing.store.password=$STORE_PASSWORD \
-Pandroid.injected.signing.key.alias=$KEY_ALIAS \ log_info "Building debug APK"
-Pandroid.injected.signing.key.password=$KEY_PASSWORD || fail ./gradlew assembleDebug --stacktrace || fail
else cp -v uhabits-android/build/outputs/apk/debug/uhabits-android-debug.apk build/loop-$VERSION-debug.apk
log_info "Building debug APK"
./gradlew assembleDebug || fail
fi
} }
build_instrumentation_apk() { build_instrumentation_apk() {
log_info "Building instrumentation APK" log_info "Building instrumentation APK"
if [ ! -z $RELEASE ]; then if [ ! -z $RELEASE ]; then
$GRADLE assembleAndroidTest \ $GRADLE assembleAndroidTest \
-Pandroid.injected.signing.store.file=$KEY_FILE \ -Pandroid.injected.signing.store.file=$LOOP_KEY_STORE \
-Pandroid.injected.signing.store.password=$STORE_PASSWORD \ -Pandroid.injected.signing.store.password=$LOOP_STORE_PASSWORD \
-Pandroid.injected.signing.key.alias=$KEY_ALIAS \ -Pandroid.injected.signing.key.alias=$LOOP_KEY_ALIAS \
-Pandroid.injected.signing.key.password=$KEY_PASSWORD || fail -Pandroid.injected.signing.key.password=$LOOP_KEY_PASSWORD || fail
else else
$GRADLE assembleAndroidTest || fail $GRADLE assembleAndroidTest || fail
fi fi
}
clean_output_dir() {
log_info "Cleaning output directory"
rm -rf ${OUTPUTS_DIR}
mkdir -p ${OUTPUTS_DIR}
} }
uninstall_apk() { uninstall_apk() {
log_info "Uninstalling existing APK" log_info "Uninstalling existing APK"
$ADB uninstall ${PACKAGE_NAME} $ADB uninstall ${PACKAGE_NAME}
} }
install_test_butler() { install_test_butler() {
log_info "Installing Test Butler" log_info "Installing Test Butler"
$ADB uninstall com.linkedin.android.testbutler $ADB uninstall com.linkedin.android.testbutler
$ADB install tools/test-butler-app-2.0.2.apk $ADB install tools/test-butler-app-2.0.2.apk
} }
install_apk() { install_apk() {
log_info "Installing APK" log_info "Installing APK"
if [ ! -z $RELEASE ]; then if [ ! -z $RELEASE ]; then
$ADB install -r ${OUTPUTS_DIR}/apk/release/uhabits-android-release.apk || fail $ADB install -r ${OUTPUTS_DIR}/apk/release/uhabits-android-release.apk || fail
else else
$ADB install -t -r ${OUTPUTS_DIR}/apk/debug/uhabits-android-debug.apk || fail $ADB install -t -r ${OUTPUTS_DIR}/apk/debug/uhabits-android-debug.apk || fail
fi fi
} }
install_test_apk() { install_test_apk() {
log_info "Uninstalling existing test APK" log_info "Uninstalling existing test APK"
$ADB uninstall ${PACKAGE_NAME}.test $ADB uninstall ${PACKAGE_NAME}.test
log_info "Installing test APK" log_info "Installing test APK"
$ADB install -r ${OUTPUTS_DIR}/apk/androidTest/debug/uhabits-android-debug-androidTest.apk || fail $ADB install -r ${OUTPUTS_DIR}/apk/androidTest/debug/uhabits-android-debug-androidTest.apk || fail
} }
run_instrumented_tests() { run_instrumented_tests() {
SIZE=$1 SIZE=$1
log_info "Running instrumented tests" log_info "Running instrumented tests"
$ADB shell am instrument \ $ADB shell am instrument \
-r -e coverage true -e size $SIZE \ -r -e coverage true -e size $SIZE \
-w ${PACKAGE_NAME}.test/android.support.test.runner.AndroidJUnitRunner \ -w ${PACKAGE_NAME}.test/androidx.test.runner.AndroidJUnitRunner \
| tee ${OUTPUTS_DIR}/instrument.txt | tee ${OUTPUTS_DIR}/instrument.txt
mkdir -p ${OUTPUTS_DIR}/code-coverage/connected/ if grep "\(INSTRUMENTATION_STATUS_CODE.*-1\|FAILURES\)" $OUTPUTS_DIR/instrument.txt; then
$ADB pull /data/user/0/${PACKAGE_NAME}/files/coverage.ec \ log_error "Some instrumented tests failed"
${OUTPUTS_DIR}/code-coverage/connected/ \ fetch_images
|| log_error "COVERAGE REPORT NOT AVAILABLE" fetch_logcat
exit 1
fi
#mkdir -p ${OUTPUTS_DIR}/code-coverage/connected/
#$ADB pull /data/user/0/${PACKAGE_NAME}/files/coverage.ec \
# ${OUTPUTS_DIR}/code-coverage/connected/ \
# || log_error "COVERAGE REPORT NOT AVAILABLE"
} }
parse_instrumentation_results() { parse_instrumentation_results() {
log_info "Parsing instrumented test results" log_info "Parsing instrumented test results"
java -jar tools/automator-log-converter-1.5.0.jar ${OUTPUTS_DIR}/instrument.txt || fail java -jar tools/automator-log-converter-1.5.0.jar ${OUTPUTS_DIR}/instrument.txt || fail
} }
generate_coverage_badge() { generate_coverage_badge() {
log_info "Generating code coverage badge" log_info "Generating code coverage badge"
CORE_REPORT=uhabits-core/build/reports/jacoco/test/jacocoTestReport.xml CORE_REPORT=uhabits-core/build/reports/jacoco/test/jacocoTestReport.xml
rm -f ${OUTPUTS_DIR}/coverage-badge.svg rm -f ${OUTPUTS_DIR}/coverage-badge.svg
python3 tools/coverage-badge/badge.py -i $CORE_REPORT -o ${OUTPUTS_DIR}/coverage-badge python3 tools/coverage-badge/badge.py -i $CORE_REPORT -o ${OUTPUTS_DIR}/coverage-badge
}
fetch_artifacts() {
log_info "Fetching generated artifacts"
mkdir -p ${OUTPUTS_DIR}/failed
$ADB pull /mnt/sdcard/test-screenshots/ ${OUTPUTS_DIR}/failed
$ADB pull /storage/sdcard/test-screenshots/ ${OUTPUTS_DIR}/failed
$ADB pull /sdcard/Android/data/${PACKAGE_NAME}/files/test-screenshots/ ${OUTPUTS_DIR}/failed
} }
fetch_logcat() { fetch_logcat() {
log_info "Fetching logcat to ${OUTPUTS_DIR}/logcat.txt" log_info "Fetching logcat"
$ADB logcat -d > ${OUTPUTS_DIR}/logcat.txt $ADB logcat -d > ${OUTPUTS_DIR}/logcat.txt
} }
run_jvm_tests() { run_jvm_tests() {
log_info "Running JVM tests" log_info "Running JVM tests"
if [ ! -z $RELEASE ]; then if [ ! -z $RELEASE ]; then
$GRADLE testReleaseUnitTest :uhabits-core:check || fail $GRADLE testReleaseUnitTest :uhabits-core:check || fail
else else
$GRADLE testDebugUnitTest :uhabits-core:check || fail $GRADLE testDebugUnitTest :uhabits-core:check || fail
fi fi
} }
uninstall_test_apk() { uninstall_test_apk() {
log_info "Uninstalling test APK" log_info "Uninstalling test APK"
$ADB uninstall ${PACKAGE_NAME}.test $ADB uninstall ${PACKAGE_NAME}.test
} }
fetch_images() { fetch_images() {
rm -rf tmp/test-screenshots > /dev/null log_info "Fetching images"
mkdir -p tmp/ rm -rf $OUTPUTS_DIR/test-screenshots
$ADB pull /mnt/sdcard/test-screenshots/ tmp/ $ADB pull /sdcard/Android/data/${PACKAGE_NAME}/files/test-screenshots/ $OUTPUTS_DIR
$ADB pull /storage/sdcard/test-screenshots/ tmp/ $ADB shell rm -r /sdcard/Android/data/${PACKAGE_NAME}/files/test-screenshots/
$ADB pull /sdcard/Android/data/${PACKAGE_NAME}/files/test-screenshots/ tmp/
$ADB shell rm -r /mnt/sdcard/test-screenshots/
$ADB shell rm -r /storage/sdcard/test-screenshots/
$ADB shell rm -r /sdcard/Android/data/${PACKAGE_NAME}/files/test-screenshots/
} }
accept_images() { accept_images() {
find tmp/test-screenshots -name '*.expected*' -delete find $OUTPUTS_DIR/test-screenshots -name '*.expected*' -delete
rsync -av tmp/test-screenshots/ uhabits-android/src/androidTest/assets/ rsync -av $OUTPUTS_DIR/test-screenshots/ uhabits-android/src/androidTest/assets/
} }
run_tests() { run_tests() {
SIZE=$1 SIZE=$1
run_adb_as_root run_adb_as_root
install_test_butler install_test_butler
uninstall_apk uninstall_apk
install_apk install_apk
install_test_apk install_test_apk
run_instrumented_tests $SIZE run_instrumented_tests $SIZE
parse_instrumentation_results parse_instrumentation_results
fetch_artifacts fetch_logcat
fetch_logcat uninstall_test_apk
uninstall_test_apk
} }
parse_opts() { parse_opts() {
OPTS=`getopt -o ur --long uninstall-first,release -n 'build.sh' -- "$@"` OPTS=`getopt -o r --long release -n 'build.sh' -- "$@"`
if [ $? != 0 ] ; then exit 1; fi if [ $? != 0 ] ; then exit 1; fi
eval set -- "$OPTS" eval set -- "$OPTS"
while true; do while true; do
case "$1" in case "$1" in
-u | --uninstall-first ) UNINSTALL_FIRST=1; shift ;; -r | --release ) RELEASE=1; shift ;;
-r | --release ) RELEASE=1; shift ;; * ) break ;;
* ) break ;; esac
esac done
done }
remove_build_dir() {
rm -rfv .gradle
rm -rfv build
rm -rfv android-base/build
rm -rfv android-pickers/build
rm -rfv uhabits-android/build
rm -rfv uhabits-core/build
} }
case "$1" in case "$1" in
build) build)
shift; parse_opts $* shift; parse_opts $*
build_apk build_apk
build_instrumentation_apk build_instrumentation_apk
run_jvm_tests run_jvm_tests
#generate_coverage_badge #generate_coverage_badge
;; ;;
ci-tests) medium-tests)
if [ -z $3 ]; then shift; parse_opts $*
cat <<- END run_tests medium
Usage: $0 ci-tests AVD_NAME AVD_SERIAL [options] ;;
Parameters: large-tests)
AVD_NAME name of the virtual android device to start shift; parse_opts $*
AVD_SERIAL adb port to use (e.g. 5560) run_tests large
;;
Options:
-u --uninstall-first Uninstall existing APK first fetch-images)
-r --release Build and install release version, instead of debug fetch_images
END ;;
exit 1
fi accept-images)
accept_images
shift; AVD_NAME=$1 ;;
shift; AVD_SERIAL=$1
shift; parse_opts $* install)
ADB="${ADB} -s emulator-${AVD_SERIAL}" shift; parse_opts $*
build_apk
start_emulator install_apk
run_tests medium ;;
stop_emulator
stop_gradle_daemon clean)
;; remove_build_dir
;;
medium-tests)
shift; parse_opts $* *)
run_tests medium cat <<END
;; Usage: $0 <command> [options]
Builds, installs and tests Loop Habit Tracker
large-tests)
shift; parse_opts $* Commands:
run_tests large accept-images Copies fetched images to corresponding assets folder
;; build Build APK and run JVM tests
clean Remove build directory
fetch-images) fetch-images Fetches failed view test images from device
fetch_images install Install app on connected device
;; large-tests Run large-sized tests on connected device
medium-tests Run medium-sized tests on connected device
accept-images)
accept_images Options:
;; -r --release Build and test release APK, instead of debug
END
install) exit 1
shift; parse_opts $* ;;
build_apk
install_apk
;;
*)
cat <<- END
Usage: $0 <command> [options]
Builds, installs and tests Loop Habit Tracker
Commands:
ci-tests Start emulator silently, run tests then kill emulator
local-tests Run all tests on connected device
install Install app on connected device
fetch-images Fetches failed view test images from device
accept-images Copies fetched images to corresponding assets folder
Options:
-r --release Build and install release version, instead of debug
END
exit 1
esac esac

@ -1,16 +1,18 @@
VERSION_CODE = 39 VERSION_CODE = 51
VERSION_NAME = 1.8.0 VERSION_NAME = 1.8.8
MIN_SDK_VERSION = 19 MIN_SDK_VERSION = 21
TARGET_SDK_VERSION = 29 TARGET_SDK_VERSION = 29
COMPILE_SDK_VERSION = 29 COMPILE_SDK_VERSION = 29
DAGGER_VERSION = 2.25.2 DAGGER_VERSION = 2.25.4
KOTLIN_VERSION = 1.3.50 KOTLIN_VERSION = 1.3.61
SUPPORT_LIBRARY_VERSION = 28.0.0 SUPPORT_LIBRARY_VERSION = 28.0.0
AUTO_FACTORY_VERSION = 1.0-beta6 AUTO_FACTORY_VERSION = 1.0-beta6
BUILD_TOOLS_VERSION = 3.5.2 BUILD_TOOLS_VERSION = 4.0.0
org.gradle.parallel=false org.gradle.parallel=false
org.gradle.daemon=true org.gradle.daemon=true
org.gradle.jvmargs=-Xms2048m -Xmx2048m -XX:MaxPermSize=2048m org.gradle.jvmargs=-Xms2048m -Xmx2048m -XX:MaxPermSize=2048m
android.useAndroidX=true
android.enableJetifier=true

Binary file not shown.

@ -1,6 +1,5 @@
#Wed Sep 04 13:05:58 MSK 2019
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip

115
android/gradlew vendored

@ -1,4 +1,20 @@
#!/usr/bin/env bash #!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
############################################################################## ##############################################################################
## ##
@ -6,20 +22,38 @@
## ##
############################################################################## ##############################################################################
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. # Attempt to set APP_HOME
DEFAULT_JVM_OPTS="" # Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle" APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"` APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value. # Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum" MAX_FD="maximum"
warn ( ) { warn () {
echo "$*" echo "$*"
} }
die ( ) { die () {
echo echo
echo "$*" echo "$*"
echo echo
@ -30,6 +64,7 @@ die ( ) {
cygwin=false cygwin=false
msys=false msys=false
darwin=false darwin=false
nonstop=false
case "`uname`" in case "`uname`" in
CYGWIN* ) CYGWIN* )
cygwin=true cygwin=true
@ -40,31 +75,11 @@ case "`uname`" in
MINGW* ) MINGW* )
msys=true msys=true
;; ;;
NONSTOP* )
nonstop=true
;;
esac esac
# For Cygwin, ensure paths are in UNIX format before anything is touched.
if $cygwin ; then
[ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
fi
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >&-
APP_HOME="`pwd -P`"
cd "$SAVED" >&-
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM. # Determine the Java command to use to start the JVM.
@ -90,7 +105,7 @@ location of your Java installation."
fi fi
# Increase the maximum file descriptors if we can. # Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n` MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
@ -110,10 +125,11 @@ if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi fi
# For Cygwin, switch paths to Windows format before running java # For Cygwin or MSYS, switch paths to Windows format before running java
if $cygwin ; then if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"` APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath # We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
@ -138,27 +154,30 @@ if $cygwin ; then
else else
eval `echo args$i`="\"$arg\"" eval `echo args$i`="\"$arg\""
fi fi
i=$((i+1)) i=`expr $i + 1`
done done
case $i in case $i in
(0) set -- ;; 0) set -- ;;
(1) set -- "$args0" ;; 1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;; 2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;; 3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;; 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac esac
fi fi
# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules # Escape application args
function splitJvmOpts() { save () {
JVM_OPTS=("$@") for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
} }
eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS APP_ARGS=`save "$@"`
JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" exec "$JAVACMD" "$@"

@ -1,3 +1,19 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off @if "%DEBUG%" == "" @echo off
@rem ########################################################################## @rem ##########################################################################
@rem @rem
@ -8,14 +24,14 @@
@rem Set local scope for the variables with windows NT shell @rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal if "%OS%"=="Windows_NT" setlocal
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS=
set DIRNAME=%~dp0 set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=. if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0 set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME% set APP_HOME=%DIRNAME%
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe @rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome if defined JAVA_HOME goto findJavaFromJavaHome
@ -46,10 +62,9 @@ echo location of your Java installation.
goto fail goto fail
:init :init
@rem Get command-line arguments, handling Windowz variants @rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args if not "%OS%" == "Windows_NT" goto win9xME_args
if "%@eval[2+2]" == "4" goto 4NT_args
:win9xME_args :win9xME_args
@rem Slurp the command line arguments. @rem Slurp the command line arguments.
@ -60,11 +75,6 @@ set _SKIP=2
if "x%~1" == "x" goto execute if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%* set CMD_LINE_ARGS=%*
goto execute
:4NT_args
@rem Get arguments from the 4NT Shell from JP Software
set CMD_LINE_ARGS=%$
:execute :execute
@rem Setup the command line @rem Setup the command line

@ -0,0 +1 @@
uhabits-android/src/main/play/

@ -55,7 +55,7 @@ def get_total(report):
def get_color(total): def get_color(total):
""" """
Return color for current coverage precent Return color for current coverage percent
""" """
try: try:
xtotal = int(total) xtotal = int(total)

@ -1,25 +1,25 @@
apply plugin: 'idea' plugins {
apply plugin: 'com.android.application' id 'idea'
apply plugin: 'kotlin-android' id 'com.android.application'
apply plugin: 'kotlin-kapt' id 'kotlin-android'
import org.ajoberstar.grgit.Grgit id 'kotlin-kapt'
id 'com.github.triplet.play' version '2.6.2'
ext { id 'kotlin-android-extensions'
git = Grgit.open(currentDir: projectDir)
GIT_COMMIT = git.head().id.substring(0, 8)
GIT_BRANCH = git.branch.current.name
} }
android { android {
compileSdkVersion COMPILE_SDK_VERSION as Integer compileSdkVersion COMPILE_SDK_VERSION as Integer
if(project.hasProperty("LOOP_STORE_FILE")) { def secretPropsFile = file("../../.secret/gradle.properties")
if (secretPropsFile.exists()) {
def secrets = new Properties()
secretPropsFile.withInputStream { secrets.load(it) }
signingConfigs { signingConfigs {
release { release {
storeFile file(LOOP_STORE_FILE) storeFile file(secrets.LOOP_KEY_STORE)
storePassword LOOP_STORE_PASSWORD storePassword secrets.LOOP_STORE_PASSWORD
keyAlias LOOP_KEY_ALIAS keyAlias secrets.LOOP_KEY_ALIAS
keyPassword LOOP_KEY_PASSWORD keyPassword secrets.LOOP_KEY_PASSWORD
} }
} }
buildTypes.release.signingConfig signingConfigs.release buildTypes.release.signingConfig signingConfigs.release
@ -27,12 +27,12 @@ android {
defaultConfig { defaultConfig {
versionCode VERSION_CODE as Integer versionCode VERSION_CODE as Integer
versionName "$VERSION_NAME ($GIT_BRANCH $GIT_COMMIT)" versionName "$VERSION_NAME"
minSdkVersion MIN_SDK_VERSION as Integer minSdkVersion MIN_SDK_VERSION as Integer
targetSdkVersion TARGET_SDK_VERSION as Integer targetSdkVersion TARGET_SDK_VERSION as Integer
applicationId "org.isoron.uhabits" applicationId "org.isoron.uhabits"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
} }
buildTypes { buildTypes {
@ -49,6 +49,7 @@ android {
lintOptions { lintOptions {
checkReleaseBuilds false checkReleaseBuilds false
abortOnError false abortOnError false
disable 'GoogleAppIndexingWarning'
} }
compileOptions { compileOptions {
@ -69,6 +70,10 @@ android {
sourceSets { sourceSets {
main.assets.srcDirs += '../uhabits-core/src/main/resources/' main.assets.srcDirs += '../uhabits-core/src/main/resources/'
} }
buildFeatures {
viewBinding true
}
} }
dependencies { dependencies {
@ -76,34 +81,36 @@ dependencies {
implementation project(":android-base") implementation project(":android-base")
implementation project(":android-pickers") implementation project(":android-pickers")
implementation "com.android.support:appcompat-v7:$SUPPORT_LIBRARY_VERSION" implementation 'androidx.appcompat:appcompat:1.0.0'
implementation "com.android.support:design:$SUPPORT_LIBRARY_VERSION" implementation 'com.google.android.material:material:1.0.0'
implementation "com.android.support:preference-v14:$SUPPORT_LIBRARY_VERSION" implementation 'androidx.legacy:legacy-preference-v14:1.0.0'
implementation "com.android.support:support-v4:$SUPPORT_LIBRARY_VERSION" implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation "com.github.paolorotolo:appintro:3.4.0" implementation "com.github.paolorotolo:appintro:3.4.0"
implementation "com.google.dagger:dagger:$DAGGER_VERSION" implementation "com.google.dagger:dagger:$DAGGER_VERSION"
implementation "com.jakewharton:butterknife:8.6.1-SNAPSHOT" implementation "com.jakewharton:butterknife:8.6.1-SNAPSHOT"
implementation "org.apmem.tools:layouts:1.10" implementation "org.apmem.tools:layouts:1.10"
implementation "com.google.code.gson:gson:2.7" implementation "com.google.code.gson:gson:2.8.5"
implementation "com.google.code.findbugs:jsr305:3.0.2" implementation "com.google.code.findbugs:jsr305:3.0.2"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$KOTLIN_VERSION" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$KOTLIN_VERSION"
implementation "androidx.constraintlayout:constraintlayout:2.0.0-beta4"
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
compileOnly "javax.annotation:jsr250-api:1.0" compileOnly "javax.annotation:jsr250-api:1.0"
compileOnly "com.google.auto.factory:auto-factory:${AUTO_FACTORY_VERSION}" compileOnly "com.google.auto.factory:auto-factory:$AUTO_FACTORY_VERSION"
kapt "com.google.dagger:dagger-compiler:$DAGGER_VERSION" kapt "com.google.dagger:dagger-compiler:$DAGGER_VERSION"
kapt "com.jakewharton:butterknife-compiler:9.0.0" kapt "com.jakewharton:butterknife-compiler:10.2.1"
annotationProcessor "com.google.auto.factory:auto-factory:${AUTO_FACTORY_VERSION}" annotationProcessor "com.google.auto.factory:auto-factory:$AUTO_FACTORY_VERSION"
androidTestImplementation "com.android.support.test.espresso:espresso-contrib:2.2.2" androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.1.0'
androidTestImplementation "com.android.support.test.espresso:espresso-core:2.2.2" androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0'
androidTestImplementation "com.android.support.test.uiautomator:uiautomator-v18:2.1.1" androidTestImplementation 'androidx.test.uiautomator:uiautomator:2.2.0'
androidTestImplementation "com.google.dagger:dagger:$DAGGER_VERSION" androidTestImplementation "com.google.dagger:dagger:$DAGGER_VERSION"
androidTestImplementation "com.linkedin.testbutler:test-butler-library:1.3.1" androidTestImplementation "com.linkedin.testbutler:test-butler-library:1.3.1"
androidTestCompileOnly "com.google.auto.factory:auto-factory:${AUTO_FACTORY_VERSION}" androidTestCompileOnly "com.google.auto.factory:auto-factory:$AUTO_FACTORY_VERSION"
androidTestAnnotationProcessor "com.google.auto.factory:auto-factory:${AUTO_FACTORY_VERSION}" androidTestAnnotationProcessor "com.google.auto.factory:auto-factory:$AUTO_FACTORY_VERSION"
androidTestImplementation "com.android.support:support-annotations:$SUPPORT_LIBRARY_VERSION" androidTestImplementation 'androidx.annotation:annotation:1.0.0'
androidTestImplementation "com.android.support.test:rules:0.5" androidTestImplementation 'androidx.test:rules:1.1.1'
androidTestImplementation "com.android.support.test:runner:0.5" androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation "com.google.guava:guava:24.1-android" androidTestImplementation "com.google.guava:guava:24.1-android"
androidTestImplementation project(":uhabits-core") androidTestImplementation project(":uhabits-core")
kaptAndroidTest "com.google.dagger:dagger-compiler:$DAGGER_VERSION" kaptAndroidTest "com.google.dagger:dagger-compiler:$DAGGER_VERSION"
@ -111,15 +118,15 @@ dependencies {
// mockito-android 2+ includes net.bytebuddy, which causes tests to fail. // mockito-android 2+ includes net.bytebuddy, which causes tests to fail.
// Excluding the package net.bytebuddy on AndroidManifest.xml breaks some // Excluding the package net.bytebuddy on AndroidManifest.xml breaks some
// AndroidJUnitRunner functionality, such as running individual methods. // AndroidJUnitRunner functionality, such as running individual methods.
androidTestImplementation "org.mockito:mockito-core:1+" androidTestImplementation "org.mockito:mockito-core:1.10.19"
androidTestImplementation "com.google.dexmaker:dexmaker-mockito:+" androidTestImplementation "com.google.dexmaker:dexmaker-mockito:1.2"
testImplementation "com.google.dagger:dagger:$DAGGER_VERSION" testImplementation "com.google.dagger:dagger:$DAGGER_VERSION"
testImplementation "org.mockito:mockito-core:2.8.9" testImplementation "org.mockito:mockito-core:2.8.9"
testImplementation "org.mockito:mockito-inline:2.8.9" testImplementation "org.mockito:mockito-inline:2.8.9"
testImplementation "junit:junit:4+" testImplementation "junit:junit:4.12"
implementation('com.opencsv:opencsv:3.9') { implementation('com.opencsv:opencsv:3.10') {
exclude group: 'commons-logging', module: 'commons-logging' exclude group: 'commons-logging', module: 'commons-logging'
} }
implementation('io.socket:socket.io-client:0.8.3') { implementation('io.socket:socket.io-client:0.8.3') {
@ -127,11 +134,11 @@ dependencies {
} }
} }
repositories {
google()
jcenter()
}
kapt { kapt {
correctErrorTypes = true correctErrorTypes = true
} }
play {
serviceAccountCredentials = file("../../.secret/gcp-key.json")
track = "alpha"
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

@ -23,11 +23,15 @@ import android.appwidget.*;
import android.content.*; import android.content.*;
import android.content.res.*; import android.content.res.*;
import android.os.*; import android.os.*;
import android.support.annotation.*;
import android.support.test.*; import androidx.annotation.NonNull;
import android.support.test.filters.*; import androidx.annotation.StyleRes;
import androidx.test.*;
import androidx.test.filters.*;
import android.util.*; import android.util.*;
import androidx.test.platform.app.InstrumentationRegistry;
import junit.framework.*; import junit.framework.*;
import org.isoron.androidbase.*; import org.isoron.androidbase.*;
@ -62,8 +66,6 @@ public class BaseAndroidTest extends TestCase
protected TaskRunner taskRunner; protected TaskRunner taskRunner;
protected HabitLogger logger;
protected HabitFixtures fixtures; protected HabitFixtures fixtures;
protected CountDownLatch latch; protected CountDownLatch latch;
@ -82,8 +84,8 @@ public class BaseAndroidTest extends TestCase
{ {
if (Looper.myLooper() == null) Looper.prepare(); if (Looper.myLooper() == null) Looper.prepare();
targetContext = InstrumentationRegistry.getTargetContext(); targetContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
testContext = InstrumentationRegistry.getContext(); testContext = InstrumentationRegistry.getInstrumentation().getContext();
DateUtils.setFixedLocalTime(FIXED_LOCAL_TIME); DateUtils.setFixedLocalTime(FIXED_LOCAL_TIME);
setResolution(2.0f); setResolution(2.0f);
@ -101,7 +103,6 @@ public class BaseAndroidTest extends TestCase
prefs = appComponent.getPreferences(); prefs = appComponent.getPreferences();
habitList = appComponent.getHabitList(); habitList = appComponent.getHabitList();
taskRunner = appComponent.getTaskRunner(); taskRunner = appComponent.getTaskRunner();
logger = appComponent.getHabitsLogger();
modelFactory = appComponent.getModelFactory(); modelFactory = appComponent.getModelFactory();
prefs.clear(); prefs.clear();

@ -20,7 +20,8 @@
package org.isoron.uhabits; package org.isoron.uhabits;
import android.content.*; import android.content.*;
import android.support.test.uiautomator.*;
import androidx.test.uiautomator.*;
import com.linkedin.android.testbutler.*; import com.linkedin.android.testbutler.*;
@ -30,12 +31,15 @@ import org.isoron.uhabits.core.ui.screens.habits.list.*;
import org.isoron.uhabits.core.utils.*; import org.isoron.uhabits.core.utils.*;
import org.junit.*; import org.junit.*;
import static android.support.test.InstrumentationRegistry.*; import static androidx.test.InstrumentationRegistry.getContext;
import static android.support.test.uiautomator.UiDevice.*; import static androidx.test.InstrumentationRegistry.getTargetContext;
import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
import static androidx.test.uiautomator.UiDevice.*;
public class BaseUserInterfaceTest public class BaseUserInterfaceTest
{ {
private static final String PKG = "org.isoron.uhabits"; private static final String PKG = "org.isoron.uhabits";
public static final String EMPTY_DESCRIPTION_HABIT_NAME = "Read books";
public static UiDevice device; public static UiDevice device;
@ -93,26 +97,36 @@ public class BaseUserInterfaceTest
Habit h1 = fixtures.createEmptyHabit(); Habit h1 = fixtures.createEmptyHabit();
h1.setName("Wake up early"); h1.setName("Wake up early");
h1.setDescription("Did you wake up early today?"); h1.setQuestion("Did you wake up early today?");
h1.setDescription("test description 1");
h1.setColor(5); h1.setColor(5);
habitList.update(h1); habitList.update(h1);
Habit h2 = fixtures.createShortHabit(); Habit h2 = fixtures.createShortHabit();
h2.setName("Track time"); h2.setName("Track time");
h2.setDescription("Did you track your time?"); h2.setQuestion("Did you track your time?");
h2.setDescription("test description 2");
h2.setColor(5); h2.setColor(5);
habitList.update(h2); habitList.update(h2);
Habit h3 = fixtures.createLongHabit(); Habit h3 = fixtures.createLongHabit();
h3.setName("Meditate"); h3.setName("Meditate");
h3.setDescription("Did meditate today?"); h3.setQuestion("Did meditate today?");
h3.setDescription("test description 3");
h3.setColor(10); h3.setColor(10);
habitList.update(h3); habitList.update(h3);
Habit h4 = fixtures.createEmptyHabit(); Habit h4 = fixtures.createEmptyHabit();
h4.setName("Read books"); h4.setName(EMPTY_DESCRIPTION_HABIT_NAME);
h4.setDescription("Did you read books today?"); h4.setQuestion("Did you read books today?");
h4.setDescription("");
h4.setColor(2); h4.setColor(2);
habitList.update(h4); habitList.update(h4);
} }
protected void rotateDevice() throws Exception
{
device.setOrientationLeft();
device.setOrientationNatural();
}
} }

@ -20,11 +20,14 @@
package org.isoron.uhabits; package org.isoron.uhabits;
import android.graphics.*; import android.graphics.*;
import android.support.annotation.*;
import android.support.test.*; import androidx.annotation.NonNull;
import androidx.test.*;
import android.view.*; import android.view.*;
import android.widget.*; import android.widget.*;
import androidx.test.platform.app.InstrumentationRegistry;
import org.isoron.androidbase.*; import org.isoron.androidbase.*;
import org.isoron.androidbase.utils.*; import org.isoron.androidbase.utils.*;
import org.isoron.uhabits.widgets.*; import org.isoron.uhabits.widgets.*;

@ -52,7 +52,8 @@ public class HabitFixtures
{ {
Habit habit = modelFactory.buildHabit(); Habit habit = modelFactory.buildHabit();
habit.setName("Meditate"); habit.setName("Meditate");
habit.setDescription("Did you meditate this morning?"); habit.setQuestion("Did you meditate this morning?");
habit.setDescription("This is a test description");
habit.setColor(5); habit.setColor(5);
habit.setFrequency(Frequency.DAILY); habit.setFrequency(Frequency.DAILY);
habit.setId(id); habit.setId(id);
@ -81,7 +82,7 @@ public class HabitFixtures
{ {
Habit habit = modelFactory.buildHabit(); Habit habit = modelFactory.buildHabit();
habit.setName("Take a walk"); habit.setName("Take a walk");
habit.setDescription("How many steps did you walk today?"); habit.setQuestion("How many steps did you walk today?");
habit.setType(Habit.NUMBER_HABIT); habit.setType(Habit.NUMBER_HABIT);
habit.setTargetType(Habit.AT_LEAST); habit.setTargetType(Habit.AT_LEAST);
habit.setTargetValue(200.0); habit.setTargetValue(200.0);
@ -103,7 +104,7 @@ public class HabitFixtures
{ {
Habit habit = modelFactory.buildHabit(); Habit habit = modelFactory.buildHabit();
habit.setName("Wake up early"); habit.setName("Wake up early");
habit.setDescription("Did you wake up before 6am?"); habit.setQuestion("Did you wake up before 6am?");
habit.setFrequency(new Frequency(2, 3)); habit.setFrequency(new Frequency(2, 3));
habitList.add(habit); habitList.add(habit);

@ -1,65 +0,0 @@
/*
* 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.os.*;
import android.support.test.filters.*;
import android.support.test.runner.*;
import org.isoron.androidbase.*;
import org.isoron.uhabits.core.models.*;
import org.junit.*;
import org.junit.runner.*;
import java.io.*;
import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.MatcherAssert.*;
@RunWith(AndroidJUnit4.class)
@MediumTest
public class HabitLoggerTest extends BaseAndroidTest
{
@Test
public void testLogReminderScheduled() throws IOException
{
if (!isLogcatAvailable()) return;
long time = 1422277200000L; // 13:00 jan 26, 2015 (UTC)
Habit habit = fixtures.createEmptyHabit();
habit.setName("Write journal");
logger.logReminderScheduled(habit, time);
String expectedMsg = "Setting alarm (2015-01-26 130000): Wri\n";
assertLogcatContains(expectedMsg);
}
protected void assertLogcatContains(String expectedMsg) throws IOException
{
String logcat = new AndroidBugReporter(targetContext).getLogcat();
assertThat(logcat, containsString(expectedMsg));
}
protected boolean isLogcatAvailable()
{
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN;
}
}

@ -37,7 +37,6 @@ class TestModule {
@ActivityScope @ActivityScope
@Component(modules = arrayOf( @Component(modules = arrayOf(
ActivityContextModule::class, ActivityContextModule::class,
AboutModule::class,
HabitsActivityModule::class, HabitsActivityModule::class,
ListHabitsModule::class, ListHabitsModule::class,
ShowHabitModule::class, ShowHabitModule::class,

@ -19,8 +19,10 @@
package org.isoron.uhabits; package org.isoron.uhabits;
import android.support.test.filters.*; import androidx.test.filters.*;
import android.support.test.runner.*; import androidx.test.runner.*;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.isoron.androidbase.*; import org.isoron.androidbase.*;
import org.junit.*; import org.junit.*;

@ -19,8 +19,10 @@
package org.isoron.uhabits.acceptance; package org.isoron.uhabits.acceptance;
import android.support.test.filters.*; import androidx.test.filters.*;
import android.support.test.runner.*; import androidx.test.runner.*;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.isoron.uhabits.*; import org.isoron.uhabits.*;
import org.junit.*; import org.junit.*;

@ -19,8 +19,9 @@
package org.isoron.uhabits.acceptance; package org.isoron.uhabits.acceptance;
import android.support.test.filters.*; import androidx.test.filters.*;
import android.support.test.runner.*;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.isoron.uhabits.*; import org.isoron.uhabits.*;
import org.junit.*; import org.junit.*;
@ -37,22 +38,36 @@ import static org.isoron.uhabits.acceptance.steps.ListHabitsSteps.*;
public class HabitsTest extends BaseUserInterfaceTest public class HabitsTest extends BaseUserInterfaceTest
{ {
@Test @Test
public void shouldCreateHabit() throws Exception public void shouldCreateHabit() throws Exception {
shouldCreateHabit("this is a test description");
}
@Test
public void shouldCreateHabitBlankDescription() throws Exception {
shouldCreateHabit("");
}
private void shouldCreateHabit(String description) throws Exception
{ {
launchApp(); launchApp();
verifyShowsScreen(LIST_HABITS); verifyShowsScreen(LIST_HABITS);
clickMenu(ADD); clickMenu(ADD);
verifyShowsScreen(SELECT_HABIT_TYPE);
clickText("Yes or No");
verifyShowsScreen(EDIT_HABIT); verifyShowsScreen(EDIT_HABIT);
typeName("Hello world"); String testName = "Hello world";
typeName(testName);
typeQuestion("Did you say hello to the world today?"); typeQuestion("Did you say hello to the world today?");
pickFrequency("Every week"); typeDescription(description);
pickFrequency();
pickColor(5); pickColor(5);
clickSave(); clickSave();
verifyShowsScreen(LIST_HABITS); verifyShowsScreen(LIST_HABITS);
verifyDisplaysText("Hello world"); verifyDisplaysText(testName);
} }
@Test @Test
@ -79,7 +94,16 @@ public class HabitsTest extends BaseUserInterfaceTest
} }
@Test @Test
public void shouldEditHabit() throws Exception public void shouldEditHabit() throws Exception {
shouldEditHabit("this is a test description");
}
@Test
public void shouldEditHabitBlankDescription() throws Exception {
shouldEditHabit("");
}
private void shouldEditHabit(String description) throws Exception
{ {
launchApp(); launchApp();
@ -90,6 +114,7 @@ public class HabitsTest extends BaseUserInterfaceTest
verifyShowsScreen(EDIT_HABIT); verifyShowsScreen(EDIT_HABIT);
typeName("Take a walk"); typeName("Take a walk");
typeQuestion("Did you take a walk today?"); typeQuestion("Did you take a walk today?");
typeDescription(description);
clickSave(); clickSave();
verifyShowsScreen(LIST_HABITS); verifyShowsScreen(LIST_HABITS);
@ -172,4 +197,12 @@ public class HabitsTest extends BaseUserInterfaceTest
verifyDisplaysText("Track time"); verifyDisplaysText("Track time");
verifyDisplaysText("Wake up early"); verifyDisplaysText("Wake up early");
} }
@Test
public void shouldHideNotesCard() throws Exception
{
launchApp();
clickText(EMPTY_DESCRIPTION_HABIT_NAME);
verifyShowsScreen(SHOW_HABIT, false);
}
} }

@ -19,8 +19,10 @@
package org.isoron.uhabits.acceptance; package org.isoron.uhabits.acceptance;
import android.support.test.filters.*; import androidx.test.filters.*;
import android.support.test.runner.*; import androidx.test.runner.*;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.isoron.uhabits.*; import org.isoron.uhabits.*;
import org.junit.*; import org.junit.*;

@ -19,7 +19,7 @@
package org.isoron.uhabits.acceptance; package org.isoron.uhabits.acceptance;
import android.support.test.filters.*; import androidx.test.filters.*;
import org.isoron.uhabits.*; import org.isoron.uhabits.*;
import org.junit.*; import org.junit.*;

@ -19,24 +19,26 @@
package org.isoron.uhabits.acceptance.steps; package org.isoron.uhabits.acceptance.steps;
import android.os.*; import android.view.View;
import android.support.annotation.*;
import android.support.test.espresso.*;
import android.support.test.espresso.contrib.*;
import android.support.test.uiautomator.*;
import android.support.v7.widget.*;
import androidx.annotation.StringRes;
import androidx.test.espresso.*;
import androidx.test.espresso.contrib.*;
import androidx.test.uiautomator.*;
import androidx.recyclerview.widget.RecyclerView;
import org.hamcrest.Matcher;
import org.isoron.uhabits.*; import org.isoron.uhabits.*;
import org.isoron.uhabits.R; import org.isoron.uhabits.R;
import org.isoron.uhabits.activities.habits.list.*; import org.isoron.uhabits.activities.habits.list.*;
import static android.os.Build.VERSION.SDK_INT; import static android.os.Build.VERSION.*;
import static android.os.Build.VERSION_CODES.LOLLIPOP; import static androidx.test.espresso.Espresso.*;
import static android.support.test.espresso.Espresso.*; import static androidx.test.espresso.action.ViewActions.*;
import static android.support.test.espresso.action.ViewActions.*; import static androidx.test.espresso.assertion.PositionAssertions.*;
import static android.support.test.espresso.assertion.PositionAssertions.*; import static androidx.test.espresso.assertion.ViewAssertions.*;
import static android.support.test.espresso.assertion.ViewAssertions.*; import static androidx.test.espresso.matcher.ViewMatchers.*;
import static android.support.test.espresso.matcher.ViewMatchers.*;
import static junit.framework.Assert.*; import static junit.framework.Assert.*;
import static org.hamcrest.CoreMatchers.*; import static org.hamcrest.CoreMatchers.*;
@ -150,10 +152,14 @@ public class CommonSteps extends BaseUserInterfaceTest
public enum Screen public enum Screen
{ {
LIST_HABITS, SHOW_HABIT, EDIT_HABIT LIST_HABITS, SHOW_HABIT, EDIT_HABIT, SELECT_HABIT_TYPE
}
public static void verifyShowsScreen(Screen screen) {
verifyShowsScreen(screen, true);
} }
public static void verifyShowsScreen(Screen screen) public static void verifyShowsScreen(Screen screen, boolean notesCardVisibleExpected)
{ {
switch(screen) switch(screen)
{ {
@ -163,12 +169,22 @@ public class CommonSteps extends BaseUserInterfaceTest
break; break;
case SHOW_HABIT: case SHOW_HABIT:
Matcher<View> noteCardViewMatcher = notesCardVisibleExpected ? isDisplayed() :
withEffectiveVisibility(Visibility.GONE);
onView(withId(R.id.subtitleCard)).check(matches(isDisplayed())); onView(withId(R.id.subtitleCard)).check(matches(isDisplayed()));
onView(withId(R.id.notesCard)).check(matches(noteCardViewMatcher));
break; break;
case EDIT_HABIT: case EDIT_HABIT:
onView(withId(R.id.tvDescription)).check(matches(isDisplayed())); onView(withId(R.id.questionInput)).check(matches(isDisplayed()));
break; break;
case SELECT_HABIT_TYPE:
onView(withText(R.string.yes_or_no_example)).check(matches(isDisplayed()));
break;
default:
throw new IllegalStateException();
} }
} }
} }

@ -19,14 +19,14 @@
package org.isoron.uhabits.acceptance.steps; package org.isoron.uhabits.acceptance.steps;
import android.support.test.uiautomator.*; import androidx.test.uiautomator.*;
import org.isoron.uhabits.*; import org.isoron.uhabits.*;
import static android.support.test.espresso.Espresso.*; import static androidx.test.espresso.Espresso.*;
import static android.support.test.espresso.action.ViewActions.*; import static androidx.test.espresso.action.ViewActions.*;
import static android.support.test.espresso.action.ViewActions.closeSoftKeyboard; import static androidx.test.espresso.action.ViewActions.closeSoftKeyboard;
import static android.support.test.espresso.matcher.ViewMatchers.*; import static androidx.test.espresso.matcher.ViewMatchers.*;
import static org.isoron.uhabits.BaseUserInterfaceTest.*; import static org.isoron.uhabits.BaseUserInterfaceTest.*;
public class EditHabitSteps public class EditHabitSteps
@ -36,26 +36,53 @@ public class EditHabitSteps
onView(withId(R.id.buttonSave)).perform(click()); onView(withId(R.id.buttonSave)).perform(click());
} }
public static void pickFrequency(String freq) public static void pickFrequency()
{ {
onView(withId(R.id.spinner)).perform(click()); onView(withId(R.id.boolean_frequency_picker)).perform(click());
device.findObject(By.text(freq)).click(); onView(withText("SAVE")).perform(click());
} }
public static void pickColor(int color) public static void pickColor(int color)
{ {
onView(withId(R.id.buttonPickColor)).perform(click()); onView(withId(R.id.colorButton)).perform(click());
device.findObject(By.descStartsWith(String.format("Color %d", color))).click(); device.findObject(By.descStartsWith(String.format("Color %d", color))).click();
} }
public static void typeName(String name) public static void typeName(String name)
{ {
typeTextWithId(R.id.tvName, name); typeTextWithId(R.id.nameInput, name);
} }
public static void typeQuestion(String name) public static void typeQuestion(String name)
{ {
typeTextWithId(R.id.tvDescription, name); typeTextWithId(R.id.questionInput, name);
}
public static void typeDescription(String description)
{
typeTextWithId(R.id.notesInput, description);
}
public static void setReminder()
{
onView(withId(R.id.reminderTimePicker)).perform(click());
onView(withId(R.id.done_button)).perform(click());
}
public static void clickReminderDays()
{
onView(withId(R.id.reminderDatePicker)).perform(click());
}
public static void unselectAllDays()
{
onView(withText("Saturday")).perform(click());
onView(withText("Sunday")).perform(click());
onView(withText("Monday")).perform(click());
onView(withText("Tuesday")).perform(click());
onView(withText("Wednesday")).perform(click());
onView(withText("Thursday")).perform(click());
onView(withText("Friday")).perform(click());
} }
private static void typeTextWithId(int id, String name) private static void typeTextWithId(int id, String name)

@ -19,7 +19,7 @@
package org.isoron.uhabits.acceptance.steps; package org.isoron.uhabits.acceptance.steps;
import android.support.test.espresso.*; import androidx.test.espresso.*;
import android.view.*; import android.view.*;
import org.hamcrest.*; import org.hamcrest.*;
@ -28,15 +28,15 @@ import org.isoron.uhabits.activities.habits.list.views.*;
import java.util.*; import java.util.*;
import static android.support.test.espresso.Espresso.onView; import static androidx.test.espresso.Espresso.onView;
import static android.support.test.espresso.action.ViewActions.click; import static androidx.test.espresso.action.ViewActions.click;
import static android.support.test.espresso.matcher.ViewMatchers.hasDescendant; import static androidx.test.espresso.matcher.ViewMatchers.hasDescendant;
import static android.support.test.espresso.matcher.ViewMatchers.isEnabled; import static androidx.test.espresso.matcher.ViewMatchers.isEnabled;
import static android.support.test.espresso.matcher.ViewMatchers.withClassName; import static androidx.test.espresso.matcher.ViewMatchers.withClassName;
import static android.support.test.espresso.matcher.ViewMatchers.withContentDescription; import static androidx.test.espresso.matcher.ViewMatchers.withContentDescription;
import static android.support.test.espresso.matcher.ViewMatchers.withId; import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static android.support.test.espresso.matcher.ViewMatchers.withParent; import static androidx.test.espresso.matcher.ViewMatchers.withParent;
import static android.support.test.espresso.matcher.ViewMatchers.withText; import static androidx.test.espresso.matcher.ViewMatchers.withText;
import static org.hamcrest.CoreMatchers.*; import static org.hamcrest.CoreMatchers.*;
import static org.isoron.uhabits.BaseUserInterfaceTest.device; import static org.isoron.uhabits.BaseUserInterfaceTest.device;
import static org.isoron.uhabits.acceptance.steps.CommonSteps.clickText; import static org.isoron.uhabits.acceptance.steps.CommonSteps.clickText;
@ -60,7 +60,7 @@ public abstract class ListHabitsSteps
break; break;
case ADD: case ADD:
clickViewWithId(R.id.actionCreateBooleanHabit); clickViewWithId(R.id.actionCreateHabit);
break; break;
case EDIT: case EDIT:

@ -19,7 +19,7 @@
package org.isoron.uhabits.acceptance.steps; package org.isoron.uhabits.acceptance.steps;
import android.support.test.uiautomator.*; import androidx.test.uiautomator.*;
import static android.os.Build.VERSION.SDK_INT; import static android.os.Build.VERSION.SDK_INT;
import static junit.framework.Assert.*; import static junit.framework.Assert.*;

@ -19,8 +19,10 @@
package org.isoron.uhabits.activities.common.views; package org.isoron.uhabits.activities.common.views;
import android.support.test.filters.*; import androidx.test.filters.*;
import android.support.test.runner.*; import androidx.test.runner.*;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.isoron.uhabits.*; import org.isoron.uhabits.*;
import org.isoron.uhabits.core.models.*; import org.isoron.uhabits.core.models.*;

@ -19,8 +19,10 @@
package org.isoron.uhabits.activities.common.views; package org.isoron.uhabits.activities.common.views;
import android.support.test.filters.*; import androidx.test.filters.*;
import android.support.test.runner.*; import androidx.test.runner.*;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.isoron.uhabits.*; import org.isoron.uhabits.*;
import org.isoron.uhabits.core.models.*; import org.isoron.uhabits.core.models.*;

@ -19,8 +19,10 @@
package org.isoron.uhabits.activities.common.views; package org.isoron.uhabits.activities.common.views;
import android.support.test.filters.*; import androidx.test.filters.*;
import android.support.test.runner.*; import androidx.test.runner.*;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.isoron.uhabits.*; import org.isoron.uhabits.*;
import org.isoron.uhabits.core.models.*; import org.isoron.uhabits.core.models.*;

@ -20,8 +20,10 @@
package org.isoron.uhabits.activities.common.views; package org.isoron.uhabits.activities.common.views;
import android.graphics.*; import android.graphics.*;
import android.support.test.filters.*; import androidx.test.filters.*;
import android.support.test.runner.*; import androidx.test.runner.*;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.isoron.uhabits.*; import org.isoron.uhabits.*;
import org.isoron.uhabits.utils.*; import org.isoron.uhabits.utils.*;

@ -19,8 +19,10 @@
package org.isoron.uhabits.activities.common.views; package org.isoron.uhabits.activities.common.views;
import android.support.test.filters.*; import androidx.test.filters.*;
import android.support.test.runner.*; import androidx.test.runner.*;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.isoron.uhabits.*; import org.isoron.uhabits.*;
import org.isoron.uhabits.core.models.*; import org.isoron.uhabits.core.models.*;
@ -29,6 +31,8 @@ import org.isoron.uhabits.utils.*;
import org.junit.*; import org.junit.*;
import org.junit.runner.*; import org.junit.runner.*;
import java.util.*;
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
@MediumTest @MediumTest
public class ScoreChartTest extends BaseViewTest public class ScoreChartTest extends BaseViewTest
@ -80,7 +84,7 @@ public class ScoreChartTest extends BaseViewTest
@Test @Test
public void testRender_withMonthlyBucket() throws Throwable public void testRender_withMonthlyBucket() throws Throwable
{ {
view.setScores(habit.getScores().groupBy(DateUtils.TruncateField.MONTH)); view.setScores(habit.getScores().groupBy(DateUtils.TruncateField.MONTH, Calendar.SUNDAY));
view.setBucketSize(30); view.setBucketSize(30);
view.invalidate(); view.invalidate();
@ -97,7 +101,7 @@ public class ScoreChartTest extends BaseViewTest
@Test @Test
public void testRender_withYearlyBucket() throws Throwable public void testRender_withYearlyBucket() throws Throwable
{ {
view.setScores(habit.getScores().groupBy(DateUtils.TruncateField.YEAR)); view.setScores(habit.getScores().groupBy(DateUtils.TruncateField.YEAR, Calendar.SUNDAY));
view.setBucketSize(365); view.setBucketSize(365);
view.invalidate(); view.invalidate();

@ -19,8 +19,10 @@
package org.isoron.uhabits.activities.common.views; package org.isoron.uhabits.activities.common.views;
import android.support.test.filters.*; import androidx.test.filters.*;
import android.support.test.runner.*; import androidx.test.runner.*;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.isoron.uhabits.*; import org.isoron.uhabits.*;
import org.isoron.uhabits.core.models.*; import org.isoron.uhabits.core.models.*;

@ -19,8 +19,9 @@
package org.isoron.uhabits.activities.habits.list.views package org.isoron.uhabits.activities.habits.list.views
import android.support.test.filters.* import androidx.test.ext.junit.runners.AndroidJUnit4
import android.support.test.runner.* import androidx.test.filters.*
import androidx.test.runner.*
import org.isoron.uhabits.* import org.isoron.uhabits.*
import org.isoron.uhabits.core.models.* import org.isoron.uhabits.core.models.*
import org.isoron.uhabits.utils.* import org.isoron.uhabits.utils.*

@ -19,8 +19,9 @@
package org.isoron.uhabits.activities.habits.list.views package org.isoron.uhabits.activities.habits.list.views
import android.support.test.filters.* import androidx.test.ext.junit.runners.AndroidJUnit4
import android.support.test.runner.* import androidx.test.filters.*
import androidx.test.runner.*
import org.hamcrest.CoreMatchers.* import org.hamcrest.CoreMatchers.*
import org.hamcrest.MatcherAssert.* import org.hamcrest.MatcherAssert.*
import org.isoron.uhabits.* import org.isoron.uhabits.*
@ -61,7 +62,6 @@ class CheckmarkPanelViewTest : BaseViewTest() {
@After @After
public override fun tearDown() { public override fun tearDown() {
// view.onDetachedFromWindow()
super.tearDown() super.tearDown()
} }
@ -70,23 +70,26 @@ class CheckmarkPanelViewTest : BaseViewTest() {
assertRenders(view, "$PATH/render.png") assertRenders(view, "$PATH/render.png")
} }
@Test // // Flaky test
fun testRender_withDifferentColor() { // @Test
view.color = PaletteUtils.getAndroidTestColor(1) // fun testRender_withDifferentColor() {
assertRenders(view, "$PATH/render_different_color.png") // view.color = PaletteUtils.getAndroidTestColor(1)
} // assertRenders(view, "$PATH/render_different_color.png")
// }
@Test // // Flaky test
fun testRender_Reversed() { // @Test
prefs.isCheckmarkSequenceReversed = true // fun testRender_Reversed() {
assertRenders(view, "$PATH/render_reversed.png") // prefs.isCheckmarkSequenceReversed = true
} // assertRenders(view, "$PATH/render_reversed.png")
// }
@Test // // Flaky test
fun testRender_withOffset() { // @Test
view.dataOffset = 3 // fun testRender_withOffset() {
assertRenders(view, "$PATH/render_offset.png") // view.dataOffset = 3
} // assertRenders(view, "$PATH/render_offset.png")
// }
@Test @Test
fun testToggle() { fun testToggle() {

@ -19,8 +19,9 @@
package org.isoron.uhabits.activities.habits.list.views package org.isoron.uhabits.activities.habits.list.views
import android.support.test.filters.* import androidx.test.ext.junit.runners.AndroidJUnit4
import android.support.test.runner.* import androidx.test.filters.*
import androidx.test.runner.*
import org.isoron.uhabits.* import org.isoron.uhabits.*
import org.isoron.uhabits.core.models.* import org.isoron.uhabits.core.models.*
import org.junit.* import org.junit.*

@ -19,8 +19,10 @@
package org.isoron.uhabits.activities.habits.list.views; package org.isoron.uhabits.activities.habits.list.views;
import android.support.test.filters.*; import androidx.test.filters.*;
import android.support.test.runner.*; import androidx.test.runner.*;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.isoron.uhabits.*; import org.isoron.uhabits.*;
import org.isoron.uhabits.core.preferences.*; import org.isoron.uhabits.core.preferences.*;

@ -19,8 +19,10 @@
package org.isoron.uhabits.activities.habits.list.views; package org.isoron.uhabits.activities.habits.list.views;
import android.support.test.filters.*; import androidx.test.filters.*;
import android.support.test.runner.*; import androidx.test.runner.*;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.isoron.uhabits.*; import org.isoron.uhabits.*;
import org.isoron.uhabits.core.ui.screens.habits.list.*; import org.isoron.uhabits.core.ui.screens.habits.list.*;

@ -19,8 +19,9 @@
package org.isoron.uhabits.activities.habits.list.views package org.isoron.uhabits.activities.habits.list.views
import android.support.test.filters.* import androidx.test.ext.junit.runners.AndroidJUnit4
import android.support.test.runner.* import androidx.test.filters.*
import androidx.test.runner.*
import org.hamcrest.CoreMatchers.* import org.hamcrest.CoreMatchers.*
import org.hamcrest.MatcherAssert.* import org.hamcrest.MatcherAssert.*
import org.isoron.uhabits.* import org.isoron.uhabits.*

@ -19,8 +19,9 @@
package org.isoron.uhabits.activities.habits.list.views package org.isoron.uhabits.activities.habits.list.views
import android.support.test.filters.* import androidx.test.ext.junit.runners.AndroidJUnit4
import android.support.test.runner.* import androidx.test.filters.*
import androidx.test.runner.*
import org.hamcrest.CoreMatchers.* import org.hamcrest.CoreMatchers.*
import org.hamcrest.MatcherAssert.* import org.hamcrest.MatcherAssert.*
import org.isoron.uhabits.* import org.isoron.uhabits.*
@ -65,23 +66,26 @@ class NumberPanelViewTest : BaseViewTest() {
assertRenders(view, "$PATH/render.png") assertRenders(view, "$PATH/render.png")
} }
@Test // // Flaky test
fun testRender_withDifferentColor() { // @Test
view.color = PaletteUtils.getAndroidTestColor(1) // fun testRender_withDifferentColor() {
assertRenders(view, "$PATH/render_different_color.png") // view.color = PaletteUtils.getAndroidTestColor(1)
} // assertRenders(view, "$PATH/render_different_color.png")
// }
@Test // // Flaky test
fun testRender_Reversed() { // @Test
prefs.isCheckmarkSequenceReversed = true // fun testRender_Reversed() {
assertRenders(view, "$PATH/render_reversed.png") // prefs.isCheckmarkSequenceReversed = true
} // assertRenders(view, "$PATH/render_reversed.png")
// }
@Test // // Flaky test
fun testRender_withOffset() { // @Test
view.dataOffset = 3 // fun testRender_withOffset() {
assertRenders(view, "$PATH/render_offset.png") // view.dataOffset = 3
} // assertRenders(view, "$PATH/render_offset.png")
// }
@Test @Test
fun testEdit() { fun testEdit() {

@ -19,10 +19,12 @@
package org.isoron.uhabits.activities.habits.show.views; package org.isoron.uhabits.activities.habits.show.views;
import android.support.test.filters.*; import androidx.test.filters.*;
import android.support.test.runner.*; import androidx.test.runner.*;
import android.view.*; import android.view.*;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.isoron.uhabits.*; import org.isoron.uhabits.*;
import org.isoron.uhabits.core.models.*; import org.isoron.uhabits.core.models.*;
import org.junit.*; import org.junit.*;

@ -19,10 +19,12 @@
package org.isoron.uhabits.activities.habits.show.views; package org.isoron.uhabits.activities.habits.show.views;
import android.support.test.filters.*; import androidx.test.filters.*;
import android.support.test.runner.*; import androidx.test.runner.*;
import android.view.*; import android.view.*;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.isoron.uhabits.*; import org.isoron.uhabits.*;
import org.isoron.uhabits.core.models.*; import org.isoron.uhabits.core.models.*;
import org.junit.*; import org.junit.*;

@ -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.activities.habits.show.views;
import android.view.LayoutInflater;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.MediumTest;
import org.isoron.uhabits.BaseViewTest;
import org.isoron.uhabits.R;
import org.isoron.uhabits.core.models.Habit;
import org.isoron.uhabits.core.models.Reminder;
import org.isoron.uhabits.core.models.WeekdayList;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@RunWith(AndroidJUnit4.class)
@MediumTest
public class NotesCardTest extends BaseViewTest
{
public static final String PATH = "habits/show/NotesCard/";
private NotesCard view;
private Habit habit;
@Before
@Override
public void setUp()
{
super.setUp();
habit = fixtures.createLongHabit();
habit.setReminder(new Reminder(8, 30, WeekdayList.EVERY_DAY));
view = LayoutInflater
.from(targetContext)
.inflate(R.layout.show_habit, null)
.findViewById(R.id.notesCard);
view.setHabit(habit);
view.refreshData();
measureView(view, 800, 200);
}
@Test
public void testRender() throws Exception
{
assertRenders(view, PATH + "render.png");
}
@Test
public void testRenderEmptyDescription() throws Exception
{
habit.setDescription("");
view.refreshData();
assertRenders(view, PATH + "render-empty-description.png");
}
}

@ -19,10 +19,12 @@
package org.isoron.uhabits.activities.habits.show.views; package org.isoron.uhabits.activities.habits.show.views;
import android.support.test.filters.*; import androidx.test.filters.*;
import android.support.test.runner.*; import androidx.test.runner.*;
import android.view.*; import android.view.*;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.isoron.uhabits.*; import org.isoron.uhabits.*;
import org.isoron.uhabits.core.models.*; import org.isoron.uhabits.core.models.*;
import org.junit.*; import org.junit.*;

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save