diff --git a/app/build.gradle b/app/build.gradle index 6bdb20d23..b6e28d599 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -4,13 +4,13 @@ apply plugin: 'me.tatarka.retrolambda' apply plugin: 'jacoco' android { - compileSdkVersion 23 - buildToolsVersion "23.0.3" + compileSdkVersion 25 + buildToolsVersion "25.0.2" defaultConfig { applicationId "org.isoron.uhabits" minSdkVersion 15 - targetSdkVersion 23 + targetSdkVersion 25 buildConfigField "Integer", "databaseVersion", "15" buildConfigField "String", "databaseFilename", "\"uhabits.db\"" @@ -53,7 +53,7 @@ dependencies { androidTestApt 'com.google.dagger:dagger-compiler:2.2' - androidTestCompile 'com.android.support:support-annotations:23.3.0' + androidTestCompile 'com.android.support:support-annotations:25.1.0' androidTestCompile 'com.android.support.test:rules:0.5' androidTestCompile 'com.android.support.test:runner:0.5' androidTestCompile 'com.google.auto.factory:auto-factory:1.0-beta3' @@ -64,10 +64,10 @@ dependencies { apt 'com.google.dagger:dagger-compiler:2.2' apt 'com.jakewharton:butterknife-compiler:8.0.1' - compile 'com.android.support:appcompat-v7:23.3.0' - compile 'com.android.support:design:23.3.0' - compile 'com.android.support:preference-v14:23.3.0' - compile 'com.android.support:support-v4:23.3.0' + compile 'com.android.support:appcompat-v7:25.1.0' + compile 'com.android.support:design:25.1.0' + compile 'com.android.support:preference-v14:25.1.0' + compile 'com.android.support:support-v4:25.1.0' compile 'com.getpebble:pebblekit:3.0.0' compile 'com.github.paolorotolo:appintro:3.4.0' compile 'com.google.auto.factory:auto-factory:1.0-beta3' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ee57323d5..1bab9be06 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -223,5 +223,15 @@ + + + + diff --git a/app/src/main/java/org/isoron/uhabits/activities/BaseScreen.java b/app/src/main/java/org/isoron/uhabits/activities/BaseScreen.java index 1954c7c42..64621414c 100644 --- a/app/src/main/java/org/isoron/uhabits/activities/BaseScreen.java +++ b/app/src/main/java/org/isoron/uhabits/activities/BaseScreen.java @@ -40,6 +40,7 @@ import java.io.*; import static android.os.Build.VERSION.*; import static android.os.Build.VERSION_CODES.*; +import static android.support.v4.content.FileProvider.*; /** * Base class for all screens in the application. @@ -50,6 +51,8 @@ import static android.os.Build.VERSION_CODES.*; */ public class BaseScreen { + public static final int REQUEST_CREATE_DOCUMENT = 1; + protected BaseActivity activity; @Nullable @@ -230,11 +233,14 @@ public class BaseScreen public void showSendFileScreen(@NonNull String archiveFilename) { + File file = new File(archiveFilename); + Uri fileUri = getUriForFile(activity, "org.isoron.uhabits", file); + Intent intent = new Intent(); intent.setAction(Intent.ACTION_SEND); intent.setType("application/zip"); - intent.putExtra(Intent.EXTRA_STREAM, - Uri.fromFile(new File(archiveFilename))); + intent.putExtra(Intent.EXTRA_STREAM, fileUri); + intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); activity.startActivity(intent); } diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsActivity.java b/app/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsActivity.java index d5529c2a1..deb1b3649 100644 --- a/app/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsActivity.java +++ b/app/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsActivity.java @@ -25,6 +25,7 @@ import org.isoron.uhabits.*; import org.isoron.uhabits.activities.*; import org.isoron.uhabits.activities.habits.list.model.*; import org.isoron.uhabits.preferences.*; +import org.isoron.uhabits.utils.*; /** * Activity that allows the user to see and modify the list of habits. @@ -43,6 +44,8 @@ public class ListHabitsActivity extends BaseActivity private Preferences prefs; + private MidnightTimer midnightTimer; + public ListHabitsComponent getListHabitsComponent() { return component; @@ -77,6 +80,8 @@ public class ListHabitsActivity extends BaseActivity screen.setSelectionMenu(selectionMenu); rootView.setController(controller, selectionMenu); + midnightTimer = component.getMidnightTimer(); + setScreen(screen); controller.onStartup(); } @@ -84,6 +89,7 @@ public class ListHabitsActivity extends BaseActivity @Override protected void onPause() { + midnightTimer.onPause(); screen.onDettached(); adapter.cancelRefresh(); super.onPause(); @@ -95,6 +101,7 @@ public class ListHabitsActivity extends BaseActivity adapter.refresh(); screen.onAttached(); rootView.postInvalidate(); + midnightTimer.onResume(); if (prefs.getTheme() == ThemeSwitcher.THEME_DARK && prefs.isPureBlackEnabled() != pureBlack) diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsComponent.java b/app/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsComponent.java index 658d6c573..554c22a3b 100644 --- a/app/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsComponent.java +++ b/app/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsComponent.java @@ -23,13 +23,14 @@ import org.isoron.uhabits.*; import org.isoron.uhabits.activities.*; import org.isoron.uhabits.activities.habits.list.controllers.*; import org.isoron.uhabits.activities.habits.list.model.*; +import org.isoron.uhabits.utils.*; import dagger.*; @ActivityScope @Component(modules = { ActivityModule.class }, dependencies = { AppComponent.class }) -public interface ListHabitsComponent extends ActivityComponent +public interface ListHabitsComponent { CheckmarkButtonControllerFactory getCheckmarkButtonControllerFactory(); @@ -44,4 +45,6 @@ public interface ListHabitsComponent extends ActivityComponent ListHabitsScreen getScreen(); ListHabitsSelectionMenu getSelectionMenu(); + + MidnightTimer getMidnightTimer(); } diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsController.java b/app/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsController.java index 875124f73..a52c4b4d6 100644 --- a/app/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsController.java +++ b/app/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsController.java @@ -133,7 +133,8 @@ public class ListHabitsController taskRunner.execute(() -> habitList.reorder(from, to)); } - public void onImportData(@NonNull File file) + public void onImportData(@NonNull File file, + @NonNull OnFinishedListener finishedListener) { taskRunner.execute(importTaskFactory.create(file, result -> { switch (result) @@ -151,6 +152,8 @@ public class ListHabitsController screen.showMessage(R.string.could_not_import); break; } + + finishedListener.onFinish(); })); } @@ -213,4 +216,9 @@ public class ListHabitsController prefs.updateLastHint(-1, DateUtils.getStartOfToday()); screen.showIntroScreen(); } + + public interface OnFinishedListener + { + void onFinish(); + } } diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsScreen.java b/app/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsScreen.java index 3fb92e817..045505a90 100644 --- a/app/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsScreen.java +++ b/app/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsScreen.java @@ -19,7 +19,9 @@ package org.isoron.uhabits.activities.habits.list; +import android.app.*; import android.content.*; +import android.net.*; import android.support.annotation.*; import org.isoron.uhabits.*; @@ -31,24 +33,32 @@ import org.isoron.uhabits.commands.*; import org.isoron.uhabits.intents.*; import org.isoron.uhabits.io.*; import org.isoron.uhabits.models.*; +import org.isoron.uhabits.utils.*; import java.io.*; import javax.inject.*; +import static android.os.Build.VERSION.*; +import static android.os.Build.VERSION_CODES.*; + @ActivityScope public class ListHabitsScreen extends BaseScreen implements CommandRunner.Listener { - public static final int RESULT_BUG_REPORT = 4; + public static final int RESULT_IMPORT_DATA = 1; public static final int RESULT_EXPORT_CSV = 2; public static final int RESULT_EXPORT_DB = 3; + public static final int RESULT_BUG_REPORT = 4; + public static final int RESULT_REPAIR_DB = 5; - public static final int RESULT_IMPORT_DATA = 1; + public static final int REQUEST_OPEN_DOCUMENT = 6; + + public static final int REQUEST_SETTINGS = 7; @Nullable private ListHabitsController controller; @@ -125,6 +135,15 @@ public class ListHabitsScreen extends BaseScreen @Override public void onResult(int requestCode, int resultCode, Intent data) + { + if (requestCode == REQUEST_OPEN_DOCUMENT) + onOpenDocumentResult(resultCode, data); + + if (requestCode == REQUEST_SETTINGS) + onSettingsResult(resultCode); + } + + private void onSettingsResult(int resultCode) { if (controller == null) return; @@ -152,6 +171,30 @@ public class ListHabitsScreen extends BaseScreen } } + private void onOpenDocumentResult(int resultCode, Intent data) + { + if (controller == null) return; + if (resultCode != Activity.RESULT_OK) return; + + try + { + Uri uri = data.getData(); + ContentResolver cr = activity.getContentResolver(); + InputStream is = cr.openInputStream(uri); + + File cacheDir = activity.getExternalCacheDir(); + File tempFile = File.createTempFile("import", "", cacheDir); + + FileUtils.copy(is, tempFile); + controller.onImportData(tempFile, () -> tempFile.delete()); + } + catch (IOException e) + { + showMessage(R.string.could_not_import); + e.printStackTrace(); + } + } + public void setController(@Nullable ListHabitsController controller) { this.controller = controller; @@ -208,6 +251,21 @@ public class ListHabitsScreen extends BaseScreen } public void showImportScreen() + { + if (SDK_INT < KITKAT) + { + showImportScreenPreKitKat(); + return; + } + + Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); + intent.addCategory(Intent.CATEGORY_OPENABLE); + intent.setType("*/*"); + + activity.startActivityForResult(intent, REQUEST_OPEN_DOCUMENT); + } + + public void showImportScreenPreKitKat() { File dir = dirFinder.findStorageDir(null); @@ -220,7 +278,8 @@ public class ListHabitsScreen extends BaseScreen FilePickerDialog picker = filePickerDialogFactory.create(dir); if (controller != null) - picker.setListener(file -> controller.onImportData(file)); + picker.setListener(file -> controller.onImportData(file, () -> {})); + activity.showDialog(picker.getDialog()); } @@ -233,7 +292,7 @@ public class ListHabitsScreen extends BaseScreen public void showSettingsScreen() { Intent intent = intentFactory.startSettingsActivity(activity); - activity.startActivityForResult(intent, 0); + activity.startActivityForResult(intent, REQUEST_SETTINGS); } public void toggleNightMode() diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/list/model/HabitCardListAdapter.java b/app/src/main/java/org/isoron/uhabits/activities/habits/list/model/HabitCardListAdapter.java index 7b91c3df8..55656b542 100644 --- a/app/src/main/java/org/isoron/uhabits/activities/habits/list/model/HabitCardListAdapter.java +++ b/app/src/main/java/org/isoron/uhabits/activities/habits/list/model/HabitCardListAdapter.java @@ -28,6 +28,7 @@ import org.isoron.uhabits.activities.habits.list.*; import org.isoron.uhabits.activities.habits.list.views.*; import org.isoron.uhabits.models.*; import org.isoron.uhabits.preferences.*; +import org.isoron.uhabits.utils.*; import java.util.*; @@ -42,7 +43,7 @@ import javax.inject.*; @ActivityScope public class HabitCardListAdapter extends RecyclerView.Adapter - implements HabitCardListCache.Listener + implements HabitCardListCache.Listener, MidnightTimer.MidnightListener { @NonNull private ModelObservable observable; @@ -59,15 +60,20 @@ public class HabitCardListAdapter @NonNull private Preferences preferences; + private final MidnightTimer midnightTimer; + @Inject public HabitCardListAdapter(@NonNull HabitCardListCache cache, - @NonNull Preferences preferences) + @NonNull Preferences preferences, + @NonNull MidnightTimer midnightTimer) { this.preferences = preferences; this.selected = new LinkedList<>(); this.observable = new ModelObservable(); this.cache = cache; + this.midnightTimer = midnightTimer; + cache.setListener(this); cache.setCheckmarkCount(ListHabitsRootView.MAX_CHECKMARK_COUNT); cache.setOrder(preferences.getDefaultOrder()); @@ -75,6 +81,12 @@ public class HabitCardListAdapter setHasStableIds(true); } + @Override + public void atMidnight() + { + cache.refreshAllHabits(); + } + public void cancelRefresh() { cache.cancelTasks(); @@ -148,6 +160,7 @@ public class HabitCardListAdapter public void onAttached() { cache.onAttached(); + midnightTimer.addListener(this); } @Override @@ -180,6 +193,7 @@ public class HabitCardListAdapter public void onDetached() { cache.onDetached(); + midnightTimer.removeListener(this); } @Override diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/list/views/HeaderView.java b/app/src/main/java/org/isoron/uhabits/activities/habits/list/views/HeaderView.java index 70ed6ec91..8f9d7c4d0 100644 --- a/app/src/main/java/org/isoron/uhabits/activities/habits/list/views/HeaderView.java +++ b/app/src/main/java/org/isoron/uhabits/activities/habits/list/views/HeaderView.java @@ -26,12 +26,14 @@ import android.view.*; import android.widget.*; import org.isoron.uhabits.*; +import org.isoron.uhabits.activities.habits.list.*; import org.isoron.uhabits.preferences.*; import org.isoron.uhabits.utils.*; import java.util.*; -public class HeaderView extends LinearLayout implements Preferences.Listener +public class HeaderView extends LinearLayout + implements Preferences.Listener, MidnightTimer.MidnightListener { private final Context context; @@ -40,6 +42,9 @@ public class HeaderView extends LinearLayout implements Preferences.Listener @Nullable private Preferences prefs; + @Nullable + private MidnightTimer midnightTimer; + public HeaderView(Context context, AttributeSet attrs) { super(context, attrs); @@ -56,6 +61,18 @@ public class HeaderView extends LinearLayout implements Preferences.Listener HabitsApplication app = (HabitsApplication) appContext; prefs = app.getComponent().getPreferences(); } + + if (context instanceof ListHabitsActivity) + { + ListHabitsActivity activity = (ListHabitsActivity) context; + midnightTimer = activity.getListHabitsComponent().getMidnightTimer(); + } + } + + @Override + public void atMidnight() + { + post(() -> createButtons()); } @Override @@ -75,11 +92,13 @@ public class HeaderView extends LinearLayout implements Preferences.Listener { super.onAttachedToWindow(); if (prefs != null) prefs.addListener(this); + if (midnightTimer != null) midnightTimer.addListener(this); } @Override protected void onDetachedFromWindow() { + if (midnightTimer != null) midnightTimer.removeListener(this); if (prefs != null) prefs.removeListener(this); super.onDetachedFromWindow(); } diff --git a/app/src/main/java/org/isoron/uhabits/utils/DateUtils.java b/app/src/main/java/org/isoron/uhabits/utils/DateUtils.java index c85f90df2..cce452145 100644 --- a/app/src/main/java/org/isoron/uhabits/utils/DateUtils.java +++ b/app/src/main/java/org/isoron/uhabits/utils/DateUtils.java @@ -187,6 +187,11 @@ public abstract class DateUtils return getStartOfDay(DateUtils.getLocalTime()); } + public static long millisecondsUntilTomorrow() + { + return getStartOfToday() + millisecondsInOneDay - getLocalTime(); + } + public static GregorianCalendar getStartOfTodayCalendar() { return getCalendar(getStartOfToday()); diff --git a/app/src/main/java/org/isoron/uhabits/utils/MidnightTimer.java b/app/src/main/java/org/isoron/uhabits/utils/MidnightTimer.java new file mode 100644 index 000000000..30021ae71 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/utils/MidnightTimer.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2016 Álinson Santos Xavier + * + * 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 . + */ + +package org.isoron.uhabits.utils; + +import org.isoron.uhabits.activities.*; + +import java.util.*; +import java.util.concurrent.*; + +import javax.inject.*; + +/** + * A class that emits events when a new day starts. + */ +@ActivityScope +public class MidnightTimer +{ + private final List listeners; + + private ScheduledExecutorService executor; + + @Inject + public MidnightTimer() + { + this.listeners = new LinkedList<>(); + } + + public synchronized void addListener(MidnightListener listener) + { + this.listeners.add(listener); + } + + public synchronized void onPause() + { + executor.shutdownNow(); + } + + public synchronized void onResume() + { + executor = Executors.newSingleThreadScheduledExecutor(); + executor.scheduleAtFixedRate(() -> notifyListeners(), + DateUtils.millisecondsUntilTomorrow() + 1000, + DateUtils.millisecondsInOneDay, TimeUnit.MILLISECONDS); + } + + public synchronized void removeListener(MidnightListener listener) + { + this.listeners.remove(listener); + } + + private synchronized void notifyListeners() + { + for (MidnightListener l : listeners) l.atMidnight(); + } + + public interface MidnightListener + { + void atMidnight(); + } +} diff --git a/app/src/main/res/values/pickers.xml b/app/src/main/res/values/pickers.xml index 354c3ae89..c83a58066 100644 --- a/app/src/main/res/values/pickers.xml +++ b/app/src/main/res/values/pickers.xml @@ -41,12 +41,12 @@ 14sp 6dip 4dip - 96dip + 80dip 48dip 48dip 24dip - 270dip - 270dp + 250dip + 250dp 30dp 155dp 270dp diff --git a/app/src/main/res/xml/file_paths.xml b/app/src/main/res/xml/file_paths.xml new file mode 100644 index 000000000..d97286ddf --- /dev/null +++ b/app/src/main/res/xml/file_paths.xml @@ -0,0 +1,27 @@ + + + + + + + + + + diff --git a/app/src/test/java/org/isoron/uhabits/activities/habits/list/ListHabitsScreenTest.java b/app/src/test/java/org/isoron/uhabits/activities/habits/list/ListHabitsScreenTest.java index 5c4b413ae..da6c04587 100644 --- a/app/src/test/java/org/isoron/uhabits/activities/habits/list/ListHabitsScreenTest.java +++ b/app/src/test/java/org/isoron/uhabits/activities/habits/list/ListHabitsScreenTest.java @@ -38,10 +38,11 @@ import org.junit.runners.*; import java.io.*; +import static org.isoron.uhabits.activities.habits.list.ListHabitsScreen.*; import static org.mockito.Matchers.any; import static org.mockito.Mockito.anyInt; -import static org.mockito.Mockito.eq; import static org.mockito.Mockito.*; +import static org.mockito.Mockito.eq; @RunWith(JUnit4.class) public class ListHabitsScreenTest extends BaseUnitTest @@ -124,28 +125,28 @@ public class ListHabitsScreenTest extends BaseUnitTest @Test public void testOnResult_bugReport() { - screen.onResult(0, ListHabitsScreen.RESULT_BUG_REPORT, null); + screen.onResult(REQUEST_SETTINGS, RESULT_BUG_REPORT, null); verify(controller).onSendBugReport(); } @Test public void testOnResult_exportCSV() { - screen.onResult(0, ListHabitsScreen.RESULT_EXPORT_CSV, null); + screen.onResult(REQUEST_SETTINGS, RESULT_EXPORT_CSV, null); verify(controller).onExportCSV(); } @Test public void testOnResult_exportDB() { - screen.onResult(0, ListHabitsScreen.RESULT_EXPORT_DB, null); + screen.onResult(REQUEST_SETTINGS, RESULT_EXPORT_DB, null); verify(controller).onExportDB(); } @Test public void testOnResult_importData() { - screen.onResult(0, ListHabitsScreen.RESULT_IMPORT_DATA, null); + screen.onResult(REQUEST_SETTINGS, RESULT_IMPORT_DATA, null); testShowImportScreen(); } diff --git a/circle.yml b/circle.yml index 243168196..f0e7ffa46 100644 --- a/circle.yml +++ b/circle.yml @@ -1,7 +1,9 @@ dependencies: pre: - echo y | android update sdk --no-ui --all --filter "tools" - - echo y | android update sdk --no-ui --all --filter "build-tools-23.0.3" + - echo y | android update sdk --no-ui --all --filter "android-25" + - echo y | android update sdk --no-ui --all --filter "build-tools-25.0.2" + - echo y | android update sdk --no-ui --all --filter "extra-android-m2repository" checkout: post: - git submodule sync @@ -24,4 +26,4 @@ general: - poeditor machine: java: - version: oraclejdk8 \ No newline at end of file + version: oraclejdk8