1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2024-12-24 01:25:50 +01:00

Merge branch 'master' into y5_patch_test

# Conflicts:
#	GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java
#	app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/jyou/TeclastH30/TeclastH30Coordinator.java
#	app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java
#	app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceSupportFactory.java
#	app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DeviceHelper.java
#	app/src/main/res/values/strings.xml
This commit is contained in:
Da Pa 2020-01-01 06:55:54 +02:00
commit 35dbbd6a31
809 changed files with 45351 additions and 11824 deletions

17
.gitattributes vendored Normal file
View File

@ -0,0 +1,17 @@
# Set the default behavior, in case people don't have core.autocrlf set.
* text=auto
# Explicitly declare text files you want to always be normalized and converted
# to native line endings on checkout.
*.java text
*.gradle text
*.md text
*.properties text
*.xml text
# Declare files that will always have CRLF line endings on checkout.
#*.sln text eol=crlf
# Denote all files that are truly binary and should not be modified.
#*.png binary
#*.jpg binary

View File

@ -1,10 +1,22 @@
#### Before opening an issue please confirm the following:
---
name: Bug report
about: Create a report to help us improve
---
#### Before reporting a bug, please confirm the following:
- [ ] I have read the [wiki](https://github.com/Freeyourgadget/Gadgetbridge/wiki), and I didn't find a solution to my problem / an answer to my question.
- [ ] I have searched the [issues](https://github.com/Freeyourgadget/Gadgetbridge/issues), and I didn't find a solution to my problem / an answer to my question.
- [ ] If you upload an image or other content, please make sure you have read and understood the [github policies and terms of services](https://help.github.com/articles/github-terms-of-service/#1-responsibility-for-user-generated-content)
### I got Gadgetbridge from:
* [ ] F-Droid
* [ ] I built it myself from source code (specify tag / commit)
If you got it from Google Play, please note [that version](https://github.com/TaaviE/Gadgetbridge) is unofficial and not supported here; it's also often quite outdated. Please switch to one of the above versions if you can.
#### Your issue is:
*In case of a bug, do not forget to attach logs!*
*If possible, please attach [logs](https://github.com/Freeyourgadget/Gadgetbridge/wiki/Log-Files)! that might help identifying the problem.*
#### Your wearable device is:

View File

@ -9,6 +9,12 @@ about: Create a report to help us improve
- [ ] I have searched the [issues](https://github.com/Freeyourgadget/Gadgetbridge/issues), and I didn't find a solution to my problem / an answer to my question.
- [ ] If you upload an image or other content, please make sure you have read and understood the [github policies and terms of services](https://help.github.com/articles/github-terms-of-service/#1-responsibility-for-user-generated-content)
### I got Gadgetbridge from:
* [ ] F-Droid
* [ ] I built it myself from source code (specify tag / commit)
If you got it from Google Play, please note [that version](https://github.com/TaaviE/Gadgetbridge) is unofficial and not supported here; it's also often quite outdated. Please switch to one of the above versions if you can.
#### Your issue is:
*If possible, please attach [logs](https://github.com/Freeyourgadget/Gadgetbridge/wiki/Log-Files)! that might help identifying the problem.*

3
.gitignore vendored
View File

@ -31,3 +31,6 @@ proguard/
MPChartLib
fw.dirs
**/.project
**/.settings
**/.classpath

View File

@ -0,0 +1,2 @@
connection.project.dir=
eclipse.preferences.version=1

View File

@ -1,3 +1,6 @@
os: linux
dist: trusty
language: android
jdk:
@ -16,10 +19,10 @@ android:
- tools
# The BuildTools version used by your project
- build-tools-27.0.3
- build-tools-28.0.3
# The SDK version used to compile your project
- android-27
- android-28
# Additional components
- extra-android-m2repository
@ -31,7 +34,7 @@ android:
#- sys-img-x86-android-17
before_install:
- yes | sdkmanager "platforms;android-27"
- yes | sdkmanager "platforms;android-28"
script:
- ./gradlew build connectedCheck --stacktrace

View File

@ -1,5 +1,206 @@
### Changelog
#### Version 0.40.1
* Mi Band/Amazfit: Recogize changes when toggling alarm on device (immediately when connected, else when connecting)
* Mi Band/Amazfit: Fix some bugs with stuck connection when re-connecting
* Mi Band 4: Support higher MTU for multiple times faster firmware transfer (probably also Amazfit GTR/GTS)
* Amazfit Cor: Fix setting language to Chinese manually
#### Version 0.40.0
* Fossil Q Hybrid: Initial support
* Bangle.js: Initial support
* Reserve Alarm for Calendar feature restricted to Mi Band 1/2 and moved to per-device settings
* New icon for App Manager
#### Version 0.39.1
* Try to actively re-connect when a connection gets interrupted (interval grows up to 64 seconds)
* Mi Band2/Amazfip Bip: Make button action settings per-device and enable for Amazfit Bip
#### Version 0.39.0
* Amazfit GTS: Initial and incomplete support, mostly untested
* Add forward/backward buttons to charts for faster navigation
* Debug: allow to reset last fetch date for Huami devices
#### Version 0.38.0
* Amazfit GTR: Initial and incomplete support, mostly untested
* Amazfit Bip: add Portuguese to the list of selectable languages
* Mi Band 4: Enable emoji font setting
* Makibes HR3: Support the english version
* Makibes HR3: Enable bluetooth pairing for working reconnection
* Work around crash when trying to display changelog
* Sleep detection settings: Rolling 24 hours (existing style) or Noon to noon
* Add alternative color to heartrate in chart settings
#### Version 0.37.1
* Amazfit Bip Lite: Support flashing firmware and watchfaces
#### Version 0.37.0
* Initial Makibes HR3 support
* Amazfit Bip Lite: Initial working support, firmware update is disabled for now (we do not have any firmware for testing)
* Amazfit Cor 2: Enable Emoji Font setting and 3rd party HR access
* Find Phone now also vibration in addition to playing the ring tone
* ID115: All settings are now per-device
* Time format settings are now per-device for all supported devices
* Wrist location settings are now per-device for all supported devices
* Work around broken layout in database management activity
* Show toast in case no app is installed which can handle GPX files
* Mi Band 4/Amazfit Bip Lite: Trim white spaces and new lines from auth key
* Mi Band 4/Amazfit Bip Lite: Display a toast and do not try to pair if there was no auth key supplied
* Skip service scan if supported device could be recognized without uuids during discovery
#### Version 0.36.2
* Amazfit Bip: Untested support for Lite variant
* Force Lineage OS to ask for permission when Trust is used to fix non-working incoming calls
* Charts: List multiple sleep sessions per day
#### Version 0.36.1
* Mi Band 2/3/4, Amazfit Bip/Cor: Add setting to expose the HR sensor to 3rd party apps
* Mi Band 4: Really fix weather location not being updated on the Band
* Mi Band 4: Fix call notifcation not stopping when call gets answered or rejected on the phone
* Amazfit Bip/Cor: Support for custom emoji font
* ZeTime: Enable emoji support
* ZeTime: Make watch language the same as the phone language by default
* New status and alarms widget
* Fix crash when entering notification filter settings
* Make diagram settings accessible from charts activity
* Add option to hide the floating plus button in the main activity
* Fix a potential crash on Android 4.4 KitKat
#### Version 0.36.0
* Initial Mijia LYWSD02 support (Smart Clock with Humidity and Temperature Sensor), just for setting the time
* Mi Band 3/4: Allow enabling the NFC menu where supported (useless for now)
* Mi Band 3/4, Amazfit Cor/Bip: Set language immediately when changing it (not only on connect)
* Mi Band 3/4, Amazfir Cor/Bip: Add icons for "swimming" and "exercise"
* Mi Band 4: Support flashing the V2 font
* Mi Band 4: Fix weather location not being updated on the Band
* Mi Band 4: remove unsupported DND setting from settings menu
* Amazfit Bip/Cor: Fix resetting of last fetched date for sports activities
* Amazfit Bip: Fix sharing GPX files for some Apps
* Pebble: Use Rebble Store URI
* Support LineageOS 16.0 weather provider
* Add Averages to Charts
* Allow togging between weekly and monthly charts
#### Version 0.35.2
* Mi Band 1/2: Crash when updating firmware while phone is set to Spanish
* Mi Band 4: Enable music info support (displays now on the band)
* Mi Band 4: Support setting date format (for built-in watchfaces)
* Amazfit Cor 2: Try to fix empty menu on device
#### Version 0.35.1
* Mi Band 4: Support flashing watchfaces, res and firmware (.ft untested)
#### Version 0.35.0
* Mi Band 4: Initial support (WARNING: INITIAL SETUP NEEDS MI FIT WITH ACCOUNT AND ROOT, NOT A RECOMMENDED DEVICE FOR GADGETBRIDGE)
#### Version 0.34.1
* Mi Band 1: Fix crash when entering per-device settings
* Mi Band 3: Allow setting date format in per-device settings
* ZeTime: Fix timestmaps
* Fix a crash when flashing an non-whitelisted firmware while using Gadgetbridge in Spanish
#### Version 0.34.0
* Mi Band 1/2/3/Bip/Cor: Migrate many settings to per-device settings (new settings icon in device card in main activity)
* Mi Band 3: Fix setting menu items with 2.4 firmware and add support for the new timer menu
* Amazfit Bip/Cor, Casio: Add support for muting incoming calls
* ZeTime: Remove endless recursion in ZeTime settings
* Recognize FairEmail notifications as generic email notifications
#### Version 0.33.1
* Mi Band 3: Recognize "Xiaomi Band 3"
* Amazfit Bip: Add German, Italian, French and Turkish to language settings
#### Version 0.33.0
* BFH-16: Initial support
* Mi Band 2/3/Bip/Cor: Generate random per-device security keys when pairing, allow manual override to still support multiple android devices connecting to the same device
* Mi Band 3: Add Indonesian, Thai, Arabic, Vietnamese, Portuguese, Dutch, Turkish and Ukrainian to language settings
* Mi Band 3: Support flashing latest Japanese-Korean font
* Amazfit Cor 2: Initial experimental support (untested)
* Pebble: Add pebblekit extension for reopening last app
* Casio: Bugfixes and improvements
* Lookup contacts also in work profile
* Fix searching in application name when blacklisting
* Remove misleading title from database management activity when no legacy database is available
#### Version 0.32.4
* Make voip call support optional (disabled by default)
* Amazfit Bip: GPX export corrections
* ZeTime: Fix setting alarms
* ZeTime: Fix wrong activity timestamps
* ZeTime: Set HR alarm limits when changed, not only on connect
* ZeTime: Sync preferences from the watch to Gadgetbridge settings
#### Version 0.32.3
* Fix a crash in charts due to a broken German translation
* Fix a crash when transliterating emoji
* Amazfit Bip/Cor: Support disconnect notification (must be configured in Bip settings for Cor also for now)
#### Version 0.32.2
* Fix setting alarms under some circumstances
* Support calls notifications for some VoIP apps
* Mi Band 3: Enable fetching sports activities (currently only useful for flushing activities)
* Casio: Improve stability
* Casio: Add explicit support for GB-6900B, GB-X6900B and GB-5600B
#### Version 0.32.1
* Fix db deadlock on alarm migration
#### Version 0.32.0
* Initial support for Casio GB-6900B
* Increase number of alarms and store them per-device
* Support factory reset in debug activity (Mi Band 1/2/3, Bip, Cor)
* Filter out unicode control sequences (fixes problems with Telegram and probably others)
* Fix endless loop resulting in OOM when RTL support is enabled
* Recoginize p≡p as an email app
* No longer display Android paired devices in that were not a paired with Gadgetbridge
* Amazfit Bip: Allow flashing latest GPS firmware
* Pebble: Native support for M7S watch face
* No1 F1: Support for a Chinese clone
#### Version 0.31.3
* Pebble: Fix crash with DISMISS and OPEN actions
#### Version 0.31.2
* Pebble: Fix a regression that caused non-working mute, open and dismiss actions
* Fix setting language to Czech manually
* Ignore summary notification from K-9 Mail (caused notification spamming)
#### Version 0.31.1
* Pebble: Fix crash when no canned replies have been set
* Pebble: Let the firmware show localized default canned replies if none have been set
* Amazfit Bip: Fix importing GPS tracks that have been recorded with Firmware 1.1.5.02
* Display measured hr value in debug screen
#### Version 0.31.0
* Pebble: Send all wearable notification actions (not only reply)
* Pebble: Always allow reply action even if untested features are turned off
* Pebble: Temporarily disable broken autoremove notification feature
* Amazfit Bip: Allow flashing latest gps firmware (Mili_dth.gps)
* Mi Band 3/Amazfit Bip/Amazfit Cor: Send Fahrenheit if units are set to imperial
* Roidmi 3: Fix and enable support
* Mi Band 3/Amazfit Bip: fix find phone crash
* Prevent re-sending old notifications to the wearable
* Enhancement and Fixes for Bengali Transliteration
* Disable excessive logging in RTL support
#### Version 0.30.0
* Amazfit Bip + Mi Band 3: Support for right to left display (configurable) (#976)
* Add Arabic, Bengali Farsi, Persian, Scandinavian transliteration
* Add support for some Roidmi FM receivers
* Mi Band 3: Allow enabling the "Workout" menu item
* Mi Band 3: Support for night mode configuration
* Huami devices: fix seldom activity/sports synchronization problem (#1264)
* Preferences: Make minimum heart rate configurable (lower values will be disregarded)
* Preferences: Configure minimum time between notifications
* Preferences: Group language settings
* Attempt to fix BLE connection issues on Samsung S devices
* Week sleep and steps charts: display balance (actual value vs. desired value)
* Live Activity: show current/maximum heart rate, display minute steps and total steps and more improvements
* Live Activity: fix discrepancy between number of steps in Gadgetbridge and wearable device
* Fix missing caller ID for incoming calls on Android 9
* Support for easy sharing of log files via the Debug screen
* Misc small bugfixes
#### Version 0.29.1
* Mi Band 3: Support setting language to to German, Italian, French, Polish, Japanese, Korean (read wiki)
* Mi Band 3: Support flashing latest RES files

View File

@ -27,68 +27,90 @@
* Daniele Gobbetti <daniele+github@gobbetti.name>
* João Paulo Barraca <jpbarraca@gmail.com>
* Yaron Shahrabani <sh.yaron@gmail.com>
* Jonas <jonasdcdm@posteo.net>
* Allan Nordhøy <epost@anotheragency.no>
* postsorino <postsorino@krutt.org>
* Jonas <jonasdcdm@posteo.net>
* Roi Greenberg <roigreenberg@gmail.com>
* Sebastian Kranz <tklightforce@googlemail.com>
* Vadim Kaushan <admin@disasm.info>
* Allan Nordhøy <epost@anotheragency.no>
* protomors <protomors@gmail.com>
* José Rebelo <joserebelo@outlook.com>
* mesnevi <shams@airpost.net>
* naofum <naofum@gmail.com>
* youzhiran <2668760098@qq.com>
* TaaviE <taavi.eomae+weblate@gmail.com>
* mueller-ma <mueller-ma@users.noreply.github.com>
* ivanovlev <ivanovlev@mail.ru>
* naofum <naofum@gmail.com>
* youzhiran <2668760098@qq.com>
* Tijl Schepens <tijl.schepens@hotmail.com>
* mesnevi <shams@airpost.net>
* Hadrián Candela <hadrian.candela@gmail.com>
* Julien Pivotto <roidelapluie@inuits.eu>
* Andreas Böhler <dev@aboehler.at>
* 陈少举 <oshirisu.red@gmail.com>
* Taavi Eomäe <taavi.eomae+github@gmail.com>
* Steffen Liebergeld <perl@gmx.org>
* Lem Dulfo <lemuel.dulfo@gmail.com>
* Hadrián Candela <hadrian.candela@gmail.com>
* Felix Konstantin Maurer <maufl@maufl.de>
* Sergey Trofimov <sarg@sarg.org.ru>
* Robert Barat <rbarat07@gmail.com>
* Pavel Elagin <pelagin@techcd.ru>
* Lem Dulfo <lemuel.dulfo@gmail.com>
* Matthieu Baerts <matttbe@gmail.com>
* Felix Konstantin Maurer <maufl@maufl.de>
* Utsob Roy <uroybd@gmail.com>
* Sergey Trofimov <sarg@sarg.org.ru>
* Full Name <petr+weblate@linuks.cz>
* Robert Barat <rbarat07@gmail.com>
* JohnnySun <bmy001@gmail.com>
* Uwe Hermann <uwe@hermann-uwe.de>
* Kranz <Kranz>
* Gilles Émilien MOREL <contact@gilles-morel.fr>
* Edoardo Rosa <edoardo.rosa90@gmail.com>
* Bożydar <trening302@o2.pl>
* Alberto <albertsal83@gmail.com>
* Vladislav Serkov <vladserkoff@protonmail.com>
* Vebryn <vebryn@gmail.com>
* Gilles Émilien MOREL <contact@gilles-morel.fr>
* M. Hadi <hhhadddi@yahoo.com>
* Giuseppe Caliendo <giuseppe.caliendo@gmail.com>
* Gergely Peidl <gergely@peidl.net>
* Emre <wenigerpluesch@mailbox.org>
* Bożydar <trening302@o2.pl>
* Elwood <elwood21@gmail.com>
* AndrewBedscastle <1462953+AndrewBedscastle@users.noreply.github.com>
* abettenburg <a.bettenburg@gmail.com>
* 0nse <0nse@users.noreply.github.com>
* Максим Якимчук <xpinovo@gmail.com>
* Rimas Raguliūnas <rarimas@gmail.com>
* nautilusx <mail.ka@mailbox.org>
* Minori Hiraoka (미노리) <minori@mnetwork.co.kr>
* masakoodaa <masakoodaa@protonmail.com>
* Marius Cornescu <marius_cornescu@yahoo.com>
* Lukas Veneziano <fs@venezilu.de>
* LL <lu.lecocq@free.fr>
* Kompact <joaorafael123@hotmail.com>
* K0L0B0G <github@gorobav.ru>
* Johann C. Rode <jcrode@ece.ucsb.edu>
* Jasper <jespiex456@hotmail.com>
* Dikay900 <dark900@xyz.de>
* Christian Fischer <sw-dev@computerlyrik.de>
* c4ndel4 <hadrian.candela@gmail.com>
* 6arms1leg <m.brnsfld@googlemail.com>
* Zhong Jianxin <azuwis@gmail.com>
* walkjivefly <mark@walkjivefly.com>
* WaldiS <admin@sto.ugu.pl>
* Thomas <tutonis@gmail.com>
* Ted Stein <me@tedstein.net>
* ssantos <ssantos@web.de>
* Sebastian Obrusiewicz <sobrus2@o2.pl>
* Ranved Sticon <the7bulk@gmail.com>
* petronovak <petro.novak@gmail.com>
* Petr Kadlec <mormegil@centrum.cz>
* Pascal <pascal.tannich@gmail.com>
* NotAFIle <nota@notafile.com>
* Normano64 <per.bergqwist@gmail.com>
* NicoBuntu <nicolas__du95@hotmail.fr>
* Minori Hiraoka (미노리) <minori@mnetwork.co.kr>
* Moarc <aldwulf@gmail.com>
* Michal Novotny <mignov@gmail.com>
* Martin <ritualz@users.noreply.github.com>
* LL <lu.lecocq@free.fr>
* Louis-Marie Croisez <louis.croisez@gmail.com>
* Jesús <zaagur@gmail.com>
* Irul <wedesignthing@gmail.com>
* HenRy <helge1o1o1@gmail.com>
* exit-failure <hakrala@web.de>
* Dreamwalker <aristojeff@gmail.com>
* Denis <korden@sky-play.ru>
* Avamander <Avamander@users.noreply.github.com>
* AnthonyDiGirolamo <anthony.digirolamo@gmail.com>
@ -98,26 +120,32 @@
* Yar <yaroslav.isakov@gmail.com>
* xzovy <caleb@caleb-cooper.net>
* xphnx <xphnx@users.noreply.github.com>
* Xavier RENE-CORAIL <xavier.renecorail@gmail.com>
* Vitaliy Shuruta <vshuruta@gmail.com>
* Vincèn PUJOL <vincen@vincen.org>
* veecue <veecue@ventos.tk>
* Tomer Rosenfeld <tomerosenfeld007@gmail.com>
* Tomas Radej <tradej@redhat.com>
* tiparega <11555126+tiparega@users.noreply.github.com>
* Tarik Sekmen <tarik@ilixi.org>
* Szymon Tomasz Stefanek <s.stefanek@gmail.com>
* szilardx <15869670+szilardx@users.noreply.github.com>
* Stan Gomin <stan@gomin.me>
* SinMan <emilio.galvan@gmail.com>
* Sergio Lopez <slp@sinrega.org>
* S Dantas <dantasosteney@gmail.com>
* Sami Alaoui <4ndroidgeek@gmail.com>
* Roman Plevka <rplevka@redhat.com>
* rober <rober@prtl.nodomain.net>
* redking <redking974@gmail.com>
* Quallenauge <Hamsi2k@freenet.de>
* Pavel Motyrev <legioner.r@gmail.com>
* Pavel <elagin.pasha@gmail.com>
* Olexandr Nesterenko <olexn@ukr.net>
* Nicolò Balzarotti <anothersms@gmail.com>
* Natanael Arndt <arndtn@gmail.com>
* Nabil BENDAFI <nabil@bendafi.fr>
* Molnár Barnabás <nsd4rkn3ss@gmail.com>
* Moarc <aldwulf@gmail.com>
* Mike van Rossum <mike@vanrossum.net>
* Michal Novak <michal.novak@post.cz>
* michaelneu <git@michaeln.eu>
@ -142,9 +170,14 @@
* kalaee <alex.kalaee@gmail.com>
* Joseph Kim <official.jkim@gmail.com>
* jonnsoft <>
* Johannes Tysiak <vinyl@users.sf.net>
* jcrode <46062294+jcrode@users.noreply.github.com>
* Jan Lolek <janlolek@seznam.cz>
* Jakub Jelínek <jakub.jelinek@gmail.com>
* Ivan <ivan_tizhanin@mail.ru>
* Hüseyin Aslan <ha098784@gmail.com>
* hr-sales <hericsonregis@hotmail.com>
* Hirnchirurg <anonymous11@posteo.net>
* Hasan Ammar <ammarh@gmail.com>
* Grzegorz Dznsk <grantmlody96@gmail.com>
* Gilles MOREL <contact@gilles-morel.fr>
@ -152,7 +185,8 @@
* Gabe Schrecker <gabe@pbrb.co.uk>
* freezed-or-frozen <freezed.or.frozen@gmail.com>
* Frank Slezak <KazWolfe@users.noreply.github.com>
* Dreamwalker <aristojeff@gmail.com>
* Francesco Franchina <cescus92@gmail.com>
* Edoardo Tronconi <edoardo.tronconi@gmail.com>
* Dougal19 <4662351+Dougal19@users.noreply.github.com>
* Davis Mosenkovs <davikovs@gmail.com>
* Daniel Hauck <maill@dhauck.eu>
@ -164,14 +198,18 @@
* Carlos Ferreira <calbertoferreira@gmail.com>
* bucala <marcel.bucala@gmail.com>
* boun <boun@gmx.de>
* Benjamin Kahlau <nyhkkbjyek@roanapur.de>
* batataspt@gmail.com <batataspt@gmail.com>
* atkyritsis <at.kyritsis@gmail.com>
* apre <adrienpre+github@gmail.com>
* Aniruddha Adhikary <aniruddha@adhikary.net>
* andrewlytvyn <indusfreelancer@gmail.com>
* AndrewH <36428679+andrewheadricke@users.noreply.github.com>
* andre <andre.buesgen@yahoo.de>
* Allen B <28495335+Allen-B1@users.noreply.github.com>
* Alfeu Lucas Guedes dos Santos <alfeugds@gmail.com>
* Alexey Afanasev <avafanasiev@gmail.com>
* Alexandra Sevostyanova <asevostyanova@gmail.com>
And all the Transifex translators, which I cannot automatically list, at the moment.

View File

@ -1,5 +1,5 @@
apply plugin: 'java'
//apply plugin: 'maven'
apply plugin: 'maven'
apply plugin:'application'
archivesBaseName = 'gadgetbridge-daogenerator'
@ -8,7 +8,7 @@ archivesBaseName = 'gadgetbridge-daogenerator'
dependencies {
// compile 'org.greenrobot:greendao-generator:2.2.0'
// compile project(":DaoGenerator")
compile 'com.github.freeyourgadget:greendao:1998d7cd2d21f662c6044f6ccf3b3a251bbad341'
compile 'com.github.Freeyourgadget:greendao:1998d7cd2d21f662c6044f6ccf3b3a251bbad341'
}
sourceSets {

View File

@ -15,8 +15,6 @@
*/
package nodomain.freeyourgadget.gadgetbridge.daogen;
import java.util.Date;
import de.greenrobot.daogenerator.DaoGenerator;
import de.greenrobot.daogenerator.Entity;
import de.greenrobot.daogenerator.Index;
@ -45,7 +43,7 @@ public class GBDaoGenerator {
public static void main(String[] args) throws Exception {
Schema schema = new Schema(19, MAIN_PACKAGE + ".entities");
Schema schema = new Schema(22, MAIN_PACKAGE + ".entities");
Entity userAttributes = addUserAttributes(schema);
Entity user = addUserInfo(schema, userAttributes);
@ -60,6 +58,7 @@ public class GBDaoGenerator {
Entity tag = addTag(schema);
Entity userDefinedActivityOverlay = addActivityDescription(schema, tag, user);
addMakibesHR3ActivitySample(schema, user, device);
addMiBandActivitySample(schema, user, device);
addPebbleHealthActivitySample(schema, user, device);
addPebbleHealthActivityKindOverlay(schema, user, device);
@ -73,6 +72,11 @@ public class GBDaoGenerator {
addID115ActivitySample(schema, user, device);
addJYouActivitySample(schema, user, device);
addCalendarSyncState(schema, device);
addAlarms(schema, user, device);
Entity notificationFilter = addNotificationFilters(schema);
addNotificationFilterEntry(schema, notificationFilter);
addBipActivitySummary(schema, user, device);
@ -181,6 +185,16 @@ public class GBDaoGenerator {
return deviceAttributes;
}
private static Entity addMakibesHR3ActivitySample(Schema schema, Entity user, Entity device) {
Entity activitySample = addEntity(schema, "MakibesHR3ActivitySample");
activitySample.implementsSerializable();
addCommonActivitySampleProperties("AbstractActivitySample", activitySample, user, device);
activitySample.addIntProperty(SAMPLE_STEPS).notNull().codeBeforeGetterAndSetter(OVERRIDE);
activitySample.addIntProperty(SAMPLE_RAW_KIND).notNull().codeBeforeGetterAndSetter(OVERRIDE);
addHeartRateProperties(activitySample);
return activitySample;
}
private static Entity addMiBandActivitySample(Schema schema, Entity user, Entity device) {
Entity activitySample = addEntity(schema, "MiBandActivitySample");
activitySample.implementsSerializable();
@ -355,6 +369,55 @@ public class GBDaoGenerator {
calendarSyncState.addIntProperty("hash").notNull();
}
private static void addAlarms(Schema schema, Entity user, Entity device) {
Entity alarm = addEntity(schema, "Alarm");
alarm.implementsInterface("nodomain.freeyourgadget.gadgetbridge.model.Alarm");
Property deviceId = alarm.addLongProperty("deviceId").notNull().getProperty();
Property userId = alarm.addLongProperty("userId").notNull().getProperty();
Property position = alarm.addIntProperty("position").notNull().getProperty();
Index indexUnique = new Index();
indexUnique.addProperty(deviceId);
indexUnique.addProperty(userId);
indexUnique.addProperty(position);
indexUnique.makeUnique();
alarm.addIndex(indexUnique);
alarm.addBooleanProperty("enabled").notNull();
alarm.addBooleanProperty("smartWakeup").notNull();
alarm.addIntProperty("repetition").notNull().codeBeforeGetter(
"public boolean isRepetitive() { return getRepetition() != ALARM_ONCE; } " +
"public boolean getRepetition(int dow) { return (this.repetition & dow) > 0; }"
);
alarm.addIntProperty("hour").notNull();
alarm.addIntProperty("minute").notNull();
alarm.addBooleanProperty("unused").notNull();
alarm.addToOne(user, userId);
alarm.addToOne(device, deviceId);
}
private static void addNotificationFilterEntry(Schema schema, Entity notificationFilterEntity) {
Entity notificatonFilterEntry = addEntity(schema, "NotificationFilterEntry");
notificatonFilterEntry.addIdProperty().autoincrement();
Property notificationFilterId = notificatonFilterEntry.addLongProperty("notificationFilterId").notNull().getProperty();
notificatonFilterEntry.addStringProperty("notificationFilterContent").notNull().getProperty();
notificatonFilterEntry.addToOne(notificationFilterEntity, notificationFilterId);
}
private static Entity addNotificationFilters(Schema schema) {
Entity notificatonFilter = addEntity(schema, "NotificationFilter");
Property appIdentifier = notificatonFilter.addStringProperty("appIdentifier").notNull().getProperty();
notificatonFilter.addIdProperty().autoincrement();
Index indexUnique = new Index();
indexUnique.addProperty(appIdentifier);
indexUnique.makeUnique();
notificatonFilter.addIndex(indexUnique);
Property notificationFilterMode = notificatonFilter.addIntProperty("notificationFilterMode").notNull().getProperty();
Property notificationFilterSubMode = notificatonFilter.addIntProperty("notificationFilterSubMode").notNull().getProperty();
return notificatonFilter;
}
private static void addBipActivitySummary(Schema schema, Entity user, Entity device) {
Entity summary = addEntity(schema, "BaseActivitySummary");
summary.implementsInterface(ACTIVITY_SUMMARY);

103
README.md
View File

@ -1,3 +1,5 @@
Gadgetbridge is now hosted on [codeberg.org](https://codeberg.org/Freeyourgadget/Gadgetbridge/)
Gadgetbridge
============
@ -15,75 +17,48 @@ vendor's servers.
[![Build](https://travis-ci.org/Freeyourgadget/Gadgetbridge.svg?branch=master)](https://travis-ci.org/Freeyourgadget/Gadgetbridge)
[![Code Quality: Java](https://img.shields.io/lgtm/grade/java/g/Freeyourgadget/Gadgetbridge.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/Freeyourgadget/Gadgetbridge/context:java)
[![Total Alerts](https://img.shields.io/lgtm/alerts/g/Freeyourgadget/Gadgetbridge.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/Freeyourgadget/Gadgetbridge/alerts)
[![Translate](https://hosted.weblate.org/widgets/freeyourgadget/-/gadgetbridge/svg-badge.svg)](https://hosted.weblate.org/projects/freeyourgadget/gadgetbridge)
## Download
[<img src="https://f-droid.org/badge/get-it-on.png" alt="Get it on F-Droid" height="80">](https://f-droid.org/app/nodomain.freeyourgadget.gadgetbridge)
[List of changes](https://github.com/Freeyourgadget/Gadgetbridge/blob/master/CHANGELOG.md)
[List of changes](https://codeberg.org/Freeyourgadget/Gadgetbridge/src/master/CHANGELOG.md)
## Supported Devices
* Amazfit Bip [Wiki](https://github.com/Freeyourgadget/Gadgetbridge/wiki/Amazfit-Bip)
* Amazfit Cor (no maintainer) [Wiki](https://github.com/Freeyourgadget/Gadgetbridge/wiki/Amazfit-Cor)
* HPlus Devices (e.g. ZeBand) [Wiki](https://github.com/Freeyourgadget/Gadgetbridge/wiki/HPlus)
* ID115 (WIP)
* Lenovo Watch 9 (WIP)
* Liveview (WIP)
* Mi Band, Mi Band 1A, Mi Band 1S [Wiki](https://github.com/Freeyourgadget/Gadgetbridge/wiki/Mi-Band)
* Mi Band 2 [Wiki](https://github.com/Freeyourgadget/Gadgetbridge/wiki/Mi-Band-2)
* Mi Band 3 [Wiki](https://github.com/Freeyourgadget/Gadgetbridge/wiki/Mi-Band-3)
* NO.1 F1 (WIP)
* Pebble, Pebble Steel, Pebble Time, Pebble Time Steel, Pebble Time Round [Wiki](https://github.com/Freeyourgadget/Gadgetbridge/wiki/Pebble)
* Pebble 2 [Wiki](https://github.com/Freeyourgadget/Gadgetbridge/wiki/Pebble)
* Teclast H10, H30 (WIP)
## Supported Devices (Some of them WIP and some of them without maintainer)
* Amazfit Bip [Wiki](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Amazfit-Bip)
* Amazfit Bip Lite (NOT RECOMMENDED, NEEDS MI FIT WITH ACCOUNT ONCE) [Wiki](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Amazfit-Bip-Lite)
* Amazfit Cor [Wiki](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Amazfit-Cor)
* Amazfit Cor 2 [Wiki](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Amazfit-Cor-2)
* Amazfit GTR (NOT RECOMMENDED, NEEDS MI FIT WITH ACCOUNT ONCE) [Wiki](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Amazfit-GTR)
* Amazfit GTS (NOT RECOMMENDED, NEEDS MI FIT WITH ACCOUNT ONCE) [Wiki](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Amazfit-GTS)
* BFH-16
* Casio GB-6900B
* Fossil Q Hybrid
* HPlus Devices (e.g. ZeBand) [Wiki](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/HPlus)
* ID115
* Lenovo Watch 9
* Liveview
* Makibes HR3
* Mi Band, Mi Band 1A, Mi Band 1S [Wiki](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Mi-Band)
* Mi Band 2 [Wiki](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Mi-Band-2)
* Mi Band 3 [Wiki](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Mi-Band-3)
* Mi Band 4 (NOT RECOMMENDED, NEEDS MI FIT WITH ACCOUNT AND ROOT ONCE) [Wiki](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Mi-Band-4)
* Mi Scale 2 (currently only displays a toast after stepping on the scale)
* NO.1 F1
* Pebble, Pebble Steel, Pebble Time, Pebble Time Steel, Pebble Time Round [Wiki](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Pebble)
* Pebble 2 [Wiki](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Pebble)
* Teclast H10, H30
* XWatch (Affordable Chinese Casio-like smartwatches)
* Vibratissimo (experimental)
* ZeTime (WIP)
* ZeTime [Wiki](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/MyKronoz-ZeTime)
## Features
Please see [FEATURES.md](https://github.com/Freeyourgadget/Gadgetbridge/blob/master/FEATURES.md)
## Getting Started (Pebble)
1. Pair your Pebble through the Android's Bluetooth Settings or Gadgetbridge. Pebble 2 MUST be paired though Gadgetbridge (tap on the + in Control Center)
2. Start Gadgetbridge, tap on the device you want to connect to
3. To test, choose "Debug" from the menu and play around
For more information read [this wiki article](https://github.com/Freeyourgadget/Gadgetbridge/wiki/Pebble-Getting-Started)
## How to use (Mi Band 1+2)
* When starting Gadgetbridge the first time, it will automatically
attempt to discover and pair your Mi Band. Alternatively you can invoke discovery
manually via the "+" button. It will ask you for some personal info that appears
to be needed for proper steps calculation on the band. If you do not provide these,
some hardcoded default "dummy" values will be used instead.
When your Mi Band starts to vibrate and blink during the pairing process,
tap it quickly a few times in a row to confirm the pairing with the band.
1. Configure other notifications as desired
2. Go back to the "Gadgetbridge" activity
3. Tap the Mi Band item to connect if you're not connected yet
4. To test, chose "Debug" from the menu and play around
**Known Issues:**
* The initial connection to a Mi Band sometimes takes a little patience. Try to connect a few times, wait,
and try connecting again. This only happens until you have "bonded" with the Mi Band, i.e. until it
knows your MAC address. This behavior may also only occur with older firmware versions.
* If you use other apps like Mi Fit, and "bonding" with Gadgetbridge does not work, please
try to unpair the band in the other app and try again with Gadgetbridge.
* While all Mi Band devices are supported, some firmware versions might work better than others.
You can consult the [projects wiki pages](https://github.com/Freeyourgadget/Gadgetbridge/wiki/Mi-Band)
to check if your firmware version is fully supported or if an upgrade/downgrade might be beneficial.
* In order to display text notifications on the Mi Band 2, you have to [install a font on the band](https://github.com/Freeyourgadget/Gadgetbridge/wiki/Mi-Band-2).
## Features (Liveview)
* set time (automatically upon connection)
* display notifications and vibrate
Please see [FEATURES.md](https://codeberg.org/Freeyourgadget/Gadgetbridge/src/master/FEATURES.md)
## Authors
### Core Team (in order of first code contribution)
@ -93,7 +68,6 @@ For more information read [this wiki article](https://github.com/Freeyourgadget/
* Daniele Gobbetti
### Additional device support
* João Paulo Barraca (HPlus)
* Vitaly Svyastyn (NO.1 F1)
* Sami Alaoui (Teclast H30)
@ -101,11 +75,17 @@ For more information read [this wiki article](https://github.com/Freeyourgadget/
* Sebastian Kranz (ZeTime)
* Vadim Kaushan (ID115)
* "maxirnilian" (Lenovo Watch 9)
* Andreas Böhler (Casio GB-6900B)
* Jean-François Greffier (Mi Scale 2)
* Johannes Schmitt (BFH-16)
* Lukas Schwichtenberg (Makibes HR3)
* Daniel Dakhno (Fossil Q Hybrid)
* Gordon Williams (Bangle.js)
## Contribute
Contributions are welcome, be it feedback, bug reports, documentation, translation, research or code. Feel free to work
on any of the open [issues](https://github.com/Freeyourgadget/Gadgetbridge/issues?q=is%3Aopen+is%3Aissue);
on any of the open [issues](https://codeberg.org/Freeyourgadget/Gadgetbridge/issues);
just leave a comment that you're working on one to avoid duplicated work.
Translations can be contributed via https://hosted.weblate.org/projects/freeyourgadget/gadgetbridge/
@ -116,6 +96,7 @@ Feel free to open an issue on our issue tracker, but please:
- do not use the issue tracker as a forum, do not ask for ETAs and read the issue conversation before posting
- use the search functionality to ensure that your question wasn't already answered. Don't forget to check the **closed** issues as well!
- remember that this is a community project, people are contributing in their free time because they like doing so: don't take the fun away! Be kind and constructive.
- Do not ask for help regarding your own projects, unless they are Gadgetbridge related
## Having problems?
@ -123,7 +104,7 @@ Feel free to open an issue on our issue tracker, but please:
1. Open Gadgetbridge's settings and check the option to write log files
2. Reproduce the problem you encountered
3. Check the logfile at /sdcard/Android/data/nodomain.freeyourgadget.gadgetbridge/files/gadgetbridge.log
4. File an issue at https://github.com/Freeyourgadget/Gadgetbridge/issues/new and possibly provide the logfile
4. File an issue at https://codeberg.org/Freeyourgadget/Gadgetbridge/issues/new and possibly provide the logfile
Alternatively you may use the standard logcat functionality to access the log.

View File

@ -1,5 +1,5 @@
apply plugin: "com.android.application"
apply plugin: "findbugs"
apply plugin: "com.github.spotbugs"
apply plugin: "pmd"
def ABORT_ON_CHECK_FAILURE = false
@ -16,8 +16,8 @@ android {
sourceCompatibility JavaVersion.VERSION_1_7
targetCompatibility JavaVersion.VERSION_1_7
}
compileSdkVersion 27
buildToolsVersion "27.0.3"
compileSdkVersion 28
buildToolsVersion '28.0.3'
defaultConfig {
applicationId "nodomain.freeyourgadget.gadgetbridge"
@ -25,8 +25,8 @@ android {
targetSdkVersion 27
// Note: always bump BOTH versionCode and versionName!
versionName "0.29.1"
versionCode 137
versionName "0.40.1"
versionCode 164
vectorDrawables.useSupportLibrary = true
}
buildTypes {
@ -60,33 +60,38 @@ pmd {
dependencies {
// testImplementation "ch.qos.logback:logback-classic:1.1.3"
// testImplementation "ch.qos.logback:logback-core:1.1.3"
implementation 'com.android.support.constraint:constraint-layout:1.1.3'
testImplementation "junit:junit:4.12"
testImplementation "org.mockito:mockito-core:1.10.19"
testImplementation "org.robolectric:robolectric:3.6.1"
testImplementation "org.robolectric:robolectric:4.2.1"
testImplementation "com.google.code.gson:gson:2.8.5"
implementation fileTree(dir: "libs", include: ["*.jar"])
implementation "com.android.support:appcompat-v7:27.1.1"
implementation "com.android.support:cardview-v7:27.1.1"
implementation "com.android.support:recyclerview-v7:27.1.1"
implementation "com.android.support:support-v4:27.1.1"
implementation "com.android.support:gridlayout-v7:27.1.1"
implementation "com.android.support:design:27.1.1"
implementation "com.android.support:palette-v7:27.1.1"
implementation "androidx.appcompat:appcompat:1.1.0"
implementation "androidx.preference:preference:1.1.0"
implementation "androidx.cardview:cardview:1.0.0"
implementation "androidx.recyclerview:recyclerview:1.1.0"
implementation "androidx.legacy:legacy-support-v4:1.0.0"
implementation "androidx.gridlayout:gridlayout:1.0.0"
implementation "com.google.android.material:material:1.0.0"
implementation "androidx.palette:palette:1.0.0"
implementation("com.github.tony19:logback-android-classic:1.1.1-6") {
exclude group: "com.google.android", module: "android"
}
implementation "org.slf4j:slf4j-api:1.7.12"
implementation "com.github.Freeyourgadget:MPAndroidChart:5e5bd6c1d3e95c515d4853647ae554e48ee1d593"
implementation "com.github.PhilJay:MPAndroidChart:v3.1.0"
implementation "com.github.pfichtner:durationformatter:0.1.1"
implementation "de.cketti.library.changelog:ckchangelog:1.2.2"
implementation "net.e175.klaus:solarpositioning:0.0.9"
// use pristine greendao instead of our custom version, since our custom jitpack-packaged
// version contains way too much and our custom patches are in the generator only.
implementation "org.greenrobot:greendao:2.2.1"
implementation "org.apache.commons:commons-lang3:3.5"
implementation "org.apache.commons:commons-lang3:3.7"
implementation "org.cyanogenmod:platform.sdk:6.0"
implementation 'com.jaredrummler:colorpicker:1.0.2'
// implementation project(":DaoCore")
implementation 'com.github.wax911:android-emojify:0.1.7'
}
preBuild.dependsOn(":GBDaoGenerator:genSources")
@ -95,7 +100,7 @@ gradle.beforeProject {
preBuild.dependsOn(":GBDaoGenerator:genSources")
}
check.dependsOn "findbugs", "pmd", "lint"
check.dependsOn "spotbugsMain", "pmd", "lint"
task pmd(type: Pmd) {
ruleSetFiles = files("${project.rootDir}/config/pmd/pmd-ruleset.xml")
@ -130,30 +135,40 @@ task pmd(type: Pmd) {
xml.enabled = false
html.enabled = true
xml {
destination "$project.buildDir/reports/pmd/pmd.xml"
destination file("$project.buildDir/reports/pmd/pmd.xml")
}
html {
destination "$project.buildDir/reports/pmd/pmd.html"
destination file("$project.buildDir/reports/pmd/pmd.html")
}
}
}
task findbugs(type: FindBugs) {
// this is just for spotbugs to let the plugin create the task
sourceSets {
main {
java.srcDirs = []
}
}
spotbugs {
toolVersion = "3.1.12"
ignoreFailures = !ABORT_ON_CHECK_FAILURE
effort = "default"
reportLevel = "medium"
}
tasks.withType(com.github.spotbugs.SpotBugsTask) {
source = fileTree('src/main/java')
classes = files("${project.rootDir}/app/build/intermediates/javac/debug/classes")
excludeFilter = new File("${project.rootDir}/config/findbugs/findbugs-filter.xml")
classes = files("${project.rootDir}/app/build/intermediates/classes")
source = fileTree("src/main/java/")
classpath = files()
reports {
xml.enabled = false
html.enabled = true
xml {
destination "$project.buildDir/reports/findbugs/findbugs-output.xml"
destination file ("$project.buildDir/reports/spotbugs/spotbugs-output.xml")
}
html {
destination "$project.buildDir/reports/findbugs/findbugs-output.html"
destination file ("$project.buildDir/reports/spotbugs/spotbugs-output.html")
}
}
}

View File

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" path="src"/>
<classpathentry kind="src" path="gen"/>
<classpathentry kind="src" path="java"/>
<classpathentry kind="src" path="aidl"/>
<classpathentry kind="con" path="com.android.ide.eclipse.adt.ANDROID_FRAMEWORK"/>
<classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.LIBRARIES"/>
<classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.DEPENDENCIES"/>
<classpathentry kind="output" path="bin/classes"/>
</classpath>

View File

@ -1,33 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>Gadgetbridge</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>com.android.ide.eclipse.adt.ResourceManagerBuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>com.android.ide.eclipse.adt.PreCompilerBuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>com.android.ide.eclipse.adt.ApkBuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>com.android.ide.eclipse.adt.AndroidNature</nature>
<nature>org.eclipse.jdt.core.javanature</nature>
</natures>
</projectDescription>

View File

@ -1,12 +0,0 @@
eclipse.preferences.version=1
org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
org.eclipse.jdt.core.compiler.codegen.methodParameters=do not generate
org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.7
org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
org.eclipse.jdt.core.compiler.compliance=1.7
org.eclipse.jdt.core.compiler.debug.lineNumber=generate
org.eclipse.jdt.core.compiler.debug.localVariable=generate
org.eclipse.jdt.core.compiler.debug.sourceFile=generate
org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
org.eclipse.jdt.core.compiler.source=1.7

View File

@ -20,9 +20,13 @@
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.MEDIA_CONTENT_CONTROL" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.VIBRATE" /> <!-- Used for reverse find device -->
<uses-permission android:name="cyanogenmod.permission.ACCESS_WEATHER_MANAGER" />
<uses-permission android:name="cyanogenmod.permission.READ_WEATHER" />
<uses-permission android:name="lineageos.permission.ACCESS_WEATHER_MANAGER" />
<uses-permission android:name="lineageos.permission.READ_WEATHER" />
<uses-permission android:name="org.omnirom.omnijaws.READ_WEATHER" />
<uses-feature
@ -57,10 +61,18 @@
android:name=".activities.SettingsActivity"
android:label="@string/title_activity_settings"
android:parentActivityName=".activities.ControlCenterv2" />
<activity
android:name=".activities.charts.ChartsPreferencesActivity"
android:label="@string/activity_prefs_charts"
android:parentActivityName=".activities.charts.ChartsPreferencesActivity" />
<activity
android:name=".devices.miband.MiBandPreferencesActivity"
android:label="@string/preferences_miband_settings"
android:parentActivityName=".activities.SettingsActivity" />
<activity
android:name=".devices.zetime.ZeTimePreferenceActivity"
android:label="@string/zetime_title_settings"
android:parentActivityName=".activities.SettingsActivity" />
<activity
android:name=".activities.ActivitySummariesActivity"
android:label="@string/activity_summaries"
@ -401,16 +413,24 @@
<activity
android:name=".activities.ConfigureAlarms"
android:label="@string/title_activity_set_alarm"
android:parentActivityName=".activities.SettingsActivity" />
android:parentActivityName=".activities.ControlCenterv2" />
<activity
android:name=".activities.devicesettings.DeviceSettingsActivity"
android:label="@string/title_activity_device_specific_settings" />
<activity
android:name=".activities.AlarmDetails"
android:label="@string/title_activity_alarm_details"
android:screenOrientation="portrait"
android:parentActivityName=".activities.ConfigureAlarms" />
android:parentActivityName=".activities.ConfigureAlarms"
android:screenOrientation="portrait" />
<activity
android:name=".activities.VibrationActivity"
android:label="@string/title_activity_vibration"
android:parentActivityName=".activities.ControlCenterv2" />
<activity
android:name=".activities.NotificationFilterActivity"
android:label="@string/title_activity_notification_filter"
android:windowSoftInputMode="stateHidden|adjustPan"
android:parentActivityName=".activities.AppBlacklistActivity" />
<activity
android:name=".activities.FindPhoneActivity"
android:label="Find Phone" />
@ -421,7 +441,7 @@
android:exported="true" />
<provider
android:name="android.support.v4.content.FileProvider"
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.screenshot_provider"
android:exported="false"
android:grantUriPermissions="true">
@ -430,7 +450,8 @@
android:resource="@xml/shared_paths" />
</provider>
<receiver android:name=".SleepAlarmWidget">
<receiver android:name=".SleepAlarmWidget"
android:label="@string/appwidget_sleep_alarm_widget_label">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
<action android:name="nodomain.freeyourgadget.gadgetbridge.SLEEP_ALARM_WIDGET_CLICK" />
@ -441,6 +462,26 @@
android:resource="@xml/sleep_alarm_widget_info" />
</receiver>
<receiver
android:name=".Widget"
android:label="@string/widget_listing_label">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
<action android:name="nodomain.freeyourgadget.gadgetbridge.WidgetClick" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/widget_info" />
</receiver>
<activity
android:name=".activities.WidgetAlarmsActivity"
android:launchMode="singleInstance"
android:theme="@style/Theme.AppCompat.Light.Dialog"
android:excludeFromRecents="true"/>
<activity
android:launchMode="singleTask"
android:allowTaskReparenting="true"
@ -458,7 +499,12 @@
<data android:scheme="gadgetbridge" />
</intent-filter>
</activity>
<activity
android:name=".devices.qhybrid.ConfigActivity"
android:exported="true" />
<activity
android:name=".devices.qhybrid.QHybridAppChoserActivity"
android:exported="true" />
</application>
</manifest>

View File

@ -0,0 +1,31 @@
/*
* Copyright (C) 2016 The CyanogenMod Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package lineageos.weather;
import lineageos.weather.IWeatherServiceProviderChangeListener;
import lineageos.weather.RequestInfo;
interface ILineageWeatherManager {
oneway void updateWeather(in RequestInfo info);
oneway void lookupCity(in RequestInfo info);
oneway void registerWeatherServiceProviderChangeListener(
in IWeatherServiceProviderChangeListener listener);
oneway void unregisterWeatherServiceProviderChangeListener(
in IWeatherServiceProviderChangeListener listener);
String getActiveWeatherServiceProviderLabel();
oneway void cancelRequest(int requestId);
}

View File

@ -0,0 +1,30 @@
/*
* Copyright (C) 2016 The CyanogenMod Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package lineageos.weather;
import lineageos.weather.RequestInfo;
import lineageos.weather.WeatherInfo;
import lineageos.weather.WeatherLocation;
import java.util.List;
interface IRequestInfoListener {
void onWeatherRequestCompleted(in RequestInfo requestInfo, int status,
in WeatherInfo weatherInfo);
void onLookupCityRequestCompleted(in RequestInfo requestInfo, int status,
in List<WeatherLocation> weatherLocation);
}

View File

@ -0,0 +1,22 @@
/*
* Copyright (C) 2016 The CyanogenMod Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package lineageos.weather;
/** @hide */
oneway interface IWeatherServiceProviderChangeListener {
void onWeatherServiceProviderChanged(String providerLabel);
}

View File

@ -0,0 +1,19 @@
/*
* Copyright (C) 2016 The CyanogenMod Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package lineageos.weather;
parcelable RequestInfo;

View File

@ -0,0 +1,19 @@
/*
* Copyright (C) 2016 The CyanongenMod Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package lineageos.weather;
parcelable WeatherInfo;

View File

@ -0,0 +1,19 @@
/*
* Copyright (C) 2016 The CyanogenMod Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package lineageos.weather;
parcelable WeatherLocation;

View File

@ -0,0 +1,28 @@
/*
* Copyright (C) 2016 The CyanogenMod Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package lineageos.weatherservice;
import lineageos.weatherservice.IWeatherProviderServiceClient;
import lineageos.weather.RequestInfo;
interface IWeatherProviderService {
void processWeatherUpdateRequest(in RequestInfo request);
void processCityNameLookupRequest(in RequestInfo request);
void setServiceClient(in IWeatherProviderServiceClient client);
void cancelOngoingRequests();
void cancelRequest(int requestId);
}

View File

@ -0,0 +1,25 @@
/*
* Copyright (C) 2016 The CyanogenMod Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package lineageos.weatherservice;
import lineageos.weather.RequestInfo;
import lineageos.weatherservice.ServiceRequestResult;
interface IWeatherProviderServiceClient {
void setServiceRequestState(in RequestInfo requestInfo, in ServiceRequestResult result,
int state);
}

View File

@ -0,0 +1,19 @@
/*
* Copyright (C) 2016 The CyanogenMod Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package lineageos.weatherservice;
parcelable ServiceRequestResult;

View File

@ -9,7 +9,7 @@ navigator.geolocation.getCurrentPosition = function(success, failure, options) {
reportedPositionFailures = 0;
success(geoposition);
}
}
};
if (window.Storage){
var prefix = GBjs.getAppLocalstoragePrefix();

View File

@ -0,0 +1,31 @@
/**
* Copyright (c) 2015, The CyanogenMod Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package lineageos.app;
public final class LineageContextConstants {
private LineageContextConstants() {
// Empty constructor
}
public static final String LINEAGE_WEATHER_SERVICE = "lineageweather";
public static class Features {
public static final String WEATHER_SERVICES = "org.lineageos.weather";
}
}

View File

@ -0,0 +1,32 @@
/*
* Copyright (C) 2015 The CyanogenMod Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package lineageos.os;
public class Build {
public static class LINEAGE_VERSION_CODES {
public static final int APRICOT = 1;
public static final int BOYSENBERRY = 2;
public static final int CANTALOUPE = 3;
public static final int DRAGON_FRUIT = 4;
public static final int ELDERBERRY = 5;
public static final int FIG = 6;
public static final int GUAVA = 7;
public static final int HACKBERRY = 8;
public static final int ILAMA = 9;
}
}

View File

@ -0,0 +1,153 @@
/*
* Copyright (C) 2016 The CyanogenMod Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package lineageos.os;
import android.os.Parcel;
import lineageos.os.Build.LINEAGE_VERSION_CODES;
/**
* Simply, Concierge handles your parcels and makes sure they get marshalled and unmarshalled
* correctly when cross IPC boundaries even when there is a version mismatch between the client
* sdk level and the framework implementation.
*
* <p>On incoming parcel (to be unmarshalled):
*
* <pre class="prettyprint">
* ParcelInfo incomingParcelInfo = Concierge.receiveParcel(incomingParcel);
* int parcelableVersion = incomingParcelInfo.getParcelVersion();
*
* // Do unmarshalling steps here iterating over every plausible version
*
* // Complete the process
* incomingParcelInfo.complete();
* </pre>
*
* <p>On outgoing parcel (to be marshalled):
*
* <pre class="prettyprint">
* ParcelInfo outgoingParcelInfo = Concierge.prepareParcel(incomingParcel);
*
* // Do marshalling steps here iterating over every plausible version
*
* // Complete the process
* outgoingParcelInfo.complete();
* </pre>
*/
public final class Concierge {
/** Not instantiable */
private Concierge() {
// Don't instantiate
}
/**
* Since there might be a case where new versions of the lineage framework use applications running
* old versions of the protocol (and thus old versions of this class), we need a versioning
* system for the parcels sent between the core framework and its sdk users.
*
* This parcelable version should be the latest version API version listed in
* {@link LINEAGE_VERSION_CODES}
* @hide
*/
public static final int PARCELABLE_VERSION = LINEAGE_VERSION_CODES.ILAMA;
/**
* Tell the concierge to receive our parcel, so we can get information from it.
*
* MUST CALL {@link ParcelInfo#complete()} AFTER UNMARSHALLING.
*
* @param parcel Incoming parcel to be unmarshalled
* @return {@link ParcelInfo} containing parcel information, specifically the version.
*/
public static ParcelInfo receiveParcel(Parcel parcel) {
return new ParcelInfo(parcel);
}
/**
* Prepare a parcel for the Concierge.
*
* MUST CALL {@link ParcelInfo#complete()} AFTER MARSHALLING.
*
* @param parcel Outgoing parcel to be marshalled
* @return {@link ParcelInfo} containing parcel information, specifically the version.
*/
public static ParcelInfo prepareParcel(Parcel parcel) {
return new ParcelInfo(parcel, PARCELABLE_VERSION);
}
/**
* Parcel header info specific to the Parcel object that is passed in via
* {@link #prepareParcel(Parcel)} or {@link #receiveParcel(Parcel)}. The exposed method
* of {@link #getParcelVersion()} gets the api level of the parcel object.
*/
public final static class ParcelInfo {
private Parcel mParcel;
private int mParcelableVersion;
private int mParcelableSize;
private int mStartPosition;
private int mSizePosition;
private boolean mCreation = false;
ParcelInfo(Parcel parcel) {
mCreation = false;
mParcel = parcel;
mParcelableVersion = parcel.readInt();
mParcelableSize = parcel.readInt();
mStartPosition = parcel.dataPosition();
}
ParcelInfo(Parcel parcel, int parcelableVersion) {
mCreation = true;
mParcel = parcel;
mParcelableVersion = parcelableVersion;
// Write parcelable version, make sure to define explicit changes
// within {@link #PARCELABLE_VERSION);
mParcel.writeInt(mParcelableVersion);
// Inject a placeholder that will store the parcel size from this point on
// (not including the size itself).
mSizePosition = parcel.dataPosition();
mParcel.writeInt(0);
mStartPosition = parcel.dataPosition();
}
/**
* Get the parcel version from the {@link Parcel} received by the Concierge.
* @return {@link #PARCELABLE_VERSION} of the {@link Parcel}
*/
public int getParcelVersion() {
return mParcelableVersion;
}
/**
* Complete the {@link ParcelInfo} for the Concierge.
*/
public void complete() {
if (mCreation) {
// Go back and write size
mParcelableSize = mParcel.dataPosition() - mStartPosition;
mParcel.setDataPosition(mSizePosition);
mParcel.writeInt(mParcelableSize);
mParcel.setDataPosition(mStartPosition + mParcelableSize);
} else {
mParcel.setDataPosition(mStartPosition + mParcelableSize);
}
}
}
}

View File

@ -0,0 +1,245 @@
/*
* Copyright (C) 2016 The CyanogenMod Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package lineageos.providers;
import android.net.Uri;
/**
* The contract between the weather provider and applications.
*/
public class WeatherContract {
/**
* The authority of the weather content provider
*/
public static final String AUTHORITY = "org.lineageos.weather";
/**
* A content:// style uri to the authority for the weather provider
*/
public static final Uri AUTHORITY_URI = Uri.parse("content://" + AUTHORITY);
public static class WeatherColumns {
public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "weather");
public static final Uri CURRENT_AND_FORECAST_WEATHER_URI
= Uri.withAppendedPath(CONTENT_URI, "current_and_forecast");
public static final Uri CURRENT_WEATHER_URI
= Uri.withAppendedPath(CONTENT_URI, "current");
public static final Uri FORECAST_WEATHER_URI
= Uri.withAppendedPath(CONTENT_URI, "forecast");
/**
* The city name
* <P>Type: TEXT</P>
*/
public static final String CURRENT_CITY = "city";
/**
* A Valid {@link WeatherCode}
* <P>Type: INTEGER</P>
*/
public static final String CURRENT_CONDITION_CODE = "condition_code";
/**
* A localized string mapped to the current weather condition code. Note that, if no
* locale is found, the string will be in english
* <P>Type: TEXT</P>
*/
public static final String CURRENT_CONDITION = "condition";
/**
* The current weather temperature
* <P>Type: DOUBLE</P>
*/
public static final String CURRENT_TEMPERATURE = "temperature";
/**
* The unit in which current temperature is reported
* <P>Type: INTEGER</P>
* Can be one of the following:
* <ul>
* <li>{@link TempUnit#CELSIUS}</li>
* <li>{@link TempUnit#FAHRENHEIT}</li>
* </ul>
*/
public static final String CURRENT_TEMPERATURE_UNIT = "temperature_unit";
/**
* The current weather humidity
* <P>Type: DOUBLE</P>
*/
public static final String CURRENT_HUMIDITY = "humidity";
/**
* The current wind direction (in degrees)
* <P>Type: DOUBLE</P>
*/
public static final String CURRENT_WIND_DIRECTION = "wind_direction";
/**
* The current wind speed
* <P>Type: DOUBLE</P>
*/
public static final String CURRENT_WIND_SPEED = "wind_speed";
/**
* The unit in which the wind speed is reported
* <P>Type: INTEGER</P>
* Can be one of the following:
* <ul>
* <li>{@link WindSpeedUnit#KPH}</li>
* <li>{@link WindSpeedUnit#MPH}</li>
* </ul>
*/
public static final String CURRENT_WIND_SPEED_UNIT = "wind_speed_unit";
/**
* The timestamp when this weather was reported
* <P>Type: LONG</P>
*/
public static final String CURRENT_TIMESTAMP = "timestamp";
/**
* Today's high temperature.
* <p>Type: DOUBLE</p>
*/
public static final String TODAYS_HIGH_TEMPERATURE = "todays_high";
/**
* Today's low temperature.
* <p>Type: DOUBLE</p>
*/
public static final String TODAYS_LOW_TEMPERATURE = "todays_low";
/**
* The forecasted low temperature
* <P>Type: DOUBLE</P>
*/
public static final String FORECAST_LOW = "forecast_low";
/**
* The forecasted high temperature
* <P>Type: DOUBLE</P>
*/
public static final String FORECAST_HIGH = "forecast_high";
/**
* A localized string mapped to the forecasted weather condition code. Note that, if no
* locale is found, the string will be in english
* <P>Type: TEXT</P>
*/
public static final String FORECAST_CONDITION = "forecast_condition";
/**
* The code identifying the forecasted weather condition.
* @see #CURRENT_CONDITION_CODE
*/
public static final String FORECAST_CONDITION_CODE = "forecast_condition_code";
/**
* Temperature units
*/
public static final class TempUnit {
private TempUnit() {}
public final static int CELSIUS = 1;
public final static int FAHRENHEIT = 2;
}
/**
* Wind speed units
*/
public static final class WindSpeedUnit {
private WindSpeedUnit() {}
/**
* Kilometers per hour
*/
public final static int KPH = 1;
/**
* Miles per hour
*/
public final static int MPH = 2;
}
/**
* Weather condition codes
*/
public static final class WeatherCode {
private WeatherCode() {}
/**
* @hide
*/
public final static int WEATHER_CODE_MIN = 0;
public final static int TORNADO = 0;
public final static int TROPICAL_STORM = 1;
public final static int HURRICANE = 2;
public final static int SEVERE_THUNDERSTORMS = 3;
public final static int THUNDERSTORMS = 4;
public final static int MIXED_RAIN_AND_SNOW = 5;
public final static int MIXED_RAIN_AND_SLEET = 6;
public final static int MIXED_SNOW_AND_SLEET = 7;
public final static int FREEZING_DRIZZLE = 8;
public final static int DRIZZLE = 9;
public final static int FREEZING_RAIN = 10;
public final static int SHOWERS = 11;
public final static int SNOW_FLURRIES = 12;
public final static int LIGHT_SNOW_SHOWERS = 13;
public final static int BLOWING_SNOW = 14;
public final static int SNOW = 15;
public final static int HAIL = 16;
public final static int SLEET = 17;
public final static int DUST = 18;
public final static int FOGGY = 19;
public final static int HAZE = 20;
public final static int SMOKY = 21;
public final static int BLUSTERY = 22;
public final static int WINDY = 23;
public final static int COLD = 24;
public final static int CLOUDY = 25;
public final static int MOSTLY_CLOUDY_NIGHT = 26;
public final static int MOSTLY_CLOUDY_DAY = 27;
public final static int PARTLY_CLOUDY_NIGHT = 28;
public final static int PARTLY_CLOUDY_DAY = 29;
public final static int CLEAR_NIGHT = 30;
public final static int SUNNY = 31;
public final static int FAIR_NIGHT = 32;
public final static int FAIR_DAY = 33;
public final static int MIXED_RAIN_AND_HAIL = 34;
public final static int HOT = 35;
public final static int ISOLATED_THUNDERSTORMS = 36;
public final static int SCATTERED_THUNDERSTORMS = 37;
public final static int SCATTERED_SHOWERS = 38;
public final static int HEAVY_SNOW = 39;
public final static int SCATTERED_SNOW_SHOWERS = 40;
public final static int PARTLY_CLOUDY = 41;
public final static int THUNDERSHOWER = 42;
public final static int SNOW_SHOWERS = 43;
public final static int ISOLATED_THUNDERSHOWERS = 44;
/**
* @hide
*/
public final static int WEATHER_CODE_MAX = 44;
public final static int NOT_AVAILABLE = 3200;
}
}
}

View File

@ -0,0 +1,435 @@
/*
* Copyright (C) 2016 The CyanogenMod Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package lineageos.weather;
import android.annotation.SuppressLint;
import android.content.Context;
import android.location.Location;
import android.os.Build;
import android.os.Handler;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.ArraySet;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import lineageos.app.LineageContextConstants;
import lineageos.providers.WeatherContract;
/**
* Provides access to the weather services in the device.
*/
@RequiresApi(api = Build.VERSION_CODES.M)
public class LineageWeatherManager {
private static ILineageWeatherManager sWeatherManagerService;
private static LineageWeatherManager sInstance;
private Context mContext;
private Map<RequestInfo,WeatherUpdateRequestListener> mWeatherUpdateRequestListeners
= Collections.synchronizedMap(new HashMap<RequestInfo,WeatherUpdateRequestListener>());
private Map<RequestInfo,LookupCityRequestListener> mLookupNameRequestListeners
= Collections.synchronizedMap(new HashMap<RequestInfo,LookupCityRequestListener>());
private Handler mHandler;
private Set<WeatherServiceProviderChangeListener> mProviderChangedListeners = new ArraySet<>();
private static final String TAG = LineageWeatherManager.class.getSimpleName();
/**
* The different request statuses
*/
public static final class RequestStatus {
private RequestStatus() {}
/**
* Request successfully completed
*/
public static final int COMPLETED = 1;
/**
* An error occurred while trying to honor the request
*/
public static final int FAILED = -1;
/**
* The request can't be processed at this time
*/
public static final int SUBMITTED_TOO_SOON = -2;
/**
* Another request is already in progress
*/
public static final int ALREADY_IN_PROGRESS = -3;
/**
* No match found for the query
*/
public static final int NO_MATCH_FOUND = -4;
}
private LineageWeatherManager(Context context) {
Context appContext = context.getApplicationContext();
mContext = (appContext != null) ? appContext : context;
sWeatherManagerService = getService();
if (context.getPackageManager().hasSystemFeature(
LineageContextConstants.Features.WEATHER_SERVICES) && (sWeatherManagerService == null)) {
Log.wtf(TAG, "Unable to bind the LineageWeatherManagerService");
}
mHandler = new Handler(appContext.getMainLooper());
}
/**
* Gets or creates an instance of the {@link lineageos.weather.LineageWeatherManager}
* @param context
* @return {@link LineageWeatherManager}
*/
public static LineageWeatherManager getInstance(Context context) {
if (sInstance == null) {
sInstance = new LineageWeatherManager(context);
}
return sInstance;
}
/**
* @hide
*/
@SuppressLint("PrivateApi")
public static ILineageWeatherManager getService() {
if (sWeatherManagerService != null) {
return sWeatherManagerService;
}
// This is a Gadgetbridge hack
IBinder binder = null;
try {
Class localClass = Class.forName("android.os.ServiceManager");
Method getService = localClass.getMethod("getService", String.class);
Object result = getService.invoke(localClass, LineageContextConstants.LINEAGE_WEATHER_SERVICE);
if (result != null) {
binder = (IBinder) result;
}
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
if (binder != null) {
sWeatherManagerService = ILineageWeatherManager.Stub.asInterface(binder);
return sWeatherManagerService;
}
return null;
}
/**
* Forces the weather service to request the latest available weather information for
* the supplied {@link android.location.Location} location.
*
* @param location The location you want to get the latest weather data from.
* @param listener {@link WeatherUpdateRequestListener} To be notified once the active weather
* service provider has finished
* processing your request
* @return An integer that identifies the request submitted to the weather service
* Note that this method might return -1 if an error occurred while trying to submit
* the request.
*/
public int requestWeatherUpdate(@NonNull Location location,
@NonNull WeatherUpdateRequestListener listener) {
if (sWeatherManagerService == null) {
return -1;
}
try {
int tempUnit = WeatherContract.WeatherColumns.TempUnit.CELSIUS;
RequestInfo info = new RequestInfo
.Builder(mRequestInfoListener)
.setLocation(location)
.setTemperatureUnit(tempUnit)
.build();
if (listener != null) mWeatherUpdateRequestListeners.put(info, listener);
sWeatherManagerService.updateWeather(info);
return info.hashCode();
} catch (RemoteException e) {
return -1;
}
}
/**
* Forces the weather service to request the latest weather information for the provided
* WeatherLocation. This is the preferred method for requesting a weather update.
*
* @param weatherLocation A {@link lineageos.weather.WeatherLocation} that was previously
* obtained by calling
* {@link #lookupCity(String, LookupCityRequestListener)}
* @param listener {@link WeatherUpdateRequestListener} To be notified once the active weather
* service provider has finished
* processing your request
* @return An integer that identifies the request submitted to the weather service.
* Note that this method might return -1 if an error occurred while trying to submit
* the request.
*/
public int requestWeatherUpdate(@NonNull WeatherLocation weatherLocation,
@NonNull WeatherUpdateRequestListener listener) {
if (sWeatherManagerService == null) {
return -1;
}
try {
int tempUnit = WeatherContract.WeatherColumns.TempUnit.CELSIUS;
RequestInfo info = new RequestInfo
.Builder(mRequestInfoListener)
.setWeatherLocation(weatherLocation)
.setTemperatureUnit(tempUnit)
.build();
if (listener != null) mWeatherUpdateRequestListeners.put(info, listener);
sWeatherManagerService.updateWeather(info);
return info.hashCode();
} catch (RemoteException e) {
return -1;
}
}
/**
* Request the active weather provider service to lookup the supplied city name.
*
* @param city The city name
* @param listener {@link LookupCityRequestListener} To be notified once the request has been
* completed. Upon success, a list of
* {@link lineageos.weather.WeatherLocation}
* will be provided
* @return An integer that identifies the request submitted to the weather service.
* Note that this method might return -1 if an error occurred while trying to submit
* the request.
*/
public int lookupCity(@NonNull String city, @NonNull LookupCityRequestListener listener) {
if (sWeatherManagerService == null) {
return -1;
}
try {
RequestInfo info = new RequestInfo
.Builder(mRequestInfoListener)
.setCityName(city)
.build();
if (listener != null) mLookupNameRequestListeners.put(info, listener);
sWeatherManagerService.lookupCity(info);
return info.hashCode();
} catch (RemoteException e) {
return -1;
}
}
/**
* Cancels a request that was previously submitted to the weather service.
* @param requestId The ID that you received when the request was submitted
*/
public void cancelRequest(int requestId) {
if (sWeatherManagerService == null) {
return;
}
try {
sWeatherManagerService.cancelRequest(requestId);
}catch (RemoteException e){
}
}
/**
* Registers a {@link WeatherServiceProviderChangeListener} to be notified when a new weather
* service provider becomes active.
* @param listener {@link WeatherServiceProviderChangeListener} to register
*/
public void registerWeatherServiceProviderChangeListener(
@NonNull WeatherServiceProviderChangeListener listener) {
if (sWeatherManagerService == null) return;
synchronized (mProviderChangedListeners) {
if (mProviderChangedListeners.contains(listener)) {
throw new IllegalArgumentException("Listener already registered");
}
if (mProviderChangedListeners.size() == 0) {
try {
sWeatherManagerService.registerWeatherServiceProviderChangeListener(
mProviderChangeListener);
} catch (RemoteException e){
}
}
mProviderChangedListeners.add(listener);
}
}
/**
* Unregisters a listener
* @param listener A previously registered {@link WeatherServiceProviderChangeListener}
*/
public void unregisterWeatherServiceProviderChangeListener(
@NonNull WeatherServiceProviderChangeListener listener) {
if (sWeatherManagerService == null) return;
synchronized (mProviderChangedListeners) {
if (!mProviderChangedListeners.contains(listener)) {
throw new IllegalArgumentException("Listener was never registered");
}
mProviderChangedListeners.remove(listener);
if (mProviderChangedListeners.size() == 0) {
try {
sWeatherManagerService.unregisterWeatherServiceProviderChangeListener(
mProviderChangeListener);
} catch(RemoteException e){
}
}
}
}
/**
* Gets the service's label as declared by the active weather service provider in its manifest
* @return the service's label
*/
public String getActiveWeatherServiceProviderLabel() {
if (sWeatherManagerService == null) return null;
try {
return sWeatherManagerService.getActiveWeatherServiceProviderLabel();
} catch(RemoteException e){
}
return null;
}
private final IWeatherServiceProviderChangeListener mProviderChangeListener =
new IWeatherServiceProviderChangeListener.Stub() {
@Override
public void onWeatherServiceProviderChanged(final String providerName) {
mHandler.post(new Runnable() {
@Override
public void run() {
synchronized (mProviderChangedListeners) {
List<WeatherServiceProviderChangeListener> deadListeners
= new ArrayList<>();
for (WeatherServiceProviderChangeListener listener
: mProviderChangedListeners) {
try {
listener.onWeatherServiceProviderChanged(providerName);
} catch (Throwable e) {
deadListeners.add(listener);
}
}
if (deadListeners.size() > 0) {
for (WeatherServiceProviderChangeListener listener : deadListeners) {
mProviderChangedListeners.remove(listener);
}
}
}
}
});
}
};
private final IRequestInfoListener mRequestInfoListener = new IRequestInfoListener.Stub() {
@Override
public void onWeatherRequestCompleted(final RequestInfo requestInfo, final int status,
final WeatherInfo weatherInfo) {
final WeatherUpdateRequestListener listener
= mWeatherUpdateRequestListeners.remove(requestInfo);
if (listener != null) {
mHandler.post(new Runnable() {
@Override
public void run() {
listener.onWeatherRequestCompleted(status, weatherInfo);
}
});
}
}
@Override
public void onLookupCityRequestCompleted(RequestInfo requestInfo, final int status,
final List<WeatherLocation> weatherLocations) {
final LookupCityRequestListener listener
= mLookupNameRequestListeners.remove(requestInfo);
if (listener != null) {
mHandler.post(new Runnable() {
@Override
public void run() {
listener.onLookupCityRequestCompleted(status, weatherLocations);
}
});
}
}
};
/**
* Interface used to receive notifications upon completion of a weather update request
*/
public interface WeatherUpdateRequestListener {
/**
* This method will be called when the weather service provider has finished processing the
* request
*
* @param status See {@link RequestStatus}
*
* @param weatherInfo A fully populated {@link WeatherInfo} if state is
* {@link RequestStatus#COMPLETED}, null otherwise
*/
void onWeatherRequestCompleted(int status, WeatherInfo weatherInfo);
}
/**
* Interface used to receive notifications upon completion of a request to lookup a city name
*/
public interface LookupCityRequestListener {
/**
* This method will be called when the weather service provider has finished processing the
* request.
*
* @param status See {@link RequestStatus}
*
* @param locations A list of {@link WeatherLocation} if the status is
* {@link RequestStatus#COMPLETED}, null otherwise
*/
void onLookupCityRequestCompleted(int status, List<WeatherLocation> locations);
}
/**
* Interface used to be notified when the user changes the weather service provider
*/
public interface WeatherServiceProviderChangeListener {
/**
* This method will be called when a new weather service provider becomes active in the
* system. The parameter can be null when
* <p>The user removed the active weather service provider from the system </p>
* <p>The active weather provider was disabled.</p>
*
* @param providerLabel The label as declared on the weather service provider manifest
*/
void onWeatherServiceProviderChanged(String providerLabel);
}
}

View File

@ -0,0 +1,379 @@
/*
* Copyright (C) 2016 The CyanogenMod Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package lineageos.weather;
import android.location.Location;
import android.os.Parcel;
import android.os.Parcelable;
import android.text.TextUtils;
import lineageos.os.Build;
import lineageos.os.Concierge;
import lineageos.os.Concierge.ParcelInfo;
import lineageos.providers.WeatherContract;
import java.util.UUID;
/**
* This class holds the information of a request submitted to the active weather provider service
*/
public final class RequestInfo implements Parcelable {
private Location mLocation;
private String mCityName;
private WeatherLocation mWeatherLocation;
private int mRequestType;
private IRequestInfoListener mListener;
private int mTempUnit;
private String mKey;
private boolean mIsQueryOnly;
/**
* A request to update the weather data using a geographical {@link android.location.Location}
*/
public static final int TYPE_WEATHER_BY_GEO_LOCATION_REQ = 1;
/**
* A request to update the weather data using a {@link WeatherLocation}
*/
public static final int TYPE_WEATHER_BY_WEATHER_LOCATION_REQ = 2;
/**
* A request to look up a city name
*/
public static final int TYPE_LOOKUP_CITY_NAME_REQ = 3;
private RequestInfo() {}
/* package */ static class Builder {
private Location mLocation;
private String mCityName;
private WeatherLocation mWeatherLocation;
private int mRequestType;
private IRequestInfoListener mListener;
private int mTempUnit = WeatherContract.WeatherColumns.TempUnit.FAHRENHEIT;
private boolean mIsQueryOnly = false;
public Builder(IRequestInfoListener listener) {
this.mListener = listener;
}
/**
* Sets the city name and identifies this request as a {@link #TYPE_LOOKUP_CITY_NAME_REQ}
* request. If set, will null out the location and weather location. Attempting to set
* a null city name will get you an IllegalArgumentException
*/
public Builder setCityName(String cityName) {
if (cityName == null) {
throw new IllegalArgumentException("City name can't be null");
}
this.mCityName = cityName;
this.mRequestType = TYPE_LOOKUP_CITY_NAME_REQ;
this.mLocation = null;
this.mWeatherLocation = null;
return this;
}
/**
* Sets the Location and identifies this request as a
* {@link #TYPE_WEATHER_BY_GEO_LOCATION_REQ}. If set, will null out the city name and
* weather location. Attempting to set a null location will get you an
* IllegalArgumentException
*/
public Builder setLocation(Location location) {
if (location == null) {
throw new IllegalArgumentException("Location can't be null");
}
this.mLocation = new Location(location);
this.mCityName = null;
this.mWeatherLocation = null;
this.mRequestType = TYPE_WEATHER_BY_GEO_LOCATION_REQ;
return this;
}
/**
* Sets the weather location and identifies this request as a
* {@link #TYPE_WEATHER_BY_WEATHER_LOCATION_REQ}. If set, will null out the location and
* city name. Attempting to set a null weather location will get you an
* IllegalArgumentException
*/
public Builder setWeatherLocation(WeatherLocation weatherLocation) {
if (weatherLocation == null) {
throw new IllegalArgumentException("WeatherLocation can't be null");
}
this.mWeatherLocation = weatherLocation;
this.mLocation = null;
this.mCityName = null;
this.mRequestType = TYPE_WEATHER_BY_WEATHER_LOCATION_REQ;
return this;
}
/**
* Sets the unit in which the temperature will be reported if the request is honored.
* Valid values are:
* <ul>
* {@link lineageos.providers.WeatherContract.WeatherColumns.TempUnit#CELSIUS}
* {@link lineageos.providers.WeatherContract.WeatherColumns.TempUnit#FAHRENHEIT}
* </ul>
* Any other value will generate an IllegalArgumentException. If the temperature unit is not
* set, the default will be degrees Fahrenheit
* @param unit A valid temperature unit
*/
public Builder setTemperatureUnit(int unit) {
if (!isValidTempUnit(unit)) {
throw new IllegalArgumentException("Invalid temperature unit");
}
this.mTempUnit = unit;
return this;
}
/**
* If this is a weather request, marks the request as a query only, meaning that the
* content provider won't be updated after the active weather service has finished
* processing the request.
*/
public Builder queryOnly() {
switch (mRequestType) {
case TYPE_WEATHER_BY_GEO_LOCATION_REQ:
case TYPE_WEATHER_BY_WEATHER_LOCATION_REQ:
this.mIsQueryOnly = true;
break;
default:
this.mIsQueryOnly = false;
break;
}
return this;
}
/**
* Combine all of the options that have been set and return a new {@link RequestInfo} object
* @return {@link RequestInfo}
*/
public RequestInfo build() {
RequestInfo info = new RequestInfo();
info.mListener = this.mListener;
info.mRequestType = this.mRequestType;
info.mCityName = this.mCityName;
info.mWeatherLocation = this.mWeatherLocation;
info.mLocation = this.mLocation;
info.mTempUnit = this.mTempUnit;
info.mIsQueryOnly = this.mIsQueryOnly;
info.mKey = UUID.randomUUID().toString();
return info;
}
private boolean isValidTempUnit(int unit) {
switch (unit) {
case WeatherContract.WeatherColumns.TempUnit.CELSIUS:
case WeatherContract.WeatherColumns.TempUnit.FAHRENHEIT:
return true;
default:
return false;
}
}
}
private RequestInfo(Parcel parcel) {
// Read parcelable version via the Concierge
ParcelInfo parcelInfo = Concierge.receiveParcel(parcel);
int parcelableVersion = parcelInfo.getParcelVersion();
if (parcelableVersion >= Build.LINEAGE_VERSION_CODES.ELDERBERRY) {
mKey = parcel.readString();
mRequestType = parcel.readInt();
switch (mRequestType) {
case TYPE_WEATHER_BY_GEO_LOCATION_REQ:
mLocation = Location.CREATOR.createFromParcel(parcel);
mTempUnit = parcel.readInt();
break;
case TYPE_WEATHER_BY_WEATHER_LOCATION_REQ:
mWeatherLocation = WeatherLocation.CREATOR.createFromParcel(parcel);
mTempUnit = parcel.readInt();
break;
case TYPE_LOOKUP_CITY_NAME_REQ:
mCityName = parcel.readString();
break;
}
mIsQueryOnly = (parcel.readInt() == 1);
mListener = IRequestInfoListener.Stub.asInterface(parcel.readStrongBinder());
}
// Complete parcel info for the concierge
parcelInfo.complete();
}
/**
* @return The request type
*/
public int getRequestType() {
return mRequestType;
}
/**
* @return the {@link android.location.Location} if this is a request by location, null
* otherwise
*/
public Location getLocation() {
return new Location(mLocation);
}
/**
* @return the {@link lineageos.weather.WeatherLocation} if this is a request by weather
* location, null otherwise
*/
public WeatherLocation getWeatherLocation() {
return mWeatherLocation;
}
/**
* @hide
*/
public IRequestInfoListener getRequestListener() {
return mListener;
}
/**
* @return the city name if this is a lookup request, null otherwise
*/
public String getCityName() {
return mCityName;
}
/**
* @return the temperature unit if this is a weather request, -1 otherwise
*/
public int getTemperatureUnit() {
switch (mRequestType) {
case TYPE_WEATHER_BY_GEO_LOCATION_REQ:
case TYPE_WEATHER_BY_WEATHER_LOCATION_REQ:
return mTempUnit;
default:
return -1;
}
}
/**
* @return if this is a weather request, whether the request will update the content provider.
* False for other kind of requests
* @hide
*/
public boolean isQueryOnlyWeatherRequest() {
switch (mRequestType) {
case TYPE_WEATHER_BY_GEO_LOCATION_REQ:
case TYPE_WEATHER_BY_WEATHER_LOCATION_REQ:
return mIsQueryOnly;
default:
return false;
}
}
public static final Creator<RequestInfo> CREATOR = new Creator<RequestInfo>() {
@Override
public RequestInfo createFromParcel(Parcel in) {
return new RequestInfo(in);
}
@Override
public RequestInfo[] newArray(int size) {
return new RequestInfo[size];
}
};
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
// Tell the concierge to prepare the parcel
ParcelInfo parcelInfo = Concierge.prepareParcel(dest);
// ==== ELDERBERRY =====
dest.writeString(mKey);
dest.writeInt(mRequestType);
switch (mRequestType) {
case TYPE_WEATHER_BY_GEO_LOCATION_REQ:
mLocation.writeToParcel(dest, 0);
dest.writeInt(mTempUnit);
break;
case TYPE_WEATHER_BY_WEATHER_LOCATION_REQ:
mWeatherLocation.writeToParcel(dest, 0);
dest.writeInt(mTempUnit);
break;
case TYPE_LOOKUP_CITY_NAME_REQ:
dest.writeString(mCityName);
break;
}
dest.writeInt(mIsQueryOnly == true ? 1 : 0);
dest.writeStrongBinder(mListener.asBinder());
// Complete parcel info for the concierge
parcelInfo.complete();
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("{ Request for ");
switch (mRequestType) {
case TYPE_WEATHER_BY_GEO_LOCATION_REQ:
builder.append("Location: ").append(mLocation);
builder.append(" Temp Unit: ");
if (mTempUnit == WeatherContract.WeatherColumns.TempUnit.FAHRENHEIT) {
builder.append("Fahrenheit");
} else {
builder.append(" Celsius");
}
break;
case TYPE_WEATHER_BY_WEATHER_LOCATION_REQ:
builder.append("WeatherLocation: ").append(mWeatherLocation);
builder.append(" Temp Unit: ");
if (mTempUnit == WeatherContract.WeatherColumns.TempUnit.FAHRENHEIT) {
builder.append("Fahrenheit");
} else {
builder.append(" Celsius");
}
break;
case TYPE_LOOKUP_CITY_NAME_REQ:
builder.append("Lookup City: ").append(mCityName);
break;
}
return builder.append(" }").toString();
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((mKey != null) ? mKey.hashCode() : 0);
return result;
}
@Override
public boolean equals(Object obj) {
if (obj == null) return false;
if (getClass() == obj.getClass()) {
RequestInfo info = (RequestInfo) obj;
return (TextUtils.equals(mKey, info.mKey));
}
return false;
}
}

View File

@ -0,0 +1,642 @@
/*
* Copyright (C) 2016 The CyanongenMod Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package lineageos.weather;
import android.os.Parcel;
import android.os.Parcelable;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import lineageos.os.Build;
import lineageos.os.Concierge;
import lineageos.os.Concierge.ParcelInfo;
import lineageos.providers.WeatherContract;
import lineageos.weatherservice.ServiceRequest;
import lineageos.weatherservice.ServiceRequestResult;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
/**
* This class represents the weather information that a
* {@link lineageos.weatherservice.WeatherProviderService} will use to update the weather content
* provider. A weather provider service will be called by the system to process an update
* request at any time. If the service successfully processes the request, then the weather provider
* service is responsible of calling
* {@link ServiceRequest#complete(ServiceRequestResult)} to notify the
* system that the request was completed and that the weather content provider should be updated
* with the supplied weather information.
*/
public final class WeatherInfo implements Parcelable {
private String mCity;
private int mConditionCode;
private double mTemperature;
private int mTempUnit;
private double mTodaysHighTemp;
private double mTodaysLowTemp;
private double mHumidity;
private double mWindSpeed;
private double mWindDirection;
private int mWindSpeedUnit;
private long mTimestamp;
private List<DayForecast> mForecastList;
private String mKey;
private WeatherInfo() {}
/**
* Builder class for {@link WeatherInfo}
*/
public static class Builder {
private String mCity;
private int mConditionCode = WeatherContract.WeatherColumns.WeatherCode.NOT_AVAILABLE;
private double mTemperature;
private int mTempUnit;
private double mTodaysHighTemp = Double.NaN;
private double mTodaysLowTemp = Double.NaN;
private double mHumidity = Double.NaN;
private double mWindSpeed = Double.NaN;
private double mWindDirection = Double.NaN;
private int mWindSpeedUnit = WeatherContract.WeatherColumns.WindSpeedUnit.MPH;
private long mTimestamp = -1;
private List<DayForecast> mForecastList = new ArrayList<>(0);
/**
* @param cityName A valid city name. Attempting to pass null will get you an
* IllegalArgumentException
* @param temperature A valid temperature value. Attempting pass an invalid double value,
* will get you an IllegalArgumentException
* @param tempUnit A valid temperature unit value. See
* {@link lineageos.providers.WeatherContract.WeatherColumns.TempUnit} for
* valid values. Attempting to pass an invalid temperature unit will get you
* an IllegalArgumentException
*/
public Builder(@NonNull String cityName, double temperature, int tempUnit) {
if (cityName == null) {
throw new IllegalArgumentException("City name can't be null");
}
if (Double.isNaN(temperature)) {
throw new IllegalArgumentException("Invalid temperature");
}
if (!isValidTempUnit(tempUnit)) {
throw new IllegalArgumentException("Invalid temperature unit");
}
this.mCity = cityName;
this.mTemperature = temperature;
this.mTempUnit = tempUnit;
}
/**
* @param timeStamp A timestamp indicating when this data was generated. If timestamps is
* not set, then the builder will set it to the time of object creation
* @return The {@link Builder} instance
*/
public Builder setTimestamp(long timeStamp) {
mTimestamp = timeStamp;
return this;
}
/**
* @param humidity The weather humidity. Attempting to pass an invalid double value will get
* you an IllegalArgumentException
* @return The {@link Builder} instance
*/
public Builder setHumidity(double humidity) {
if (Double.isNaN(humidity)) {
throw new IllegalArgumentException("Invalid humidity value");
}
mHumidity = humidity;
return this;
}
/**
* @param windSpeed The wind speed. Attempting to pass an invalid double value will get you
* an IllegalArgumentException
* @param windDirection The wind direction. Attempting to pass an invalid double value will
* get you an IllegalArgumentException
* @param windSpeedUnit A valid wind speed direction unit. See
* {@link lineageos.providers.WeatherContract.WeatherColumns.WindSpeedUnit}
* for valid values. Attempting to pass an invalid speed unit will get
* you an IllegalArgumentException
* @return The {@link Builder} instance
*/
public Builder setWind(double windSpeed, double windDirection, int windSpeedUnit) {
if (Double.isNaN(windSpeed)) {
throw new IllegalArgumentException("Invalid wind speed value");
}
if (Double.isNaN(windDirection)) {
throw new IllegalArgumentException("Invalid wind direction value");
}
if (!isValidWindSpeedUnit(windSpeedUnit)) {
throw new IllegalArgumentException("Invalid speed unit");
}
mWindSpeed = windSpeed;
mWindSpeedUnit = windSpeedUnit;
mWindDirection = windDirection;
return this;
}
/**
* @param conditionCode A valid weather condition code. See
* {@link lineageos.providers.WeatherContract.WeatherColumns.WeatherCode}
* for valid codes. Attempting to pass an invalid code will get you an
* IllegalArgumentException.
* @return The {@link Builder} instance
*/
public Builder setWeatherCondition(int conditionCode) {
if (!isValidWeatherCode(conditionCode)) {
throw new IllegalArgumentException("Invalid weather condition code");
}
mConditionCode = conditionCode;
return this;
}
/**
* @param forecasts A valid array list of {@link DayForecast} objects. Attempting to pass
* null will get you an IllegalArgumentException'
* @return The {@link Builder} instance
*/
public Builder setForecast(@NonNull List<DayForecast> forecasts) {
if (forecasts == null) {
throw new IllegalArgumentException("Forecast list can't be null");
}
mForecastList = forecasts;
return this;
}
/**
*
* @param todaysHigh Today's high temperature. Attempting to pass an invalid double value
* will get you an IllegalArgumentException
* @return The {@link Builder} instance
*/
public Builder setTodaysHigh(double todaysHigh) {
if (Double.isNaN(todaysHigh)) {
throw new IllegalArgumentException("Invalid temperature value");
}
mTodaysHighTemp = todaysHigh;
return this;
}
/**
* @param todaysLow Today's low temperature. Attempting to pass an invalid double value will
* get you an IllegalArgumentException
* @return
*/
public Builder setTodaysLow(double todaysLow) {
if (Double.isNaN(todaysLow)) {
throw new IllegalArgumentException("Invalid temperature value");
}
mTodaysLowTemp = todaysLow;
return this;
}
/**
* Combine all of the options that have been set and return a new {@link WeatherInfo} object
* @return {@link WeatherInfo}
*/
public WeatherInfo build() {
WeatherInfo info = new WeatherInfo();
info.mCity = this.mCity;
info.mConditionCode = this.mConditionCode;
info.mTemperature = this.mTemperature;
info.mTempUnit = this.mTempUnit;
info.mHumidity = this.mHumidity;
info.mWindSpeed = this.mWindSpeed;
info.mWindDirection = this.mWindDirection;
info.mWindSpeedUnit = this.mWindSpeedUnit;
info.mTimestamp = this.mTimestamp == -1 ? System.currentTimeMillis() : this.mTimestamp;
info.mForecastList = this.mForecastList;
info.mTodaysHighTemp = this.mTodaysHighTemp;
info.mTodaysLowTemp = this.mTodaysLowTemp;
info.mKey = UUID.randomUUID().toString();
return info;
}
private boolean isValidTempUnit(int unit) {
switch (unit) {
case WeatherContract.WeatherColumns.TempUnit.CELSIUS:
case WeatherContract.WeatherColumns.TempUnit.FAHRENHEIT:
return true;
default:
return false;
}
}
private boolean isValidWindSpeedUnit(int unit) {
switch (unit) {
case WeatherContract.WeatherColumns.WindSpeedUnit.KPH:
case WeatherContract.WeatherColumns.WindSpeedUnit.MPH:
return true;
default:
return false;
}
}
}
private static boolean isValidWeatherCode(int code) {
if (code < WeatherContract.WeatherColumns.WeatherCode.WEATHER_CODE_MIN
|| code > WeatherContract.WeatherColumns.WeatherCode.WEATHER_CODE_MAX) {
if (code != WeatherContract.WeatherColumns.WeatherCode.NOT_AVAILABLE) {
return false;
}
}
return true;
}
/**
* @return city name
*/
public String getCity() {
return mCity;
}
/**
* @return An implementation specific weather condition code
*/
public int getConditionCode() {
return mConditionCode;
}
/**
* @return humidity
*/
public double getHumidity() {
return mHumidity;
}
/**
* @return time stamp when the request was processed
*/
public long getTimestamp() {
return mTimestamp;
}
/**
* @return wind direction (degrees)
*/
public double getWindDirection() {
return mWindDirection;
}
/**
* @return wind speed
*/
public double getWindSpeed() {
return mWindSpeed;
}
/**
* @return wind speed unit
*/
public int getWindSpeedUnit() {
return mWindSpeedUnit;
}
/**
* @return current temperature
*/
public double getTemperature() {
return mTemperature;
}
/**
* @return temperature unit
*/
public int getTemperatureUnit() {
return mTempUnit;
}
/**
* @return today's high temperature
*/
public double getTodaysHigh() {
return mTodaysHighTemp;
}
/**
* @return today's low temperature
*/
public double getTodaysLow() {
return mTodaysLowTemp;
}
/**
* @return List of {@link lineageos.weather.WeatherInfo.DayForecast}. This list will contain
* the forecast weather for the upcoming days. If you want to know today's high and low
* temperatures, use {@link WeatherInfo#getTodaysHigh()} and {@link WeatherInfo#getTodaysLow()}
*/
public List<DayForecast> getForecasts() {
return new ArrayList<>(mForecastList);
}
private WeatherInfo(Parcel parcel) {
// Read parcelable version via the Concierge
ParcelInfo parcelInfo = Concierge.receiveParcel(parcel);
int parcelableVersion = parcelInfo.getParcelVersion();
if (parcelableVersion >= Build.LINEAGE_VERSION_CODES.ELDERBERRY) {
mKey = parcel.readString();
mCity = parcel.readString();
mConditionCode = parcel.readInt();
mTemperature = parcel.readDouble();
mTempUnit = parcel.readInt();
mHumidity = parcel.readDouble();
mWindSpeed = parcel.readDouble();
mWindDirection = parcel.readDouble();
mWindSpeedUnit = parcel.readInt();
mTodaysHighTemp = parcel.readDouble();
mTodaysLowTemp = parcel.readDouble();
mTimestamp = parcel.readLong();
int forecastListSize = parcel.readInt();
mForecastList = new ArrayList<>();
while (forecastListSize > 0) {
mForecastList.add(DayForecast.CREATOR.createFromParcel(parcel));
forecastListSize--;
}
}
// Complete parcel info for the concierge
parcelInfo.complete();
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
// Tell the concierge to prepare the parcel
ParcelInfo parcelInfo = Concierge.prepareParcel(dest);
// ==== ELDERBERRY =====
dest.writeString(mKey);
dest.writeString(mCity);
dest.writeInt(mConditionCode);
dest.writeDouble(mTemperature);
dest.writeInt(mTempUnit);
dest.writeDouble(mHumidity);
dest.writeDouble(mWindSpeed);
dest.writeDouble(mWindDirection);
dest.writeInt(mWindSpeedUnit);
dest.writeDouble(mTodaysHighTemp);
dest.writeDouble(mTodaysLowTemp);
dest.writeLong(mTimestamp);
dest.writeInt(mForecastList.size());
for (DayForecast dayForecast : mForecastList) {
dayForecast.writeToParcel(dest, 0);
}
// Complete parcel info for the concierge
parcelInfo.complete();
}
public static final Parcelable.Creator<WeatherInfo> CREATOR =
new Parcelable.Creator<WeatherInfo>() {
@Override
public WeatherInfo createFromParcel(Parcel source) {
return new WeatherInfo(source);
}
@Override
public WeatherInfo[] newArray(int size) {
return new WeatherInfo[size];
}
};
/**
* This class represents the weather forecast for a given day. Do not add low and high
* temperatures for the current day in this list. Use
* {@link WeatherInfo.Builder#setTodaysHigh(double)} and
* {@link WeatherInfo.Builder#setTodaysLow(double)} instead.
*/
public static class DayForecast implements Parcelable{
double mLow;
double mHigh;
int mConditionCode;
String mKey;
private DayForecast() {}
/**
* Builder class for {@link DayForecast}
*/
public static class Builder {
double mLow = Double.NaN;
double mHigh = Double.NaN;
int mConditionCode;
/**
* @param conditionCode A valid weather condition code. See
* {@link lineageos.providers.WeatherContract.WeatherColumns.WeatherCode} for valid
* values. Attempting to pass an invalid code will get you an
* IllegalArgumentException
*/
public Builder(int conditionCode) {
if (!isValidWeatherCode(conditionCode)) {
throw new IllegalArgumentException("Invalid weather condition code");
}
mConditionCode = conditionCode;
}
/**
* @param high Forecast high temperature for this day. Attempting to pass an invalid
* double value will get you an IllegalArgumentException
* @return The {@link Builder} instance
*/
public Builder setHigh(double high) {
if (Double.isNaN(high)) {
throw new IllegalArgumentException("Invalid high forecast temperature");
}
mHigh = high;
return this;
}
/**
* @param low Forecast low temperate for this day. Attempting to pass an invalid double
* value will get you an IllegalArgumentException
* @return The {@link Builder} instance
*/
public Builder setLow(double low) {
if (Double.isNaN(low)) {
throw new IllegalArgumentException("Invalid low forecast temperature");
}
mLow = low;
return this;
}
/**
* Combine all of the options that have been set and return a new {@link DayForecast}
* object
* @return {@link DayForecast}
*/
public DayForecast build() {
DayForecast forecast = new DayForecast();
forecast.mLow = this.mLow;
forecast.mHigh = this.mHigh;
forecast.mConditionCode = this.mConditionCode;
forecast.mKey = UUID.randomUUID().toString();
return forecast;
}
}
/**
* @return forecasted low temperature
*/
public double getLow() {
return mLow;
}
/**
* @return not what you think. Returns the forecasted high temperature
*/
public double getHigh() {
return mHigh;
}
/**
* @return forecasted weather condition code. Implementation specific
*/
public int getConditionCode() {
return mConditionCode;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
// Tell the concierge to prepare the parcel
ParcelInfo parcelInfo = Concierge.prepareParcel(dest);
// ==== ELDERBERRY =====
dest.writeString(mKey);
dest.writeDouble(mLow);
dest.writeDouble(mHigh);
dest.writeInt(mConditionCode);
// Complete parcel info for the concierge
parcelInfo.complete();
}
public static final Parcelable.Creator<DayForecast> CREATOR =
new Parcelable.Creator<DayForecast>() {
@Override
public DayForecast createFromParcel(Parcel source) {
return new DayForecast(source);
}
@Override
public DayForecast[] newArray(int size) {
return new DayForecast[size];
}
};
private DayForecast(Parcel parcel) {
// Read parcelable version via the Concierge
ParcelInfo parcelInfo = Concierge.receiveParcel(parcel);
int parcelableVersion = parcelInfo.getParcelVersion();
if (parcelableVersion >= Build.LINEAGE_VERSION_CODES.ELDERBERRY) {
mKey = parcel.readString();
mLow = parcel.readDouble();
mHigh = parcel.readDouble();
mConditionCode = parcel.readInt();
}
// Complete parcel info for the concierge
parcelInfo.complete();
}
@Override
public String toString() {
return new StringBuilder()
.append("{Low temp: ").append(mLow)
.append(" High temp: ").append(mHigh)
.append(" Condition code: ").append(mConditionCode)
.append("}").toString();
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((mKey != null) ? mKey.hashCode() : 0);
return result;
}
@Override
public boolean equals(Object obj) {
if (obj == null) return false;
if (getClass() == obj.getClass()) {
DayForecast forecast = (DayForecast) obj;
return (TextUtils.equals(mKey, forecast.mKey));
}
return false;
}
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder()
.append(" City Name: ").append(mCity)
.append(" Condition Code: ").append(mConditionCode)
.append(" Temperature: ").append(mTemperature)
.append(" Temperature Unit: ").append(mTempUnit)
.append(" Humidity: ").append(mHumidity)
.append(" Wind speed: ").append(mWindSpeed)
.append(" Wind direction: ").append(mWindDirection)
.append(" Wind Speed Unit: ").append(mWindSpeedUnit)
.append(" Today's high temp: ").append(mTodaysHighTemp)
.append(" Today's low temp: ").append(mTodaysLowTemp)
.append(" Timestamp: ").append(mTimestamp).append(" Forecasts: [");
for (DayForecast dayForecast : mForecastList) {
builder.append(dayForecast.toString());
}
return builder.append("]}").toString();
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((mKey != null) ? mKey.hashCode() : 0);
return result;
}
@Override
public boolean equals(Object obj) {
if (obj == null) return false;
if (getClass() == obj.getClass()) {
WeatherInfo info = (WeatherInfo) obj;
return (TextUtils.equals(mKey, info.mKey));
}
return false;
}
}

View File

@ -0,0 +1,274 @@
/*
* Copyright (C) 2016 The CyanogenMod Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package lineageos.weather;
import android.os.Parcel;
import android.os.Parcelable;
import android.text.TextUtils;
import lineageos.os.Build;
import lineageos.os.Concierge;
import lineageos.os.Concierge.ParcelInfo;
import java.util.UUID;
/**
* A class representing a geographical location that a weather service provider can use to
* get weather data from. Each service provider will potentially populate objects of this class
* with different content, so make sure you don't preserve the values when a service provider
* is changed
*/
public final class WeatherLocation implements Parcelable{
private String mCityId;
private String mCity;
private String mState;
private String mPostal;
private String mCountryId;
private String mCountry;
private String mKey;
private WeatherLocation() {}
/**
* Builder class for {@link WeatherLocation}
*/
public static class Builder {
String mCityId = "";
String mCity = "";
String mState = "";
String mPostal = "";
String mCountryId = "";
String mCountry = "";
/**
* @param cityId An identifier for the city (for example WOEID - Where On Earth IDentifier)
* @param cityName The name of the city
*/
public Builder(String cityId, String cityName) {
if (cityId == null || cityName == null) {
throw new IllegalArgumentException("Illegal to set city id AND city to null");
}
this.mCityId = cityId;
this.mCity = cityName;
}
/**
* @param cityName The name of the city
*/
public Builder(String cityName) {
if (cityName == null) {
throw new IllegalArgumentException("City name can't be null");
}
this.mCity = cityName;
}
/**
* @param countryId An identifier for the country (for example ISO alpha-2, ISO alpha-3,
* ISO 3166-1 numeric-3, etc)
* @return The {@link Builder} instance
*/
public Builder setCountryId(String countryId) {
if (countryId == null) {
throw new IllegalArgumentException("Country ID can't be null");
}
this.mCountryId = countryId;
return this;
}
/**
* @param country The country name
* @return The {@link Builder} instance
*/
public Builder setCountry(String country) {
if (country == null) {
throw new IllegalArgumentException("Country can't be null");
}
this.mCountry = country;
return this;
}
/**
* @param postalCode The postal/ZIP code
* @return The {@link Builder} instance
*/
public Builder setPostalCode(String postalCode) {
if (postalCode == null) {
throw new IllegalArgumentException("Postal code/ZIP can't be null");
}
this.mPostal = postalCode;
return this;
}
/**
* @param state The state or territory where the city is located
* @return The {@link Builder} instance
*/
public Builder setState(String state) {
if (state == null) {
throw new IllegalArgumentException("State can't be null");
}
this.mState = state;
return this;
}
/**
* Combine all of the options that have been set and return a new {@link WeatherLocation}
* object
* @return {@link WeatherLocation}
*/
public WeatherLocation build() {
WeatherLocation weatherLocation = new WeatherLocation();
weatherLocation.mCityId = this.mCityId;
weatherLocation.mCity = this.mCity;
weatherLocation.mState = this.mState;
weatherLocation.mPostal = this.mPostal;
weatherLocation.mCountryId = this.mCountryId;
weatherLocation.mCountry = this.mCountry;
weatherLocation.mKey = UUID.randomUUID().toString();
return weatherLocation;
}
}
/**
* @return The city ID. This method will return an empty string if the city ID was not set
*/
public String getCityId() {
return mCityId;
}
/**
* @return The city name. This method will return an empty string if the city name was not set
*/
public String getCity() {
return mCity;
}
/**
* @return The state name. This method will return an empty string if the state was not set
*/
public String getState() {
return mState;
}
/**
* @return The postal/ZIP code. This method will return an empty string if the postal/ZIP code
* was not set
*/
public String getPostalCode() {
return mPostal;
}
/**
* @return The country ID. This method will return an empty string if the country ID was not set
*/
public String getCountryId() {
return mCountryId;
}
/**
* @return The country name. This method will return an empty string if the country ID was not
* set
*/
public String getCountry() {
return mCountry;
}
private WeatherLocation(Parcel in) {
// Read parcelable version via the Concierge
ParcelInfo parcelInfo = Concierge.receiveParcel(in);
int parcelableVersion = parcelInfo.getParcelVersion();
if (parcelableVersion >= Build.LINEAGE_VERSION_CODES.ELDERBERRY) {
mKey = in.readString();
mCityId = in.readString();
mCity = in.readString();
mState = in.readString();
mPostal = in.readString();
mCountryId = in.readString();
mCountry = in.readString();
}
// Complete parcel info for the concierge
parcelInfo.complete();
}
public static final Creator<WeatherLocation> CREATOR = new Creator<WeatherLocation>() {
@Override
public WeatherLocation createFromParcel(Parcel in) {
return new WeatherLocation(in);
}
@Override
public WeatherLocation[] newArray(int size) {
return new WeatherLocation[size];
}
};
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
// Tell the concierge to prepare the parcel
ParcelInfo parcelInfo = Concierge.prepareParcel(dest);
// ==== ELDERBERRY =====
dest.writeString(mKey);
dest.writeString(mCityId);
dest.writeString(mCity);
dest.writeString(mState);
dest.writeString(mPostal);
dest.writeString(mCountryId);
dest.writeString(mCountry);
// Complete parcel info for the concierge
parcelInfo.complete();
}
@Override
public String toString() {
return new StringBuilder()
.append("{ City ID: ").append(mCityId)
.append(" City: ").append(mCity)
.append(" State: ").append(mState)
.append(" Postal/ZIP Code: ").append(mPostal)
.append(" Country Id: ").append(mCountryId)
.append(" Country: ").append(mCountry).append("}")
.toString();
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((mKey != null) ? mKey.hashCode() : 0);
return result;
}
@Override
public boolean equals(Object obj) {
if (obj == null) return false;
if (getClass() == obj.getClass()) {
WeatherLocation location = (WeatherLocation) obj;
return (TextUtils.equals(mKey, location.mKey));
}
return false;
}
}

View File

@ -0,0 +1,84 @@
/*
* Copyright (C) 2016 The CyanogenMod Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package lineageos.weather.util;
import lineageos.providers.WeatherContract;
import java.text.DecimalFormat;
/**
* Helper class to perform operations and formatting of weather data
*/
public class WeatherUtils {
/**
* Converts a temperature expressed in degrees Celsius to degrees Fahrenheit
* @param celsius temperature in Celsius
* @return the temperature in degrees Fahrenheit
*/
public static double celsiusToFahrenheit(double celsius) {
return ((celsius * (9d/5d)) + 32d);
}
/**
* Converts a temperature expressed in degrees Fahrenheit to degrees Celsius
* @param fahrenheit temperature in Fahrenheit
* @return the temperature in degrees Celsius
*/
public static double fahrenheitToCelsius(double fahrenheit) {
return ((fahrenheit - 32d) * (5d/9d));
}
/**
* Returns a string representation of the temperature and unit supplied. The temperature value
* will be half-even rounded.
* @param temperature the temperature value
* @param tempUnit A valid {@link WeatherContract.WeatherColumns.TempUnit}
* @return A string with the format XX&deg;F or XX&deg;C (where XX is the temperature)
* depending on the temperature unit that was provided or null if an invalid unit is supplied
*/
public static String formatTemperature(double temperature, int tempUnit) {
if (!isValidTempUnit(tempUnit)) return null;
if (Double.isNaN(temperature)) return "-";
DecimalFormat noDigitsFormat = new DecimalFormat("0");
String noDigitsTemp = noDigitsFormat.format(temperature);
if (noDigitsTemp.equals("-0")) {
noDigitsTemp = "0";
}
StringBuilder formatted = new StringBuilder()
.append(noDigitsTemp).append("\u00b0");
if (tempUnit == WeatherContract.WeatherColumns.TempUnit.CELSIUS) {
formatted.append("C");
} else if (tempUnit == WeatherContract.WeatherColumns.TempUnit.FAHRENHEIT) {
formatted.append("F");
}
return formatted.toString();
}
private static boolean isValidTempUnit(int unit) {
switch (unit) {
case WeatherContract.WeatherColumns.TempUnit.CELSIUS:
case WeatherContract.WeatherColumns.TempUnit.FAHRENHEIT:
return true;
default:
return false;
}
}
}

View File

@ -0,0 +1,161 @@
/*
* Copyright (C) 2016 The CyanogenMod Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package lineageos.weatherservice;
import android.os.RemoteException;
import androidx.annotation.NonNull;
import lineageos.weatherservice.IWeatherProviderServiceClient;
import lineageos.weather.LineageWeatherManager;
import lineageos.weather.RequestInfo;
/**
* This class represents a request submitted by the system to the active weather provider service
*/
public final class ServiceRequest {
private final RequestInfo mInfo;
private final IWeatherProviderServiceClient mClient;
private enum Status {
IN_PROGRESS, COMPLETED, CANCELLED, FAILED, REJECTED
}
private Status mStatus;
/* package */ ServiceRequest(RequestInfo info, IWeatherProviderServiceClient client) {
mInfo = info;
mClient = client;
mStatus = Status.IN_PROGRESS;
}
/**
* Obtains the request information
* @return {@link lineageos.weather.RequestInfo}
*/
public RequestInfo getRequestInfo() {
return mInfo;
}
/**
* This method should be called once the request has been completed
*/
public void complete(@NonNull ServiceRequestResult result) {
synchronized (this) {
if (mStatus.equals(Status.IN_PROGRESS)) {
try {
final int requestType = mInfo.getRequestType();
switch (requestType) {
case RequestInfo.TYPE_WEATHER_BY_GEO_LOCATION_REQ:
case RequestInfo.TYPE_WEATHER_BY_WEATHER_LOCATION_REQ:
if (result.getWeatherInfo() == null) {
throw new IllegalStateException("The service request result doesn't"
+ " contain a valid WeatherInfo object");
}
mClient.setServiceRequestState(mInfo, result,
LineageWeatherManager.RequestStatus.COMPLETED);
break;
case RequestInfo.TYPE_LOOKUP_CITY_NAME_REQ:
if (result.getLocationLookupList() == null
|| result.getLocationLookupList().size() <= 0) {
//In case the user decided to mark this request as completed with
//null or empty list. It's not necessarily a failure
mClient.setServiceRequestState(mInfo, null,
LineageWeatherManager.RequestStatus.NO_MATCH_FOUND);
} else {
mClient.setServiceRequestState(mInfo, result,
LineageWeatherManager.RequestStatus.COMPLETED);
}
break;
}
} catch (RemoteException e) {
}
mStatus = Status.COMPLETED;
}
}
}
/**
* This method should be called if the service failed to process the request
* (no internet connection, time out, etc.)
*/
public void fail() {
synchronized (this) {
if (mStatus.equals(Status.IN_PROGRESS)) {
try {
final int requestType = mInfo.getRequestType();
switch (requestType) {
case RequestInfo.TYPE_WEATHER_BY_GEO_LOCATION_REQ:
case RequestInfo.TYPE_WEATHER_BY_WEATHER_LOCATION_REQ:
mClient.setServiceRequestState(mInfo, null,
LineageWeatherManager.RequestStatus.FAILED);
break;
case RequestInfo.TYPE_LOOKUP_CITY_NAME_REQ:
mClient.setServiceRequestState(mInfo, null,
LineageWeatherManager.RequestStatus.FAILED);
break;
}
} catch (RemoteException e) {
}
mStatus = Status.FAILED;
}
}
}
/**
* This method should be called if the service decides not to honor the request. Note this
* method will accept only the following values.
* <ul>
* <li>{@link lineageos.weather.LineageWeatherManager.RequestStatus#SUBMITTED_TOO_SOON}</li>
* <li>{@link lineageos.weather.LineageWeatherManager.RequestStatus#ALREADY_IN_PROGRESS}</li>
* </ul>
* Attempting to pass any other value will get you an IllegalArgumentException
* @param status
*/
public void reject(int status) {
synchronized (this) {
if (mStatus.equals(Status.IN_PROGRESS)) {
switch (status) {
case LineageWeatherManager.RequestStatus.ALREADY_IN_PROGRESS:
case LineageWeatherManager.RequestStatus.SUBMITTED_TOO_SOON:
try {
mClient.setServiceRequestState(mInfo, null, status);
} catch (RemoteException e) {
e.printStackTrace();
}
break;
default:
throw new IllegalArgumentException("Can't reject with status " + status);
}
mStatus = Status.REJECTED;
}
}
}
/**
* Called by the WeatherProviderService base class to notify we don't want this request anymore.
* The service implementing the WeatherProviderService will be notified of this action
* via onRequestCancelled()
* @hide
*/
public void cancel() {
synchronized (this) {
mStatus = Status.CANCELLED;
}
}
}

View File

@ -0,0 +1,197 @@
/*
* Copyright (C) 2016 The CyanogenMod Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package lineageos.weatherservice;
import android.os.Parcel;
import android.os.Parcelable;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import lineageos.os.Build;
import lineageos.os.Concierge;
import lineageos.os.Concierge.ParcelInfo;
import lineageos.weather.WeatherLocation;
import lineageos.weather.WeatherInfo;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
/**
* Use this class to build a request result.
*/
public final class ServiceRequestResult implements Parcelable {
private WeatherInfo mWeatherInfo;
private List<WeatherLocation> mLocationLookupList;
private String mKey;
private ServiceRequestResult() {}
private ServiceRequestResult(Parcel in) {
// Read parcelable version via the Concierge
ParcelInfo parcelInfo = Concierge.receiveParcel(in);
int parcelableVersion = parcelInfo.getParcelVersion();
if (parcelableVersion >= Build.LINEAGE_VERSION_CODES.ELDERBERRY) {
mKey = in.readString();
int hasWeatherInfo = in.readInt();
if (hasWeatherInfo == 1) {
mWeatherInfo = WeatherInfo.CREATOR.createFromParcel(in);
}
int hasLocationLookupList = in.readInt();
if (hasLocationLookupList == 1) {
mLocationLookupList = new ArrayList<>();
int listSize = in.readInt();
while (listSize > 0) {
mLocationLookupList.add(WeatherLocation.CREATOR.createFromParcel(in));
listSize--;
}
}
}
// Complete parcel info for the concierge
parcelInfo.complete();
}
public static final Creator<ServiceRequestResult> CREATOR
= new Creator<ServiceRequestResult>() {
@Override
public ServiceRequestResult createFromParcel(Parcel in) {
return new ServiceRequestResult(in);
}
@Override
public ServiceRequestResult[] newArray(int size) {
return new ServiceRequestResult[size];
}
};
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
// Tell the concierge to prepare the parcel
ParcelInfo parcelInfo = Concierge.prepareParcel(dest);
// ==== ELDERBERRY =====
dest.writeString(mKey);
if (mWeatherInfo != null) {
dest.writeInt(1);
mWeatherInfo.writeToParcel(dest, 0);
} else {
dest.writeInt(0);
}
if (mLocationLookupList != null) {
dest.writeInt(1);
dest.writeInt(mLocationLookupList.size());
for (WeatherLocation lookup : mLocationLookupList) {
lookup.writeToParcel(dest, 0);
}
} else {
dest.writeInt(0);
}
// Complete parcel info for the concierge
parcelInfo.complete();
}
/**
* Builder class for {@link ServiceRequestResult}
*/
public static class Builder {
private WeatherInfo mWeatherInfo;
private List<WeatherLocation> mLocationLookupList;
public Builder() {
this.mWeatherInfo = null;
this.mLocationLookupList = null;
}
/**
* @param weatherInfo The WeatherInfo object holding the data that will be used to update
* the weather content provider
*/
public Builder(@NonNull WeatherInfo weatherInfo) {
if (weatherInfo == null) {
throw new IllegalArgumentException("WeatherInfo can't be null");
}
mWeatherInfo = weatherInfo;
}
/**
* @param locations The list of WeatherLocation objects. The list should not be null
*/
public Builder(@NonNull List<WeatherLocation> locations) {
if (locations == null) {
throw new IllegalArgumentException("Weather location list can't be null");
}
mLocationLookupList = locations;
}
/**
* Creates a {@link ServiceRequestResult} with the arguments
* supplied to this builder
* @return {@link ServiceRequestResult}
*/
public ServiceRequestResult build() {
ServiceRequestResult result = new ServiceRequestResult();
result.mWeatherInfo = this.mWeatherInfo;
result.mLocationLookupList = this.mLocationLookupList;
result.mKey = UUID.randomUUID().toString();
return result;
}
}
/**
* @return The WeatherInfo object supplied by the weather provider service
*/
public WeatherInfo getWeatherInfo() {
return mWeatherInfo;
}
/**
* @return The list of WeatherLocation objects supplied by the weather provider service
*/
public List<WeatherLocation> getLocationLookupList() {
return new ArrayList<>(mLocationLookupList);
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((mKey != null) ? mKey.hashCode() : 0);
return result;
}
@Override
public boolean equals(Object obj) {
if (obj == null) return false;
if (getClass() == obj.getClass()) {
ServiceRequestResult request = (ServiceRequestResult) obj;
return (TextUtils.equals(mKey, request.mKey));
}
return false;
}
}

View File

@ -0,0 +1,181 @@
/*
* Copyright (C) 2016 The CyanogenMod Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package lineageos.weatherservice;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
import lineageos.weather.RequestInfo;
import java.util.Collections;
import java.util.Set;
import java.util.WeakHashMap;
public abstract class WeatherProviderService extends Service {
private Handler mHandler;
private IWeatherProviderServiceClient mClient;
private Set<ServiceRequest> mWeakRequestsSet
= Collections.newSetFromMap(new WeakHashMap<ServiceRequest, Boolean>());
/**
* The {@link android.content.Intent} action that must be declared as handled by a service in
* its manifest for the system to recognize it as a weather provider service
*/
public static final String SERVICE_INTERFACE
= "lineageos.weatherservice.WeatherProviderService";
/**
* Name under which a {@link WeatherProviderService} publishes information about itself.
* This meta-data must reference an XML resource containing
* a <code>&lt;weather-provider-service&gt;</code>
* tag.
*/
public static final String SERVICE_META_DATA = "lineageos.weatherservice";
@Override
protected final void attachBaseContext(Context base) {
super.attachBaseContext(base);
mHandler = new ServiceHandler(base.getMainLooper());
}
@Override
public final IBinder onBind(Intent intent) {
return mBinder;
}
private final IWeatherProviderService.Stub mBinder = new IWeatherProviderService.Stub() {
@Override
public void processWeatherUpdateRequest(final RequestInfo info) {
mHandler.obtainMessage(ServiceHandler.MSG_ON_NEW_REQUEST, info).sendToTarget();
}
@Override
public void processCityNameLookupRequest(final RequestInfo info) {
mHandler.obtainMessage(ServiceHandler.MSG_ON_NEW_REQUEST, info).sendToTarget();
}
@Override
public void setServiceClient(IWeatherProviderServiceClient client) {
mHandler.obtainMessage(ServiceHandler.MSG_SET_CLIENT, client).sendToTarget();
}
@Override
public void cancelOngoingRequests() {
synchronized (mWeakRequestsSet) {
for (final ServiceRequest request : mWeakRequestsSet) {
request.cancel();
mWeakRequestsSet.remove(request);
mHandler.obtainMessage(ServiceHandler.MSG_CANCEL_REQUEST, request)
.sendToTarget();
}
}
}
@Override
public void cancelRequest(int requestId) {
synchronized (mWeakRequestsSet) {
for (final ServiceRequest request : mWeakRequestsSet) {
if (request.getRequestInfo().hashCode() == requestId) {
mWeakRequestsSet.remove(request);
request.cancel();
mHandler.obtainMessage(ServiceHandler.MSG_CANCEL_REQUEST, request)
.sendToTarget();
break;
}
}
}
}
};
private class ServiceHandler extends Handler {
public ServiceHandler(Looper looper) {
super(looper);
}
public static final int MSG_SET_CLIENT = 1;
public static final int MSG_ON_NEW_REQUEST = 2;
public static final int MSG_CANCEL_REQUEST = 3;
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_SET_CLIENT: {
mClient = (IWeatherProviderServiceClient) msg.obj;
if (mClient != null) {
onConnected();
} else {
onDisconnected();
}
return;
}
case MSG_ON_NEW_REQUEST: {
RequestInfo info = (RequestInfo) msg.obj;
if (info != null) {
ServiceRequest request = new ServiceRequest(info, mClient);
synchronized (mWeakRequestsSet) {
mWeakRequestsSet.add(request);
}
onRequestSubmitted(request);
}
return;
}
case MSG_CANCEL_REQUEST: {
ServiceRequest request = (ServiceRequest) msg.obj;
onRequestCancelled(request);
return;
}
}
}
}
/**
* The system has connected to this service.
*/
protected void onConnected() {
/* Do nothing */
}
/**
* The system has disconnected from this service.
*/
protected void onDisconnected() {
/* Do nothing */
}
/**
* A new request has been submitted to this service
* @param request The service request to be processed by this service
*/
protected abstract void onRequestSubmitted(ServiceRequest request);
/**
* Called when the system is not interested on this request anymore. Note that the service
* <b>has marked the request as cancelled</b> and you must stop any ongoing operation
* (such as pulling data from internet) that this service could've been performing to honor the
* request.
*
* @param request The request cancelled by the system
*/
protected abstract void onRequestCancelled(ServiceRequest request);
}

View File

@ -1,5 +1,5 @@
/* Copyright (C) 2015-2018 Andreas Shimokawa, Carsten Pfeiffer, Daniele
Gobbetti, Martin, Normano64, Pavel Elagin, Taavi Eomäe
/* Copyright (C) 2015-2019 Andreas Shimokawa, Carsten Pfeiffer, Daniele
Gobbetti, Martin, Matthieu Baerts, Normano64, Pavel Elagin, Taavi Eomäe
This file is part of Gadgetbridge.
@ -39,12 +39,16 @@ import android.os.Build;
import android.os.Build.VERSION;
import android.preference.PreferenceManager;
import android.provider.ContactsContract.PhoneLookup;
import android.support.v4.content.LocalBroadcastManager;
import android.util.Log;
import android.util.TypedValue;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
@ -58,11 +62,14 @@ import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
import nodomain.freeyourgadget.gadgetbridge.database.DBOpenHelper;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceManager;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoMaster;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.externalevents.BluetoothStateChangeReceiver;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceService;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceService;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
import nodomain.freeyourgadget.gadgetbridge.service.NotificationCollectorMonitorService;
import nodomain.freeyourgadget.gadgetbridge.util.AndroidUtils;
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
@ -71,6 +78,17 @@ import nodomain.freeyourgadget.gadgetbridge.util.GBPrefs;
import nodomain.freeyourgadget.gadgetbridge.util.LimitedQueue;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceType.AMAZFITBIP;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceType.AMAZFITCOR;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceType.AMAZFITCOR2;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceType.HPLUS;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceType.ID115;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceType.MIBAND;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceType.MIBAND2;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceType.MIBAND3;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceType.MIBAND4;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceType.ZETIME;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceType.fromKey;
import static nodomain.freeyourgadget.gadgetbridge.util.GB.NOTIFICATION_CHANNEL_ID;
/**
@ -88,7 +106,7 @@ public class GBApplication extends Application {
private static SharedPreferences sharedPrefs;
private static final String PREFS_VERSION = "shared_preferences_version";
//if preferences have to be migrated, increment the following and add the migration logic in migratePrefs below; see http://stackoverflow.com/questions/16397848/how-can-i-migrate-android-preferences-with-a-new-version
private static final int CURRENT_PREFS_VERSION = 2;
private static final int CURRENT_PREFS_VERSION = 7;
private static LimitedQueue mIDSenderLookup = new LimitedQueue(16);
private static Prefs prefs;
private static GBPrefs gbPrefs;
@ -101,6 +119,7 @@ public class GBApplication extends Application {
public static final String ACTION_QUIT
= "nodomain.freeyourgadget.gadgetbridge.gbapplication.action.quit";
public static final String ACTION_LANGUAGE_CHANGE = "nodomain.freeyourgadget.gadgetbridge.gbapplication.action.language_change";
public static final String ACTION_NEW_DATA = "nodomain.freeyourgadget.gadgetbridge.action.new_data";
private static GBApplication app;
@ -183,9 +202,9 @@ public class GBApplication extends Application {
if (isRunningMarshmallowOrLater()) {
notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
//the following will ensure the notification manager is kept alive
if(isRunningOreoOrLater()) {
if (isRunningOreoOrLater()) {
NotificationChannel channel = notificationManager.getNotificationChannel(NOTIFICATION_CHANNEL_ID);
if(channel == null) {
if (channel == null) {
channel = new NotificationChannel(NOTIFICATION_CHANNEL_ID,
getString(R.string.notification_channel_name),
NotificationManager.IMPORTANCE_LOW);
@ -228,7 +247,7 @@ public class GBApplication extends Application {
logging.setupLogging(enabled);
}
public static String getLogPath(){
public static String getLogPath() {
return logging.getLogPath();
}
@ -316,11 +335,12 @@ public class GBApplication extends Application {
public static boolean isRunningMarshmallowOrLater() {
return VERSION.SDK_INT >= Build.VERSION_CODES.M;
}
public static boolean isRunningNougatOrLater() {
return VERSION.SDK_INT >= Build.VERSION_CODES.N;
}
public static boolean isRunningOreoOrLater(){
public static boolean isRunningOreoOrLater() {
return VERSION.SDK_INT >= Build.VERSION_CODES.O;
}
@ -399,7 +419,7 @@ public class GBApplication extends Application {
private static void loadAppsNotifBlackList() {
GB.log("Loading apps_notification_blacklist", GB.INFO, null);
apps_notification_blacklist = (HashSet<String>) sharedPrefs.getStringSet(GBPrefs.PACKAGE_BLACKLIST, null);
apps_notification_blacklist = (HashSet<String>) sharedPrefs.getStringSet(GBPrefs.PACKAGE_BLACKLIST, null); // lgtm [java/abstract-to-concrete-cast]
if (apps_notification_blacklist == null) {
apps_notification_blacklist = new HashSet<>();
}
@ -451,7 +471,7 @@ public class GBApplication extends Application {
private static void loadAppsPebbleBlackList() {
GB.log("Loading apps_pebblemsg_blacklist", GB.INFO, null);
apps_pebblemsg_blacklist = (HashSet<String>) sharedPrefs.getStringSet(GBPrefs.PACKAGE_PEBBLEMSG_BLACKLIST, null);
apps_pebblemsg_blacklist = (HashSet<String>) sharedPrefs.getStringSet(GBPrefs.PACKAGE_PEBBLEMSG_BLACKLIST, null); // lgtm [java/abstract-to-concrete-cast]
if (apps_pebblemsg_blacklist == null) {
apps_pebblemsg_blacklist = new HashSet<>();
}
@ -481,14 +501,14 @@ public class GBApplication extends Application {
saveAppsPebbleBlackList();
}
public static String packageNameToPebbleMsgSender(String packageName) {
if ("eu.siacs.conversations".equals(packageName)){
return("Conversations");
} else if ("net.osmand.plus".equals(packageName)) {
return("OsmAnd");
public static String packageNameToPebbleMsgSender(String packageName) {
if ("eu.siacs.conversations".equals(packageName)) {
return ("Conversations");
} else if ("net.osmand.plus".equals(packageName)) {
return ("OsmAnd");
}
return packageName;
}
return packageName;
}
private static HashSet<String> calendars_blacklist = null;
@ -523,7 +543,7 @@ public static String packageNameToPebbleMsgSender(String packageName) {
private static void loadCalendarsBlackList() {
GB.log("Loading calendars_blacklist", GB.INFO, null);
calendars_blacklist = (HashSet<String>) sharedPrefs.getStringSet(GBPrefs.CALENDAR_BLACKLIST, null);
calendars_blacklist = (HashSet<String>) sharedPrefs.getStringSet(GBPrefs.CALENDAR_BLACKLIST, null); // lgtm [java/abstract-to-concrete-cast]
if (calendars_blacklist == null) {
calendars_blacklist = new HashSet<>();
}
@ -579,49 +599,310 @@ public static String packageNameToPebbleMsgSender(String packageName) {
}
}
private void migrateStringPrefToPerDevicePref(String globalPref, String globalPrefDefault, String perDevicePref, ArrayList<DeviceType> deviceTypes) {
SharedPreferences.Editor editor = sharedPrefs.edit();
String globalPrefValue = prefs.getString(globalPref, globalPrefDefault);
try (DBHandler db = acquireDB()) {
DaoSession daoSession = db.getDaoSession();
List<Device> activeDevices = DBHelper.getActiveDevices(daoSession);
for (Device dbDevice : activeDevices) {
SharedPreferences deviceSpecificSharedPrefs = GBApplication.getDeviceSpecificSharedPrefs(dbDevice.getIdentifier());
if (deviceSpecificSharedPrefs != null) {
SharedPreferences.Editor deviceSharedPrefsEdit = deviceSpecificSharedPrefs.edit();
DeviceType deviceType = fromKey(dbDevice.getType());
if (deviceTypes.contains(deviceType)) {
Log.i(TAG, "migrating global string preference " + globalPref + " for " + deviceType.name() + " " + dbDevice.getIdentifier() );
deviceSharedPrefsEdit.putString(perDevicePref, globalPrefValue);
}
deviceSharedPrefsEdit.apply();
}
}
editor.remove(globalPref);
editor.apply();
} catch (Exception e) {
Log.w(TAG, "error acquiring DB lock");
}
}
private void migrateBooleanPrefToPerDevicePref(String globalPref, Boolean globalPrefDefault, String perDevicePref, ArrayList<DeviceType> deviceTypes) {
SharedPreferences.Editor editor = sharedPrefs.edit();
boolean globalPrefValue = prefs.getBoolean(globalPref, globalPrefDefault);
try (DBHandler db = acquireDB()) {
DaoSession daoSession = db.getDaoSession();
List<Device> activeDevices = DBHelper.getActiveDevices(daoSession);
for (Device dbDevice : activeDevices) {
SharedPreferences deviceSpecificSharedPrefs = GBApplication.getDeviceSpecificSharedPrefs(dbDevice.getIdentifier());
if (deviceSpecificSharedPrefs != null) {
SharedPreferences.Editor deviceSharedPrefsEdit = deviceSpecificSharedPrefs.edit();
DeviceType deviceType = fromKey(dbDevice.getType());
if (deviceTypes.contains(deviceType)) {
Log.i(TAG, "migrating global boolean preference " + globalPref + " for " + deviceType.name() + " " + dbDevice.getIdentifier() );
deviceSharedPrefsEdit.putBoolean(perDevicePref, globalPrefValue);
}
deviceSharedPrefsEdit.apply();
}
}
editor.remove(globalPref);
editor.apply();
} catch (Exception e) {
Log.w(TAG, "error acquiring DB lock");
}
}
private void migratePrefs(int oldVersion) {
SharedPreferences.Editor editor = sharedPrefs.edit();
switch (oldVersion) {
case 0:
String legacyGender = sharedPrefs.getString("mi_user_gender", null);
String legacyHeight = sharedPrefs.getString("mi_user_height_cm", null);
String legacyWeight = sharedPrefs.getString("mi_user_weight_kg", null);
String legacyYOB = sharedPrefs.getString("mi_user_year_of_birth", null);
if (legacyGender != null) {
int gender = "male".equals(legacyGender) ? 1 : "female".equals(legacyGender) ? 0 : 2;
editor.putString(ActivityUser.PREF_USER_GENDER, Integer.toString(gender));
editor.remove("mi_user_gender");
}
if (legacyHeight != null) {
editor.putString(ActivityUser.PREF_USER_HEIGHT_CM, legacyHeight);
editor.remove("mi_user_height_cm");
}
if (legacyWeight != null) {
editor.putString(ActivityUser.PREF_USER_WEIGHT_KG, legacyWeight);
editor.remove("mi_user_weight_kg");
}
if (legacyYOB != null) {
editor.putString(ActivityUser.PREF_USER_YEAR_OF_BIRTH, legacyYOB);
editor.remove("mi_user_year_of_birth");
}
editor.putString(PREFS_VERSION, Integer.toString(CURRENT_PREFS_VERSION));
break;
case 1:
//migrate the integer version of gender introduced in version 1 to a string value, needed for the way Android accesses the shared preferences
int legacyGender_1 = 2;
try {
legacyGender_1 = sharedPrefs.getInt(ActivityUser.PREF_USER_GENDER, 2);
} catch (Exception e) {
Log.e(TAG, "Could not access legacy activity gender", e);
}
editor.putString(ActivityUser.PREF_USER_GENDER, Integer.toString(legacyGender_1));
//also silently migrate the version to a string value
editor.putString(PREFS_VERSION, Integer.toString(CURRENT_PREFS_VERSION));
break;
if (oldVersion == 0) {
String legacyGender = sharedPrefs.getString("mi_user_gender", null);
String legacyHeight = sharedPrefs.getString("mi_user_height_cm", null);
String legacyWeight = sharedPrefs.getString("mi_user_weight_kg", null);
String legacyYOB = sharedPrefs.getString("mi_user_year_of_birth", null);
if (legacyGender != null) {
int gender = "male".equals(legacyGender) ? 1 : "female".equals(legacyGender) ? 0 : 2;
editor.putString(ActivityUser.PREF_USER_GENDER, Integer.toString(gender));
editor.remove("mi_user_gender");
}
if (legacyHeight != null) {
editor.putString(ActivityUser.PREF_USER_HEIGHT_CM, legacyHeight);
editor.remove("mi_user_height_cm");
}
if (legacyWeight != null) {
editor.putString(ActivityUser.PREF_USER_WEIGHT_KG, legacyWeight);
editor.remove("mi_user_weight_kg");
}
if (legacyYOB != null) {
editor.putString(ActivityUser.PREF_USER_YEAR_OF_BIRTH, legacyYOB);
editor.remove("mi_user_year_of_birth");
}
}
if (oldVersion < 2) {
//migrate the integer version of gender introduced in version 1 to a string value, needed for the way Android accesses the shared preferences
int legacyGender_1 = 2;
try {
legacyGender_1 = sharedPrefs.getInt(ActivityUser.PREF_USER_GENDER, 2);
} catch (Exception e) {
Log.e(TAG, "Could not access legacy activity gender", e);
}
editor.putString(ActivityUser.PREF_USER_GENDER, Integer.toString(legacyGender_1));
}
if (oldVersion < 3) {
try (DBHandler db = acquireDB()) {
DaoSession daoSession = db.getDaoSession();
List<Device> activeDevices = DBHelper.getActiveDevices(daoSession);
for (Device dbDevice : activeDevices) {
SharedPreferences deviceSpecificSharedPrefs = GBApplication.getDeviceSpecificSharedPrefs(dbDevice.getIdentifier());
if (deviceSpecificSharedPrefs != null) {
SharedPreferences.Editor deviceSharedPrefsEdit = deviceSpecificSharedPrefs.edit();
String preferenceKey = dbDevice.getIdentifier() + "_lastSportsActivityTimeMillis";
long lastSportsActivityTimeMillis = sharedPrefs.getLong(preferenceKey, 0);
if (lastSportsActivityTimeMillis != 0) {
deviceSharedPrefsEdit.putLong("lastSportsActivityTimeMillis", lastSportsActivityTimeMillis);
editor.remove(preferenceKey);
}
preferenceKey = dbDevice.getIdentifier() + "_lastSyncTimeMillis";
long lastSyncTimeMillis = sharedPrefs.getLong(preferenceKey, 0);
if (lastSyncTimeMillis != 0) {
deviceSharedPrefsEdit.putLong("lastSyncTimeMillis", lastSyncTimeMillis);
editor.remove(preferenceKey);
}
String newLanguage = null;
Set<String> displayItems = null;
DeviceType deviceType = fromKey(dbDevice.getType());
if (deviceType == AMAZFITBIP || deviceType == AMAZFITCOR || deviceType == AMAZFITCOR2) {
int oldLanguage = prefs.getInt("amazfitbip_language", -1);
newLanguage = "auto";
String[] oldLanguageLookup = {"zh_CN", "zh_TW", "en_US", "es_ES", "ru_RU", "de_DE", "it_IT", "fr_FR", "tr_TR"};
if (oldLanguage >= 0 && oldLanguage < oldLanguageLookup.length) {
newLanguage = oldLanguageLookup[oldLanguage];
}
}
if (deviceType == AMAZFITBIP || deviceType == AMAZFITCOR) {
deviceSharedPrefsEdit.putString("disconnect_notification", prefs.getString("disconnect_notification", "off"));
deviceSharedPrefsEdit.putString("disconnect_notification_start", prefs.getString("disconnect_notification_start", "8:00"));
deviceSharedPrefsEdit.putString("disconnect_notification_end", prefs.getString("disconnect_notification_end", "22:00"));
}
if (deviceType == MIBAND2 || deviceType == MIBAND3) {
deviceSharedPrefsEdit.putString("do_not_disturb", prefs.getString("mi2_do_not_disturb", "off"));
deviceSharedPrefsEdit.putString("do_not_disturb_start", prefs.getString("mi2_do_not_disturb_start", "1:00"));
deviceSharedPrefsEdit.putString("do_not_disturb_end", prefs.getString("mi2_do_not_disturb_end", "6:00"));
}
if (dbDevice.getManufacturer().equals("Huami")) {
deviceSharedPrefsEdit.putString("activate_display_on_lift_wrist", prefs.getString("activate_display_on_lift_wrist", "off"));
deviceSharedPrefsEdit.putString("display_on_lift_start", prefs.getString("display_on_lift_start", "0:00"));
deviceSharedPrefsEdit.putString("display_on_lift_end", prefs.getString("display_on_lift_end", "0:00"));
}
switch (deviceType) {
case MIBAND:
deviceSharedPrefsEdit.putBoolean("low_latency_fw_update", prefs.getBoolean("mi_low_latency_fw_update", true));
deviceSharedPrefsEdit.putString("device_time_offset_hours", String.valueOf(prefs.getInt("mi_device_time_offset_hours", 0)));
break;
case AMAZFITCOR:
displayItems = prefs.getStringSet("cor_display_items", null);
break;
case AMAZFITBIP:
displayItems = prefs.getStringSet("bip_display_items", null);
break;
case MIBAND2:
displayItems = prefs.getStringSet("mi2_display_items", null);
deviceSharedPrefsEdit.putBoolean("mi2_enable_text_notifications", prefs.getBoolean("mi2_enable_text_notifications", true));
deviceSharedPrefsEdit.putString("mi2_dateformat", prefs.getString("mi2_dateformat", "dateformat_time"));
deviceSharedPrefsEdit.putBoolean("rotate_wrist_to_cycle_info", prefs.getBoolean("mi2_rotate_wrist_to_switch_info", false));
break;
case MIBAND3:
newLanguage = prefs.getString("miband3_language", "auto");
displayItems = prefs.getStringSet("miband3_display_items", null);
deviceSharedPrefsEdit.putBoolean("swipe_unlock", prefs.getBoolean("mi3_band_screen_unlock", false));
deviceSharedPrefsEdit.putString("night_mode", prefs.getString("mi3_night_mode", "off"));
deviceSharedPrefsEdit.putString("night_mode_start", prefs.getString("mi3_night_mode_start", "16:00"));
deviceSharedPrefsEdit.putString("night_mode_end", prefs.getString("mi3_night_mode_end", "7:00"));
}
if (displayItems != null) {
deviceSharedPrefsEdit.putStringSet("display_items", displayItems);
}
if (newLanguage != null) {
deviceSharedPrefsEdit.putString("language", newLanguage);
}
deviceSharedPrefsEdit.apply();
}
}
editor.remove("amazfitbip_language");
editor.remove("bip_display_items");
editor.remove("cor_display_items");
editor.remove("disconnect_notification");
editor.remove("disconnect_notification_start");
editor.remove("disconnect_notification_end");
editor.remove("activate_display_on_lift_wrist");
editor.remove("display_on_lift_start");
editor.remove("display_on_lift_end");
editor.remove("mi_low_latency_fw_update");
editor.remove("mi_device_time_offset_hours");
editor.remove("mi2_do_not_disturb");
editor.remove("mi2_do_not_disturb_start");
editor.remove("mi2_do_not_disturb_end");
editor.remove("mi2_dateformat");
editor.remove("mi2_display_items");
editor.remove("mi2_rotate_wrist_to_switch_info");
editor.remove("mi2_enable_text_notifications");
editor.remove("mi3_band_screen_unlock");
editor.remove("mi3_night_mode");
editor.remove("mi3_night_mode_start");
editor.remove("mi3_night_mode_end");
editor.remove("miband3_language");
} catch (Exception e) {
Log.w(TAG, "error acquiring DB lock");
}
}
if (oldVersion < 4) {
try (DBHandler db = acquireDB()) {
DaoSession daoSession = db.getDaoSession();
List<Device> activeDevices = DBHelper.getActiveDevices(daoSession);
for (Device dbDevice : activeDevices) {
SharedPreferences deviceSharedPrefs = GBApplication.getDeviceSpecificSharedPrefs(dbDevice.getIdentifier());
SharedPreferences.Editor deviceSharedPrefsEdit = deviceSharedPrefs.edit();
DeviceType deviceType = fromKey(dbDevice.getType());
if (deviceType == MIBAND) {
int deviceTimeOffsetHours = deviceSharedPrefs.getInt("device_time_offset_hours",0);
deviceSharedPrefsEdit.putString("device_time_offset_hours", Integer.toString(deviceTimeOffsetHours) );
}
deviceSharedPrefsEdit.apply();
}
} catch (Exception e) {
Log.w(TAG, "error acquiring DB lock");
}
}
if (oldVersion < 5) {
try (DBHandler db = acquireDB()) {
DaoSession daoSession = db.getDaoSession();
List<Device> activeDevices = DBHelper.getActiveDevices(daoSession);
for (Device dbDevice : activeDevices) {
SharedPreferences deviceSpecificSharedPrefs = GBApplication.getDeviceSpecificSharedPrefs(dbDevice.getIdentifier());
if (deviceSpecificSharedPrefs != null) {
SharedPreferences.Editor deviceSharedPrefsEdit = deviceSpecificSharedPrefs.edit();
DeviceType deviceType = fromKey(dbDevice.getType());
String newWearside = null;
String newOrientation = null;
String newTimeformat = null;
switch (deviceType) {
case AMAZFITBIP:
case AMAZFITCOR:
case AMAZFITCOR2:
case MIBAND:
case MIBAND2:
case MIBAND3:
case MIBAND4:
newWearside = prefs.getString("mi_wearside", "left");
break;
case HPLUS:
newWearside = prefs.getString("hplus_wrist", "left");
newTimeformat = prefs.getString("hplus_timeformat", "24h");
break;
case ID115:
newWearside = prefs.getString("id115_wrist", "left");
newOrientation = prefs.getString("id115_screen_orientation", "horizontal");
break;
case ZETIME:
newWearside = prefs.getString("zetime_wrist", "left");
newTimeformat = prefs.getInt("zetime_timeformat", 1) == 2 ? "am/pm" : "24h";
break;
}
if (newWearside != null) {
deviceSharedPrefsEdit.putString("wearlocation", newWearside);
}
if (newOrientation != null) {
deviceSharedPrefsEdit.putString("screen_orientation", newOrientation);
}
if (newTimeformat != null) {
deviceSharedPrefsEdit.putString("timeformat", newTimeformat);
}
deviceSharedPrefsEdit.apply();
}
}
editor.remove("hplus_timeformat");
editor.remove("hplus_wrist");
editor.remove("id115_wrist");
editor.remove("id115_screen_orientation");
editor.remove("mi_wearside");
editor.remove("zetime_timeformat");
editor.remove("zetime_wrist");
} catch (Exception e) {
Log.w(TAG, "error acquiring DB lock");
}
}
if (oldVersion < 6) {
migrateBooleanPrefToPerDevicePref("mi2_enable_button_action", false, "button_action_enable", new ArrayList<>(Collections.singletonList(MIBAND2)));
migrateBooleanPrefToPerDevicePref("mi2_button_action_vibrate", false, "button_action_vibrate", new ArrayList<>(Collections.singletonList(MIBAND2)));
migrateStringPrefToPerDevicePref("mi_button_press_count", "6", "button_action_press_count", new ArrayList<>(Collections.singletonList(MIBAND2)));
migrateStringPrefToPerDevicePref("mi_button_press_count_max_delay", "2000", "button_action_press_max_interval", new ArrayList<>(Collections.singletonList(MIBAND2)));
migrateStringPrefToPerDevicePref("mi_button_press_count_match_delay", "0", "button_action_broadcast_delay", new ArrayList<>(Collections.singletonList(MIBAND2)));
migrateStringPrefToPerDevicePref("mi_button_press_broadcast", "nodomain.freeyourgadget.gadgetbridge.ButtonPressed", "button_action_broadcast", new ArrayList<>(Collections.singletonList(MIBAND2)));
}
if (oldVersion < 7) {
migrateStringPrefToPerDevicePref("mi_reserve_alarm_calendar","0","reserve_alarms_calendar", new ArrayList<>(Arrays.asList(MIBAND, MIBAND2)));
}
editor.putString(PREFS_VERSION, Integer.toString(CURRENT_PREFS_VERSION));
editor.apply();
}
public static SharedPreferences getDeviceSpecificSharedPrefs(String deviceIdentifier) {
if (deviceIdentifier == null || deviceIdentifier.isEmpty()) {
return null;
}
return context.getSharedPreferences("devicesettings_" + deviceIdentifier, Context.MODE_PRIVATE);
}
public static void setLanguage(String lang) {
if (lang.equals("default")) {
language = Resources.getSystem().getConfiguration().locale;

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2015-2018 Andreas Shimokawa, Carsten Pfeiffer
/* Copyright (C) 2015-2019 Andreas Shimokawa, Carsten Pfeiffer
This file is part of Gadgetbridge.

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2015-2018 Andreas Shimokawa, Carsten Pfeiffer
/* Copyright (C) 2015-2019 Andreas Shimokawa, Carsten Pfeiffer
This file is part of Gadgetbridge.

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2016-2018 Andreas Shimokawa, Carsten Pfeiffer, Taavi Eomäe
/* Copyright (C) 2016-2019 Andreas Shimokawa, Carsten Pfeiffer, Taavi Eomäe
This file is part of Gadgetbridge.

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2016-2018 Carsten Pfeiffer, Daniele Gobbetti, Pavel Elagin
/* Copyright (C) 2016-2019 Carsten Pfeiffer, Daniele Gobbetti, Pavel Elagin
This file is part of Gadgetbridge.

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2015-2018 Carsten Pfeiffer
/* Copyright (C) 2015-2019 Carsten Pfeiffer
This file is part of Gadgetbridge.

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2016-2018 0nse, Carsten Pfeiffer
/* Copyright (C) 2016-2019 0nse, Andreas Shimokawa, Carsten Pfeiffer
This file is part of Gadgetbridge.
@ -27,12 +27,15 @@ import android.os.Build;
import android.widget.RemoteViews;
import android.widget.Toast;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.GregorianCalendar;
import nodomain.freeyourgadget.gadgetbridge.activities.ConfigureAlarms;
import nodomain.freeyourgadget.gadgetbridge.impl.GBAlarm;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser;
import nodomain.freeyourgadget.gadgetbridge.model.Alarm;
import nodomain.freeyourgadget.gadgetbridge.util.AlarmUtils;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
/**
@ -55,7 +58,8 @@ public class SleepAlarmWidget extends AppWidgetProvider {
RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.sleep_alarm_widget);
// Add our own click intent
Intent intent = new Intent(ACTION);
Intent intent = new Intent(context, SleepAlarmWidget.class);
intent.setAction(ACTION);
PendingIntent clickPI = PendingIntent.getBroadcast(
context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
views.setOnClickPendingIntent(R.id.sleepalarmwidget_text, clickPI);
@ -90,23 +94,41 @@ public class SleepAlarmWidget extends AppWidgetProvider {
// current timestamp
GregorianCalendar calendar = new GregorianCalendar();
// add preferred sleep duration
calendar.add(Calendar.HOUR_OF_DAY, userSleepDuration);
if (userSleepDuration > 0) {
calendar.add(Calendar.HOUR_OF_DAY, userSleepDuration);
} else { // probably testing
calendar.add(Calendar.MINUTE, 1);
}
// overwrite the first alarm and activate it, without
// overwrite the first alarm and activate it
GBAlarm alarm = GBAlarm.createSingleShot(0, true, calendar);
alarm.store();
if (GBApplication.isRunningLollipopOrLater()) {
setAlarmViaAlarmManager(context, calendar.getTimeInMillis());
Context appContext = context.getApplicationContext();
if (appContext instanceof GBApplication) {
GBApplication gbApp = (GBApplication) appContext;
GBDevice selectedDevice = gbApp.getDeviceManager().getSelectedDevice();
if (selectedDevice == null || !selectedDevice.isInitialized()) {
GB.toast(context,
context.getString(R.string.appwidget_not_connected),
Toast.LENGTH_LONG, GB.WARN);
return;
}
}
int hours = calendar.get(Calendar.HOUR_OF_DAY);
int minutes = calendar.get(Calendar.MINUTE);
GB.toast(context,
String.format(context.getString(R.string.appwidget_alarms_set), hours, minutes),
context.getString(R.string.appwidget_setting_alarm, hours, minutes),
Toast.LENGTH_SHORT, GB.INFO);
Alarm alarm = AlarmUtils.createSingleShot(0,true, calendar);
ArrayList<Alarm> alarms = new ArrayList<>(1);
alarms.add(alarm);
GBApplication.deviceService().onSetAlarms(alarms);
// if (GBApplication.isRunningLollipopOrLater()) {
// setAlarmViaAlarmManager(context, calendar.getTimeInMillis());
// }
}
}

View File

@ -0,0 +1,225 @@
/* Copyright (C) 2019 Andreas Shimokawa, vanous
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge 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 Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge;
import android.app.PendingIntent;
import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProvider;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.widget.RemoteViews;
import android.widget.Toast;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.concurrent.TimeUnit;
import nodomain.freeyourgadget.gadgetbridge.activities.ControlCenterv2;
import nodomain.freeyourgadget.gadgetbridge.activities.WidgetAlarmsActivity;
import nodomain.freeyourgadget.gadgetbridge.activities.charts.ChartsActivity;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.DailyTotals;
import nodomain.freeyourgadget.gadgetbridge.model.RecordedDataTypes;
import nodomain.freeyourgadget.gadgetbridge.util.AndroidUtils;
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
public class Widget extends AppWidgetProvider {
public static final String WIDGET_CLICK = "nodomain.freeyourgadget.gadgetbridge.WidgetClick";
public static final String APPWIDGET_DELETED = "nodomain.freeyourgadget.gadgetbridge.APPWIDGET_DELETED";
private static final Logger LOG = LoggerFactory.getLogger(Widget.class);
static BroadcastReceiver broadcastReceiver = null;
private GBDevice getSelectedDevice() {
Context context = GBApplication.getContext();
if (!(context instanceof GBApplication)) {
return null;
}
GBApplication gbApp = (GBApplication) context;
return gbApp.getDeviceManager().getSelectedDevice();
}
private int[] getSteps() {
Context context = GBApplication.getContext();
Calendar day = GregorianCalendar.getInstance();
if (!(context instanceof GBApplication)) {
return new int[]{0, 0, 0};
}
DailyTotals ds = new DailyTotals();
return ds.getDailyTotalsForAllDevices(day);
}
private String getHM(long value) {
return DateTimeUtils.formatDurationHoursMinutes(value, TimeUnit.MINUTES);
}
private void updateAppWidget(Context context, AppWidgetManager appWidgetManager,
int appWidgetId) {
GBDevice device = getSelectedDevice();
RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.widget);
//onclick refresh
Intent intent = new Intent(context, Widget.class);
intent.setAction(WIDGET_CLICK);
PendingIntent refreshDataIntent = PendingIntent.getBroadcast(
context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
//views.setOnClickPendingIntent(R.id.todaywidget_bottom_layout, refreshDataIntent);
views.setOnClickPendingIntent(R.id.todaywidget_header_bar, refreshDataIntent);
//open GB main window
Intent startMainIntent = new Intent(context, ControlCenterv2.class);
PendingIntent startMainPIntent = PendingIntent.getActivity(context, 0, startMainIntent, 0);
views.setOnClickPendingIntent(R.id.todaywidget_header_icon, startMainPIntent);
//alarms popup menu
Intent startAlarmListIntent = new Intent(context, WidgetAlarmsActivity.class);
PendingIntent startAlarmListPIntent = PendingIntent.getActivity(context, 0, startAlarmListIntent, 0);
views.setOnClickPendingIntent(R.id.todaywidget_header_plus, startAlarmListPIntent);
//charts, requires device
if (device != null) {
Intent startChartsIntent = new Intent(context, ChartsActivity.class);
startChartsIntent.putExtra(GBDevice.EXTRA_DEVICE, device);
PendingIntent startChartsPIntent = PendingIntent.getActivity(context, 0, startChartsIntent, 0);
views.setOnClickPendingIntent(R.id.todaywidget_bottom_layout, startChartsPIntent);
}
int[] DailyTotals = getSteps();
views.setTextViewText(R.id.todaywidget_steps, context.getString(R.string.widget_steps_label, (int) DailyTotals[0]));
views.setTextViewText(R.id.todaywidget_sleep, context.getString(R.string.widget_sleep_label, getHM((long) DailyTotals[1])));
if (device != null) {
String status = String.format("%1s", device.getStateString());
if (device.isConnected()) {
if (device.getBatteryLevel() > 1) {
status = String.format("Battery %1s%%", device.getBatteryLevel());
}
}
views.setTextViewText(R.id.todaywidget_device_status, status);
views.setTextViewText(R.id.todaywidget_device_name, device.getName());
}
// Instruct the widget manager to update the widget
appWidgetManager.updateAppWidget(appWidgetId, views);
}
public void refreshData() {
Context context = GBApplication.getContext();
GBDevice device = getSelectedDevice();
if (device == null || !device.isInitialized()) {
GB.toast(context,
context.getString(R.string.device_not_connected),
Toast.LENGTH_SHORT, GB.ERROR);
GBApplication.deviceService().connect();
GB.toast(context,
context.getString(R.string.connecting),
Toast.LENGTH_SHORT, GB.INFO);
return;
}
GB.toast(context,
context.getString(R.string.busy_task_fetch_activity_data),
Toast.LENGTH_SHORT, GB.INFO);
GBApplication.deviceService().onFetchRecordedData(RecordedDataTypes.TYPE_ACTIVITY);
}
public void updateWidget() {
Context context = GBApplication.getContext();
AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
ComponentName thisAppWidget = new ComponentName(context.getPackageName(), Widget.class.getName());
int[] appWidgetIds = appWidgetManager.getAppWidgetIds(thisAppWidget);
onUpdate(context, appWidgetManager, appWidgetIds);
}
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
// There may be multiple widgets active, so update all of them
for (int appWidgetId : appWidgetIds) {
updateAppWidget(context, appWidgetManager, appWidgetId);
}
}
@Override
public void onEnabled(Context context) {
if (broadcastReceiver == null) {
LOG.debug("gbwidget BROADCAST receiver initialized.");
broadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
LOG.debug("gbwidget BROADCAST, action" + intent.getAction());
updateWidget();
}
};
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(GBApplication.ACTION_NEW_DATA);
intentFilter.addAction(GBDevice.ACTION_DEVICE_CHANGED);
LocalBroadcastManager.getInstance(context).registerReceiver(broadcastReceiver, intentFilter);
}
}
@Override
public void onDisabled(Context context) {
if (broadcastReceiver != null) {
AndroidUtils.safeUnregisterBroadcastReceiver(context,broadcastReceiver);
broadcastReceiver = null;
}
}
@Override
public void onReceive(Context context, Intent intent) {
super.onReceive(context, intent);
LOG.debug("gbwidget LOCAL onReceive, action: " + intent.getAction());
//this handles widget re-connection after apk updates
if (WIDGET_CLICK.equals(intent.getAction())) {
if (broadcastReceiver == null) {
onEnabled(context);
}
refreshData();
//updateWidget();
} else if (APPWIDGET_DELETED.equals(intent.getAction())) {
onDisabled(context);
}
}
}

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2015-2018 Carsten Pfeiffer
/* Copyright (C) 2015-2019 Carsten Pfeiffer, Daniele Gobbetti
This file is part of Gadgetbridge.
@ -16,13 +16,14 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.activities;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentStatePagerAdapter;
import android.view.ViewGroup;
import java.util.HashSet;
import java.util.Set;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentStatePagerAdapter;
public abstract class AbstractFragmentPagerAdapter extends FragmentStatePagerAdapter {
private final Set<AbstractGBFragment> fragments = new HashSet<>();
private Object primaryFragment;

View File

@ -1,4 +1,5 @@
/* Copyright (C) 2015-2018 Andreas Shimokawa, Carsten Pfeiffer, Lem Dulfo
/* Copyright (C) 2015-2019 Andreas Shimokawa, Carsten Pfeiffer, Daniele
Gobbetti, Lem Dulfo
This file is part of Gadgetbridge.
@ -22,11 +23,11 @@ import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import android.support.v4.content.LocalBroadcastManager;
import android.support.v7.app.AppCompatActivity;
import java.util.Locale;
import androidx.appcompat.app.AppCompatActivity;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.util.AndroidUtils;

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2015-2018 Andreas Shimokawa, Carsten Pfeiffer, Daniele
/* Copyright (C) 2015-2019 Andreas Shimokawa, Carsten Pfeiffer, Daniele
Gobbetti, walkjivefly
This file is part of Gadgetbridge.
@ -17,8 +17,8 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.activities;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
/**
* Abstract base class for fragments. Provides hooks that are called when

View File

@ -1,4 +1,5 @@
/* Copyright (C) 2015-2018 Andreas Shimokawa, Carsten Pfeiffer
/* Copyright (C) 2015-2019 Andreas Shimokawa, Carsten Pfeiffer, Daniele
Gobbetti
This file is part of Gadgetbridge.
@ -17,8 +18,11 @@
package nodomain.freeyourgadget.gadgetbridge.activities;
import android.os.Bundle;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentPagerAdapter;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentPagerAdapter;
import androidx.fragment.app.FragmentStatePagerAdapter;
import androidx.viewpager.widget.PagerAdapter;
/**
* A base activity that supports paging through fragments by swiping.
@ -35,12 +39,12 @@ import android.support.v4.app.FragmentPagerAdapter;
*/
public abstract class AbstractGBFragmentActivity extends AbstractGBActivity {
/**
* The {@link android.support.v4.view.PagerAdapter} that will provide
* The {@link PagerAdapter} that will provide
* fragments for each of the sections. We use a
* {@link FragmentPagerAdapter} derivative, which will keep every
* loaded fragment in memory. If this becomes too memory intensive, it
* may be best to switch to a
* {@link android.support.v4.app.FragmentStatePagerAdapter}.
* {@link FragmentStatePagerAdapter}.
*/
private AbstractFragmentPagerAdapter mSectionsPagerAdapter;

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2017-2018 Andreas Shimokawa, Carsten Pfeiffer
/* Copyright (C) 2017-2019 Andreas Shimokawa, Carsten Pfeiffer
This file is part of Gadgetbridge.

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2015-2018 Andreas Shimokawa, Carsten Pfeiffer, Christian
/* Copyright (C) 2015-2019 Andreas Shimokawa, Carsten Pfeiffer, Christian
Fischer, Daniele Gobbetti, Lem Dulfo
This file is part of Gadgetbridge.
@ -26,8 +26,6 @@ import android.preference.EditTextPreference;
import android.preference.ListPreference;
import android.preference.Preference;
import android.preference.PreferenceManager;
import android.support.v4.app.NavUtils;
import android.support.v4.content.LocalBroadcastManager;
import android.text.InputType;
import android.view.MenuItem;
@ -36,6 +34,8 @@ import org.slf4j.LoggerFactory;
import java.util.Locale;
import androidx.core.app.NavUtils;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.util.AndroidUtils;

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2017-2018 Andreas Shimokawa, Carsten Pfeiffer, Daniele
/* Copyright (C) 2017-2019 Andreas Shimokawa, Carsten Pfeiffer, Daniele
Gobbetti
This file is part of Gadgetbridge.
@ -25,10 +25,6 @@ import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Bundle;
import android.support.design.widget.FloatingActionButton;
import android.support.v4.content.FileProvider;
import android.support.v4.content.LocalBroadcastManager;
import android.support.v4.widget.SwipeRefreshLayout;
import android.util.SparseBooleanArray;
import android.view.ActionMode;
import android.view.Menu;
@ -40,6 +36,12 @@ import android.widget.DatePicker;
import android.widget.ListView;
import android.widget.Toast;
import androidx.core.content.FileProvider;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
@ -239,10 +241,10 @@ public class ActivitySummariesActivity extends AbstractListActivity<BaseActivity
date.set(year, monthOfYear, dayOfMonth);
long timestamp = date.getTimeInMillis() - 1000;
SharedPreferences.Editor editor = GBApplication.getPrefs().getPreferences().edit();
editor.remove(mGBDevice.getAddress() + "_" + "lastSportsActivityTimeMillis"); //FIXME: key reconstruction is BAD
editor.putLong(mGBDevice.getAddress() + "_" + "lastSportsActivityTimeMillis", timestamp);
editor.commit();
SharedPreferences.Editor editor = GBApplication.getDeviceSpecificSharedPrefs(mGBDevice.getAddress()).edit();
editor.remove("lastSportsActivityTimeMillis"); //FIXME: key reconstruction is BAD
editor.putLong("lastSportsActivityTimeMillis", timestamp);
editor.apply();
}
}, currentDate.get(Calendar.YEAR), currentDate.get(Calendar.MONTH), currentDate.get(Calendar.DATE)).show();
}

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2015-2018 Andreas Shimokawa, Carsten Pfeiffer, Daniele
/* Copyright (C) 2015-2019 Andreas Shimokawa, Carsten Pfeiffer, Daniele
Gobbetti, Lem Dulfo
This file is part of Gadgetbridge.
@ -26,14 +26,16 @@ import android.widget.TimePicker;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.impl.GBAlarm;
import nodomain.freeyourgadget.gadgetbridge.entities.Alarm;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.util.AlarmUtils;
import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper;
public class AlarmDetails extends AbstractGBActivity {
private GBAlarm alarm;
private Alarm alarm;
private TimePicker timePicker;
private CheckedTextView cbSmartWakeup;
private CheckedTextView cbMonday;
@ -50,7 +52,7 @@ public class AlarmDetails extends AbstractGBActivity {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_alarm_details);
alarm = getIntent().getParcelableExtra("alarm");
alarm = (Alarm) getIntent().getSerializableExtra(nodomain.freeyourgadget.gadgetbridge.model.Alarm.EXTRA_ALARM);
device = getIntent().getParcelableExtra(GBDevice.EXTRA_DEVICE);
timePicker = findViewById(R.id.alarm_time_picker);
@ -109,17 +111,17 @@ public class AlarmDetails extends AbstractGBActivity {
timePicker.setCurrentHour(alarm.getHour());
timePicker.setCurrentMinute(alarm.getMinute());
cbSmartWakeup.setChecked(alarm.isSmartWakeup());
cbSmartWakeup.setChecked(alarm.getSmartWakeup());
int smartAlarmVisibility = supportsSmartWakeup() ? View.VISIBLE : View.GONE;
cbSmartWakeup.setVisibility(smartAlarmVisibility);
cbMonday.setChecked(alarm.getRepetition(GBAlarm.ALARM_MON));
cbTuesday.setChecked(alarm.getRepetition(GBAlarm.ALARM_TUE));
cbWednesday.setChecked(alarm.getRepetition(GBAlarm.ALARM_WED));
cbThursday.setChecked(alarm.getRepetition(GBAlarm.ALARM_THU));
cbFriday.setChecked(alarm.getRepetition(GBAlarm.ALARM_FRI));
cbSaturday.setChecked(alarm.getRepetition(GBAlarm.ALARM_SAT));
cbSunday.setChecked(alarm.getRepetition(GBAlarm.ALARM_SUN));
cbMonday.setChecked(alarm.getRepetition(Alarm.ALARM_MON));
cbTuesday.setChecked(alarm.getRepetition(Alarm.ALARM_TUE));
cbWednesday.setChecked(alarm.getRepetition(Alarm.ALARM_WED));
cbThursday.setChecked(alarm.getRepetition(Alarm.ALARM_THU));
cbFriday.setChecked(alarm.getRepetition(Alarm.ALARM_FRI));
cbSaturday.setChecked(alarm.getRepetition(Alarm.ALARM_SAT));
cbSunday.setChecked(alarm.getRepetition(Alarm.ALARM_SUN));
}
@ -144,10 +146,11 @@ public class AlarmDetails extends AbstractGBActivity {
private void updateAlarm() {
alarm.setSmartWakeup(supportsSmartWakeup() && cbSmartWakeup.isChecked());
alarm.setRepetition(cbMonday.isChecked(), cbTuesday.isChecked(), cbWednesday.isChecked(), cbThursday.isChecked(), cbFriday.isChecked(), cbSaturday.isChecked(), cbSunday.isChecked());
int repetitionMask = AlarmUtils.createRepetitionMassk(cbMonday.isChecked(), cbTuesday.isChecked(), cbWednesday.isChecked(), cbThursday.isChecked(), cbFriday.isChecked(), cbSaturday.isChecked(), cbSunday.isChecked());
alarm.setRepetition(repetitionMask);
alarm.setHour(timePicker.getCurrentHour());
alarm.setMinute(timePicker.getCurrentMinute());
alarm.store();
DBHelper.store(alarm);
}
@Override

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2015-2018 Andreas Shimokawa, Carsten Pfeiffer, Lem Dulfo
/* Copyright (C) 2015-2019 Andreas Shimokawa, Carsten Pfeiffer, Lem Dulfo
This file is part of Gadgetbridge.

View File

@ -1,5 +1,5 @@
/* Copyright (C) 2015-2018 Andreas Shimokawa, Carsten Pfeiffer, Daniele
Gobbetti, Lem Dulfo
/* Copyright (C) 2015-2019 abettenburg, Andreas Shimokawa, Carsten Pfeiffer,
Daniele Gobbetti, Lem Dulfo
This file is part of Gadgetbridge.
@ -18,10 +18,6 @@
package nodomain.freeyourgadget.gadgetbridge.activities;
import android.os.Bundle;
import android.support.v4.app.NavUtils;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.SearchView;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
@ -29,6 +25,10 @@ import android.view.MenuItem;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import androidx.appcompat.widget.SearchView;
import androidx.core.app.NavUtils;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.adapter.AppBlacklistAdapter;

View File

@ -19,15 +19,16 @@ package nodomain.freeyourgadget.gadgetbridge.activities;
import android.content.res.Configuration;
import android.os.Bundle;
import android.preference.PreferenceActivity;
import android.support.annotation.LayoutRes;
import android.support.annotation.Nullable;
import android.support.v7.app.ActionBar;
import android.support.v7.app.AppCompatDelegate;
import android.support.v7.widget.Toolbar;
import android.view.MenuInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.LayoutRes;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatDelegate;
import androidx.appcompat.widget.Toolbar;
/**
* A {@link android.preference.PreferenceActivity} which implements and proxies the necessary calls
* to be used with AppCompat.

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2017-2018 Carsten Pfeiffer, Daniele Gobbetti
/* Copyright (C) 2017-2019 Carsten Pfeiffer, Daniele Gobbetti
This file is part of Gadgetbridge.
@ -24,10 +24,6 @@ import android.graphics.Paint;
import android.net.Uri;
import android.os.Bundle;
import android.provider.CalendarContract;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.ActivityCompat;
import android.support.v4.app.NavUtils;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
@ -42,6 +38,10 @@ import android.widget.Toast;
import java.util.ArrayList;
import java.util.List;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.app.ActivityCompat;
import androidx.core.app.NavUtils;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.util.GB;

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2015-2018 Andreas Shimokawa, Carsten Pfeiffer, Daniele
/* Copyright (C) 2015-2019 Andreas Shimokawa, Carsten Pfeiffer, Daniele
Gobbetti, Lem Dulfo
This file is part of Gadgetbridge.
@ -17,35 +17,47 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.activities;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.MenuItem;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import androidx.annotation.NonNull;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.adapter.GBAlarmListAdapter;
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst;
import nodomain.freeyourgadget.gadgetbridge.impl.GBAlarm;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.entities.Alarm;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.entities.User;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_MIBAND_ALARMS;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceService;
import nodomain.freeyourgadget.gadgetbridge.util.AlarmUtils;
import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper;
public class ConfigureAlarms extends AbstractGBActivity {
private static final Logger LOG = LoggerFactory.getLogger(ConfigureAlarms.class);
private static final int REQ_CONFIGURE_ALARM = 1;
private GBAlarmListAdapter mGBAlarmListAdapter;
private Set<String> preferencesAlarmListSet;
private boolean avoidSendAlarmsToDevice;
private GBDevice device;
private GBDevice gbDevice;
@Override
protected void onCreate(Bundle savedInstanceState) {
@ -53,28 +65,24 @@ public class ConfigureAlarms extends AbstractGBActivity {
setContentView(R.layout.activity_configure_alarms);
device = getIntent().getParcelableExtra(GBDevice.EXTRA_DEVICE);
IntentFilter filterLocal = new IntentFilter();
filterLocal.addAction(DeviceService.ACTION_SAVE_ALARMS);
LocalBroadcastManager.getInstance(this).registerReceiver(mReceiver, filterLocal);
Prefs prefs = GBApplication.getPrefs();
preferencesAlarmListSet = prefs.getStringSet(PREF_MIBAND_ALARMS, new HashSet<String>());
if (preferencesAlarmListSet.isEmpty()) {
//initialize the preferences
preferencesAlarmListSet = new HashSet<>(Arrays.asList(GBAlarm.DEFAULT_ALARMS));
prefs.getPreferences().edit().putStringSet(PREF_MIBAND_ALARMS, preferencesAlarmListSet).apply();
}
gbDevice = getIntent().getParcelableExtra(GBDevice.EXTRA_DEVICE);
mGBAlarmListAdapter = new GBAlarmListAdapter(this, preferencesAlarmListSet);
mGBAlarmListAdapter = new GBAlarmListAdapter(this);
RecyclerView alarmsRecyclerView = (RecyclerView) findViewById(R.id.alarm_list);
RecyclerView alarmsRecyclerView = findViewById(R.id.alarm_list);
alarmsRecyclerView.setHasFixedSize(true);
alarmsRecyclerView.setLayoutManager(new LinearLayoutManager(this));
alarmsRecyclerView.setAdapter(mGBAlarmListAdapter);
updateAlarmsFromPrefs();
updateAlarmsFromDB();
}
@Override
protected void onPause() {
if (!avoidSendAlarmsToDevice) {
if (!avoidSendAlarmsToDevice && gbDevice.isInitialized()) {
sendAlarmsToDevice();
}
super.onPause();
@ -82,21 +90,65 @@ public class ConfigureAlarms extends AbstractGBActivity {
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQ_CONFIGURE_ALARM) {
avoidSendAlarmsToDevice = false;
updateAlarmsFromPrefs();
updateAlarmsFromDB();
}
}
private void updateAlarmsFromPrefs() {
Prefs prefs = GBApplication.getPrefs();
preferencesAlarmListSet = prefs.getStringSet(PREF_MIBAND_ALARMS, new HashSet<String>());
int reservedSlots = prefs.getInt(MiBandConst.PREF_MIBAND_RESERVE_ALARM_FOR_CALENDAR, 0);
/**
* Reads the available alarms from the database and updates the view afterwards.
*/
private void updateAlarmsFromDB() {
List<Alarm> alarms = DBHelper.getAlarms(getGbDevice());
if (alarms.isEmpty()) {
alarms = AlarmUtils.readAlarmsFromPrefs(getGbDevice());
storeMigratedAlarms(alarms);
}
addMissingAlarms(alarms);
mGBAlarmListAdapter.setAlarmList(preferencesAlarmListSet, reservedSlots);
mGBAlarmListAdapter.setAlarmList(alarms);
mGBAlarmListAdapter.notifyDataSetChanged();
}
private void storeMigratedAlarms(List<Alarm> alarms) {
for (Alarm alarm : alarms) {
DBHelper.store(alarm);
}
}
private void addMissingAlarms(List<Alarm> alarms) {
DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(getGbDevice());
int supportedNumAlarms = coordinator.getAlarmSlotCount();
if (supportedNumAlarms > alarms.size()) {
try (DBHandler db = GBApplication.acquireDB()) {
DaoSession daoSession = db.getDaoSession();
Device device = DBHelper.getDevice(getGbDevice(), daoSession);
User user = DBHelper.getUser(daoSession);
for (int position = 0; position < supportedNumAlarms; position++) {
boolean found = false;
for (Alarm alarm : alarms) {
if (alarm.getPosition() == position) {
found = true;
break;
}
}
if (!found) {
LOG.info("adding missing alarm at position " + position);
alarms.add(position, createDefaultAlarm(device, user, position));
}
}
} catch (Exception e) {
LOG.error("Error accessing database", e);
}
}
}
private Alarm createDefaultAlarm(@NonNull Device device, @NonNull User user, int position) {
return new Alarm(device.getId(), user.getId(), position, false, false, 0, 6, 30, false);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
@ -108,19 +160,40 @@ public class ConfigureAlarms extends AbstractGBActivity {
return super.onOptionsItemSelected(item);
}
public void configureAlarm(GBAlarm alarm) {
public void configureAlarm(Alarm alarm) {
avoidSendAlarmsToDevice = true;
Intent startIntent = new Intent(getApplicationContext(), AlarmDetails.class);
startIntent.putExtra("alarm", alarm);
startIntent.putExtra(GBDevice.EXTRA_DEVICE, getDevice());
startIntent.putExtra(Alarm.EXTRA_ALARM, alarm);
startIntent.putExtra(GBDevice.EXTRA_DEVICE, getGbDevice());
startActivityForResult(startIntent, REQ_CONFIGURE_ALARM);
}
private GBDevice getDevice() {
return device;
private GBDevice getGbDevice() {
return gbDevice;
}
private void sendAlarmsToDevice() {
GBApplication.deviceService().onSetAlarms(mGBAlarmListAdapter.getAlarmList());
}
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
switch (action) {
case DeviceService.ACTION_SAVE_ALARMS: {
updateAlarmsFromDB();
break;
}
}
}
};
@Override
protected void onDestroy() {
LocalBroadcastManager.getInstance(this).unregisterReceiver(mReceiver);
super.onDestroy();
}
}

View File

@ -1,5 +1,5 @@
/* Copyright (C) 2016-2018 Andreas Shimokawa, Carsten Pfeiffer, Daniele
Gobbetti, Taavi Eomäe
/* Copyright (C) 2016-2019 Andreas Shimokawa, Carsten Pfeiffer, Daniele
Gobbetti, Johannes Tysiak, Taavi Eomäe, vanous
This file is part of Gadgetbridge.
@ -27,22 +27,27 @@ import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.design.widget.FloatingActionButton;
import android.support.design.widget.NavigationView;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.support.v4.content.LocalBroadcastManager;
import android.support.v4.view.GravityCompat;
import android.support.v4.widget.DrawerLayout;
import android.support.v7.app.ActionBarDrawerToggle;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.app.AppCompatDelegate;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.Toolbar;
import android.telephony.PhoneStateListener;
import android.telephony.TelephonyManager;
import android.view.MenuItem;
import android.view.View;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.ActionBarDrawerToggle;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.app.AppCompatDelegate;
import androidx.appcompat.widget.Toolbar;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.core.view.GravityCompat;
import androidx.drawerlayout.widget.DrawerLayout;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.google.android.material.navigation.NavigationView;
import java.util.ArrayList;
import java.util.List;
@ -72,9 +77,14 @@ public class ControlCenterv2 extends AppCompatActivity
private GBDeviceAdapterv2 mGBDeviceAdapter;
private RecyclerView deviceListView;
private FloatingActionButton fab;
private boolean isLanguageInvalid = false;
public static final int MENU_REFRESH_CODE=1;
private static PhoneStateListener fakeStateListener;
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
@ -102,14 +112,6 @@ public class ControlCenterv2 extends AppCompatActivity
Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
FloatingActionButton fab = findViewById(R.id.fab);
fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
launchDiscoveryActivity();
}
});
DrawerLayout drawer = findViewById(R.id.drawer_layout);
ActionBarDrawerToggle toggle = new ActionBarDrawerToggle(
this, drawer, toolbar, R.string.controlcenter_navigation_drawer_open, R.string.controlcenter_navigation_drawer_close);
@ -131,6 +133,16 @@ public class ControlCenterv2 extends AppCompatActivity
deviceListView.setAdapter(this.mGBDeviceAdapter);
fab = findViewById(R.id.fab);
fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
launchDiscoveryActivity();
}
});
showFabIfNeccessary();
/* uncomment to enable fixed-swipe to reveal more actions
ItemTouchHelper swipeToDismissTouchHelper = new ItemTouchHelper(new ItemTouchHelper.SimpleCallback(
@ -191,7 +203,12 @@ public class ControlCenterv2 extends AppCompatActivity
ChangeLog cl = createChangeLog();
if (cl.isFirstRun()) {
cl.getLogDialog().show();
try {
cl.getLogDialog().show();
} catch (Exception ignored){
GB.toast(getBaseContext(), "Error showing Changelog", Toast.LENGTH_LONG, GB.ERROR);
}
}
GBApplication.deviceService().start();
@ -229,6 +246,15 @@ public class ControlCenterv2 extends AppCompatActivity
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == MENU_REFRESH_CODE) {
showFabIfNeccessary();
}
}
@Override
public boolean onNavigationItemSelected(@NonNull MenuItem item) {
@ -238,7 +264,7 @@ public class ControlCenterv2 extends AppCompatActivity
switch (item.getItemId()) {
case R.id.action_settings:
Intent settingsIntent = new Intent(this, SettingsActivity.class);
startActivity(settingsIntent);
startActivityForResult(settingsIntent, MENU_REFRESH_CODE);
return true;
case R.id.action_debug:
Intent debugIntent = new Intent(this, DebugActivity.class);
@ -252,6 +278,9 @@ public class ControlCenterv2 extends AppCompatActivity
Intent blIntent = new Intent(this, AppBlacklistActivity.class);
startActivity(blIntent);
return true;
case R.id.device_action_discover:
launchDiscoveryActivity();
return true;
case R.id.action_quit:
GBApplication.quit();
return true;
@ -262,7 +291,11 @@ public class ControlCenterv2 extends AppCompatActivity
return true;
case R.id.external_changelog:
ChangeLog cl = createChangeLog();
cl.getFullLogDialog().show();
try {
cl.getLogDialog().show();
} catch (Exception ignored) {
GB.toast(getBaseContext(), "Error showing Changelog", Toast.LENGTH_LONG, GB.ERROR);
}
return true;
}
@ -276,7 +309,8 @@ public class ControlCenterv2 extends AppCompatActivity
+ "background-color: " + AndroidUtils.getBackgroundColorHex(getBaseContext()) + ";" +
"}";
return new ChangeLog(this, css);
}
}
private void launchDiscoveryActivity() {
startActivity(new Intent(this, DiscoveryActivity.class));
}
@ -285,6 +319,18 @@ public class ControlCenterv2 extends AppCompatActivity
mGBDeviceAdapter.notifyDataSetChanged();
}
private void showFabIfNeccessary() {
if (GBApplication.getPrefs().getBoolean("display_add_device_fab", true)) {
fab.show();
} else {
if (deviceListView.getChildCount() < 1) {
fab.show();
} else {
fab.hide();
}
}
}
@TargetApi(Build.VERSION_CODES.M)
private void checkAndRequestPermissions() {
List<String> wantedPermissions = new ArrayList<>();
@ -320,7 +366,15 @@ public class ControlCenterv2 extends AppCompatActivity
}
if (!wantedPermissions.isEmpty())
ActivityCompat.requestPermissions(this, wantedPermissions.toArray(new String[wantedPermissions.size()]), 0);
ActivityCompat.requestPermissions(this, wantedPermissions.toArray(new String[0]), 0);
// HACK: On Lineage we have to do this so that the permission dialog pops up
if (fakeStateListener == null) {
fakeStateListener = new PhoneStateListener();
TelephonyManager telephonyManager = (TelephonyManager) getSystemService(TELEPHONY_SERVICE);
telephonyManager.listen(fakeStateListener, PhoneStateListener.LISTEN_CALL_STATE);
telephonyManager.listen(fakeStateListener, PhoneStateListener.LISTEN_NONE);
}
}
public void setLanguage(Locale language, boolean invalidateLanguage) {

View File

@ -1,5 +1,5 @@
/* Copyright (C) 2016-2018 Alberto, Andreas Shimokawa, Carsten Pfeiffer,
Daniele Gobbetti
/* Copyright (C) 2016-2019 Alberto, Andreas Shimokawa, Carsten Pfeiffer,
Daniele Gobbetti, vanous
This file is part of Gadgetbridge.
@ -17,61 +17,67 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.activities;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.database.sqlite.SQLiteOpenHelper;
import android.net.Uri;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.support.v4.app.NavUtils;
import android.provider.DocumentsContract;
import android.view.MenuItem;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
import androidx.core.app.NavUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
import nodomain.freeyourgadget.gadgetbridge.database.PeriodicExporter;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.util.AndroidUtils;
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
import nodomain.freeyourgadget.gadgetbridge.util.GBPrefs;
import nodomain.freeyourgadget.gadgetbridge.util.ImportExportSharedPreferences;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
public class DbManagementActivity extends AbstractGBActivity {
private static final Logger LOG = LoggerFactory.getLogger(DbManagementActivity.class);
private static SharedPreferences sharedPrefs;
private ImportExportSharedPreferences shared_file = new ImportExportSharedPreferences();
private Button exportDBButton;
private Button importDBButton;
private Button deleteOldActivityDBButton;
private Button deleteDBButton;
private TextView dbPath;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_db_management);
dbPath = (TextView) findViewById(R.id.activity_db_management_path);
TextView dbPath = findViewById(R.id.activity_db_management_path);
dbPath.setText(getExternalPath());
exportDBButton = (Button) findViewById(R.id.exportDBButton);
Button exportDBButton = findViewById(R.id.exportDBButton);
exportDBButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
exportDB();
}
});
importDBButton = (Button) findViewById(R.id.importDBButton);
Button importDBButton = findViewById(R.id.importDBButton);
importDBButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
@ -81,7 +87,10 @@ public class DbManagementActivity extends AbstractGBActivity {
int oldDBVisibility = hasOldActivityDatabase() ? View.VISIBLE : View.GONE;
deleteOldActivityDBButton = (Button) findViewById(R.id.deleteOldActivityDB);
TextView deleteOldActivityTitle = findViewById(R.id.mergeOldActivityDataTitle);
deleteOldActivityTitle.setVisibility(oldDBVisibility);
Button deleteOldActivityDBButton = findViewById(R.id.deleteOldActivityDB);
deleteOldActivityDBButton.setVisibility(oldDBVisibility);
deleteOldActivityDBButton.setOnClickListener(new View.OnClickListener() {
@Override
@ -90,7 +99,7 @@ public class DbManagementActivity extends AbstractGBActivity {
}
});
deleteDBButton = (Button) findViewById(R.id.emptyDBButton);
Button deleteDBButton = findViewById(R.id.emptyDBButton);
deleteDBButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
@ -98,9 +107,68 @@ public class DbManagementActivity extends AbstractGBActivity {
}
});
Prefs prefs = GBApplication.getPrefs();
boolean autoExportEnabled = prefs.getBoolean(GBPrefs.AUTO_EXPORT_ENABLED, false);
int autoExportInterval = prefs.getInt(GBPrefs.AUTO_EXPORT_INTERVAL, 0);
//returns an ugly content://...
//String autoExportLocation = prefs.getString(GBPrefs.AUTO_EXPORT_LOCATION, "");
int testExportVisibility = (autoExportInterval > 0 && autoExportEnabled) ? View.VISIBLE : View.GONE;
TextView autoExportLocation_label = findViewById(R.id.autoExportLocation_label);
autoExportLocation_label.setVisibility(testExportVisibility);
TextView autoExportLocation_intro = findViewById(R.id.autoExportLocation_intro);
autoExportLocation_intro.setVisibility(testExportVisibility);
TextView autoExportLocation_path = findViewById(R.id.autoExportLocation_path);
autoExportLocation_path.setVisibility(testExportVisibility);
autoExportLocation_path.setText(getAutoExportLocationSummary());
final Context context = getApplicationContext();
Button testExportDBButton = findViewById(R.id.testExportDBButton);
testExportDBButton.setVisibility(testExportVisibility);
testExportDBButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
sendBroadcast(new Intent(context, PeriodicExporter.class));
GB.toast(context,
context.getString(R.string.activity_DB_test_export_message),
Toast.LENGTH_SHORT, GB.INFO);
}
});
sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this);
}
//would rather re-use method of SettingsActivity... but lifecycle...
private String getAutoExportLocationSummary() {
String autoExportLocation = GBApplication.getPrefs().getString(GBPrefs.AUTO_EXPORT_LOCATION, null);
if (autoExportLocation == null) {
return "";
}
Uri uri = Uri.parse(autoExportLocation);
try {
return AndroidUtils.getFilePath(getApplicationContext(), uri);
} catch (IllegalArgumentException e) {
try {
Cursor cursor = getContentResolver().query(
uri,
new String[]{DocumentsContract.Document.COLUMN_DISPLAY_NAME},
null, null, null, null
);
if (cursor != null && cursor.moveToFirst()) {
return cursor.getString(cursor.getColumnIndex(DocumentsContract.Document.COLUMN_DISPLAY_NAME));
}
}
catch (Exception fdfsdfds) {
LOG.warn("fuck");
}
}
return "";
}
private boolean hasOldActivityDatabase() {
return new DBHelper(this).existsDB("ActivityDatabase");
}
@ -115,27 +183,57 @@ public class DbManagementActivity extends AbstractGBActivity {
}
private void exportShared() {
// BEGIN EXAMPLE
File myPath = null;
try {
myPath = FileUtils.getExternalFilesDir();
File myPath = FileUtils.getExternalFilesDir();
File myFile = new File(myPath, "Export_preference");
shared_file.exportToFile(sharedPrefs,myFile,null);
ImportExportSharedPreferences.exportToFile(sharedPrefs, myFile, null);
} catch (IOException ex) {
GB.toast(this, getString(R.string.dbmanagementactivity_error_exporting_shared, ex.getMessage()), Toast.LENGTH_LONG, GB.ERROR, ex);
}
try (DBHandler lockHandler = GBApplication.acquireDB()) {
List<Device> activeDevices = DBHelper.getActiveDevices(lockHandler.getDaoSession());
for (Device dbDevice : activeDevices) {
SharedPreferences deviceSharedPrefs = GBApplication.getDeviceSpecificSharedPrefs(dbDevice.getIdentifier());
if (sharedPrefs != null) {
File myPath = FileUtils.getExternalFilesDir();
File myFile = new File(myPath, "Export_preference_" + dbDevice.getIdentifier());
try {
ImportExportSharedPreferences.exportToFile(deviceSharedPrefs, myFile, null);
} catch (Exception ignore) {
// some devices no not have device specific preferences
}
}
}
} catch (Exception e) {
GB.toast("Error exporting device specific preferences", Toast.LENGTH_SHORT, GB.ERROR);
}
}
private void importShared() {
// BEGIN EXAMPLE
File myPath = null;
try {
myPath = FileUtils.getExternalFilesDir();
File myPath = FileUtils.getExternalFilesDir();
File myFile = new File(myPath, "Export_preference");
shared_file.importFromFile(sharedPrefs,myFile );
ImportExportSharedPreferences.importFromFile(sharedPrefs, myFile);
} catch (Exception ex) {
GB.toast(DbManagementActivity.this, getString(R.string.dbmanagementactivity_error_importing_db, ex.getMessage()), Toast.LENGTH_LONG, GB.ERROR, ex);
}
try (DBHandler lockHandler = GBApplication.acquireDB()) {
List<Device> activeDevices = DBHelper.getActiveDevices(lockHandler.getDaoSession());
for (Device dbDevice : activeDevices) {
SharedPreferences deviceSharedPrefs = GBApplication.getDeviceSpecificSharedPrefs(dbDevice.getIdentifier());
if (sharedPrefs != null) {
File myPath = FileUtils.getExternalFilesDir();
File myFile = new File(myPath, "Export_preference_" + dbDevice.getIdentifier());
try {
ImportExportSharedPreferences.importFromFile(deviceSharedPrefs, myFile);
} catch (Exception ignore) {
// some devices no not have device specific preferences
}
}
}
} catch (Exception e) {
GB.toast("Error importing device specific preferences", Toast.LENGTH_SHORT, GB.ERROR);
}
}
private void exportDB() {
@ -159,7 +257,6 @@ public class DbManagementActivity extends AbstractGBActivity {
@Override
public void onClick(DialogInterface dialog, int which) {
try (DBHandler dbHandler = GBApplication.acquireDB()) {
importShared();
DBHelper helper = new DBHelper(DbManagementActivity.this);
File dir = FileUtils.getExternalFilesDir();
SQLiteOpenHelper sqLiteOpenHelper = dbHandler.getHelper();
@ -170,6 +267,7 @@ public class DbManagementActivity extends AbstractGBActivity {
} catch (Exception ex) {
GB.toast(DbManagementActivity.this, getString(R.string.dbmanagementactivity_error_importing_db, ex.getMessage()), Toast.LENGTH_LONG, GB.ERROR, ex);
}
importShared();
}
})
.setNegativeButton(R.string.Cancel, new DialogInterface.OnClickListener() {
@ -227,10 +325,9 @@ public class DbManagementActivity extends AbstractGBActivity {
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
NavUtils.navigateUpFromSameTask(this);
return true;
if (item.getItemId() == android.R.id.home) {
NavUtils.navigateUpFromSameTask(this);
return true;
}
return super.onOptionsItemSelected(item);
}

View File

@ -1,6 +1,6 @@
/* Copyright (C) 2015-2018 Andreas Shimokawa, Carsten Pfeiffer, Daniele
/* Copyright (C) 2015-2019 Andreas Shimokawa, Carsten Pfeiffer, Daniele
Gobbetti, Frank Slezak, ivanovlev, Kasha, Lem Dulfo, Pavel Elagin, Steffen
Liebergeld
Liebergeld, vanous
This file is part of Gadgetbridge.
@ -19,6 +19,7 @@
package nodomain.freeyourgadget.gadgetbridge.activities;
import android.app.AlertDialog;
import android.app.DatePickerDialog;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
@ -26,29 +27,36 @@ import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Bundle;
import android.support.v4.app.NavUtils;
import android.support.v4.app.NotificationCompat;
import android.support.v4.app.RemoteInput;
import android.support.v4.content.LocalBroadcastManager;
import android.view.MenuItem;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.DatePicker;
import android.widget.EditText;
import android.widget.Spinner;
import android.widget.Toast;
import androidx.core.app.NavUtils;
import androidx.core.app.NotificationCompat;
import androidx.core.app.RemoteInput;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Objects;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceService;
import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec;
@ -56,6 +64,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec;
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
import nodomain.freeyourgadget.gadgetbridge.model.NotificationType;
import nodomain.freeyourgadget.gadgetbridge.model.RecordedDataTypes;
import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
import static android.content.Intent.EXTRA_SUBJECT;
@ -67,10 +76,6 @@ public class DebugActivity extends AbstractGBActivity {
private static final String EXTRA_REPLY = "reply";
private static final String ACTION_REPLY
= "nodomain.freeyourgadget.gadgetbridge.DebugActivity.action.reply";
private Spinner sendTypeSpinner;
private EditText editContent;
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
@ -82,12 +87,24 @@ public class DebugActivity extends AbstractGBActivity {
GB.toast(context, "got wearable reply: " + reply, Toast.LENGTH_SHORT, GB.INFO);
break;
}
case DeviceService.ACTION_REALTIME_SAMPLES:
handleRealtimeSample(intent.getSerializableExtra(DeviceService.EXTRA_REALTIME_SAMPLE));
break;
default:
LOG.info("ignoring intent action " + intent.getAction());
break;
}
}
};
private Spinner sendTypeSpinner;
private EditText editContent;
private void handleRealtimeSample(Serializable extra) {
if (extra instanceof ActivitySample) {
ActivitySample sample = (ActivitySample) extra;
GB.toast(this, "Heart Rate measured: " + sample.getHeartRate(), Toast.LENGTH_LONG, GB.INFO);
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
@ -96,7 +113,7 @@ public class DebugActivity extends AbstractGBActivity {
IntentFilter filter = new IntentFilter();
filter.addAction(ACTION_REPLY);
filter.addAction(DeviceService.ACTION_HEARTRATE_MEASUREMENT);
filter.addAction(DeviceService.ACTION_REALTIME_SAMPLES);
LocalBroadcastManager.getInstance(this).registerReceiver(mReceiver, filter);
registerReceiver(mReceiver, filter); // for ACTION_REPLY
@ -122,7 +139,6 @@ public class DebugActivity extends AbstractGBActivity {
notificationSpec.subject = testString;
notificationSpec.type = NotificationType.values()[sendTypeSpinner.getSelectedItemPosition()];
notificationSpec.pebbleColor = notificationSpec.type.color;
notificationSpec.id = -1;
GBApplication.deviceService().onNotification(notificationSpec);
}
});
@ -171,9 +187,33 @@ public class DebugActivity extends AbstractGBActivity {
rebootButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
GBApplication.deviceService().onReboot();
GBApplication.deviceService().onReset(GBDeviceProtocol.RESET_FLAGS_REBOOT);
}
});
Button factoryResetButton = findViewById(R.id.factoryResetButton);
factoryResetButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
new AlertDialog.Builder(DebugActivity.this)
.setCancelable(true)
.setTitle(R.string.debugactivity_really_factoryreset_title)
.setMessage(R.string.debugactivity_really_factoryreset)
.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
GBApplication.deviceService().onReset(GBDeviceProtocol.RESET_FLAGS_FACTORY_RESET);
}
})
.setNegativeButton(R.string.Cancel, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
}
})
.show();
}
});
Button heartRateButton = findViewById(R.id.HeartRateButton);
heartRateButton.setOnClickListener(new View.OnClickListener() {
@Override
@ -183,6 +223,43 @@ public class DebugActivity extends AbstractGBActivity {
}
});
Button setFetchTimeButton = findViewById(R.id.SetFetchTimeButton);
setFetchTimeButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
final Calendar currentDate = Calendar.getInstance();
Context context = getApplicationContext();
if (context instanceof GBApplication) {
GBApplication gbApp = (GBApplication) context;
final GBDevice device = gbApp.getDeviceManager().getSelectedDevice();
if (device != null) {
new DatePickerDialog(DebugActivity.this, new DatePickerDialog.OnDateSetListener() {
@Override
public void onDateSet(DatePicker view, int year, int monthOfYear, int dayOfMonth) {
Calendar date = Calendar.getInstance();
date.set(year, monthOfYear, dayOfMonth);
long timestamp = date.getTimeInMillis() - 1000;
GB.toast("Setting lastSyncTimeMillis: " + timestamp, Toast.LENGTH_LONG, GB.INFO);
SharedPreferences.Editor editor = GBApplication.getDeviceSpecificSharedPrefs(device.getAddress()).edit();
editor.remove("lastSyncTimeMillis"); //FIXME: key reconstruction is BAD
editor.putLong("lastSyncTimeMillis", timestamp);
editor.apply();
}
}, currentDate.get(Calendar.YEAR), currentDate.get(Calendar.MONTH), currentDate.get(Calendar.DATE)).show();
} else {
GB.toast("Device not selected/connected", Toast.LENGTH_LONG, GB.INFO);
}
}
}
});
Button setMusicInfoButton = findViewById(R.id.setMusicInfoButton);
setMusicInfoButton.setOnClickListener(new View.OnClickListener() {
@Override
@ -225,6 +302,14 @@ public class DebugActivity extends AbstractGBActivity {
}
});
Button testPebbleKitNotificationButton = findViewById(R.id.testPebbleKitNotificationButton);
testPebbleKitNotificationButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
testPebbleKitNotification();
}
});
Button fetchDebugLogsButton = findViewById(R.id.fetchDebugLogsButton);
fetchDebugLogsButton.setOnClickListener(new View.OnClickListener() {
@Override
@ -258,14 +343,7 @@ public class DebugActivity extends AbstractGBActivity {
.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
String fileName = GBApplication.getLogPath();
if (fileName != null && fileName.length() > 0) {
Intent emailIntent = new Intent(android.content.Intent.ACTION_SEND);
emailIntent.setType("*/*");
emailIntent.putExtra(EXTRA_SUBJECT, "Gadgetbridge log file");
emailIntent.putExtra(Intent.EXTRA_STREAM, Uri.fromFile(new File(fileName)));
startActivity(Intent.createChooser(emailIntent, "Share File"));
}
shareLog();
}
})
.setNegativeButton(R.string.Cancel, new DialogInterface.OnClickListener() {
@ -283,11 +361,17 @@ public class DebugActivity extends AbstractGBActivity {
private void shareLog() {
String fileName = GBApplication.getLogPath();
if(fileName != null && fileName.length() > 0) {
if (fileName != null && fileName.length() > 0) {
File logFile = new File(fileName);
if (!logFile.exists()) {
GB.toast("File does not exist", Toast.LENGTH_LONG, GB.INFO);
return;
}
Intent emailIntent = new Intent(android.content.Intent.ACTION_SEND);
emailIntent.setType("*/*");
emailIntent.putExtra(EXTRA_SUBJECT, "Gadgetbridge log file");
emailIntent.putExtra(Intent.EXTRA_STREAM, Uri.fromFile(new File(fileName)));
emailIntent.putExtra(Intent.EXTRA_STREAM, Uri.fromFile(logFile));
startActivity(Intent.createChooser(emailIntent, "Share File"));
}
}
@ -329,6 +413,13 @@ public class DebugActivity extends AbstractGBActivity {
}
}
private void testPebbleKitNotification() {
Intent pebbleKitIntent = new Intent("com.getpebble.action.SEND_NOTIFICATION");
pebbleKitIntent.putExtra("messageType", "PEBBLE_ALERT");
pebbleKitIntent.putExtra("notificationData", "[{\"title\":\"PebbleKitTest\",\"body\":\"sent from Gadgetbridge\"}]");
getApplicationContext().sendBroadcast(pebbleKitIntent);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {

View File

@ -1,5 +1,6 @@
/* Copyright (C) 2015-2018 Andreas Shimokawa, boun, Carsten Pfeiffer,
Daniele Gobbetti, JohnnySun, jonnsoft, Lem Dulfo, Taavi Eomäe, Uwe Hermann
/* Copyright (C) 2015-2019 Andreas Shimokawa, boun, Carsten Pfeiffer, Daniel
Dakhno, Daniele Gobbetti, JohnnySun, jonnsoft, Lem Dulfo, Taavi Eomäe,
Uwe Hermann
This file is part of Gadgetbridge.
@ -35,6 +36,7 @@ import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;
@ -42,7 +44,6 @@ import android.os.Handler;
import android.os.Message;
import android.os.ParcelUuid;
import android.os.Parcelable;
import android.support.v4.app.ActivityCompat;
import android.view.View;
import android.widget.AdapterView;
import android.widget.Button;
@ -50,6 +51,9 @@ import android.widget.ListView;
import android.widget.ProgressBar;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.core.app.ActivityCompat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -59,6 +63,7 @@ import java.util.Objects;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsActivity;
import nodomain.freeyourgadget.gadgetbridge.adapter.DeviceCandidateAdapter;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
@ -69,12 +74,15 @@ import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
public class DiscoveryActivity extends AbstractGBActivity implements AdapterView.OnItemClickListener {
public class DiscoveryActivity extends AbstractGBActivity implements AdapterView.OnItemClickListener, AdapterView.OnItemLongClickListener {
private static final Logger LOG = LoggerFactory.getLogger(DiscoveryActivity.class);
private static final long SCAN_DURATION = 60000; // 60s
private ScanCallback newLeScanCallback = null;
// Disabled for testing, it seems worse for a few people
private boolean disableNewBLEScanning = false;
private final Handler handler = new Handler();
private final BroadcastReceiver bluetoothReceiver = new BroadcastReceiver() {
@ -93,7 +101,7 @@ public class DiscoveryActivity extends AbstractGBActivity implements AdapterView
// continue with LE scan, if available
if (isScanning == Scanning.SCANNING_BT) {
checkAndRequestLocationPermission();
if (GBApplication.isRunningLollipopOrLater()) {
if (GBApplication.isRunningLollipopOrLater() && !disableNewBLEScanning) {
startDiscovery(Scanning.SCANNING_NEW_BTLE);
} else {
startDiscovery(Scanning.SCANNING_BTLE);
@ -189,8 +197,7 @@ public class DiscoveryActivity extends AbstractGBActivity implements AdapterView
private final BluetoothAdapter.LeScanCallback leScanCallback = new BluetoothAdapter.LeScanCallback() {
@Override
public void onLeScan(BluetoothDevice device, int rssi, byte[] scanRecord) {
LOG.warn(device.getName() + ": " + ((scanRecord != null) ? scanRecord.length : -1));
logMessageContent(scanRecord);
//logMessageContent(scanRecord);
handleDeviceFound(device, (short) rssi);
}
};
@ -261,6 +268,11 @@ public class DiscoveryActivity extends AbstractGBActivity implements AdapterView
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
disableNewBLEScanning = GBApplication.getPrefs().getBoolean("disable_new_ble_scanning", false);
if (disableNewBLEScanning) {
LOG.info("new BLE scanning disabled via settings, using old method");
}
setContentView(R.layout.activity_discovery);
startButton = findViewById(R.id.discovery_start);
startButton.setOnClickListener(new View.OnClickListener() {
@ -279,6 +291,7 @@ public class DiscoveryActivity extends AbstractGBActivity implements AdapterView
cadidateListAdapter = new DeviceCandidateAdapter(this, deviceCandidates);
deviceCandidatesView.setAdapter(cadidateListAdapter);
deviceCandidatesView.setOnItemClickListener(this);
deviceCandidatesView.setOnItemLongClickListener(this);
IntentFilter bluetoothIntents = new IntentFilter();
bluetoothIntents.addAction(BluetoothDevice.ACTION_FOUND);
@ -294,7 +307,7 @@ public class DiscoveryActivity extends AbstractGBActivity implements AdapterView
}
@Override
protected void onSaveInstanceState(Bundle outState) {
protected void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
outState.putParcelableArrayList("deviceCandidates", deviceCandidates);
}
@ -331,6 +344,12 @@ public class DiscoveryActivity extends AbstractGBActivity implements AdapterView
}
private void handleDeviceFound(BluetoothDevice device, short rssi) {
if (device.getName() != null) {
if (handleDeviceFound(device,rssi, null)) {
LOG.info("found supported device " + device.getName() + " without scanning services, skipping service scan.");
return;
}
}
ParcelUuid[] uuids = device.getUuids();
if (uuids == null) {
if (device.fetchUuidsWithSdp()) {
@ -342,7 +361,7 @@ public class DiscoveryActivity extends AbstractGBActivity implements AdapterView
}
private void handleDeviceFound(BluetoothDevice device, short rssi, ParcelUuid[] uuids) {
private boolean handleDeviceFound(BluetoothDevice device, short rssi, ParcelUuid[] uuids) {
LOG.debug("found device: " + device.getName() + ", " + device.getAddress());
if (LOG.isDebugEnabled()) {
if (uuids != null && uuids.length > 0) {
@ -352,7 +371,7 @@ public class DiscoveryActivity extends AbstractGBActivity implements AdapterView
}
}
if (device.getBondState() == BluetoothDevice.BOND_BONDED) {
return; // ignore already bonded devices
return true; // ignore already bonded devices
}
GBDeviceCandidate candidate = new GBDeviceCandidate(device, rssi, uuids);
@ -367,7 +386,9 @@ public class DiscoveryActivity extends AbstractGBActivity implements AdapterView
deviceCandidates.add(candidate);
}
cadidateListAdapter.notifyDataSetChanged();
return true;
}
return false;
}
/**
@ -577,6 +598,27 @@ public class DiscoveryActivity extends AbstractGBActivity implements AdapterView
return m;
}
@Override
public boolean onItemLongClick(AdapterView<?> adapterView, View view, int position, long id) {
GBDeviceCandidate deviceCandidate = deviceCandidates.get(position);
if (deviceCandidate == null) {
LOG.error("Device candidate clicked, but item not found");
return true;
}
DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(deviceCandidate);
GBDevice device = DeviceHelper.getInstance().toSupportedDevice(deviceCandidate);
if (coordinator.getSupportedDeviceSpecificSettings(device) == null) {
return true;
}
Intent startIntent;
startIntent = new Intent(this, DeviceSettingsActivity.class);
startIntent.putExtra(GBDevice.EXTRA_DEVICE, device);
startActivity(startIntent);
return true;
}
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
GBDeviceCandidate deviceCandidate = deviceCandidates.get(position);
@ -588,6 +630,17 @@ public class DiscoveryActivity extends AbstractGBActivity implements AdapterView
stopDiscovery();
DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(deviceCandidate);
LOG.info("Using device candidate " + deviceCandidate + " with coordinator: " + coordinator.getClass());
if (coordinator.getBondingStyle() == DeviceCoordinator.BONDING_STYLE_REQUIRE_KEY) {
SharedPreferences sharedPrefs = GBApplication.getDeviceSpecificSharedPrefs(deviceCandidate.getMacAddress());
String authKey = sharedPrefs.getString("authkey", null);
if (authKey == null || authKey.isEmpty() || authKey.getBytes().length < 34 || !authKey.substring(0, 2).equals("0x")) {
GB.toast(DiscoveryActivity.this, getString(R.string.discovery_need_to_enter_authkey), Toast.LENGTH_LONG, GB.WARN);
return;
}
}
Class<? extends Activity> pairingActivity = coordinator.getPairingActivity();
if (pairingActivity != null) {
Intent intent = new Intent(this, pairingActivity);
@ -595,7 +648,7 @@ public class DiscoveryActivity extends AbstractGBActivity implements AdapterView
startActivity(intent);
} else {
GBDevice device = DeviceHelper.getInstance().toSupportedDevice(deviceCandidate);
int bondingStyle = coordinator.getBondingStyle(device);
int bondingStyle = coordinator.getBondingStyle();
if (bondingStyle == DeviceCoordinator.BONDING_STYLE_NONE) {
LOG.info("No bonding needed, according to coordinator, so connecting right away");
connectAndFinish(device);
@ -628,7 +681,7 @@ public class DiscoveryActivity extends AbstractGBActivity implements AdapterView
super.onPause();
stopBTDiscovery();
stopBTLEDiscovery();
if (GBApplication.isRunningLollipopOrLater()) {
if (GBApplication.isRunningLollipopOrLater() && !disableNewBLEScanning) {
stopNewBTLEDiscovery();
}
}

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2016-2018 Andreas Shimokawa, Carsten Pfeiffer, Daniele
/* Copyright (C) 2016-2019 Andreas Shimokawa, Carsten Pfeiffer, Daniele
Gobbetti, Lem Dulfo, Uwe Hermann
This file is part of Gadgetbridge.
@ -20,8 +20,6 @@ package nodomain.freeyourgadget.gadgetbridge.activities;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v4.app.NavUtils;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
@ -38,6 +36,8 @@ import java.util.List;
import java.util.Objects;
import java.util.UUID;
import androidx.annotation.NonNull;
import androidx.core.app.NavUtils;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceManager;

View File

@ -1,4 +1,5 @@
/* Copyright (C) 2018 Andreas Shimokawa
/* Copyright (C) 2018-2019 Andreas Shimokawa, Carsten Pfeiffer, Cre3per,
Daniele Gobbetti
This file is part of Gadgetbridge.
@ -24,9 +25,10 @@ import android.media.AudioManager;
import android.media.MediaPlayer;
import android.media.RingtoneManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.support.v4.app.RemoteInput;
import android.support.v4.content.LocalBroadcastManager;
import android.os.VibrationEffect;
import android.os.Vibrator;
import android.view.View;
import android.widget.Button;
@ -35,9 +37,8 @@ import org.slf4j.LoggerFactory;
import java.io.IOException;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceService;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
public class FindPhoneActivity extends AbstractGBActivity {
@ -60,6 +61,7 @@ public class FindPhoneActivity extends AbstractGBActivity {
}
};
Vibrator mVibrator;
AudioManager mAudioManager;
int userVolume;
MediaPlayer mp;
@ -71,7 +73,6 @@ public class FindPhoneActivity extends AbstractGBActivity {
IntentFilter filter = new IntentFilter();
filter.addAction(ACTION_FOUND);
filter.addAction(DeviceService.ACTION_HEARTRATE_MEASUREMENT);
LocalBroadcastManager.getInstance(this).registerReceiver(mReceiver, filter);
registerReceiver(mReceiver, filter); // for ACTION_FOUND
@ -82,10 +83,26 @@ public class FindPhoneActivity extends AbstractGBActivity {
finish();
}
});
vibrate();
playRingtone();
}
public void playRingtone(){
private void vibrate(){
mVibrator = (Vibrator)getSystemService(Context.VIBRATOR_SERVICE);
long[] vibrationPattern = new long[]{ 1000, 1000 };
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
VibrationEffect vibrationEffect = VibrationEffect.createWaveform(vibrationPattern, 0);
mVibrator.vibrate(vibrationEffect);
} else {
mVibrator.vibrate(vibrationPattern, 0);
}
}
private void playRingtone(){
mAudioManager = (AudioManager)getSystemService(Context.AUDIO_SERVICE);
if (mAudioManager != null) {
userVolume = mAudioManager.getStreamVolume(AudioManager.STREAM_ALARM);
@ -101,12 +118,20 @@ public class FindPhoneActivity extends AbstractGBActivity {
mp.prepare();
mp.start();
} catch (IOException ignore) {
LOG.warn("problem playing ringtone");
}
mAudioManager.setStreamVolume(AudioManager.STREAM_ALARM, mAudioManager.getStreamMaxVolume(AudioManager.STREAM_ALARM), AudioManager.FLAG_PLAY_SOUND);
if (mAudioManager != null) {
userVolume = mAudioManager.getStreamVolume(AudioManager.STREAM_ALARM);
mAudioManager.setStreamVolume(AudioManager.STREAM_ALARM, mAudioManager.getStreamMaxVolume(AudioManager.STREAM_ALARM), AudioManager.FLAG_PLAY_SOUND);
}
}
public void stopSound() {
private void stopVibration() {
mVibrator.cancel();
}
private void stopSound() {
mAudioManager.setStreamVolume(AudioManager.STREAM_ALARM, userVolume, AudioManager.FLAG_PLAY_SOUND);
mp.stop();
mp.reset();
@ -115,7 +140,10 @@ public class FindPhoneActivity extends AbstractGBActivity {
@Override
protected void onDestroy() {
super.onDestroy();
stopVibration();
stopSound();
LocalBroadcastManager.getInstance(this).unregisterReceiver(mReceiver);
unregisterReceiver(mReceiver);
}

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2015-2018 Andreas Shimokawa, Carsten Pfeiffer, Daniele
/* Copyright (C) 2015-2019 Andreas Shimokawa, Carsten Pfeiffer, Daniele
Gobbetti, Lem Dulfo
This file is part of Gadgetbridge.
@ -23,8 +23,6 @@ import android.content.Intent;
import android.content.IntentFilter;
import android.net.Uri;
import android.os.Bundle;
import android.support.v4.app.NavUtils;
import android.support.v4.content.LocalBroadcastManager;
import android.view.MenuItem;
import android.view.View;
import android.widget.Button;
@ -39,6 +37,8 @@ import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
import androidx.core.app.NavUtils;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.adapter.ItemWithDetailsAdapter;

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2015-2018 Andreas Shimokawa, Carsten Pfeiffer, Lem Dulfo
/* Copyright (C) 2015-2019 Andreas Shimokawa, Carsten Pfeiffer, Lem Dulfo
This file is part of Gadgetbridge.

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2016-2018 Carsten Pfeiffer
/* Copyright (C) 2016-2019 Carsten Pfeiffer, Dikay900
This file is part of Gadgetbridge.

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2015-2018 Andreas Shimokawa, Carsten Pfeiffer
/* Copyright (C) 2015-2019 Andreas Shimokawa, Carsten Pfeiffer
This file is part of Gadgetbridge.

View File

@ -0,0 +1,233 @@
/* Copyright (C) 2018-2019 abettenburg, AndrewBedscastle, Carsten Pfeiffer,
Daniele Gobbetti
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge 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 Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.activities;
import android.os.Bundle;
import android.view.View;
import android.widget.AdapterView;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Spinner;
import android.widget.Toast;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
import de.greenrobot.dao.query.Query;
import nodomain.freeyourgadget.gadgetbridge.BuildConfig;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.adapter.AppBlacklistAdapter;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.entities.NotificationFilter;
import nodomain.freeyourgadget.gadgetbridge.entities.NotificationFilterDao;
import nodomain.freeyourgadget.gadgetbridge.entities.NotificationFilterEntry;
import nodomain.freeyourgadget.gadgetbridge.entities.NotificationFilterEntryDao;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
public class NotificationFilterActivity extends AbstractGBActivity {
public static final int NOTIFICATION_FILTER_MODE_NONE = 0;
public static final int NOTIFICATION_FILTER_MODE_WHITELIST = 1;
public static final int NOTIFICATION_FILTER_MODE_BLACKLIST = 2;
public static final int NOTIFICATION_FILTER_SUBMODE_ANY = 0;
public static final int NOTIFICATION_FILTER_SUBMODE_ALL = 1;
private Button mButtonSave;
private Spinner mSpinnerFilterMode;
private Spinner mSpinnerFilterSubMode;
private NotificationFilter mNotificationFilter;
private EditText mEditTextWords;
private List<String> mWordsList = new ArrayList<>();
private List<Long> mFilterEntryIds = new ArrayList<>();
private static final Logger LOG = LoggerFactory.getLogger(NotificationFilterActivity.class);
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_notification_filter);
String packageName = getIntent().getStringExtra(AppBlacklistAdapter.STRING_EXTRA_PACKAGE_NAME);
if (StringUtils.isBlank(packageName)) {
this.finish();
}
packageName = packageName.toLowerCase();
try (DBHandler db = GBApplication.acquireDB()) {
NotificationFilterDao notificationFilterDao = db.getDaoSession().getNotificationFilterDao();
NotificationFilterEntryDao notificationFilterEntryDao = db.getDaoSession().getNotificationFilterEntryDao();
Query<NotificationFilter> query = notificationFilterDao.queryBuilder().where(NotificationFilterDao.Properties.AppIdentifier.eq(packageName)).build();
mNotificationFilter = query.unique();
if (mNotificationFilter == null) {
mNotificationFilter = new NotificationFilter();
mNotificationFilter.setAppIdentifier(packageName);
LOG.debug("New Notification Filter");
} else {
LOG.debug("Loaded existing notification filter");
Query<NotificationFilterEntry> queryEntries = notificationFilterEntryDao.queryBuilder().where(NotificationFilterEntryDao.Properties.NotificationFilterId.eq(mNotificationFilter.getId())).build();
List<NotificationFilterEntry> filterEntries = queryEntries.list();
for (NotificationFilterEntry temp : filterEntries) {
mWordsList.add(temp.getNotificationFilterContent());
mFilterEntryIds.add(temp.getId());
LOG.debug("Loaded filter word: " + temp.getNotificationFilterContent());
}
}
setupView(db);
} catch (Exception e) {
GB.toast(this, "Error accessing the database: " + e.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR);
this.finish();
}
}
@Override
protected void onDestroy() {
super.onDestroy();
}
private void setupView(DBHandler db) {
mSpinnerFilterMode = findViewById(R.id.spinnerFilterMode);
mSpinnerFilterMode.setSelection(mNotificationFilter.getNotificationFilterMode());
mSpinnerFilterSubMode = findViewById(R.id.spinnerSubMode);
mSpinnerFilterMode.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> adapterView, View view, int pos, long id) {
switch (pos) {
case NOTIFICATION_FILTER_MODE_NONE:
mEditTextWords.setEnabled(false);
mSpinnerFilterSubMode.setEnabled(false);
break;
case NOTIFICATION_FILTER_MODE_BLACKLIST:
case NOTIFICATION_FILTER_MODE_WHITELIST:
mEditTextWords.setEnabled(true);
mSpinnerFilterSubMode.setEnabled(true);
break;
}
}
@Override
public void onNothingSelected(AdapterView<?> adapterView) {
}
});
mSpinnerFilterSubMode.setSelection(mNotificationFilter.getNotificationFilterSubMode());
mEditTextWords = findViewById(R.id.editTextWords);
if (!mWordsList.isEmpty()) {
StringBuilder builder = new StringBuilder();
for (String temp : mWordsList) {
builder.append(temp);
builder.append("\n");
}
mEditTextWords.setText(builder.toString());
}
mEditTextWords.setEnabled(mSpinnerFilterMode.getSelectedItemPosition() == NOTIFICATION_FILTER_MODE_NONE);
mButtonSave = findViewById(R.id.buttonSaveFilter);
mButtonSave.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
saveFilter();
}
});
}
private void saveFilter() {
// TODO: check for modifications, only save if something changed
String words = mEditTextWords.getText().toString();
if (StringUtils.isBlank(words) && mSpinnerFilterMode.getSelectedItemPosition() != NOTIFICATION_FILTER_MODE_NONE) {
Toast.makeText(NotificationFilterActivity.this, R.string.toast_notification_filter_words_empty_hint, Toast.LENGTH_SHORT).show();
return;
}
try (DBHandler db = GBApplication.acquireDB()) {
NotificationFilterDao notificationFilterDao = db.getDaoSession().getNotificationFilterDao();
NotificationFilterEntryDao notificationFilterEntryDao = db.getDaoSession().getNotificationFilterEntryDao();
debugOutput(notificationFilterDao);
mNotificationFilter.setNotificationFilterMode(mSpinnerFilterMode.getSelectedItemPosition());
mNotificationFilter.setNotificationFilterSubMode(mSpinnerFilterSubMode.getSelectedItemPosition());
notificationFilterEntryDao.deleteByKeyInTx(mFilterEntryIds);
long filterId = notificationFilterDao.insertOrReplace(mNotificationFilter);
// only save words if filter mode != none
if (mNotificationFilter.getNotificationFilterMode() != NOTIFICATION_FILTER_MODE_NONE) {
String[] wordsSplitted = words.split("\n");
for (String temp : wordsSplitted) {
if (StringUtils.isBlank(temp)) {
continue;
}
temp = temp.trim();
NotificationFilterEntry notificationFilterEntry = new NotificationFilterEntry();
notificationFilterEntry.setNotificationFilterContent(temp);
notificationFilterEntry.setNotificationFilterId(filterId);
notificationFilterEntryDao.insert(notificationFilterEntry);
}
}
Toast.makeText(NotificationFilterActivity.this, R.string.toast_notification_filter_saved_successfully, Toast.LENGTH_SHORT).show();
NotificationFilterActivity.this.finish();
} catch (Exception e) {
GB.toast(NotificationFilterActivity.this, "Error accessing the database: " + e.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR);
}
}
/**
* Only used for debugging purposes
*
* @param notificationFilterDao {@link NotificationFilterDao}
*/
private void debugOutput(NotificationFilterDao notificationFilterDao) {
if (BuildConfig.DEBUG) {
List<NotificationFilter> filters = notificationFilterDao.loadAll();
LOG.info("Saved filters");
for (NotificationFilter temp : filters) {
LOG.info("Filter: " + temp.getId() + " " + temp.getAppIdentifier());
}
}
}
}

View File

@ -1,6 +1,6 @@
/* Copyright (C) 2015-2018 0nse, Andreas Shimokawa, Carsten Pfeiffer,
Daniele Gobbetti, Felix Konstantin Maurer, José Rebelo, Martin, Normano64,
Pavel Elagin
/* Copyright (C) 2015-2019 0nse, Andreas Shimokawa, Carsten Pfeiffer,
Daniel Dakhno, Daniele Gobbetti, Felix Konstantin Maurer, José Rebelo,
Martin, Normano64, Pavel Elagin, Sebastian Kranz, vanous
This file is part of Gadgetbridge.
@ -36,10 +36,11 @@ import android.preference.Preference;
import android.preference.PreferenceCategory;
import android.preference.PreferenceManager;
import android.provider.DocumentsContract;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.LocalBroadcastManager;
import android.widget.Toast;
import androidx.core.app.ActivityCompat;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -53,8 +54,10 @@ import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.database.PeriodicExporter;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceManager;
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst;
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandPreferencesActivity;
import nodomain.freeyourgadget.gadgetbridge.devices.qhybrid.ConfigActivity;
import nodomain.freeyourgadget.gadgetbridge.devices.zetime.ZeTimePreferenceActivity;
import nodomain.freeyourgadget.gadgetbridge.activities.charts.ChartsPreferencesActivity;
import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec;
import nodomain.freeyourgadget.gadgetbridge.util.AndroidUtils;
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
@ -62,15 +65,6 @@ import nodomain.freeyourgadget.gadgetbridge.util.GB;
import nodomain.freeyourgadget.gadgetbridge.util.GBPrefs;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_MI2_DATEFORMAT;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_MI2_DISPLAY_ITEMS;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_MI2_ENABLE_TEXT_NOTIFICATIONS;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_MI3_BAND_SCREEN_UNLOCK;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_MI3_NIGHT_MODE;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_MI3_NIGHT_MODE_END;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_MI3_NIGHT_MODE_OFF;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_MI3_NIGHT_MODE_SCHEDULED;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_MI3_NIGHT_MODE_START;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivityUser.PREF_USER_HEIGHT_CM;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivityUser.PREF_USER_SLEEP_DURATION;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivityUser.PREF_USER_STEPS_GOAL;
@ -104,6 +98,16 @@ public class SettingsActivity extends AbstractSettingsActivity {
return true;
}
});
pref = findPreference("pref_charts");
pref.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
public boolean onPreferenceClick(Preference preference) {
Intent enableIntent = new Intent(SettingsActivity.this, ChartsPreferencesActivity.class);
startActivity(enableIntent);
return true;
}
});
pref = findPreference("pref_key_miband");
pref.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
public boolean onPreferenceClick(Preference preference) {
@ -113,6 +117,24 @@ public class SettingsActivity extends AbstractSettingsActivity {
}
});
pref = findPreference("pref_key_qhybrid");
pref.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
startActivity(new Intent(SettingsActivity.this, ConfigActivity.class));
return true;
}
});
pref = findPreference("pref_key_zetime");
pref.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
public boolean onPreferenceClick(Preference preference) {
Intent enableIntent = new Intent(SettingsActivity.this, ZeTimePreferenceActivity.class);
startActivity(enableIntent);
return true;
}
});
pref = findPreference("pref_key_blacklist");
pref.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
public boolean onPreferenceClick(Preference preference) {
@ -284,16 +306,16 @@ public class SettingsActivity extends AbstractSettingsActivity {
pref = findPreference("weather_city");
pref.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newVal) {
// reset city id and force a new lookup
GBApplication.getPrefs().getPreferences().edit().putString("weather_cityid", null).apply();
preference.setSummary(newVal.toString());
Intent intent = new Intent("GB_UPDATE_WEATHER");
intent.setPackage(BuildConfig.APPLICATION_ID);
sendBroadcast(intent);
return true;
}
@Override
public boolean onPreferenceChange(Preference preference, Object newVal) {
// reset city id and force a new lookup
GBApplication.getPrefs().getPreferences().edit().putString("weather_cityid", null).apply();
preference.setSummary(newVal.toString());
Intent intent = new Intent("GB_UPDATE_WEATHER");
intent.setPackage(BuildConfig.APPLICATION_ID);
sendBroadcast(intent);
return true;
}
});
pref = findPreference(GBPrefs.AUTO_EXPORT_LOCATION);
@ -356,146 +378,6 @@ public class SettingsActivity extends AbstractSettingsActivity {
autoFetchInterval);
pref.setSummary(summary);
final Preference displayPages = findPreference("bip_display_items");
displayPages.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newVal) {
invokeLater(new Runnable() {
@Override
public void run() {
GBApplication.deviceService().onSendConfiguration(PREF_MI2_DISPLAY_ITEMS);
}
});
return true;
}
});
final Preference setDateFormat = findPreference(PREF_MI2_DATEFORMAT);
setDateFormat.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newVal) {
invokeLater(new Runnable() {
@Override
public void run() {
GBApplication.deviceService().onSendConfiguration(PREF_MI2_DATEFORMAT);
}
});
return true;
}
});
final Preference miBand2DisplayItems = findPreference(PREF_MI2_DISPLAY_ITEMS);
miBand2DisplayItems.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newVal) {
invokeLater(new Runnable() {
@Override
public void run() {
GBApplication.deviceService().onSendConfiguration(PREF_MI2_DISPLAY_ITEMS);
}
});
return true;
}
});
final Preference miBand3ScreenUnlock = findPreference(PREF_MI3_BAND_SCREEN_UNLOCK);
miBand3ScreenUnlock.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newVal) {
invokeLater(new Runnable() {
@Override
public void run() {
GBApplication.deviceService().onSendConfiguration(PREF_MI3_BAND_SCREEN_UNLOCK);
}
});
return true;
}
});
final Preference miBand3DisplayItems = findPreference("miband3_display_items");
miBand3DisplayItems.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newVal) {
invokeLater(new Runnable() {
@Override
public void run() {
GBApplication.deviceService().onSendConfiguration(PREF_MI2_DISPLAY_ITEMS);
}
});
return true;
}
});
String nightModeState = prefs.getString(MiBandConst.PREF_MI3_NIGHT_MODE, PREF_MI3_NIGHT_MODE_OFF);
boolean nightModeScheduled = nightModeState.equals(PREF_MI3_NIGHT_MODE_SCHEDULED);
final Preference nightModeStart = findPreference(PREF_MI3_NIGHT_MODE_START);
nightModeStart.setEnabled(nightModeScheduled);
nightModeStart.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newVal) {
invokeLater(new Runnable() {
@Override
public void run() {
GBApplication.deviceService().onSendConfiguration(PREF_MI3_NIGHT_MODE_START);
}
});
return true;
}
});
final Preference nightModeEnd = findPreference(PREF_MI3_NIGHT_MODE_END);
nightModeEnd.setEnabled(nightModeScheduled);
nightModeEnd.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newVal) {
invokeLater(new Runnable() {
@Override
public void run() {
GBApplication.deviceService().onSendConfiguration(PREF_MI3_NIGHT_MODE_END);
}
});
return true;
}
});
final Preference nightMode = findPreference(PREF_MI3_NIGHT_MODE);
nightMode.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newVal) {
final boolean scheduled = PREF_MI3_NIGHT_MODE_SCHEDULED.equals(newVal.toString());
nightModeStart.setEnabled(scheduled);
nightModeEnd.setEnabled(scheduled);
invokeLater(new Runnable() {
@Override
public void run() {
GBApplication.deviceService().onSendConfiguration(PREF_MI3_NIGHT_MODE);
}
});
return true;
}
});
final Preference corDisplayItems = findPreference("cor_display_items");
corDisplayItems.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newVal) {
invokeLater(new Runnable() {
@Override
public void run() {
GBApplication.deviceService().onSendConfiguration(PREF_MI2_DISPLAY_ITEMS);
}
});
return true;
}
});
// Get all receivers of Media Buttons
Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON);
@ -563,8 +445,7 @@ public class SettingsActivity extends AbstractSettingsActivity {
if (cursor != null && cursor.moveToFirst()) {
return cursor.getString(cursor.getColumnIndex(DocumentsContract.Document.COLUMN_DISPLAY_NAME));
}
}
catch (Exception fdfsdfds) {
} catch (Exception fdfsdfds) {
LOG.warn("fuck");
}
}
@ -624,7 +505,6 @@ public class SettingsActivity extends AbstractSettingsActivity {
PREF_USER_WEIGHT_KG,
PREF_USER_SLEEP_DURATION,
PREF_USER_STEPS_GOAL,
PREF_MI2_ENABLE_TEXT_NOTIFICATIONS,
"weather_city",
};
}

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2016-2018 Andreas Shimokawa, Carsten Pfeiffer
/* Copyright (C) 2016-2019 Andreas Shimokawa, Carsten Pfeiffer
This file is part of Gadgetbridge.

View File

@ -0,0 +1,145 @@
/* Copyright (C) 2019 vanous
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge 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 Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.activities;
import android.app.Activity;
import android.content.Context;
import android.content.res.Resources;
import android.os.Bundle;
import android.os.Handler;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.GregorianCalendar;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser;
import nodomain.freeyourgadget.gadgetbridge.model.Alarm;
import nodomain.freeyourgadget.gadgetbridge.util.AlarmUtils;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
public class WidgetAlarmsActivity extends Activity implements View.OnClickListener {
TextView textView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Context appContext = this.getApplicationContext();
if (appContext instanceof GBApplication) {
GBApplication gbApp = (GBApplication) appContext;
GBDevice selectedDevice = gbApp.getDeviceManager().getSelectedDevice();
if (selectedDevice == null || !selectedDevice.isInitialized()) {
GB.toast(this,
this.getString(R.string.not_connected),
Toast.LENGTH_LONG, GB.WARN);
} else {
setContentView(R.layout.widget_alarms_activity_list);
int userSleepDuration = new ActivityUser().getSleepDuration();
textView = findViewById(R.id.alarm5);
if (userSleepDuration > 0) {
Resources res = getResources();
textView.setText(String.format(res.getQuantityString(R.plurals.widget_alarm_target_hours, userSleepDuration, userSleepDuration)));
} else {
textView.setVisibility(View.GONE);
}
}
}
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.alarm1:
setAlarm(5);
break;
case R.id.alarm2:
setAlarm(10);
break;
case R.id.alarm3:
setAlarm(20);
break;
case R.id.alarm4:
setAlarm(60);
break;
case R.id.alarm5:
setAlarm(0);
break;
default:
break;
}
//this is to prevent screen flashing during closing
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
finish();
}
}, 150);
}
public void setAlarm(int duration) {
// current timestamp
GregorianCalendar calendar = new GregorianCalendar();
if (duration > 0) {
calendar.add(Calendar.MINUTE, duration);
} else {
int userSleepDuration = new ActivityUser().getSleepDuration();
// add preferred sleep duration
if (userSleepDuration > 0) {
calendar.add(Calendar.HOUR_OF_DAY, userSleepDuration);
} else { // probably testing
calendar.add(Calendar.MINUTE, 1);
}
}
// overwrite the first alarm and activate it, without
Context appContext = this.getApplicationContext();
if (appContext instanceof GBApplication) {
GBApplication gbApp = (GBApplication) appContext;
GBDevice selectedDevice = gbApp.getDeviceManager().getSelectedDevice();
if (selectedDevice == null || !selectedDevice.isInitialized()) {
GB.toast(this,
this.getString(R.string.appwidget_not_connected),
Toast.LENGTH_LONG, GB.WARN);
return;
}
}
int hours = calendar.get(Calendar.HOUR_OF_DAY);
int minutes = calendar.get(Calendar.MINUTE);
GB.toast(this,
this.getString(R.string.appwidget_setting_alarm, hours, minutes),
Toast.LENGTH_SHORT, GB.INFO);
Alarm alarm = AlarmUtils.createSingleShot(0, true, calendar);
ArrayList<Alarm> alarms = new ArrayList<>(1);
alarms.add(alarm);
GBApplication.deviceService().onSetAlarms(alarms);
}
}

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2015-2018 Andreas Shimokawa, Carsten Pfeiffer, Daniele
/* Copyright (C) 2015-2019 Andreas Shimokawa, Carsten Pfeiffer, Daniele
Gobbetti, Konrad Iturbe, Lem Dulfo
This file is part of Gadgetbridge.
@ -24,12 +24,6 @@ import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Bundle;
import android.support.design.widget.FloatingActionButton;
import android.support.v4.app.Fragment;
import android.support.v4.content.LocalBroadcastManager;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.helper.ItemTouchHelper;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
@ -37,6 +31,8 @@ import android.view.View;
import android.view.ViewGroup;
import android.widget.PopupMenu;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -47,6 +43,11 @@ import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import androidx.fragment.app.Fragment;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.ExternalPebbleJSActivity;
@ -428,7 +429,7 @@ public abstract class AbstractAppManagerFragment extends Fragment {
startActivity(startIntent);
return true;
case R.id.appmanager_app_openinstore:
String url = "https://pebble-appstore.romanport.com/" + ((selectedApp.getType() == GBDeviceApp.Type.WATCHFACE) ? "watchfaces" : "watchapps") + "/0/?query=" + selectedApp.getName();
String url = "https://apps.rebble.io/en_US/search/" + ((selectedApp.getType() == GBDeviceApp.Type.WATCHFACE) ? "watchfaces" : "watchapps") + "/1/?native=true&?query=" + Uri.encode(selectedApp.getName());
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse(url));
startActivity(intent);

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2016-2018 Andreas Shimokawa, Carsten Pfeiffer, Daniele
/* Copyright (C) 2016-2019 Andreas Shimokawa, Carsten Pfeiffer, Daniele
Gobbetti
This file is part of Gadgetbridge.
@ -20,14 +20,11 @@ package nodomain.freeyourgadget.gadgetbridge.activities.appmanager;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.support.design.widget.FloatingActionButton;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.NavUtils;
import android.support.v4.view.ViewPager;
import android.view.MenuItem;
import android.view.View;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -40,6 +37,10 @@ import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import androidx.core.app.NavUtils;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.viewpager.widget.ViewPager;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractFragmentPagerAdapter;
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBFragmentActivity;

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2016-2018 Andreas Shimokawa
/* Copyright (C) 2016-2019 Andreas Shimokawa
This file is part of Gadgetbridge.

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2016-2018 Andreas Shimokawa, Daniele Gobbetti
/* Copyright (C) 2016-2019 Andreas Shimokawa, Daniele Gobbetti
This file is part of Gadgetbridge.

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2016-2018 Andreas Shimokawa
/* Copyright (C) 2016-2019 Andreas Shimokawa
This file is part of Gadgetbridge.

View File

@ -1,5 +1,5 @@
/* Copyright (C) 2015-2018 0nse, Andreas Shimokawa, Carsten Pfeiffer,
Daniele Gobbetti, walkjivefly
/* Copyright (C) 2015-2019 0nse, Andreas Shimokawa, Carsten Pfeiffer,
Daniele Gobbetti, Dikay900, Pavel Elagin, vanous, walkjivefly
This file is part of Gadgetbridge.
@ -23,23 +23,23 @@ import android.content.Intent;
import android.content.IntentFilter;
import android.os.AsyncTask;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.app.FragmentActivity;
import android.support.v4.content.ContextCompat;
import android.support.v4.content.LocalBroadcastManager;
import android.util.TypedValue;
import android.view.View;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.FragmentActivity;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import com.github.mikephil.charting.charts.BarChart;
import com.github.mikephil.charting.charts.BarLineChartBase;
import com.github.mikephil.charting.charts.Chart;
import com.github.mikephil.charting.components.AxisBase;
import com.github.mikephil.charting.components.YAxis;
import com.github.mikephil.charting.data.ChartData;
import com.github.mikephil.charting.data.Entry;
import com.github.mikephil.charting.data.LineData;
import com.github.mikephil.charting.data.LineDataSet;
import com.github.mikephil.charting.formatter.IAxisValueFormatter;
import com.github.mikephil.charting.formatter.ValueFormatter;
import com.github.mikephil.charting.interfaces.datasets.ILineDataSet;
import org.slf4j.Logger;
@ -69,6 +69,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
/**
* A base class fragment to be used with ChartsActivity. The fragment can supply
@ -151,8 +152,12 @@ public abstract class AbstractChartFragment extends AbstractGBFragment {
if (intentFilterActions != null) {
mIntentFilterActions.addAll(Arrays.asList(intentFilterActions));
}
mIntentFilterActions.add(ChartsHost.DATE_NEXT);
mIntentFilterActions.add(ChartsHost.DATE_PREV);
mIntentFilterActions.add(ChartsHost.DATE_NEXT_DAY);
mIntentFilterActions.add(ChartsHost.DATE_PREV_DAY);
mIntentFilterActions.add(ChartsHost.DATE_NEXT_WEEK);
mIntentFilterActions.add(ChartsHost.DATE_PREV_WEEK);
mIntentFilterActions.add(ChartsHost.DATE_NEXT_MONTH);
mIntentFilterActions.add(ChartsHost.DATE_PREV_MONTH);
mIntentFilterActions.add(ChartsHost.REFRESH);
}
@ -170,12 +175,18 @@ public abstract class AbstractChartFragment extends AbstractGBFragment {
}
protected void init() {
Prefs prefs = GBApplication.getPrefs();
TypedValue runningColor = new TypedValue();
BACKGROUND_COLOR = GBApplication.getBackgroundColor(getContext());
LEGEND_TEXT_COLOR = DESCRIPTION_COLOR = GBApplication.getTextColor(getContext());
CHART_TEXT_COLOR = ContextCompat.getColor(getContext(), R.color.secondarytext);
HEARTRATE_COLOR = ContextCompat.getColor(getContext(), R.color.chart_heartrate);
if (prefs.getBoolean("chart_heartrate_color", false)) {
HEARTRATE_COLOR = ContextCompat.getColor(getContext(), R.color.chart_heartrate_alternative);
}else{
HEARTRATE_COLOR = ContextCompat.getColor(getContext(), R.color.chart_heartrate);
}
HEARTRATE_FILL_COLOR = ContextCompat.getColor(getContext(), R.color.chart_heartrate_fill);
getContext().getTheme().resolveAttribute(R.attr.chart_activity, runningColor, true);
AK_ACTIVITY_COLOR = runningColor.data;
getContext().getTheme().resolveAttribute(R.attr.chart_deep_sleep, runningColor, true);
@ -243,10 +254,18 @@ public abstract class AbstractChartFragment extends AbstractGBFragment {
String action = intent.getAction();
if (ChartsHost.REFRESH.equals(action)) {
refresh();
} else if (ChartsHost.DATE_NEXT.equals(action)) {
handleDateNext(getStartDate(), getEndDate());
} else if (ChartsHost.DATE_PREV.equals(action)) {
handleDatePrev(getStartDate(), getEndDate());
} else if (ChartsHost.DATE_NEXT_DAY.equals(action)) {
handleDate(getStartDate(), getEndDate(),+1);
} else if (ChartsHost.DATE_PREV_DAY.equals(action)) {
handleDate(getStartDate(), getEndDate(),-1);
} else if (ChartsHost.DATE_NEXT_WEEK.equals(action)) {
handleDate(getStartDate(), getEndDate(),+7);
} else if (ChartsHost.DATE_PREV_WEEK.equals(action)) {
handleDate(getStartDate(), getEndDate(),-7);
} else if (ChartsHost.DATE_NEXT_MONTH.equals(action)) {
handleDate(getStartDate(), getEndDate(),+30);
} else if (ChartsHost.DATE_PREV_MONTH.equals(action)) {
handleDate(getStartDate(), getEndDate(),-30);
}
}
@ -256,31 +275,17 @@ public abstract class AbstractChartFragment extends AbstractGBFragment {
*
* @param startDate
* @param endDate
* @param Offset
*/
protected void handleDatePrev(Date startDate, Date endDate) {
protected void handleDate(Date startDate, Date endDate, Integer Offset) {
if (isVisibleInActivity()) {
if (!shiftDates(startDate, endDate, -1)) {
if (!shiftDates(startDate, endDate, Offset)) {
return;
}
}
refreshIfVisible();
}
/**
* Default implementation shifts the dates by one day, if visible
* and calls #refreshIfVisible().
*
* @param startDate
* @param endDate
*/
protected void handleDateNext(Date startDate, Date endDate) {
if (isVisibleInActivity()) {
if (!shiftDates(startDate, endDate, +1)) {
return;
}
}
refreshIfVisible();
}
protected void refreshIfVisible() {
if (isVisibleInActivity()) {
@ -572,7 +577,7 @@ public abstract class AbstractChartFragment extends AbstractGBFragment {
lineData = new LineData();
}
IAxisValueFormatter xValueFormatter = new SampleXLabelFormatter(tsTranslation);
ValueFormatter xValueFormatter = new SampleXLabelFormatter(tsTranslation);
return new DefaultChartsData(lineData, xValueFormatter);
}
@ -712,6 +717,29 @@ public abstract class AbstractChartFragment extends AbstractGBFragment {
return samples;
}
protected List<? extends ActivitySample> getSamplesofSleep(DBHandler db, GBDevice device) {
int SLEEP_HOUR_LIMIT = 12;
int tsStart = getTSStart();
Calendar day = GregorianCalendar.getInstance();
day.setTimeInMillis(tsStart * 1000L);
day.set(Calendar.HOUR_OF_DAY, SLEEP_HOUR_LIMIT);
day.set(Calendar.MINUTE, 0);
day.set(Calendar.SECOND, 0);
tsStart = toTimestamp(day.getTime());
int tsEnd = getTSEnd();
day.setTimeInMillis(tsEnd* 1000L);
day.set(Calendar.HOUR_OF_DAY, SLEEP_HOUR_LIMIT);
day.set(Calendar.MINUTE, 0);
day.set(Calendar.SECOND, 0);
tsEnd = toTimestamp(day.getTime());
List<ActivitySample> samples = (List<ActivitySample>) getSamples(db, device, tsStart, tsEnd);
ensureStartAndEndSamples(samples, tsStart, tsEnd);
return samples;
}
protected void ensureStartAndEndSamples(List<ActivitySample> samples, int tsStart, int tsEnd) {
if (samples == null || samples.isEmpty()) {
return;
@ -753,14 +781,14 @@ public abstract class AbstractChartFragment extends AbstractGBFragment {
public static class DefaultChartsData<T extends ChartData<?>> extends ChartsData {
private final T data;
private IAxisValueFormatter xValueFormatter;
private ValueFormatter xValueFormatter;
public DefaultChartsData(T data, IAxisValueFormatter xValueFormatter) {
public DefaultChartsData(T data, ValueFormatter xValueFormatter) {
this.xValueFormatter = xValueFormatter;
this.data = data;
}
public IAxisValueFormatter getXValueFormatter() {
public ValueFormatter getXValueFormatter() {
return xValueFormatter;
}
@ -769,7 +797,7 @@ public abstract class AbstractChartFragment extends AbstractGBFragment {
}
}
protected static class SampleXLabelFormatter implements IAxisValueFormatter {
protected static class SampleXLabelFormatter extends ValueFormatter {
private final TimestampTranslation tsTranslation;
SimpleDateFormat annotationDateFormat = new SimpleDateFormat("HH:mm");
// SimpleDateFormat dateFormat = new SimpleDateFormat("dd.MM.yyyy HH:mm");
@ -781,7 +809,7 @@ public abstract class AbstractChartFragment extends AbstractGBFragment {
}
// TODO: this does not work. Cannot use precomputed labels
@Override
public String getFormattedValue(float value, AxisBase axis) {
public String getFormattedValue(float value) {
cal.clear();
int ts = (int) value;
cal.setTimeInMillis(tsTranslation.toOriginalValue(ts) * 1000L);
@ -791,7 +819,7 @@ public abstract class AbstractChartFragment extends AbstractGBFragment {
}
}
protected static class PreformattedXIndexLabelFormatter implements IAxisValueFormatter {
protected static class PreformattedXIndexLabelFormatter extends ValueFormatter {
private ArrayList<String> xLabels;
public PreformattedXIndexLabelFormatter(ArrayList<String> xLabels) {
@ -799,7 +827,7 @@ public abstract class AbstractChartFragment extends AbstractGBFragment {
}
@Override
public String getFormattedValue(float value, AxisBase axis) {
public String getFormattedValue(float value) {
int index = (int) value;
if (xLabels == null || index >= xLabels.size()) {
return String.valueOf(value);

View File

@ -1,5 +1,5 @@
/* Copyright (C) 2015-2018 0nse, Alberto, Andreas Shimokawa, Carsten Pfeiffer,
Daniele Gobbetti
/* Copyright (C) 2015-2019 0nse, Alberto, Andreas Shimokawa, Carsten Pfeiffer,
Daniele Gobbetti, Pavel Elagin, vanous
This file is part of Gadgetbridge.
@ -37,8 +37,7 @@ import com.github.mikephil.charting.data.ChartData;
import com.github.mikephil.charting.data.PieData;
import com.github.mikephil.charting.data.PieDataSet;
import com.github.mikephil.charting.data.PieEntry;
import com.github.mikephil.charting.formatter.IAxisValueFormatter;
import com.github.mikephil.charting.formatter.IValueFormatter;
import com.github.mikephil.charting.formatter.ValueFormatter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -48,6 +47,7 @@ import java.util.Calendar;
import java.util.List;
import java.util.Locale;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
@ -58,7 +58,7 @@ import nodomain.freeyourgadget.gadgetbridge.util.LimitedQueue;
public abstract class AbstractWeekChartFragment extends AbstractChartFragment {
protected static final Logger LOG = LoggerFactory.getLogger(AbstractWeekChartFragment.class);
protected final int TOTAL_DAYS = 7;
protected final int TOTAL_DAYS = getRangeDays();
private Locale mLocale;
private int mTargetValue = 0;
@ -87,6 +87,10 @@ public abstract class AbstractWeekChartFragment extends AbstractChartFragment {
setupLegend(mWeekChart);
mTodayPieChart.setCenterText(mcd.getDayData().centerText);
mTodayPieChart.setData(mcd.getDayData().data);
//set custom renderer for 30days bar charts
if (GBApplication.getPrefs().getBoolean("charts_range", true)) {
mWeekChart.setRenderer(new AngledLabelsChartRenderer(mWeekChart, mWeekChart.getAnimator(), mWeekChart.getViewPortHandler()));
}
mWeekChart.setData(null); // workaround for https://github.com/PhilJay/MPAndroidChart/issues/2317
mWeekChart.setData(mcd.getWeekBeforeData().getData());
@ -102,6 +106,17 @@ public abstract class AbstractWeekChartFragment extends AbstractChartFragment {
// mBalanceView.setText(getBalanceMessage(balance));
}
private String getWeeksChartsLabel(Calendar day){
if (GBApplication.getPrefs().getBoolean("charts_range", true)) {
//month, show day date
return String.valueOf(day.get(Calendar.DAY_OF_MONTH));
}
else{
//week, show short day name
return day.getDisplayName(Calendar.DAY_OF_WEEK, Calendar.SHORT, mLocale);
}
}
private WeekChartsData<BarData> refreshWeekBeforeData(DBHandler db, BarChart barChart, Calendar day, GBDevice device) {
day = (Calendar) day.clone(); // do not modify the caller's argument
day.add(Calendar.DATE, -TOTAL_DAYS);
@ -114,7 +129,7 @@ public abstract class AbstractWeekChartFragment extends AbstractChartFragment {
balance += calculateBalance(amounts);
entries.add(new BarEntry(counter, getTotalsForActivityAmounts(amounts)));
labels.add(day.getDisplayName(Calendar.DAY_OF_WEEK, Calendar.SHORT, mLocale));
labels.add(getWeeksChartsLabel(day));
day.add(Calendar.DATE, 1);
}
@ -130,7 +145,28 @@ public abstract class AbstractWeekChartFragment extends AbstractChartFragment {
barChart.getAxisLeft().removeAllLimitLines();
barChart.getAxisLeft().addLimitLine(target);
return new WeekChartsData(barData, new PreformattedXIndexLabelFormatter(labels), getBalanceMessage(balance, mTargetValue));
float average = 0;
if (TOTAL_DAYS > 0) {
average = Math.abs(balance / TOTAL_DAYS);
}
LimitLine average_line = new LimitLine(average);
average_line.setLabel(getString(R.string.average, getAverage(average)));
if (average > (mTargetValue)) {
average_line.setLineColor(Color.GREEN);
average_line.setTextColor(Color.GREEN);
}
else {
average_line.setLineColor(Color.RED);
average_line.setTextColor(Color.RED);
}
if (average > 0) {
if (GBApplication.getPrefs().getBoolean("charts_show_average", true)) {
barChart.getAxisLeft().addLimitLine(average_line);
}
}
return new WeekChartsData(barData, new PreformattedXIndexLabelFormatter(labels), getBalanceMessage(balance, mTargetValue));
}
private DayData refreshDayPie(DBHandler db, Calendar day, GBDevice device) {
@ -315,6 +351,16 @@ public abstract class AbstractWeekChartFragment extends AbstractChartFragment {
return amounts;
}
private int getRangeDays(){
if (GBApplication.getPrefs().getBoolean("charts_range", true)) {
return 30;}
else{
return 7;
}
}
abstract String getAverage(float value);
abstract int getGoal();
abstract int getOffsetHours();
@ -325,11 +371,11 @@ public abstract class AbstractWeekChartFragment extends AbstractChartFragment {
abstract String[] getPieLabels();
abstract IValueFormatter getPieValueFormatter();
abstract ValueFormatter getPieValueFormatter();
abstract IValueFormatter getBarValueFormatter();
abstract ValueFormatter getBarValueFormatter();
abstract IAxisValueFormatter getYAxisFormatter();
abstract ValueFormatter getYAxisFormatter();
abstract int[] getColors();

View File

@ -1,5 +1,5 @@
/* Copyright (C) 2015-2018 Andreas Shimokawa, Carsten Pfeiffer, Daniele
Gobbetti, Pavel Elagin, Vebryn
/* Copyright (C) 2015-2019 Andreas Shimokawa, Carsten Pfeiffer, Daniele
Gobbetti, Pavel Elagin, vanous, Vebryn
This file is part of Gadgetbridge.
@ -17,26 +17,26 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.activities.charts;
import java.util.HashMap;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.HashMap;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityAmount;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityAmounts;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
class ActivityAnalysis {
private static final Logger LOG = LoggerFactory.getLogger(ActivityAnalysis.class);
public class ActivityAnalysis {
public static final Logger LOG = LoggerFactory.getLogger(ActivityAnalysis.class);
// store raw steps and duration
protected HashMap<Integer, Long> stats = new HashMap<Integer, Long>();
// max speed determined from samples
private int maxSpeed = 0;
ActivityAmounts calculateActivityAmounts(List<? extends ActivitySample> samples) {
public ActivityAmounts calculateActivityAmounts(List<? extends ActivitySample> samples) {
ActivityAmount deepSleep = new ActivityAmount(ActivityKind.TYPE_DEEP_SLEEP);
ActivityAmount lightSleep = new ActivityAmount(ActivityKind.TYPE_LIGHT_SLEEP);
ActivityAmount notWorn = new ActivityAmount(ActivityKind.TYPE_NOT_WORN);

View File

@ -1,5 +1,5 @@
/* Copyright (C) 2015-2018 Andreas Shimokawa, Carsten Pfeiffer, Daniele
Gobbetti, Pavel Elagin
/* Copyright (C) 2015-2019 Andreas Shimokawa, Carsten Pfeiffer, Daniele
Gobbetti, Dikay900, Pavel Elagin
This file is part of Gadgetbridge.
@ -143,7 +143,7 @@ public class ActivitySleepChartFragment extends AbstractChartFragment {
@Override
protected void renderCharts() {
mChart.animateX(ANIM_TIME, Easing.EasingOption.EaseInOutQuart);
mChart.animateX(ANIM_TIME, Easing.EaseInOutQuart);
// mChart.invalidate();
}

View File

@ -0,0 +1,46 @@
/* Copyright (C) 2019 Andreas Shimokawa, vanous
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge 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 Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.activities.charts;
import android.graphics.Canvas;
import com.github.mikephil.charting.animation.ChartAnimator;
import com.github.mikephil.charting.interfaces.dataprovider.BarDataProvider;
import com.github.mikephil.charting.renderer.BarChartRenderer;
import com.github.mikephil.charting.utils.ViewPortHandler;
public class AngledLabelsChartRenderer extends BarChartRenderer {
AngledLabelsChartRenderer(BarDataProvider chart, ChartAnimator animator, ViewPortHandler viewPortHandler) {
super(chart, animator, viewPortHandler);
}
@Override
public void drawValue(Canvas canvas, String valueText, float x, float y, int color) {
mValuePaint.setColor(color);
//move position to the center of bar
x=x+8;
y=y-25;
canvas.save();
canvas.rotate(-90, x, y);
canvas.drawText(valueText, x, y, mValuePaint);
canvas.restore();
}}

View File

@ -1,5 +1,5 @@
/* Copyright (C) 2015-2018 Andreas Shimokawa, Carsten Pfeiffer, Daniele
Gobbetti, Vebryn
/* Copyright (C) 2015-2019 Andreas Shimokawa, Carsten Pfeiffer, Daniele
Gobbetti, vanous, Vebryn
This file is part of Gadgetbridge.
@ -23,12 +23,6 @@ import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentStatePagerAdapter;
import android.support.v4.content.LocalBroadcastManager;
import android.support.v4.view.ViewPager;
import android.support.v4.widget.SwipeRefreshLayout;
import android.util.AttributeSet;
import android.view.Menu;
import android.view.MenuItem;
@ -43,6 +37,12 @@ import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Objects;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentStatePagerAdapter;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import androidx.viewpager.widget.ViewPager;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractFragmentPagerAdapter;
@ -175,20 +175,52 @@ public class ChartsActivity extends AbstractGBFragmentActivity implements Charts
}
});
Button mPrevButton = findViewById(R.id.charts_previous);
Button mPrevButton = findViewById(R.id.charts_previous_day);
mPrevButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
handlePrevButtonClicked();
handleButtonClicked(DATE_PREV_DAY);
}
});
Button mNextButton = findViewById(R.id.charts_next);
Button mNextButton = findViewById(R.id.charts_next_day);
mNextButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
handleNextButtonClicked();
handleButtonClicked(DATE_NEXT_DAY);
}
});
Button mPrevWeekButton = findViewById(R.id.charts_previous_week);
mPrevWeekButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
handleButtonClicked(DATE_PREV_WEEK);
}
});
Button mNextWeekButton = findViewById(R.id.charts_next_week);
mNextWeekButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
handleButtonClicked(DATE_NEXT_WEEK);
}
});
Button mPrevMonthButton = findViewById(R.id.charts_previous_month);
mPrevMonthButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
handleButtonClicked(DATE_PREV_MONTH);
}
});
Button mNextMonthButton = findViewById(R.id.charts_next_month);
mNextMonthButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
handleButtonClicked(DATE_NEXT_MONTH);
}
});
}
private String formatDetailedDuration() {
@ -229,12 +261,8 @@ public class ChartsActivity extends AbstractGBFragmentActivity implements Charts
return mEndDate;
}
private void handleNextButtonClicked() {
LocalBroadcastManager.getInstance(this).sendBroadcast(new Intent(DATE_NEXT));
}
private void handlePrevButtonClicked() {
LocalBroadcastManager.getInstance(this).sendBroadcast(new Intent(DATE_PREV));
private void handleButtonClicked(String Action) {
LocalBroadcastManager.getInstance(this).sendBroadcast(new Intent(Action));
}
@Override
@ -255,12 +283,24 @@ public class ChartsActivity extends AbstractGBFragmentActivity implements Charts
return true;
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == 1) {
this.recreate();
}
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.charts_fetch_activity_data:
fetchActivityData();
return true;
case R.id.prefs_charts_menu:
Intent settingsIntent = new Intent(this, ChartsPreferencesActivity.class);
startActivityForResult(settingsIntent,1);
return true;
default:
break;
}
@ -338,6 +378,24 @@ public class ChartsActivity extends AbstractGBFragmentActivity implements Charts
return 5;
}
private String getSleepTitle() {
if (GBApplication.getPrefs().getBoolean("charts_range", true)) {
return getString(R.string.weeksleepchart_sleep_a_month);
}
else{
return getString(R.string.weeksleepchart_sleep_a_week);
}
}
public String getStepsTitle() {
if (GBApplication.getPrefs().getBoolean("charts_range", true)) {
return getString(R.string.weekstepschart_steps_a_month);
}
else{
return getString(R.string.weekstepschart_steps_a_week);
}
}
@Override
public CharSequence getPageTitle(int position) {
switch (position) {
@ -346,9 +404,9 @@ public class ChartsActivity extends AbstractGBFragmentActivity implements Charts
case 1:
return getString(R.string.sleepchart_your_sleep);
case 2:
return getString(R.string.weeksleepchart_sleep_a_week);
return getSleepTitle();
case 3:
return getString(R.string.weekstepschart_steps_a_week);
return getStepsTitle();
case 4:
return getString(R.string.stats_title);
case 5:

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2016-2018 Carsten Pfeiffer
/* Copyright (C) 2016-2019 Carsten Pfeiffer
This file is part of Gadgetbridge.

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2015-2018 Andreas Shimokawa, Carsten Pfeiffer
/* Copyright (C) 2015-2019 Andreas Shimokawa, Carsten Pfeiffer, vanous
This file is part of Gadgetbridge.
@ -23,8 +23,14 @@ import java.util.Date;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
public interface ChartsHost {
String DATE_PREV = ChartsActivity.class.getName().concat(".date_prev");
String DATE_NEXT = ChartsActivity.class.getName().concat(".date_next");
String DATE_PREV_DAY = ChartsActivity.class.getName().concat(".date_prev_day");
String DATE_NEXT_DAY = ChartsActivity.class.getName().concat(".date_next_day");
String DATE_PREV_WEEK = ChartsActivity.class.getName().concat(".date_prev_week");
String DATE_NEXT_WEEK = ChartsActivity.class.getName().concat(".date_next_week");
String DATE_PREV_MONTH = ChartsActivity.class.getName().concat(".date_prev_month");
String DATE_NEXT_MONTH = ChartsActivity.class.getName().concat(".date_next_month");
String REFRESH = ChartsActivity.class.getName().concat(".refresh");
GBDevice getDevice();

View File

@ -1,4 +1,5 @@
/* Copyright (C) 2015-2018 Andreas Shimokawa
/* Copyright (C) 2015-2019 Andreas Shimokawa, Carsten Pfeiffer, Lem Dulfo,
vanous
This file is part of Gadgetbridge.
@ -14,12 +15,17 @@
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.deviceevents;
package nodomain.freeyourgadget.gadgetbridge.activities.charts;
public class GBDeviceEventSleepMonitorResult extends GBDeviceEvent {
// FIXME: this is just the low-level data from Morpheuz, we need something generic
public int smartalarm_from = -1; // time in minutes relative from 0:00 for smart alarm (earliest)
public int smartalarm_to = -1;// time in minutes relative from 0:00 for smart alarm (latest)
public int recording_base_timestamp = -1; // timestamp for the first "point", all folowing are +10 minutes offset each
public int alarm_gone_off = -1; // time in minutes relative from 0:00 when alarm gone off
import android.os.Bundle;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractSettingsActivity;
public class ChartsPreferencesActivity extends AbstractSettingsActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
addPreferencesFromResource(R.xml.charts_preferences);
}
}

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2015-2018 Andreas Shimokawa, Carsten Pfeiffer
/* Copyright (C) 2015-2019 Andreas Shimokawa, Carsten Pfeiffer
This file is part of Gadgetbridge.

View File

@ -1,4 +1,5 @@
/* Copyright (C) 2015-2018 Andreas Shimokawa, Carsten Pfeiffer
/* Copyright (C) 2015-2019 Andreas Shimokawa, Carsten Pfeiffer, Cre3per,
Daniele Gobbetti, Dikay900, Pavel, Pavel Elagin
This file is part of Gadgetbridge.
@ -22,9 +23,6 @@ import android.content.Intent;
import android.content.IntentFilter;
import android.graphics.Paint;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.app.FragmentActivity;
import android.support.v4.content.LocalBroadcastManager;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@ -55,6 +53,9 @@ import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import androidx.annotation.Nullable;
import androidx.fragment.app.FragmentActivity;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.HeartRateUtils;
@ -63,7 +64,6 @@ import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceService;
import nodomain.freeyourgadget.gadgetbridge.model.Measurement;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
public class LiveActivityFragment extends AbstractChartFragment {
@ -85,7 +85,6 @@ public class LiveActivityFragment extends AbstractChartFragment {
private final Steps mSteps = new Steps();
private ScheduledExecutorService pulseScheduler;
private int maxStepsResetCounter;
private List<Measurement> heartRateValues;
private LineDataSet mHeartRateSet;
private int mHeartRate;
private int mMaxHeartRate = 0;
@ -266,14 +265,13 @@ public class LiveActivityFragment extends AbstractChartFragment {
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
IntentFilter filterLocal = new IntentFilter();
filterLocal.addAction(DeviceService.ACTION_REALTIME_SAMPLES);
heartRateValues = new ArrayList<>();
tsTranslation = new TimestampTranslation();
View rootView = inflater.inflate(R.layout.fragment_live_activity, container, false);
mStepsPerMinuteCurrentChart = (CustomBarChart) rootView.findViewById(R.id.livechart_steps_per_minute_current);
mTotalStepsChart = (CustomBarChart) rootView.findViewById(R.id.livechart_steps_total);
mStepsPerMinuteHistoryChart = (BarLineChartBase) rootView.findViewById(R.id.livechart_steps_per_minute_history);
mStepsPerMinuteCurrentChart = rootView.findViewById(R.id.livechart_steps_per_minute_current);
mTotalStepsChart = rootView.findViewById(R.id.livechart_steps_total);
mStepsPerMinuteHistoryChart = rootView.findViewById(R.id.livechart_steps_per_minute_history);
totalStepsEntry = new BarEntry(1, 0);
stepsPerMinuteEntry = new BarEntry(1, 0);
@ -348,7 +346,7 @@ public class LiveActivityFragment extends AbstractChartFragment {
renderCharts();
// have to enable it again and again to keep it measureing
// have to enable it again and again to keep it measuring
GBApplication.deviceService().onEnableRealtimeHeartRateMeasurement(true);
}

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2015-2018 Carsten Pfeiffer
/* Copyright (C) 2015-2019 Carsten Pfeiffer
This file is part of Gadgetbridge.

View File

@ -0,0 +1,118 @@
/* Copyright (C) 2019 Q-er
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge 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 Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.activities.charts;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
public class SleepAnalysis {
public static final long MIN_SESSION_LENGTH = 5 * 60;
public static final long MAX_WAKE_PHASE_LENGTH = 2 * 60 * 60;
public List<SleepSession> calculateSleepSessions(List<? extends ActivitySample> samples) {
List<SleepSession> result = new ArrayList<>();
ActivitySample previousSample = null;
Date sleepStart = null;
Date sleepEnd = null;
long lightSleepDuration = 0;
long deepSleepDuration = 0;
long durationSinceLastSleep = 0;
for (ActivitySample sample : samples) {
if (isSleep(sample)) {
if (sleepStart == null)
sleepStart = getDateFromSample(sample);
sleepEnd = getDateFromSample(sample);
durationSinceLastSleep = 0;
}
if (previousSample != null) {
long durationSinceLastSample = sample.getTimestamp() - previousSample.getTimestamp();
if (sample.getKind() == ActivityKind.TYPE_LIGHT_SLEEP) {
lightSleepDuration += durationSinceLastSample;
} else if (sample.getKind() == ActivityKind.TYPE_DEEP_SLEEP) {
deepSleepDuration += durationSinceLastSample;
} else {
durationSinceLastSleep += durationSinceLastSample;
if (sleepStart != null && durationSinceLastSleep > MAX_WAKE_PHASE_LENGTH) {
if (lightSleepDuration + deepSleepDuration > MIN_SESSION_LENGTH)
result.add(new SleepSession(sleepStart, sleepEnd, lightSleepDuration, deepSleepDuration));
sleepStart = null;
sleepEnd = null;
lightSleepDuration = 0;
deepSleepDuration = 0;
}
}
}
previousSample = sample;
}
if (lightSleepDuration + deepSleepDuration > MIN_SESSION_LENGTH) {
result.add(new SleepSession(sleepStart, sleepEnd, lightSleepDuration, deepSleepDuration));
}
return result;
}
private boolean isSleep(ActivitySample sample) {
return sample.getKind() == ActivityKind.TYPE_DEEP_SLEEP || sample.getKind() == ActivityKind.TYPE_LIGHT_SLEEP;
}
private Date getDateFromSample(ActivitySample sample) {
return new Date(sample.getTimestamp() * 1000L);
}
public static class SleepSession {
private final Date sleepStart;
private final Date sleepEnd;
private final long lightSleepDuration;
private final long deepSleepDuration;
private SleepSession(Date sleepStart,
Date sleepEnd,
long lightSleepDuration,
long deepSleepDuration) {
this.sleepStart = sleepStart;
this.sleepEnd = sleepEnd;
this.lightSleepDuration = lightSleepDuration;
this.deepSleepDuration = deepSleepDuration;
}
public Date getSleepStart() {
return sleepStart;
}
public Date getSleepEnd() {
return sleepEnd;
}
public long getLightSleepDuration() {
return lightSleepDuration;
}
public long getDeepSleepDuration() {
return deepSleepDuration;
}
}
}

View File

@ -1,5 +1,5 @@
/* Copyright (C) 2015-2018 0nse, Andreas Shimokawa, Carsten Pfeiffer,
Daniele Gobbetti, Pavel Elagin
/* Copyright (C) 2015-2019 0nse, Andreas Shimokawa, Carsten Pfeiffer,
Daniele Gobbetti, Dikay900, Pavel Elagin, Q-er, vanous
This file is part of Gadgetbridge.
@ -20,7 +20,6 @@ package nodomain.freeyourgadget.gadgetbridge.activities.charts;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@ -33,31 +32,32 @@ import com.github.mikephil.charting.charts.PieChart;
import com.github.mikephil.charting.components.LegendEntry;
import com.github.mikephil.charting.components.XAxis;
import com.github.mikephil.charting.components.YAxis;
import com.github.mikephil.charting.data.Entry;
import com.github.mikephil.charting.data.LineData;
import com.github.mikephil.charting.data.PieData;
import com.github.mikephil.charting.data.PieDataSet;
import com.github.mikephil.charting.data.PieEntry;
import com.github.mikephil.charting.formatter.IValueFormatter;
import com.github.mikephil.charting.utils.ViewPortHandler;
import com.github.mikephil.charting.formatter.ValueFormatter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Date;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.TimeUnit;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.HeartRateUtils;
import nodomain.freeyourgadget.gadgetbridge.activities.charts.SleepAnalysis.SleepSession;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityAmount;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityAmounts;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
public class SleepChartFragment extends AbstractChartFragment {
@ -74,53 +74,69 @@ public class SleepChartFragment extends AbstractChartFragment {
@Override
protected ChartsData refreshInBackground(ChartsHost chartsHost, DBHandler db, GBDevice device) {
List<? extends ActivitySample> samples = getSamples(db, device);
Prefs prefs = GBApplication.getPrefs();
List<? extends ActivitySample> samples;
if (prefs.getBoolean("chart_sleep_range_24h", false)) {
samples = getSamples(db, device);
}else{
samples = getSamplesofSleep(db, device);
}
MySleepChartsData mySleepChartsData = refreshSleepAmounts(device, samples);
if (!prefs.getBoolean("chart_sleep_range_24h", false)) {
if (mySleepChartsData.sleepSessions.size() > 0) {
long tstart = mySleepChartsData.sleepSessions.get(0).getSleepStart().getTime() / 1000;
long tend = mySleepChartsData.sleepSessions.get(mySleepChartsData.sleepSessions.size() - 1).getSleepEnd().getTime() / 1000;
for (Iterator<ActivitySample> iterator = (Iterator<ActivitySample>) samples.iterator(); iterator.hasNext(); ) {
ActivitySample sample = iterator.next();
if (sample.getTimestamp() < tstart || sample.getTimestamp() > tend) {
iterator.remove();
}
}
}
}
DefaultChartsData chartsData = refresh(device, samples);
return new MyChartsData(mySleepChartsData, chartsData);
}
private MySleepChartsData refreshSleepAmounts(GBDevice mGBDevice, List<? extends ActivitySample> samples) {
ActivityAnalysis analysis = new ActivityAnalysis();
ActivityAmounts amounts = analysis.calculateActivityAmounts(samples);
SleepAnalysis sleepAnalysis = new SleepAnalysis();
List<SleepSession> sleepSessions = sleepAnalysis.calculateSleepSessions(samples);
PieData data = new PieData();
List<PieEntry> entries = new ArrayList<>();
List<Integer> colors = new ArrayList<>();
// int index = 0;
long totalSeconds = 0;
Date startSleep = null;
Date endSleep = null;
for (ActivityAmount amount : amounts.getAmounts()) {
if ((amount.getActivityKind() & ActivityKind.TYPE_SLEEP) != 0) {
long value = amount.getTotalSeconds();
if(startSleep == null){
startSleep = amount.getStartDate();
} else {
if(startSleep.after(amount.getStartDate()))
startSleep = amount.getStartDate();
}
if(endSleep == null){
endSleep = amount.getEndDate();
} else {
if(endSleep.before(amount.getEndDate()))
endSleep = amount.getEndDate();
}
totalSeconds += value;
// entries.add(new PieEntry(value, index++));
entries.add(new PieEntry(value, amount.getName(getActivity())));
colors.add(getColorFor(amount.getActivityKind()));
// data.addXValue(amount.getName(getActivity()));
}
final long lightSleepDuration = calculateLightSleepDuration(sleepSessions);
final long deepSleepDuration = calculateDeepSleepDuration(sleepSessions);
final long totalSeconds = lightSleepDuration + deepSleepDuration;
final List<PieEntry> entries;
final List<Integer> colors;
if (sleepSessions.isEmpty()) {
entries = Collections.emptyList();
colors = Collections.emptyList();
} else {
entries = Arrays.asList(
new PieEntry(lightSleepDuration, getActivity().getString(R.string.abstract_chart_fragment_kind_light_sleep)),
new PieEntry(deepSleepDuration, getActivity().getString(R.string.abstract_chart_fragment_kind_deep_sleep))
);
colors = Arrays.asList(
getColorFor(ActivityKind.TYPE_LIGHT_SLEEP),
getColorFor(ActivityKind.TYPE_DEEP_SLEEP)
);
}
String totalSleep = DateTimeUtils.formatDurationHoursMinutes(totalSeconds, TimeUnit.SECONDS);
PieDataSet set = new PieDataSet(entries, "");
set.setValueFormatter(new IValueFormatter() {
set.setValueFormatter(new ValueFormatter() {
@Override
public String getFormattedValue(float value, Entry entry, int dataSetIndex, ViewPortHandler viewPortHandler) {
public String getFormattedValue(float value) {
return DateTimeUtils.formatDurationHoursMinutes((long) value, TimeUnit.SECONDS);
}
});
@ -132,27 +148,54 @@ public class SleepChartFragment extends AbstractChartFragment {
data.setDataSet(set);
//setupLegend(pieChart);
return new MySleepChartsData(totalSleep, data, startSleep, endSleep);
return new MySleepChartsData(totalSleep, data, sleepSessions);
}
private long calculateLightSleepDuration(List<SleepSession> sleepSessions) {
long result = 0;
for (SleepSession sleepSession : sleepSessions) {
result += sleepSession.getLightSleepDuration();
}
return result;
}
private long calculateDeepSleepDuration(List<SleepSession> sleepSessions) {
long result = 0;
for (SleepSession sleepSession : sleepSessions) {
result += sleepSession.getDeepSleepDuration();
}
return result;
}
@Override
protected void updateChartsnUIThread(ChartsData chartsData) {
MyChartsData mcd = (MyChartsData) chartsData;
mSleepAmountChart.setCenterText(mcd.getPieData().getTotalSleep());
mSleepAmountChart.setData(mcd.getPieData().getPieData());
MySleepChartsData pieData = mcd.getPieData();
mSleepAmountChart.setCenterText(pieData.getTotalSleep());
mSleepAmountChart.setData(pieData.getPieData());
mActivityChart.setData(null); // workaround for https://github.com/PhilJay/MPAndroidChart/issues/2317
mActivityChart.getXAxis().setValueFormatter(mcd.getChartsData().getXValueFormatter());
mActivityChart.setData(mcd.getChartsData().getData());
if (mcd.getPieData().getStartSleep() != null && mcd.getPieData().getEndSleep() != null) {
mSleepchartInfo.setText(getContext().getString(
R.string.you_slept,
DateTimeUtils.timeToString(mcd.getPieData().getStartSleep()),
DateTimeUtils.timeToString(mcd.getPieData().getEndSleep())));
mSleepchartInfo.setText(buildYouSleptText(pieData));
}
private String buildYouSleptText(MySleepChartsData pieData) {
final StringBuilder result = new StringBuilder();
if (pieData.getSleepSessions().isEmpty()) {
result.append(getContext().getString(R.string.you_did_not_sleep));
} else {
mSleepchartInfo.setText(getContext().getString(R.string.you_did_not_sleep));
for (SleepSession sleepSession : pieData.getSleepSessions()) {
result.append(getContext().getString(
R.string.you_slept,
DateTimeUtils.timeToString(sleepSession.getSleepStart()),
DateTimeUtils.timeToString(sleepSession.getSleepEnd())));
result.append('\n');
}
}
return result.toString();
}
@Override
@ -269,21 +312,19 @@ public class SleepChartFragment extends AbstractChartFragment {
@Override
protected void renderCharts() {
mActivityChart.animateX(ANIM_TIME, Easing.EasingOption.EaseInOutQuart);
mActivityChart.animateX(ANIM_TIME, Easing.EaseInOutQuart);
mSleepAmountChart.invalidate();
}
private static class MySleepChartsData extends ChartsData {
private String totalSleep;
private final PieData pieData;
private @Nullable Date startSleep;
private @Nullable Date endSleep;
private final List<SleepSession> sleepSessions;
public MySleepChartsData(String totalSleep, PieData pieData, @Nullable Date startSleep, @Nullable Date endSleep) {
public MySleepChartsData(String totalSleep, PieData pieData, List<SleepSession> sleepSessions) {
this.totalSleep = totalSleep;
this.pieData = pieData;
this.startSleep = startSleep;
this.endSleep = endSleep;
this.sleepSessions = sleepSessions;
}
public PieData getPieData() {
@ -294,14 +335,8 @@ public class SleepChartFragment extends AbstractChartFragment {
return totalSleep;
}
@Nullable
public Date getStartSleep() {
return startSleep;
}
@Nullable
public Date getEndSleep() {
return endSleep;
public List<SleepSession> getSleepSessions() {
return sleepSessions;
}
}

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2015-2018 Andreas Shimokawa, Carsten Pfeiffer
/* Copyright (C) 2015-2019 Andreas Shimokawa, Carsten Pfeiffer
This file is part of Gadgetbridge.

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2015-2018 0nse, Andreas Shimokawa, Carsten Pfeiffer,
/* Copyright (C) 2015-2019 0nse, Andreas Shimokawa, Carsten Pfeiffer,
Daniele Gobbetti, Vebryn
This file is part of Gadgetbridge.

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2016-2018 Carsten Pfeiffer
/* Copyright (C) 2016-2019 Andreas Shimokawa, Carsten Pfeiffer
This file is part of Gadgetbridge.
@ -16,8 +16,7 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.activities.charts;
import com.github.mikephil.charting.components.AxisBase;
import com.github.mikephil.charting.formatter.IAxisValueFormatter;
import com.github.mikephil.charting.formatter.ValueFormatter;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
@ -25,7 +24,7 @@ import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
public class TimestampValueFormatter implements IAxisValueFormatter {
public class TimestampValueFormatter extends ValueFormatter {
private final Calendar cal;
// private DateFormat dateFormat = new SimpleDateFormat("dd.MM.yyyy HH:mm");
private DateFormat dateFormat;
@ -42,7 +41,7 @@ public class TimestampValueFormatter implements IAxisValueFormatter {
}
@Override
public String getFormattedValue(float value, AxisBase axis) {
public String getFormattedValue(float value) {
cal.setTimeInMillis((int) value * 1000L);
Date date = cal.getTime();
String dateString = dateFormat.format(date);

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2016-2018 Carsten Pfeiffer
/* Copyright (C) 2016-2019 Carsten Pfeiffer
This file is part of Gadgetbridge.

View File

@ -1,4 +1,5 @@
/* Copyright (C) 2017-2018 Andreas Shimokawa, Carsten Pfeiffer, Pavel Elagin
/* Copyright (C) 2017-2019 Andreas Shimokawa, Carsten Pfeiffer, Pavel
Elagin, vanous
This file is part of Gadgetbridge.
@ -17,13 +18,9 @@
package nodomain.freeyourgadget.gadgetbridge.activities.charts;
import com.github.mikephil.charting.charts.Chart;
import com.github.mikephil.charting.components.AxisBase;
import com.github.mikephil.charting.components.Legend;
import com.github.mikephil.charting.components.LegendEntry;
import com.github.mikephil.charting.data.Entry;
import com.github.mikephil.charting.formatter.IAxisValueFormatter;
import com.github.mikephil.charting.formatter.IValueFormatter;
import com.github.mikephil.charting.utils.ViewPortHandler;
import com.github.mikephil.charting.formatter.ValueFormatter;
import java.util.ArrayList;
import java.util.List;
@ -40,7 +37,12 @@ import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
public class WeekSleepChartFragment extends AbstractWeekChartFragment {
@Override
public String getTitle() {
return getString(R.string.weeksleepchart_sleep_a_week);
if (GBApplication.getPrefs().getBoolean("charts_range", true)) {
return getString(R.string.weeksleepchart_sleep_a_month);
}
else{
return getString(R.string.weeksleepchart_sleep_a_week);
}
}
@Override
@ -74,7 +76,7 @@ public class WeekSleepChartFragment extends AbstractWeekChartFragment {
@Override
protected String getBalanceMessage(long balance, int targetValue) {
if (balance > 0) {
final long totalBalance = balance - (targetValue * TOTAL_DAYS);
final long totalBalance = balance - ((long)targetValue * TOTAL_DAYS);
if (totalBalance > 0)
return getString(R.string.overslept, getHM(totalBalance));
else
@ -110,30 +112,30 @@ public class WeekSleepChartFragment extends AbstractWeekChartFragment {
}
@Override
IValueFormatter getPieValueFormatter() {
return new IValueFormatter() {
ValueFormatter getPieValueFormatter() {
return new ValueFormatter() {
@Override
public String getFormattedValue(float value, Entry entry, int dataSetIndex, ViewPortHandler viewPortHandler) {
public String getFormattedValue(float value) {
return formatPieValue((long) value);
}
};
}
@Override
IValueFormatter getBarValueFormatter() {
return new IValueFormatter() {
ValueFormatter getBarValueFormatter() {
return new ValueFormatter() {
@Override
public String getFormattedValue(float value, Entry entry, int dataSetIndex, ViewPortHandler viewPortHandler) {
public String getFormattedValue(float value) {
return DateTimeUtils.minutesToHHMM((int) value);
}
};
}
@Override
IAxisValueFormatter getYAxisFormatter() {
return new IAxisValueFormatter() {
ValueFormatter getYAxisFormatter() {
return new ValueFormatter() {
@Override
public String getFormattedValue(float value, AxisBase axis) {
public String getFormattedValue(float value) {
return DateTimeUtils.minutesToHHMM((int) value);
}
};
@ -167,4 +169,10 @@ public class WeekSleepChartFragment extends AbstractWeekChartFragment {
private String getHM(long value) {
return DateTimeUtils.formatDurationHoursMinutes(value, TimeUnit.MINUTES);
}
@Override
String getAverage(float value) {
return getHM((long)value);
}
}

View File

@ -1,5 +1,5 @@
/* Copyright (C) 2015-2018 0nse, Andreas Shimokawa, Carsten Pfeiffer,
Daniele Gobbetti
/* Copyright (C) 2015-2019 0nse, Andreas Shimokawa, Carsten Pfeiffer,
Daniele Gobbetti, Pavel Elagin, vanous
This file is part of Gadgetbridge.
@ -18,8 +18,7 @@
package nodomain.freeyourgadget.gadgetbridge.activities.charts;
import com.github.mikephil.charting.charts.Chart;
import com.github.mikephil.charting.formatter.IAxisValueFormatter;
import com.github.mikephil.charting.formatter.IValueFormatter;
import com.github.mikephil.charting.formatter.ValueFormatter;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
@ -30,7 +29,12 @@ import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser;
public class WeekStepsChartFragment extends AbstractWeekChartFragment {
@Override
public String getTitle() {
return getString(R.string.weekstepschart_steps_a_week);
if (GBApplication.getPrefs().getBoolean("charts_range", true)) {
return getString(R.string.weekstepschart_steps_a_month);
}
else{
return getString(R.string.weekstepschart_steps_a_week);
}
}
@Override
@ -77,17 +81,17 @@ public class WeekStepsChartFragment extends AbstractWeekChartFragment {
}
@Override
IValueFormatter getPieValueFormatter() {
ValueFormatter getPieValueFormatter() {
return null;
}
@Override
IValueFormatter getBarValueFormatter() {
ValueFormatter getBarValueFormatter() {
return null;
}
@Override
IAxisValueFormatter getYAxisFormatter() {
ValueFormatter getYAxisFormatter() {
return null;
}
@ -105,7 +109,7 @@ public class WeekStepsChartFragment extends AbstractWeekChartFragment {
@Override
protected String getBalanceMessage(long balance, int targetValue) {
if (balance > 0) {
final long totalBalance = balance - (targetValue * TOTAL_DAYS);
final long totalBalance = balance - ((long)targetValue * TOTAL_DAYS);
if (totalBalance > 0)
return getString(R.string.overstep, Math.abs(totalBalance));
else
@ -113,4 +117,9 @@ public class WeekStepsChartFragment extends AbstractWeekChartFragment {
} else
return getString(R.string.no_data);
}
@Override
String getAverage(float value) {
return String.format("%.0f", value);
}
}

View File

@ -0,0 +1,78 @@
/* Copyright (C) 2019 Andreas Shimokawa
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge 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 Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.activities.devicesettings;
import android.os.Bundle;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentTransaction;
import androidx.preference.PreferenceFragmentCompat;
import androidx.preference.PreferenceScreen;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBActivity;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper;
public class DeviceSettingsActivity extends AbstractGBActivity implements
PreferenceFragmentCompat.OnPreferenceStartScreenCallback {
private static final Logger LOG = LoggerFactory.getLogger(DeviceSettingsActivity.class);
GBDevice device;
@Override
protected void onCreate(Bundle savedInstanceState) {
device = getIntent().getParcelableExtra(GBDevice.EXTRA_DEVICE);
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_device_settings);
if (savedInstanceState == null) {
Fragment fragment = getSupportFragmentManager().findFragmentByTag(DeviceSpecificSettingsFragment.FRAGMENT_TAG);
if (fragment == null) {
DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(device);
fragment = DeviceSpecificSettingsFragment.newInstance(device.getAddress(), coordinator.getSupportedDeviceSpecificSettings(device));
}
getSupportFragmentManager()
.beginTransaction()
.replace(R.id.settings_container, fragment, DeviceSpecificSettingsFragment.FRAGMENT_TAG)
.commit();
}
}
@Override
public boolean onPreferenceStartScreen(PreferenceFragmentCompat caller, PreferenceScreen preferenceScreen) {
DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(device);
PreferenceFragmentCompat fragment = DeviceSpecificSettingsFragment.newInstance(device.getAddress(), coordinator.getSupportedDeviceSpecificSettings(device));
Bundle args = fragment.getArguments();
args.putString(PreferenceFragmentCompat.ARG_PREFERENCE_ROOT, preferenceScreen.getKey());
fragment.setArguments(args);
getSupportFragmentManager()
.beginTransaction()
.replace(R.id.settings_container, fragment, preferenceScreen.getKey())
.addToBackStack(preferenceScreen.getKey())
.commit();
return true;
}
}

View File

@ -0,0 +1,25 @@
/* Copyright (C) 2019 Andreas Shimokawa
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge 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 Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.activities.devicesettings;
public class DeviceSettingsPreferenceConst {
public static final String PREF_DATEFORMAT = "dateformat";
public static final String PREF_TIMEFORMAT = "timeformat";
public static final String PREF_WEARLOCATION = "wearlocation";
public static final String PREF_SCREEN_ORIENTATION = "screen_orientation";
public static final String PREF_RESERVER_ALARMS_CALENDAR = "reserve_alarms_calendar";
}

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